From 66d43b882e75dbeba5f250c17f9240aceb536cee Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 11:58:50 +0200 Subject: [PATCH 001/151] Fixed calls set_uniform(emission_factor, xx) to use floats --- src/slic3r/GUI/3DBed.cpp | 4 ++-- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoBase.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoMove.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoScale.cpp | 8 ++++---- src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 4 ++-- src/slic3r/GUI/Selection.cpp | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index d7fb937d04..7b08b5f793 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -119,7 +119,7 @@ void Bed3D::Axes::render() const glsafe(::glEnable(GL_DEPTH_TEST)); shader->start_using(); - shader->set_uniform("emission_factor", 0.0); + shader->set_uniform("emission_factor", 0.0f); // x axis const_cast(&m_arrow)->set_color(-1, { 0.75f, 0.0f, 0.0f, 1.0f }); @@ -498,7 +498,7 @@ void Bed3D::render_model() const GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader != nullptr) { shader->start_using(); - shader->set_uniform("emission_factor", 0.0); + shader->set_uniform("emission_factor", 0.0f); glsafe(::glPushMatrix()); glsafe(::glTranslated(m_model_offset.x(), m_model_offset.y(), m_model_offset.z())); model->render(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 090accb6a4..b9a7d23387 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -223,7 +223,7 @@ void GCodeViewer::SequentialView::Marker::render() const glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); shader->start_using(); - shader->set_uniform("emission_factor", 0.0); + shader->set_uniform("emission_factor", 0.0f); glsafe(::glPushMatrix()); glsafe(::glMultMatrixf(m_world_transform.data())); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index f366100acc..e2699fb6d7 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4148,7 +4148,7 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const glsafe(::glEnable(GL_DEPTH_TEST)); shader->start_using(); - shader->set_uniform("emission_factor", 0.0); + shader->set_uniform("emission_factor", 0.0f); for (GLVolume* vol : visible_volumes) { shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? orange : gray); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index 92c03cb753..1177c46ca7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -183,7 +183,7 @@ void GLGizmoBase::render_grabbers(float size) const if (shader == nullptr) return; shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); for (int i = 0; i < (int)m_grabbers.size(); ++i) { if (m_grabbers[i].enabled) m_grabbers[i].render(m_hover_id == i, size); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 707726e080..27b54c595c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -136,7 +136,7 @@ void GLGizmoCut::on_render() if (shader == nullptr) return; shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); m_grabbers[0].color = GrabberColor; m_grabbers[0].render(m_hover_id == 0, (float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 1211864ebc..9a056adcb3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -141,7 +141,7 @@ void GLGizmoMove3D::on_render() GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader != nullptr) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); // draw grabber float mean_size = (float)((box.size().x() + box.size().y() + box.size().z()) / 3.0); m_grabbers[m_hover_id].render(true, mean_size); @@ -208,7 +208,7 @@ void GLGizmoMove3D::render_grabber_extension(Axis axis, const BoundingBoxf3& box const_cast(&m_vbo_cone)->set_color(-1, color); if (!picking) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); } glsafe(::glPushMatrix()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index a495db4f13..2e32a25681 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -339,7 +339,7 @@ void GLGizmoRotate::render_grabber_extension(const BoundingBoxf3& box, bool pick const_cast(&m_cone)->set_color(-1, color); if (!picking) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); } glsafe(::glPushMatrix()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 8490469627..894d844d83 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -236,7 +236,7 @@ void GLGizmoScale3D::on_render() GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader != nullptr) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); // draw grabbers m_grabbers[0].render(true, grabber_mean_size); m_grabbers[1].render(true, grabber_mean_size); @@ -251,7 +251,7 @@ void GLGizmoScale3D::on_render() GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader != nullptr) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); // draw grabbers m_grabbers[2].render(true, grabber_mean_size); m_grabbers[3].render(true, grabber_mean_size); @@ -266,7 +266,7 @@ void GLGizmoScale3D::on_render() GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader != nullptr) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); // draw grabbers m_grabbers[4].render(true, grabber_mean_size); m_grabbers[5].render(true, grabber_mean_size); @@ -284,7 +284,7 @@ void GLGizmoScale3D::on_render() GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader != nullptr) { shader->start_using(); - shader->set_uniform("emission_factor", 0.1); + shader->set_uniform("emission_factor", 0.1f); // draw grabbers for (int i = 6; i < 10; ++i) { m_grabbers[i].render(true, grabber_mean_size); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index fa61779ec9..71607b3510 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -169,7 +169,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) const_cast(&m_cone)->set_color(-1, render_color); const_cast(&m_sphere)->set_color(-1, render_color); if (shader && !picking) - shader->set_uniform("emission_factor", 0.5); + shader->set_uniform("emission_factor", 0.5f); // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. glsafe(::glPushMatrix()); @@ -224,7 +224,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) render_color[3] = 0.7f; const_cast(&m_cylinder)->set_color(-1, render_color); if (shader) - shader->set_uniform("emission_factor", 0.5); + shader->set_uniform("emission_factor", 0.5f); for (const sla::DrainHole& drain_hole : m_c->selection_info()->model_object()->sla_drain_holes) { if (is_mesh_point_clipped(drain_hole.pos.cast())) continue; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index dbc2594440..6a47113fac 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1872,7 +1872,7 @@ void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) con const_cast(&m_arrow)->set_color(-1, uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); GLShaderProgram* shader = wxGetApp().get_current_shader(); if (shader != nullptr) - shader->set_uniform("emission_factor", 0.0); + shader->set_uniform("emission_factor", 0.0f); glsafe(::glTranslated(0.0, 5.0, 0.0)); m_arrow.render(); From 599a4d97e38b0c7b50eeb4433b9f1ba007444d97 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 12:47:36 +0200 Subject: [PATCH 002/151] Removed constness from a few renderXXX() methods --- src/slic3r/GUI/GCodeViewer.cpp | 40 ++++++++++++++++------------------ src/slic3r/GUI/GCodeViewer.hpp | 10 ++++----- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/GLCanvas3D.hpp | 2 +- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b9a7d23387..092ddbfd89 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -757,12 +757,12 @@ void GCodeViewer::reset() #endif // ENABLE_GCODE_VIEWER_STATISTICS } -void GCodeViewer::render() const +void GCodeViewer::render() { auto init_gl_data = [this]() { // initializes opengl data of TBuffers for (size_t i = 0; i < m_buffers.size(); ++i) { - TBuffer& buffer = const_cast(m_buffers[i]); + TBuffer& buffer = m_buffers[i]; switch (buffer_type(i)) { default: { break; } @@ -789,17 +789,17 @@ void GCodeViewer::render() const } // initializes tool marker - const_cast(&m_sequential_view)->marker.init(); + m_sequential_view.marker.init(); // initializes point sizes std::array point_sizes; ::glGetIntegerv(GL_ALIASED_POINT_SIZE_RANGE, point_sizes.data()); - *const_cast*>(&m_detected_point_sizes) = { static_cast(point_sizes[0]), static_cast(point_sizes[1]) }; - *const_cast(&m_gl_data_initialized) = true; + m_detected_point_sizes = { static_cast(point_sizes[0]), static_cast(point_sizes[1]) }; + m_gl_data_initialized = true; }; #if ENABLE_GCODE_VIEWER_STATISTICS - const_cast(&m_statistics)->reset_opengl(); + m_statistics.reset_opengl(); #endif // ENABLE_GCODE_VIEWER_STATISTICS // OpenGL data must be initialized after the glContext has been created. @@ -815,10 +815,9 @@ void GCodeViewer::render() const render_shells(); float legend_height = 0.0f; render_legend(legend_height); - SequentialView* sequential_view = const_cast(&m_sequential_view); - if (sequential_view->current.last != sequential_view->endpoints.last) { - sequential_view->marker.set_world_position(sequential_view->current_position); - sequential_view->render(legend_height); + if (m_sequential_view.current.last != m_sequential_view.endpoints.last) { + m_sequential_view.marker.set_world_position(m_sequential_view.current_position); + m_sequential_view.render(legend_height); } #if ENABLE_GCODE_VIEWER_STATISTICS render_statistics(); @@ -2360,7 +2359,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #endif // ENABLE_GCODE_VIEWER_STATISTICS } -void GCodeViewer::render_toolpaths() const +void GCodeViewer::render_toolpaths() { #if ENABLE_FIXED_SCREEN_SIZE_POINT_MARKERS float point_size = 20.0f; @@ -2404,7 +2403,7 @@ void GCodeViewer::render_toolpaths() const set_uniform_color(path.color, shader); glsafe(::glMultiDrawElements(GL_POINTS, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_SHORT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); #if ENABLE_GCODE_VIEWER_STATISTICS - ++const_cast(&m_statistics)->gl_multi_points_calls_count; + ++m_statistics.gl_multi_points_calls_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS } } @@ -2425,7 +2424,7 @@ void GCodeViewer::render_toolpaths() const set_uniform_color(path.color, shader); glsafe(::glMultiDrawElements(GL_LINES, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_SHORT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); #if ENABLE_GCODE_VIEWER_STATISTICS - ++const_cast(&m_statistics)->gl_multi_lines_calls_count; + ++m_statistics.gl_multi_lines_calls_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS } } @@ -2442,7 +2441,7 @@ void GCodeViewer::render_toolpaths() const set_uniform_color(path.color, shader); glsafe(::glMultiDrawElements(GL_TRIANGLES, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_SHORT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); #if ENABLE_GCODE_VIEWER_STATISTICS - ++const_cast(&m_statistics)->gl_multi_triangles_calls_count; + ++m_statistics.gl_multi_triangles_calls_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS } } @@ -2535,7 +2534,7 @@ void GCodeViewer::render_toolpaths() const glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); #if ENABLE_GCODE_VIEWER_STATISTICS - ++const_cast(&m_statistics)->gl_triangles_calls_count; + ++m_statistics.gl_triangles_calls_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS if (has_normals) @@ -2554,7 +2553,7 @@ void GCodeViewer::render_toolpaths() const } } -void GCodeViewer::render_shells() const +void GCodeViewer::render_shells() { if (!m_shells.visible || m_shells.volumes.empty()) return; @@ -2572,7 +2571,7 @@ void GCodeViewer::render_shells() const // glsafe(::glDepthMask(GL_TRUE)); } -void GCodeViewer::render_legend(float& legend_height) const +void GCodeViewer::render_legend(float& legend_height) { if (!m_legend_enabled) return; @@ -2935,8 +2934,7 @@ void GCodeViewer::render_legend(float& legend_height) const const bool visible = is_visible(role); append_item(EItemType::Rect, Extrusion_Role_Colors[static_cast(role)], labels[i], visible, times[i], percents[i], max_percent, offsets, used_filaments_m[i], used_filaments_g[i], [this, role, visible]() { - Extrusions* extrusions = const_cast(&m_extrusions); - extrusions->role_visibility_flags = visible ? extrusions->role_visibility_flags & ~(1 << role) : extrusions->role_visibility_flags | (1 << role); + m_extrusions.role_visibility_flags = visible ? m_extrusions.role_visibility_flags & ~(1 << role) : m_extrusions.role_visibility_flags | (1 << role); // update buffers' render paths refresh_render_paths(false, false); wxGetApp().plater()->update_preview_moves_slider(); @@ -3408,7 +3406,7 @@ void GCodeViewer::render_legend(float& legend_height) const auto show_mode_button = [this, &imgui, can_show_mode_button](const wxString& label, PrintEstimatedStatistics::ETimeMode mode) { if (can_show_mode_button(mode)) { if (imgui.button(label)) { - *const_cast(&m_time_estimate_mode) = mode; + m_time_estimate_mode = mode; wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); } @@ -3435,7 +3433,7 @@ void GCodeViewer::render_legend(float& legend_height) const } #if ENABLE_GCODE_VIEWER_STATISTICS -void GCodeViewer::render_statistics() const +void GCodeViewer::render_statistics() { static const float offset = 275.0f; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 41307bad97..ecdffc5fe5 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -632,7 +632,7 @@ public: void update_shells_color_by_extruder(const DynamicPrintConfig* config); void reset(); - void render() const; + void render(); bool has_data() const { return !m_roles.empty(); } bool can_export_toolpaths() const; @@ -678,11 +678,11 @@ private: void load_toolpaths(const GCodeProcessor::Result& gcode_result); void load_shells(const Print& print, bool initialized); void refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const; - void render_toolpaths() const; - void render_shells() const; - void render_legend(float& legend_height) const; + void render_toolpaths(); + void render_shells(); + void render_legend(float& legend_height); #if ENABLE_GCODE_VIEWER_STATISTICS - void render_statistics() const; + void render_statistics(); #endif // ENABLE_GCODE_VIEWER_STATISTICS bool is_visible(ExtrusionRole role) const { return role < erCount && (m_extrusions.role_visibility_flags & (1 << role)) != 0; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index e2699fb6d7..1e0cb0a321 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -5175,7 +5175,7 @@ void GLCanvas3D::_render_objects() m_camera_clipping_plane = ClippingPlane::ClipsNothing(); } -void GLCanvas3D::_render_gcode() const +void GLCanvas3D::_render_gcode() { m_gcode_viewer.render(); } diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index d9cd55e35b..7cdaac1a62 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -906,7 +906,7 @@ private: #else void _render_objects(); #endif // ENABLE_DELAYED_TRANSPARENT_VOLUMES_RENDERING - void _render_gcode() const; + void _render_gcode(); void _render_selection() const; void _render_sequential_clearance(); #if ENABLE_RENDER_SELECTION_CENTER From 329f9a77c2084a815708492fd0a61ca963f63fce Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 13:27:42 +0200 Subject: [PATCH 003/151] Colors defined as std::array in GCodeViewer --- src/slic3r/GUI/GCodeViewer.cpp | 165 +++++++++++++++++---------------- src/slic3r/GUI/GCodeViewer.hpp | 6 +- 2 files changed, 90 insertions(+), 81 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 092ddbfd89..055fb6fab7 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -44,10 +44,10 @@ static EMoveType buffer_type(unsigned char id) { return static_cast(static_cast(EMoveType::Retract) + id); } -static std::array decode_color(const std::string& color) { +static std::array decode_color(const std::string& color) { static const float INV_255 = 1.0f / 255.0f; - std::array ret = { 0.0f, 0.0f, 0.0f }; + std::array ret = { 0.0f, 0.0f, 0.0f, 1.0f }; const char* c = color.data() + 1; if (color.size() == 7 && color.front() == '#') { for (size_t j = 0; j < 3; ++j) { @@ -62,8 +62,8 @@ static std::array decode_color(const std::string& color) { return ret; } -static std::vector> decode_colors(const std::vector& colors) { - std::vector> output(colors.size(), { 0.0f, 0.0f, 0.0f }); +static std::vector> decode_colors(const std::vector& colors) { + std::vector> output(colors.size(), { 0.0f, 0.0f, 0.0f, 1.0f }); for (size_t i = 0; i < colors.size(); ++i) { output[i] = decode_color(colors[i]); } @@ -176,7 +176,7 @@ GCodeViewer::Color GCodeViewer::Extrusions::Range::get_color_at(float value) con const float local_t = std::clamp(global_t - static_cast(color_low_idx), 0.0f, 1.0f); // Interpolate between the low and high colors to find exactly which color the input value should get - Color ret; + Color ret = { 0.0f, 0.0f, 0.0f, 1.0f }; for (unsigned int i = 0; i < 3; ++i) { ret[i] = lerp(Range_Colors[color_low_idx][i], Range_Colors[color_high_idx][i], local_t); } @@ -195,7 +195,7 @@ void GCodeViewer::SequentialRangeCap::reset() { buffer = nullptr; ibo = 0; vbo = 0; - color = { 0.0f, 0.0f, 0.0f }; + color = { 0.0f, 0.0f, 0.0f, 1.0f }; } void GCodeViewer::SequentialView::Marker::init() @@ -484,56 +484,57 @@ void GCodeViewer::SequentialView::render(float legend_height) const } const std::vector GCodeViewer::Extrusion_Role_Colors {{ - { 0.75f, 0.75f, 0.75f }, // erNone - { 1.00f, 0.90f, 0.30f }, // erPerimeter - { 1.00f, 0.49f, 0.22f }, // erExternalPerimeter - { 0.12f, 0.12f, 1.00f }, // erOverhangPerimeter - { 0.69f, 0.19f, 0.16f }, // erInternalInfill - { 0.59f, 0.33f, 0.80f }, // erSolidInfill - { 0.94f, 0.25f, 0.25f }, // erTopSolidInfill - { 1.00f, 0.55f, 0.41f }, // erIroning - { 0.30f, 0.50f, 0.73f }, // erBridgeInfill - { 1.00f, 1.00f, 1.00f }, // erGapFill - { 0.00f, 0.53f, 0.43f }, // erSkirt - { 0.00f, 1.00f, 0.00f }, // erSupportMaterial - { 0.00f, 0.50f, 0.00f }, // erSupportMaterialInterface - { 0.70f, 0.89f, 0.67f }, // erWipeTower - { 0.37f, 0.82f, 0.58f }, // erCustom - { 0.00f, 0.00f, 0.00f } // erMixed + { 0.75f, 0.75f, 0.75f, 1.0f }, // erNone + { 1.00f, 0.90f, 0.30f, 1.0f }, // erPerimeter + { 1.00f, 0.49f, 0.22f, 1.0f }, // erExternalPerimeter + { 0.12f, 0.12f, 1.00f, 1.0f }, // erOverhangPerimeter + { 0.69f, 0.19f, 0.16f, 1.0f }, // erInternalInfill + { 0.59f, 0.33f, 0.80f, 1.0f }, // erSolidInfill + { 0.94f, 0.25f, 0.25f, 1.0f }, // erTopSolidInfill + { 1.00f, 0.55f, 0.41f, 1.0f }, // erIroning + { 0.30f, 0.50f, 0.73f, 1.0f }, // erBridgeInfill + { 1.00f, 1.00f, 1.00f, 1.0f }, // erGapFill + { 0.00f, 0.53f, 0.43f, 1.0f }, // erSkirt + { 0.00f, 1.00f, 0.00f, 1.0f }, // erSupportMaterial + { 0.00f, 0.50f, 0.00f, 1.0f }, // erSupportMaterialInterface + { 0.70f, 0.89f, 0.67f, 1.0f }, // erWipeTower + { 0.37f, 0.82f, 0.58f, 1.0f }, // erCustom + { 0.00f, 0.00f, 0.00f, 1.0f } // erMixed }}; const std::vector GCodeViewer::Options_Colors {{ - { 0.803f, 0.135f, 0.839f }, // Retractions - { 0.287f, 0.679f, 0.810f }, // Unretractions - { 0.900f, 0.900f, 0.900f }, // Seams - { 0.758f, 0.744f, 0.389f }, // ToolChanges - { 0.856f, 0.582f, 0.546f }, // ColorChanges - { 0.322f, 0.942f, 0.512f }, // PausePrints - { 0.886f, 0.825f, 0.262f } // CustomGCodes + { 0.803f, 0.135f, 0.839f, 1.0f }, // Retractions + { 0.287f, 0.679f, 0.810f, 1.0f }, // Unretractions + { 0.900f, 0.900f, 0.900f, 1.0f }, // Seams + { 0.758f, 0.744f, 0.389f, 1.0f }, // ToolChanges + { 0.856f, 0.582f, 0.546f, 1.0f }, // ColorChanges + { 0.322f, 0.942f, 0.512f, 1.0f }, // PausePrints + { 0.886f, 0.825f, 0.262f, 1.0f } // CustomGCodes }}; const std::vector GCodeViewer::Travel_Colors {{ - { 0.219f, 0.282f, 0.609f }, // Move - { 0.112f, 0.422f, 0.103f }, // Extrude - { 0.505f, 0.064f, 0.028f } // Retract + { 0.219f, 0.282f, 0.609f, 1.0f }, // Move + { 0.112f, 0.422f, 0.103f, 1.0f }, // Extrude + { 0.505f, 0.064f, 0.028f, 1.0f } // Retract }}; -const GCodeViewer::Color GCodeViewer::Wipe_Color = { 1.0f, 1.0f, 0.0f }; - const std::vector GCodeViewer::Range_Colors {{ - { 0.043f, 0.173f, 0.478f }, // bluish - { 0.075f, 0.349f, 0.522f }, - { 0.110f, 0.533f, 0.569f }, - { 0.016f, 0.839f, 0.059f }, - { 0.667f, 0.949f, 0.000f }, - { 0.988f, 0.975f, 0.012f }, - { 0.961f, 0.808f, 0.039f }, - { 0.890f, 0.533f, 0.125f }, - { 0.820f, 0.408f, 0.188f }, - { 0.761f, 0.322f, 0.235f }, - { 0.581f, 0.149f, 0.087f } // reddish + { 0.043f, 0.173f, 0.478f, 1.0f }, // bluish + { 0.075f, 0.349f, 0.522f, 1.0f }, + { 0.110f, 0.533f, 0.569f, 1.0f }, + { 0.016f, 0.839f, 0.059f, 1.0f }, + { 0.667f, 0.949f, 0.000f, 1.0f }, + { 0.988f, 0.975f, 0.012f, 1.0f }, + { 0.961f, 0.808f, 0.039f, 1.0f }, + { 0.890f, 0.533f, 0.125f, 1.0f }, + { 0.820f, 0.408f, 0.188f, 1.0f }, + { 0.761f, 0.322f, 0.235f, 1.0f }, + { 0.581f, 0.149f, 0.087f, 1.0f } // reddish }}; +const GCodeViewer::Color GCodeViewer::Wipe_Color = { 1.0f, 1.0f, 0.0f, 1.0f }; +const GCodeViewer::Color GCodeViewer::Neutral_Color = { 0.25f, 0.25f, 0.25f, 1.0f }; + GCodeViewer::GCodeViewer() { // initializes non OpenGL data of TBuffers @@ -1967,18 +1968,14 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool case EViewType::VolumetricRate: { color = m_extrusions.ranges.volumetric_rate.get_color_at(path.volumetric_rate); break; } case EViewType::Tool: { color = m_tool_colors[path.extruder_id]; break; } case EViewType::ColorPrint: { - if (path.cp_color_id >= static_cast(m_tool_colors.size())) { - color = { 0.5f, 0.5f, 0.5f }; -// // complementary color -// color = m_tool_colors[255 - path.cp_color_id]; -// color = { 1.0f - color[0], 1.0f - color[1], 1.0f - color[2] }; - } + if (path.cp_color_id >= static_cast(m_tool_colors.size())) + color = { 0.5f, 0.5f, 0.5f, 1.0f }; else color = m_tool_colors[path.cp_color_id]; break; } - default: { color = { 1.0f, 1.0f, 1.0f }; break; } + default: { color = { 1.0f, 1.0f, 1.0f, 1.0f }; break; } } return color; @@ -2142,20 +2139,20 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool Color color; switch (path.type) { - case EMoveType::Tool_change: { color = Options_Colors[static_cast(EOptionsColors::ToolChanges)]; break; } - case EMoveType::Color_change: { color = Options_Colors[static_cast(EOptionsColors::ColorChanges)]; break; } - case EMoveType::Pause_Print: { color = Options_Colors[static_cast(EOptionsColors::PausePrints)]; break; } - case EMoveType::Custom_GCode: { color = Options_Colors[static_cast(EOptionsColors::CustomGCodes)]; break; } - case EMoveType::Retract: { color = Options_Colors[static_cast(EOptionsColors::Retractions)]; break; } - case EMoveType::Unretract: { color = Options_Colors[static_cast(EOptionsColors::Unretractions)]; break; } - case EMoveType::Seam: { color = Options_Colors[static_cast(EOptionsColors::Seams)]; break; } + case EMoveType::Tool_change: + case EMoveType::Color_change: + case EMoveType::Pause_Print: + case EMoveType::Custom_GCode: + case EMoveType::Retract: + case EMoveType::Unretract: + case EMoveType::Seam: { color = option_color(path.type); break; } case EMoveType::Extrude: { if (!top_layer_only || m_sequential_view.current.last == global_endpoints.last || is_in_layers_range(path, m_layers_z_range[1], m_layers_z_range[1])) color = extrusion_color(path); else - color = { 0.25f, 0.25f, 0.25f }; + color = Neutral_Color; break; } @@ -2163,12 +2160,12 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (!top_layer_only || m_sequential_view.current.last == global_endpoints.last || is_travel_in_layers_range(path_id, m_layers_z_range[1], m_layers_z_range[1])) color = (m_view_type == EViewType::Feedrate || m_view_type == EViewType::Tool || m_view_type == EViewType::ColorPrint) ? extrusion_color(path) : travel_color(path); else - color = { 0.25f, 0.25f, 0.25f }; + color = Neutral_Color; break; } case EMoveType::Wipe: { color = Wipe_Color; break; } - default: { color = { 0.0f, 0.0f, 0.0f }; break; } + default: { color = { 0.0f, 0.0f, 0.0f, 1.0f }; break; } } RenderPath key{ tbuffer_id, color, static_cast(ibuffer_id), path_id }; @@ -2373,15 +2370,10 @@ void GCodeViewer::render_toolpaths() float near_plane_height = camera.get_type() == Camera::EType::Perspective ? static_cast(viewport[3]) / (2.0f * static_cast(2.0 * std::tan(0.5 * Geometry::deg2rad(camera.get_fov())))) : static_cast(viewport[3]) * 0.0005; - auto set_uniform_color = [](const std::array& color, GLShaderProgram& shader) { - std::array color4 = { color[0], color[1], color[2], 1.0f }; - shader.set_uniform("uniform_color", color4); - }; - #if ENABLE_GCODE_VIEWER_STATISTICS - auto render_as_points = [this, zoom, point_size, near_plane_height, set_uniform_color] + auto render_as_points = [this, zoom, point_size, near_plane_height] #else - auto render_as_points = [zoom, point_size, near_plane_height, set_uniform_color] + auto render_as_points = [zoom, point_size, near_plane_height] #endif // ENABLE_GCODE_VIEWER_STATISTICS (const TBuffer& buffer, unsigned int ibuffer_id, GLShaderProgram& shader) { #if ENABLE_FIXED_SCREEN_SIZE_POINT_MARKERS @@ -2400,7 +2392,7 @@ void GCodeViewer::render_toolpaths() for (const RenderPath& path : buffer.render_paths) { if (path.ibuffer_id == ibuffer_id) { - set_uniform_color(path.color, shader); + shader.set_uniform("uniform_color", path.color); glsafe(::glMultiDrawElements(GL_POINTS, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_SHORT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.gl_multi_points_calls_count; @@ -2413,15 +2405,15 @@ void GCodeViewer::render_toolpaths() }; #if ENABLE_GCODE_VIEWER_STATISTICS - auto render_as_lines = [this, light_intensity, set_uniform_color] + auto render_as_lines = [this, light_intensity] #else - auto render_as_lines = [light_intensity, set_uniform_color] + auto render_as_lines = [light_intensity] #endif // ENABLE_GCODE_VIEWER_STATISTICS (const TBuffer& buffer, unsigned int ibuffer_id, GLShaderProgram& shader) { shader.set_uniform("light_intensity", light_intensity); for (const RenderPath& path : buffer.render_paths) { if (path.ibuffer_id == ibuffer_id) { - set_uniform_color(path.color, shader); + shader.set_uniform("uniform_color", path.color); glsafe(::glMultiDrawElements(GL_LINES, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_SHORT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.gl_multi_lines_calls_count; @@ -2431,14 +2423,14 @@ void GCodeViewer::render_toolpaths() }; #if ENABLE_GCODE_VIEWER_STATISTICS - auto render_as_triangles = [this, set_uniform_color] + auto render_as_triangles = [this] #else - auto render_as_triangles = [set_uniform_color] + auto render_as_triangles = [] #endif // ENABLE_GCODE_VIEWER_STATISTICS (const TBuffer& buffer, unsigned int ibuffer_id, GLShaderProgram& shader) { for (const RenderPath& path : buffer.render_paths) { if (path.ibuffer_id == ibuffer_id) { - set_uniform_color(path.color, shader); + shader.set_uniform("uniform_color", path.color); glsafe(::glMultiDrawElements(GL_TRIANGLES, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_SHORT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.gl_multi_triangles_calls_count; @@ -2509,9 +2501,9 @@ void GCodeViewer::render_toolpaths() } #if ENABLE_GCODE_VIEWER_STATISTICS - auto render_sequential_range_cap = [this, set_uniform_color] + auto render_sequential_range_cap = [this] #else - auto render_sequential_range_cap = [set_uniform_color] + auto render_sequential_range_cap = [] #endif // ENABLE_GCODE_VIEWER_STATISTICS (const SequentialRangeCap& cap) { GLShaderProgram* shader = wxGetApp().get_shader(cap.buffer->shader.c_str()); @@ -2527,7 +2519,7 @@ void GCodeViewer::render_toolpaths() glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); } - set_uniform_color(cap.color, *shader); + shader->set_uniform("uniform_color", cap.color); glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cap.ibo)); glsafe(::glDrawElements(GL_TRIANGLES, (GLsizei)cap.indices_count(), GL_UNSIGNED_SHORT, nullptr)); @@ -3548,6 +3540,21 @@ void GCodeViewer::log_memory_used(const std::string& label, int64_t additional) } } +GCodeViewer::Color GCodeViewer::option_color(EMoveType move_type) const +{ + switch (move_type) + { + case EMoveType::Tool_change: { return Options_Colors[static_cast(EOptionsColors::ToolChanges)]; } + case EMoveType::Color_change: { return Options_Colors[static_cast(EOptionsColors::ColorChanges)]; } + case EMoveType::Pause_Print: { return Options_Colors[static_cast(EOptionsColors::PausePrints)]; } + case EMoveType::Custom_GCode: { return Options_Colors[static_cast(EOptionsColors::CustomGCodes)]; } + case EMoveType::Retract: { return Options_Colors[static_cast(EOptionsColors::Retractions)]; } + case EMoveType::Unretract: { return Options_Colors[static_cast(EOptionsColors::Unretractions)]; } + case EMoveType::Seam: { return Options_Colors[static_cast(EOptionsColors::Seams)]; } + default: { return { 0.0f, 0.0f, 0.0f, 1.0f }; } + } +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index ecdffc5fe5..3d9b9bb698 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -22,7 +22,7 @@ namespace GUI { class GCodeViewer { using IBufferType = unsigned short; - using Color = std::array; + using Color = std::array; using VertexBuffer = std::vector; using MultiVertexBuffer = std::vector; using IndexBuffer = std::vector; @@ -31,8 +31,9 @@ class GCodeViewer static const std::vector Extrusion_Role_Colors; static const std::vector Options_Colors; static const std::vector Travel_Colors; - static const Color Wipe_Color; static const std::vector Range_Colors; + static const Color Wipe_Color; + static const Color Neutral_Color; enum class EOptionsColors : unsigned char { @@ -689,6 +690,7 @@ private: } bool is_visible(const Path& path) const { return is_visible(path.role); } void log_memory_used(const std::string& label, int64_t additional = 0) const; + Color option_color(EMoveType move_type) const; }; } // namespace GUI From 93db27f40c7f655e65ebff29fd67eddfb561940b Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 13:49:51 +0200 Subject: [PATCH 004/151] A bit of refactoring into GCodeViewer --- src/slic3r/GUI/GCodeViewer.cpp | 200 ++++++++++++++++----------------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 055fb6fab7..c5109f07e4 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -125,7 +125,7 @@ bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const case EMoveType::Extrude: { // use rounding to reduce the number of generated paths return type == move.type && extruder_id == move.extruder_id && cp_color_id == move.cp_color_id && role == move.extrusion_role && - move.position[2] <= sub_paths.front().first.position[2] && feedrate == move.feedrate && fan_speed == move.fan_speed && + move.position.z() <= sub_paths.front().first.position.z() && feedrate == move.feedrate && fan_speed == move.fan_speed && height == round_to_nearest(move.height, 2) && width == round_to_nearest(move.width, 2) && matches_percent(volumetric_rate, move.volumetric_rate(), 0.05f); } @@ -207,7 +207,7 @@ void GCodeViewer::SequentialView::Marker::init() void GCodeViewer::SequentialView::Marker::set_world_position(const Vec3f& position) { m_world_position = position; - m_world_transform = (Geometry::assemble_transform((position + m_z_offset * Vec3f::UnitZ()).cast()) * Geometry::assemble_transform(m_model.get_bounding_box().size()[2] * Vec3d::UnitZ(), { M_PI, 0.0, 0.0 })).cast(); + m_world_transform = (Geometry::assemble_transform((position + m_z_offset * Vec3f::UnitZ()).cast()) * Geometry::assemble_transform(m_model.get_bounding_box().size().z() * Vec3d::UnitZ(), { M_PI, 0.0, 0.0 })).cast(); } void GCodeViewer::SequentialView::Marker::render() const @@ -623,23 +623,23 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& else { // adjust printbed size in dependence of toolpaths bbox const double margin = 10.0; - Vec2d min(m_paths_bounding_box.min(0) - margin, m_paths_bounding_box.min(1) - margin); - Vec2d max(m_paths_bounding_box.max(0) + margin, m_paths_bounding_box.max(1) + margin); + Vec2d min(m_paths_bounding_box.min.x() - margin, m_paths_bounding_box.min.y() - margin); + Vec2d max(m_paths_bounding_box.max.x() + margin, m_paths_bounding_box.max.y() + margin); Vec2d size = max - min; bed_shape = { - { min(0), min(1) }, - { max(0), min(1) }, - { max(0), min(1) + 0.442265 * size[1]}, - { max(0) - 10.0, min(1) + 0.4711325 * size[1]}, - { max(0) + 10.0, min(1) + 0.5288675 * size[1]}, - { max(0), min(1) + 0.557735 * size[1]}, - { max(0), max(1) }, - { min(0) + 0.557735 * size[0], max(1)}, - { min(0) + 0.5288675 * size[0], max(1) - 10.0}, - { min(0) + 0.4711325 * size[0], max(1) + 10.0}, - { min(0) + 0.442265 * size[0], max(1)}, - { min(0), max(1) } }; + { min.x(), min.y() }, + { max.x(), min.y() }, + { max.x(), min.y() + 0.442265 * size.y()}, + { max.x() - 10.0, min.y() + 0.4711325 * size.y()}, + { max.x() + 10.0, min.y() + 0.5288675 * size.y()}, + { max.x(), min.y() + 0.557735 * size.y()}, + { max.x(), max.y() }, + { min.x() + 0.557735 * size.x(), max.y()}, + { min.x() + 0.5288675 * size.x(), max.y() - 10.0}, + { min.x() + 0.4711325 * size.x(), max.y() + 10.0}, + { min.x() + 0.442265 * size.x(), max.y()}, + { min.x(), max.y() } }; } wxGetApp().plater()->set_bed_shape(bed_shape, texture, model, gcode_result.bed_shape.empty()); @@ -1036,13 +1036,13 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const // save vertices to file fprintf(fp, "\n# vertices\n"); for (const Vec3f& v : out_vertices) { - fprintf(fp, "v %g %g %g\n", v[0], v[1], v[2]); + fprintf(fp, "v %g %g %g\n", v.x(), v.y(), v.x()); } // save normals to file fprintf(fp, "\n# normals\n"); for (const Vec3f& n : out_normals) { - fprintf(fp, "vn %g %g %g\n", n[0], n[1], n[2]); + fprintf(fp, "vn %g %g %g\n", n.x(), n.y(), n.z()); } size_t i = 0; @@ -1124,9 +1124,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // format data into the buffers to be rendered as points auto add_vertices_as_point = [](const GCodeProcessor::MoveVertex& curr, VertexBuffer& vertices) { - vertices.push_back(curr.position[0]); - vertices.push_back(curr.position[1]); - vertices.push_back(curr.position[2]); + vertices.push_back(curr.position.x()); + vertices.push_back(curr.position.y()); + vertices.push_back(curr.position.z()); }; auto add_indices_as_point = [](const GCodeProcessor::MoveVertex& curr, TBuffer& buffer, unsigned int ibuffer_id, IndexBuffer& indices, size_t move_id) { @@ -1137,13 +1137,13 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // format data into the buffers to be rendered as lines auto add_vertices_as_line = [](const GCodeProcessor::MoveVertex& prev, const GCodeProcessor::MoveVertex& curr, VertexBuffer& vertices) { // x component of the normal to the current segment (the normal is parallel to the XY plane) - float normal_x = (curr.position - prev.position).normalized()[1]; + const float normal_x = (curr.position - prev.position).normalized().y(); auto add_vertex = [&vertices, normal_x](const GCodeProcessor::MoveVertex& vertex) { // add position - vertices.push_back(vertex.position[0]); - vertices.push_back(vertex.position[1]); - vertices.push_back(vertex.position[2]); + vertices.push_back(vertex.position.x()); + vertices.push_back(vertex.position.y()); + vertices.push_back(vertex.position.z()); // add normal x component vertices.push_back(normal_x); }; @@ -1177,13 +1177,13 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) auto add_vertices_as_solid = [](const GCodeProcessor::MoveVertex& prev, const GCodeProcessor::MoveVertex& curr, TBuffer& buffer, unsigned int vbuffer_id, VertexBuffer& vertices, size_t move_id) { auto store_vertex = [](VertexBuffer& vertices, const Vec3f& position, const Vec3f& normal) { // append position - vertices.push_back(position[0]); - vertices.push_back(position[1]); - vertices.push_back(position[2]); + vertices.push_back(position.x()); + vertices.push_back(position.y()); + vertices.push_back(position.z()); // append normal - vertices.push_back(normal[0]); - vertices.push_back(normal[1]); - vertices.push_back(normal[2]); + vertices.push_back(normal.x()); + vertices.push_back(normal.y()); + vertices.push_back(normal.z()); }; if (prev.type != curr.type || !buffer.paths.back().matches(curr)) { @@ -1193,19 +1193,19 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) Path& last_path = buffer.paths.back(); - Vec3f dir = (curr.position - prev.position).normalized(); - Vec3f right = Vec3f(dir[1], -dir[0], 0.0f).normalized(); - Vec3f left = -right; - Vec3f up = right.cross(dir); - Vec3f down = -up; - float half_width = 0.5f * last_path.width; - float half_height = 0.5f * last_path.height; - Vec3f prev_pos = prev.position - half_height * up; - Vec3f curr_pos = curr.position - half_height * up; - Vec3f d_up = half_height * up; - Vec3f d_down = -half_height * up; - Vec3f d_right = half_width * right; - Vec3f d_left = -half_width * right; + const Vec3f dir = (curr.position - prev.position).normalized(); + const Vec3f right = Vec3f(dir.y(), -dir.x(), 0.0f).normalized(); + const Vec3f left = -right; + const Vec3f up = right.cross(dir); + const Vec3f down = -up; + const float half_width = 0.5f * last_path.width; + const float half_height = 0.5f * last_path.height; + const Vec3f prev_pos = prev.position - half_height * up; + const Vec3f curr_pos = curr.position - half_height * up; + const Vec3f d_up = half_height * up; + const Vec3f d_down = -half_height * up; + const Vec3f d_right = half_width * right; + const Vec3f d_left = -half_width * right; // vertices 1st endpoint if (last_path.vertices_count() == 1 || vertices.empty()) { @@ -1284,14 +1284,14 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) Path& last_path = buffer.paths.back(); - Vec3f dir = (curr.position - prev.position).normalized(); - Vec3f right = Vec3f(dir[1], -dir[0], 0.0f).normalized(); - Vec3f up = right.cross(dir); - float sq_length = (curr.position - prev.position).squaredNorm(); + const Vec3f dir = (curr.position - prev.position).normalized(); + const Vec3f right = Vec3f(dir.y(), -dir.x(), 0.0f).normalized(); + const Vec3f up = right.cross(dir); + const float sq_length = (curr.position - prev.position).squaredNorm(); const std::array first_seg_v_offsets = convert_vertices_offset(vbuffer_size, { 0, 1, 2, 3, 4, 5, 6, 7 }); const std::array non_first_seg_v_offsets = convert_vertices_offset(vbuffer_size, { -4, 0, -2, 1, 2, 3, 4, 5 }); - bool is_first_segment = (last_path.vertices_count() == 1); + const bool is_first_segment = (last_path.vertices_count() == 1); if (is_first_segment || vbuffer_size == 0) { // 1st segment or restart into a new vertex buffer // =============================================== @@ -1310,20 +1310,20 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // any other segment // ================= float displacement = 0.0f; - float cos_dir = prev_dir.dot(dir); + const float cos_dir = prev_dir.dot(dir); if (cos_dir > -0.9998477f) { // if the angle between adjacent segments is smaller than 179 degrees - Vec3f med_dir = (prev_dir + dir).normalized(); - float half_width = 0.5f * last_path.width; + const Vec3f med_dir = (prev_dir + dir).normalized(); + const float half_width = 0.5f * last_path.width; displacement = half_width * ::tan(::acos(std::clamp(dir.dot(med_dir), -1.0f, 1.0f))); } - float sq_displacement = sqr(displacement); - bool can_displace = displacement > 0.0f && sq_displacement < sq_prev_length && sq_displacement < sq_length; + const float sq_displacement = sqr(displacement); + const bool can_displace = displacement > 0.0f && sq_displacement < sq_prev_length && sq_displacement < sq_length; - bool is_right_turn = prev_up.dot(prev_dir.cross(dir)) <= 0.0f; + const bool is_right_turn = prev_up.dot(prev_dir.cross(dir)) <= 0.0f; // whether the angle between adjacent segments is greater than 45 degrees - bool is_sharp = cos_dir < 0.7071068f; + const bool is_sharp = cos_dir < 0.7071068f; bool right_displaced = false; bool left_displaced = false; @@ -1434,7 +1434,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) progress_count = 0; } - unsigned char id = buffer_id(curr.type); + const unsigned char id = buffer_id(curr.type); TBuffer& t_buffer = m_buffers[id]; MultiVertexBuffer& v_multibuffer = vertices[id]; @@ -1476,24 +1476,24 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) return Vec3f(vertices[offset + 0], vertices[offset + 1], vertices[offset + 2]); }; auto update_position_at = [](VertexBuffer& vertices, size_t offset, const Vec3f& position) { - vertices[offset + 0] = position[0]; - vertices[offset + 1] = position[1]; - vertices[offset + 2] = position[2]; + vertices[offset + 0] = position.x(); + vertices[offset + 1] = position.y(); + vertices[offset + 2] = position.z(); }; auto match_right_vertices = [&](const Path::Sub_Path& prev_sub_path, const Path::Sub_Path& next_sub_path, size_t curr_s_id, size_t vertex_size_floats, const Vec3f& displacement_vec) { if (&prev_sub_path == &next_sub_path) { // previous and next segment are both contained into to the same vertex buffer VertexBuffer& vbuffer = v_multibuffer[prev_sub_path.first.b_id]; // offset into the vertex buffer of the next segment 1st vertex - size_t next_1st_offset = (prev_sub_path.last.s_id - curr_s_id) * 6 * vertex_size_floats; + const size_t next_1st_offset = (prev_sub_path.last.s_id - curr_s_id) * 6 * vertex_size_floats; // offset into the vertex buffer of the right vertex of the previous segment - size_t prev_right_offset = prev_sub_path.last.i_id - next_1st_offset - 3 * vertex_size_floats; + const size_t prev_right_offset = prev_sub_path.last.i_id - next_1st_offset - 3 * vertex_size_floats; // new position of the right vertices - Vec3f shared_vertex = extract_position_at(vbuffer, prev_right_offset) + displacement_vec; + const Vec3f shared_vertex = extract_position_at(vbuffer, prev_right_offset) + displacement_vec; // update previous segment update_position_at(vbuffer, prev_right_offset, shared_vertex); // offset into the vertex buffer of the right vertex of the next segment - size_t next_right_offset = next_sub_path.last.i_id - next_1st_offset; + const size_t next_right_offset = next_sub_path.last.i_id - next_1st_offset; // update next segment update_position_at(vbuffer, next_right_offset, shared_vertex); } @@ -1501,13 +1501,13 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) VertexBuffer& prev_vbuffer = v_multibuffer[prev_sub_path.first.b_id]; VertexBuffer& next_vbuffer = v_multibuffer[next_sub_path.first.b_id]; // offset into the previous vertex buffer of the right vertex of the previous segment - size_t prev_right_offset = prev_sub_path.last.i_id - 3 * vertex_size_floats; + const size_t prev_right_offset = prev_sub_path.last.i_id - 3 * vertex_size_floats; // new position of the right vertices - Vec3f shared_vertex = extract_position_at(prev_vbuffer, prev_right_offset) + displacement_vec; + const Vec3f shared_vertex = extract_position_at(prev_vbuffer, prev_right_offset) + displacement_vec; // update previous segment update_position_at(prev_vbuffer, prev_right_offset, shared_vertex); // offset into the next vertex buffer of the right vertex of the next segment - size_t next_right_offset = next_sub_path.first.i_id + 1 * vertex_size_floats; + const size_t next_right_offset = next_sub_path.first.i_id + 1 * vertex_size_floats; // update next segment update_position_at(next_vbuffer, next_right_offset, shared_vertex); } @@ -1517,15 +1517,15 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) if (&prev_sub_path == &next_sub_path) { // previous and next segment are both contained into to the same vertex buffer VertexBuffer& vbuffer = v_multibuffer[prev_sub_path.first.b_id]; // offset into the vertex buffer of the next segment 1st vertex - size_t next_1st_offset = (prev_sub_path.last.s_id - curr_s_id) * 6 * vertex_size_floats; + const size_t next_1st_offset = (prev_sub_path.last.s_id - curr_s_id) * 6 * vertex_size_floats; // offset into the vertex buffer of the left vertex of the previous segment - size_t prev_left_offset = prev_sub_path.last.i_id - next_1st_offset - 1 * vertex_size_floats; + const size_t prev_left_offset = prev_sub_path.last.i_id - next_1st_offset - 1 * vertex_size_floats; // new position of the left vertices - Vec3f shared_vertex = extract_position_at(vbuffer, prev_left_offset) + displacement_vec; + const Vec3f shared_vertex = extract_position_at(vbuffer, prev_left_offset) + displacement_vec; // update previous segment update_position_at(vbuffer, prev_left_offset, shared_vertex); // offset into the vertex buffer of the left vertex of the next segment - size_t next_left_offset = next_sub_path.last.i_id - next_1st_offset + 1 * vertex_size_floats; + const size_t next_left_offset = next_sub_path.last.i_id - next_1st_offset + 1 * vertex_size_floats; // update next segment update_position_at(vbuffer, next_left_offset, shared_vertex); } @@ -1533,13 +1533,13 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) VertexBuffer& prev_vbuffer = v_multibuffer[prev_sub_path.first.b_id]; VertexBuffer& next_vbuffer = v_multibuffer[next_sub_path.first.b_id]; // offset into the previous vertex buffer of the left vertex of the previous segment - size_t prev_left_offset = prev_sub_path.last.i_id - 1 * vertex_size_floats; + const size_t prev_left_offset = prev_sub_path.last.i_id - 1 * vertex_size_floats; // new position of the left vertices - Vec3f shared_vertex = extract_position_at(prev_vbuffer, prev_left_offset) + displacement_vec; + const Vec3f shared_vertex = extract_position_at(prev_vbuffer, prev_left_offset) + displacement_vec; // update previous segment update_position_at(prev_vbuffer, prev_left_offset, shared_vertex); // offset into the next vertex buffer of the left vertex of the next segment - size_t next_left_offset = next_sub_path.first.i_id + 3 * vertex_size_floats; + const size_t next_left_offset = next_sub_path.first.i_id + 3 * vertex_size_floats; // update next segment update_position_at(next_vbuffer, next_left_offset, shared_vertex); } @@ -1551,8 +1551,8 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // to two different vertex buffers size_t prev_sub_path_id = 0; size_t next_sub_path_id = 0; - size_t path_vertices_count = path.vertices_count(); - float half_width = 0.5f * path.width; + const size_t path_vertices_count = path.vertices_count(); + const float half_width = 0.5f * path.width; for (size_t j = 1; j < path_vertices_count - 1; ++j) { size_t curr_s_id = path.sub_paths.front().first.s_id + j; const Vec3f& prev = gcode_result.moves[curr_s_id - 1].position; @@ -1567,16 +1567,16 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) const Path::Sub_Path& prev_sub_path = path.sub_paths[prev_sub_path_id]; const Path::Sub_Path& next_sub_path = path.sub_paths[next_sub_path_id]; - Vec3f prev_dir = (curr - prev).normalized(); - Vec3f prev_right = Vec3f(prev_dir[1], -prev_dir[0], 0.0f).normalized(); - Vec3f prev_up = prev_right.cross(prev_dir); + const Vec3f prev_dir = (curr - prev).normalized(); + const Vec3f prev_right = Vec3f(prev_dir.y(), -prev_dir.x(), 0.0f).normalized(); + const Vec3f prev_up = prev_right.cross(prev_dir); - Vec3f next_dir = (next - curr).normalized(); + const Vec3f next_dir = (next - curr).normalized(); - bool is_right_turn = prev_up.dot(prev_dir.cross(next_dir)) <= 0.0f; - float cos_dir = prev_dir.dot(next_dir); + const bool is_right_turn = prev_up.dot(prev_dir.cross(next_dir)) <= 0.0f; + const float cos_dir = prev_dir.dot(next_dir); // whether the angle between adjacent segments is greater than 45 degrees - bool is_sharp = cos_dir < 0.7071068f; + const bool is_sharp = cos_dir < 0.7071068f; float displacement = 0.0f; if (cos_dir > -0.9998477f) { @@ -1585,10 +1585,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) displacement = half_width * ::tan(::acos(std::clamp(next_dir.dot(med_dir), -1.0f, 1.0f))); } - float sq_prev_length = (curr - prev).squaredNorm(); - float sq_next_length = (next - curr).squaredNorm(); - float sq_displacement = sqr(displacement); - bool can_displace = displacement > 0.0f && sq_displacement < sq_prev_length && sq_displacement < sq_next_length; + const float sq_prev_length = (curr - prev).squaredNorm(); + const float sq_next_length = (next - curr).squaredNorm(); + const float sq_displacement = sqr(displacement); + const bool can_displace = displacement > 0.0f && sq_displacement < sq_prev_length && sq_displacement < sq_next_length; if (can_displace) { // displacement to apply to the vertices to match @@ -1644,9 +1644,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) const MultiVertexBuffer& v_multibuffer = vertices[i]; for (const VertexBuffer& v_buffer : v_multibuffer) { - size_t size_elements = v_buffer.size(); - size_t size_bytes = size_elements * sizeof(float); - size_t vertices_count = size_elements / t_buffer.vertices.vertex_size_floats(); + const size_t size_elements = v_buffer.size(); + const size_t size_bytes = size_elements * sizeof(float); + const size_t vertices_count = size_elements / t_buffer.vertices.vertex_size_floats(); t_buffer.vertices.count += vertices_count; #if ENABLE_GCODE_VIEWER_STATISTICS @@ -1709,7 +1709,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) progress_count = 0; } - unsigned char id = buffer_id(curr.type); + const unsigned char id = buffer_id(curr.type); TBuffer& t_buffer = m_buffers[id]; MultiIndexBuffer& i_multibuffer = indices[id]; CurrVertexBuffer& curr_vertex_buffer = curr_vertex_buffers[id]; @@ -1779,8 +1779,8 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) TBuffer& t_buffer = m_buffers[i]; const MultiIndexBuffer& i_multibuffer = indices[i]; for (const IndexBuffer& i_buffer : i_multibuffer) { - size_t size_elements = i_buffer.size(); - size_t size_bytes = size_elements * sizeof(IBufferType); + const size_t size_elements = i_buffer.size(); + const size_t size_bytes = size_elements * sizeof(IBufferType); // stores index buffer informations into TBuffer t_buffer.indices.push_back(IBuffer()); @@ -1844,7 +1844,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) if (move.type == EMoveType::Extrude) { // layers zs const double* const last_z = m_layers.empty() ? nullptr : &m_layers.get_zs().back(); - double z = static_cast(move.position[2]); + const double z = static_cast(move.position.z()); if (last_z == nullptr || z < *last_z - EPSILON || *last_z + EPSILON < z) m_layers.append(z, { last_travel_s_id, i }); else @@ -1881,7 +1881,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) if (!options_zs.empty()) { TBuffer& extrude_buffer = m_buffers[buffer_id(EMoveType::Extrude)]; for (Path& path : extrude_buffer.paths) { - float z = path.sub_paths.front().first.position[2]; + const float z = path.sub_paths.front().first.position.z(); if (std::find_if(options_zs.begin(), options_zs.end(), [z](float f) { return f - EPSILON <= z && z <= f + EPSILON; }) != options_zs.end()) path.cp_color_id = 255 - path.cp_color_id; } @@ -1918,12 +1918,12 @@ void GCodeViewer::load_shells(const Print& print, bool initialized) if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF) { // adds wipe tower's volume - double max_z = print.objects()[0]->model_object()->get_model()->bounding_box().max(2); + const double max_z = print.objects()[0]->model_object()->get_model()->bounding_box().max(2); const PrintConfig& config = print.config(); - size_t extruders_count = config.nozzle_diameter.size(); + const size_t extruders_count = config.nozzle_diameter.size(); if ((extruders_count > 1) && config.wipe_tower && !config.complete_objects) { - float depth = print.wipe_tower_data(extruders_count).depth; - float brim_width = print.wipe_tower_data(extruders_count).brim_width; + const float depth = print.wipe_tower_data(extruders_count).depth; + const float brim_width = print.wipe_tower_data(extruders_count).brim_width; m_shells.volumes.load_wipe_tower_preview(1000, config.wipe_tower_x, config.wipe_tower_y, config.wipe_tower_width, depth, max_z, config.wipe_tower_rotation_angle, !print.is_step_done(psWipeTower), brim_width, initialized); @@ -2427,7 +2427,7 @@ void GCodeViewer::render_toolpaths() #else auto render_as_triangles = [] #endif // ENABLE_GCODE_VIEWER_STATISTICS -(const TBuffer& buffer, unsigned int ibuffer_id, GLShaderProgram& shader) { + (const TBuffer& buffer, unsigned int ibuffer_id, GLShaderProgram& shader) { for (const RenderPath& path : buffer.render_paths) { if (path.ibuffer_id == ibuffer_id) { shader.set_uniform("uniform_color", path.color); From c4ec355f41b705d7313b7d2bfd9f94850ff1bf1f Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 13:58:44 +0200 Subject: [PATCH 005/151] Added function to generated diamond shaped model --- src/slic3r/GUI/GLModel.cpp | 48 ++++++++++++++++++++++++++++++++++++++ src/slic3r/GUI/GLModel.hpp | 5 ++++ 2 files changed, 53 insertions(+) diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 6d54ec20e4..c4437a87ff 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -597,5 +597,53 @@ GLModel::InitializationData straight_arrow(float tip_width, float tip_height, fl return data; } +GLModel::InitializationData diamond(int resolution) +{ + resolution = std::max(4, resolution); + + GLModel::InitializationData data; + GLModel::InitializationData::Entity entity; + entity.type = GLModel::PrimitiveType::Triangles; + + const float step = 2.0f * float(PI) / float(resolution); + + // positions + for (int i = 0; i < resolution; ++i) { + float ii = float(i) * step; + entity.positions.emplace_back(0.5f * ::cos(ii), 0.5f * ::sin(ii), 0.0f); + } + entity.positions.emplace_back(0.0f, 0.0f, 0.5f); + entity.positions.emplace_back(0.0f, 0.0f, -0.5f); + + // normals + for (const Vec3f& v : entity.positions) { + entity.normals.emplace_back(v.normalized()); + } + + // triangles + // top + for (int i = 0; i < resolution; ++i) { + entity.indices.push_back(i + 0); + entity.indices.push_back(i + 1); + entity.indices.push_back(resolution); + } + entity.indices.push_back(resolution - 1); + entity.indices.push_back(0); + entity.indices.push_back(resolution); + + // bottom + for (int i = 0; i < resolution; ++i) { + entity.indices.push_back(i + 0); + entity.indices.push_back(resolution + 1); + entity.indices.push_back(i + 1); + } + entity.indices.push_back(resolution - 1); + entity.indices.push_back(resolution + 1); + entity.indices.push_back(0); + + data.entities.emplace_back(entity); + return data; +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index cab2fe220f..99c4bf4425 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -100,6 +100,11 @@ namespace GUI { // used to render sidebar hints for position and scale GLModel::InitializationData straight_arrow(float tip_width, float tip_height, float stem_width, float stem_height, float thickness); + // create a diamond with the given resolution + // the origin of the diamond is in its center + // the diamond is contained into a box with size [1, 1, 1] + GLModel::InitializationData diamond(int resolution); + } // namespace GUI } // namespace Slic3r From 55bac686031fa366f452111d84b5b58c2dd22a10 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 15:13:43 +0200 Subject: [PATCH 006/151] Tech ENABLE_SEAMS_USING_MODELS - 1st installment --- src/libslic3r/Technologies.hpp | 2 + src/slic3r/GUI/GCodeViewer.cpp | 437 ++++++++++++++++++++++----------- src/slic3r/GUI/GCodeViewer.hpp | 65 ++++- 3 files changed, 361 insertions(+), 143 deletions(-) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index f6366c18e7..768b0cb6ab 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -47,6 +47,8 @@ #define ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER (1 && ENABLE_2_4_0_ALPHA0) // Enable drawing contours, at cut level, for sinking volumes #define ENABLE_SINKING_CONTOURS (1 && ENABLE_2_4_0_ALPHA0) +// Enable rendering seams (and other options) in preview using models +#define ENABLE_SEAMS_USING_MODELS (1 && ENABLE_2_4_0_ALPHA0) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index c5109f07e4..418d009720 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -136,18 +136,26 @@ bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const } } +#if ENABLE_SEAMS_USING_MODELS +void GCodeViewer::TBuffer::Model::reset() +{ + instances.clear(); +} +#endif // ENABLE_SEAMS_USING_MODELS + void GCodeViewer::TBuffer::reset() { - // release gpu memory vertices.reset(); for (IBuffer& buffer : indices) { buffer.reset(); } - // release cpu memory indices.clear(); paths.clear(); render_paths.clear(); +#if ENABLE_SEAMS_USING_MODELS + model.reset(); +#endif // ENABLE_SEAMS_USING_MODELS } void GCodeViewer::TBuffer::add_path(const GCodeProcessor::MoveVertex& move, unsigned int b_id, size_t i_id, size_t s_id) @@ -544,6 +552,18 @@ GCodeViewer::GCodeViewer() switch (buffer_type(i)) { default: { break; } +#if ENABLE_SEAMS_USING_MODELS + case EMoveType::Tool_change: + case EMoveType::Color_change: + case EMoveType::Pause_Print: + case EMoveType::Custom_GCode: + case EMoveType::Retract: + case EMoveType::Unretract: + case EMoveType::Seam: { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; + break; + } +#else case EMoveType::Tool_change: case EMoveType::Color_change: case EMoveType::Pause_Print: @@ -555,6 +575,7 @@ GCodeViewer::GCodeViewer() buffer.vertices.format = VBuffer::EFormat::Position; break; } +#endif // ENABLE_SEAMS_USING_MODELS case EMoveType::Wipe: case EMoveType::Extrude: { buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Triangle; @@ -611,7 +632,7 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& if (!gcode_result.bed_shape.empty()) { // bed shape detected in the gcode bed_shape = gcode_result.bed_shape; - auto bundle = wxGetApp().preset_bundle; + const auto bundle = wxGetApp().preset_bundle; if (bundle != nullptr && !m_settings_ids.printer.empty()) { const Preset* preset = bundle->printers.find_preset(m_settings_ids.printer); if (preset != nullptr) { @@ -623,10 +644,10 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& else { // adjust printbed size in dependence of toolpaths bbox const double margin = 10.0; - Vec2d min(m_paths_bounding_box.min.x() - margin, m_paths_bounding_box.min.y() - margin); - Vec2d max(m_paths_bounding_box.max.x() + margin, m_paths_bounding_box.max.y() + margin); + const Vec2d min(m_paths_bounding_box.min.x() - margin, m_paths_bounding_box.min.y() - margin); + const Vec2d max(m_paths_bounding_box.max.x() + margin, m_paths_bounding_box.max.y() + margin); - Vec2d size = max - min; + const Vec2d size = max - min; bed_shape = { { min.x(), min.y() }, { max.x(), min.y() }, @@ -648,7 +669,7 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& m_print_statistics = gcode_result.print_statistics; if (m_time_estimate_mode != PrintEstimatedStatistics::ETimeMode::Normal) { - float time = m_print_statistics.modes[static_cast(m_time_estimate_mode)].time; + const float time = m_print_statistics.modes[static_cast(m_time_estimate_mode)].time; if (time == 0.0f || short_time(get_time_dhms(time)) == short_time(get_time_dhms(m_print_statistics.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time))) m_time_estimate_mode = PrintEstimatedStatistics::ETimeMode::Normal; @@ -764,7 +785,8 @@ void GCodeViewer::render() // initializes opengl data of TBuffers for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& buffer = m_buffers[i]; - switch (buffer_type(i)) + EMoveType type = buffer_type(i); + switch (type) { default: { break; } case EMoveType::Tool_change: @@ -774,8 +796,15 @@ void GCodeViewer::render() case EMoveType::Retract: case EMoveType::Unretract: case EMoveType::Seam: { +#if ENABLE_SEAMS_USING_MODELS + buffer.shader = "gouraud_light"; + buffer.model.model.init_from(diamond(16)); + buffer.model.color = option_color(type); + break; +#else buffer.shader = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20) ? "options_120" : "options_110"; break; +#endif // ENABLE_SEAMS_USING_MODELS } case EMoveType::Wipe: case EMoveType::Extrude: { @@ -844,8 +873,8 @@ void GCodeViewer::update_sequential_view_current(unsigned int first, unsigned in return false; }; - int first_diff = static_cast(first) - static_cast(m_sequential_view.last_current.first); - int last_diff = static_cast(last) - static_cast(m_sequential_view.last_current.last); + const int first_diff = static_cast(first) - static_cast(m_sequential_view.last_current.first); + const int last_diff = static_cast(last) - static_cast(m_sequential_view.last_current.last); unsigned int new_first = first; unsigned int new_last = last; @@ -1371,6 +1400,18 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) sq_prev_length = sq_length; }; +#if ENABLE_SEAMS_USING_MODELS + // format data into the buffers to be rendered as model + auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, TBuffer::Model::Instances& instances, size_t move_id) { + TBuffer::Model::Instance instance; + instance.position = curr.position; + instance.width = 1.2f * curr.width; + instance.height = 1.2f * curr.height; + instance.s_id = move_id; + instances.emplace_back(instance); + }; +#endif // ENABLE_SEAMS_USING_MODELS + #if ENABLE_GCODE_VIEWER_STATISTICS auto start_time = std::chrono::high_resolution_clock::now(); m_statistics.results_size = SLIC3R_STDVEC_MEMSIZE(gcode_result.moves, GCodeProcessor::MoveVertex); @@ -1460,6 +1501,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) case TBuffer::ERenderPrimitiveType::Point: { add_vertices_as_point(curr, v_buffer); break; } case TBuffer::ERenderPrimitiveType::Line: { add_vertices_as_line(prev, curr, v_buffer); break; } case TBuffer::ERenderPrimitiveType::Triangle: { add_vertices_as_solid(prev, curr, t_buffer, static_cast(v_multibuffer.size()) - 1, v_buffer, i); break; } +#if ENABLE_SEAMS_USING_MODELS + case TBuffer::ERenderPrimitiveType::Model: { add_model_instance(curr, t_buffer.model.instances, i); break; } +#endif // ENABLE_SEAMS_USING_MODELS } // collect options zs for later use @@ -1641,28 +1685,34 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // send vertices data to gpu for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; - - const MultiVertexBuffer& v_multibuffer = vertices[i]; - for (const VertexBuffer& v_buffer : v_multibuffer) { - const size_t size_elements = v_buffer.size(); - const size_t size_bytes = size_elements * sizeof(float); - const size_t vertices_count = size_elements / t_buffer.vertices.vertex_size_floats(); - t_buffer.vertices.count += vertices_count; +#if ENABLE_SEAMS_USING_MODELS + if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_MODELS + const MultiVertexBuffer& v_multibuffer = vertices[i]; + for (const VertexBuffer& v_buffer : v_multibuffer) { + const size_t size_elements = v_buffer.size(); + const size_t size_bytes = size_elements * sizeof(float); + const size_t vertices_count = size_elements / t_buffer.vertices.vertex_size_floats(); + t_buffer.vertices.count += vertices_count; #if ENABLE_GCODE_VIEWER_STATISTICS - m_statistics.total_vertices_gpu_size += static_cast(size_bytes); - m_statistics.max_vbuffer_gpu_size = std::max(m_statistics.max_vbuffer_gpu_size, static_cast(size_bytes)); - ++m_statistics.vbuffers_count; + m_statistics.total_vertices_gpu_size += static_cast(size_bytes); + m_statistics.max_vbuffer_gpu_size = std::max(m_statistics.max_vbuffer_gpu_size, static_cast(size_bytes)); + ++m_statistics.vbuffers_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS - GLuint id = 0; - glsafe(::glGenBuffers(1, &id)); - t_buffer.vertices.vbos.push_back(static_cast(id)); - t_buffer.vertices.sizes.push_back(size_bytes); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, id)); - glsafe(::glBufferData(GL_ARRAY_BUFFER, size_bytes, v_buffer.data(), GL_STATIC_DRAW)); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + GLuint id = 0; + glsafe(::glGenBuffers(1, &id)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, id)); + glsafe(::glBufferData(GL_ARRAY_BUFFER, size_bytes, v_buffer.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + t_buffer.vertices.vbos.push_back(static_cast(id)); + t_buffer.vertices.sizes.push_back(size_bytes); + } +#if ENABLE_SEAMS_USING_MODELS } +#endif // ENABLE_SEAMS_USING_MODELS } #if ENABLE_GCODE_VIEWER_STATISTICS @@ -1718,7 +1768,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // ensure there is at least one index buffer if (i_multibuffer.empty()) { i_multibuffer.push_back(IndexBuffer()); - vbo_index_list.push_back(t_buffer.vertices.vbos[curr_vertex_buffer.first]); +#if ENABLE_SEAMS_USING_MODELS + if (!t_buffer.vertices.vbos.empty()) +#endif // ENABLE_SEAMS_USING_MODELS + vbo_index_list.push_back(t_buffer.vertices.vbos[curr_vertex_buffer.first]); } // if adding the indices for the current segment exceeds the threshold size of the current index buffer @@ -1777,28 +1830,34 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // toolpaths data -> send indices data to gpu for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; - const MultiIndexBuffer& i_multibuffer = indices[i]; - for (const IndexBuffer& i_buffer : i_multibuffer) { - const size_t size_elements = i_buffer.size(); - const size_t size_bytes = size_elements * sizeof(IBufferType); +#if ENABLE_SEAMS_USING_MODELS + if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_MODELS + const MultiIndexBuffer& i_multibuffer = indices[i]; + for (const IndexBuffer& i_buffer : i_multibuffer) { + const size_t size_elements = i_buffer.size(); + const size_t size_bytes = size_elements * sizeof(IBufferType); - // stores index buffer informations into TBuffer - t_buffer.indices.push_back(IBuffer()); - IBuffer& ibuf = t_buffer.indices.back(); - ibuf.count = size_elements; - ibuf.vbo = vbo_indices[i][t_buffer.indices.size() - 1]; + // stores index buffer informations into TBuffer + t_buffer.indices.push_back(IBuffer()); + IBuffer& ibuf = t_buffer.indices.back(); + ibuf.count = size_elements; + ibuf.vbo = vbo_indices[i][t_buffer.indices.size() - 1]; #if ENABLE_GCODE_VIEWER_STATISTICS - m_statistics.total_indices_gpu_size += static_cast(size_bytes); - m_statistics.max_ibuffer_gpu_size = std::max(m_statistics.max_ibuffer_gpu_size, static_cast(size_bytes)); - ++m_statistics.ibuffers_count; + m_statistics.total_indices_gpu_size += static_cast(size_bytes); + m_statistics.max_ibuffer_gpu_size = std::max(m_statistics.max_ibuffer_gpu_size, static_cast(size_bytes)); + ++m_statistics.ibuffers_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS - glsafe(::glGenBuffers(1, &ibuf.ibo)); - glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibuf.ibo)); - glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER, size_bytes, i_buffer.data(), GL_STATIC_DRAW)); - glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + glsafe(::glGenBuffers(1, &ibuf.ibo)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibuf.ibo)); + glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER, size_bytes, i_buffer.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + } +#if ENABLE_SEAMS_USING_MODELS } +#endif // ENABLE_SEAMS_USING_MODELS } if (progress_dialog != nullptr) { @@ -1921,7 +1980,7 @@ void GCodeViewer::load_shells(const Print& print, bool initialized) const double max_z = print.objects()[0]->model_object()->get_model()->bounding_box().max(2); const PrintConfig& config = print.config(); const size_t extruders_count = config.nozzle_diameter.size(); - if ((extruders_count > 1) && config.wipe_tower && !config.complete_objects) { + if (extruders_count > 1 && config.wipe_tower && !config.complete_objects) { const float depth = print.wipe_tower_data(extruders_count).depth; const float brim_width = print.wipe_tower_data(extruders_count).brim_width; @@ -2014,8 +2073,8 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool path.sub_paths.back().last = buffer.paths[last].sub_paths.back().last; } - size_t min_s_id = m_layers.get_endpoints_at(min_id).first; - size_t max_s_id = m_layers.get_endpoints_at(max_id).last; + const size_t min_s_id = m_layers.get_endpoints_at(min_id).first; + const size_t max_s_id = m_layers.get_endpoints_at(max_id).last; return (min_s_id <= path.sub_paths.front().first.s_id && path.sub_paths.front().first.s_id <= max_s_id) || (min_s_id <= path.sub_paths.back().last.s_id && path.sub_paths.back().last.s_id <= max_s_id); @@ -2026,7 +2085,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool statistics->render_paths_size = 0; #endif // ENABLE_GCODE_VIEWER_STATISTICS - bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; + const bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; SequentialView::Endpoints global_endpoints = { m_moves_count , 0 }; SequentialView::Endpoints top_layer_endpoints = global_endpoints; @@ -2044,39 +2103,62 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (!buffer.visible) continue; - for (size_t i = 0; i < buffer.paths.size(); ++i) { - const Path& path = buffer.paths[i]; - if (path.type == EMoveType::Travel) { - if (!is_travel_in_layers_range(i, m_layers_z_range[0], m_layers_z_range[1])) +#if ENABLE_SEAMS_USING_MODELS + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { + for (const TBuffer::Model::Instance& inst : buffer.model.instances) { + if (inst.s_id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < inst.s_id) continue; + + global_endpoints.first = std::min(global_endpoints.first, inst.s_id); + global_endpoints.last = std::max(global_endpoints.last, inst.s_id); + + if (top_layer_only) { + if (inst.s_id < m_layers.get_endpoints_at(m_layers_z_range[1]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < inst.s_id) + continue; + + top_layer_endpoints.first = std::min(top_layer_endpoints.first, inst.s_id); + top_layer_endpoints.last = std::max(top_layer_endpoints.last, inst.s_id); + } } - else if (!is_in_layers_range(path, m_layers_z_range[0], m_layers_z_range[1])) - continue; - - if (path.type == EMoveType::Extrude && !is_visible(path)) - continue; - - // store valid path - for (size_t j = 0; j < path.sub_paths.size(); ++j) { - paths.push_back({ static_cast(b), path.sub_paths[j].first.b_id, static_cast(i), static_cast(j) }); - } - - global_endpoints.first = std::min(global_endpoints.first, path.sub_paths.front().first.s_id); - global_endpoints.last = std::max(global_endpoints.last, path.sub_paths.back().last.s_id); - - if (top_layer_only) { + } + else { +#endif // ENABLE_SEAMS_USING_MODELS + for (size_t i = 0; i < buffer.paths.size(); ++i) { + const Path& path = buffer.paths[i]; if (path.type == EMoveType::Travel) { - if (is_travel_in_layers_range(i, m_layers_z_range[1], m_layers_z_range[1])) { + if (!is_travel_in_layers_range(i, m_layers_z_range[0], m_layers_z_range[1])) + continue; + } + else if (!is_in_layers_range(path, m_layers_z_range[0], m_layers_z_range[1])) + continue; + + if (path.type == EMoveType::Extrude && !is_visible(path)) + continue; + + // store valid path + for (size_t j = 0; j < path.sub_paths.size(); ++j) { + paths.push_back({ static_cast(b), path.sub_paths[j].first.b_id, static_cast(i), static_cast(j) }); + } + + global_endpoints.first = std::min(global_endpoints.first, path.sub_paths.front().first.s_id); + global_endpoints.last = std::max(global_endpoints.last, path.sub_paths.back().last.s_id); + + if (top_layer_only) { + if (path.type == EMoveType::Travel) { + if (is_travel_in_layers_range(i, m_layers_z_range[1], m_layers_z_range[1])) { + top_layer_endpoints.first = std::min(top_layer_endpoints.first, path.sub_paths.front().first.s_id); + top_layer_endpoints.last = std::max(top_layer_endpoints.last, path.sub_paths.back().last.s_id); + } + } + else if (is_in_layers_range(path, m_layers_z_range[1], m_layers_z_range[1])) { top_layer_endpoints.first = std::min(top_layer_endpoints.first, path.sub_paths.front().first.s_id); top_layer_endpoints.last = std::max(top_layer_endpoints.last, path.sub_paths.back().last.s_id); } } - else if (is_in_layers_range(path, m_layers_z_range[1], m_layers_z_range[1])) { - top_layer_endpoints.first = std::min(top_layer_endpoints.first, path.sub_paths.front().first.s_id); - top_layer_endpoints.last = std::max(top_layer_endpoints.last, path.sub_paths.back().last.s_id); - } } +#if ENABLE_SEAMS_USING_MODELS } +#endif // ENABLE_SEAMS_USING_MODELS } // update current sequential position @@ -2086,42 +2168,57 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool // get the world position from gpu bool found = false; for (const TBuffer& buffer : m_buffers) { - // searches the path containing the current position - for (const Path& path : buffer.paths) { - if (path.contains(m_sequential_view.current.last)) { - int sub_path_id = path.get_id_of_sub_path_containing(m_sequential_view.current.last); - if (sub_path_id != -1) { - const Path::Sub_Path& sub_path = path.sub_paths[sub_path_id]; - unsigned int offset = static_cast(m_sequential_view.current.last - sub_path.first.s_id); - if (offset > 0) { - if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Line) - offset = 2 * offset - 1; - else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Triangle) { - unsigned int indices_count = buffer.indices_per_segment(); - offset = indices_count * (offset - 1) + (indices_count - 2); - if (sub_path_id == 0) - offset += 6; // add 2 triangles for starting cap - } - } - offset += static_cast(sub_path.first.i_id); - - // gets the vertex index from the index buffer on gpu - const IBuffer& i_buffer = buffer.indices[sub_path.first.b_id]; - unsigned int index = 0; - glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_buffer.ibo)); - glsafe(::glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, static_cast(offset * sizeof(IBufferType)), static_cast(sizeof(IBufferType)), static_cast(&index))); - glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); - - // gets the position from the vertices buffer on gpu - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, i_buffer.vbo)); - glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(index * buffer.vertices.vertex_size_bytes()), static_cast(3 * sizeof(float)), static_cast(sequential_view->current_position.data()))); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); - +#if ENABLE_SEAMS_USING_MODELS + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { + for (const TBuffer::Model::Instance& inst : buffer.model.instances) { + if (inst.s_id == m_sequential_view.current.last) { + sequential_view->current_position = inst.position; found = true; break; } } } + else { +#endif // ENABLE_SEAMS_USING_MODELS + // searches the path containing the current position + for (const Path& path : buffer.paths) { + if (path.contains(m_sequential_view.current.last)) { + const int sub_path_id = path.get_id_of_sub_path_containing(m_sequential_view.current.last); + if (sub_path_id != -1) { + const Path::Sub_Path& sub_path = path.sub_paths[sub_path_id]; + unsigned int offset = static_cast(m_sequential_view.current.last - sub_path.first.s_id); + if (offset > 0) { + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Line) + offset = 2 * offset - 1; + else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Triangle) { + unsigned int indices_count = buffer.indices_per_segment(); + offset = indices_count * (offset - 1) + (indices_count - 2); + if (sub_path_id == 0) + offset += 6; // add 2 triangles for starting cap + } + } + offset += static_cast(sub_path.first.i_id); + + // gets the vertex index from the index buffer on gpu + const IBuffer& i_buffer = buffer.indices[sub_path.first.b_id]; + unsigned int index = 0; + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_buffer.ibo)); + glsafe(::glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, static_cast(offset * sizeof(IBufferType)), static_cast(sizeof(IBufferType)), static_cast(&index))); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + // gets the position from the vertices buffer on gpu + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, i_buffer.vbo)); + glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(index * buffer.vertices.vertex_size_bytes()), static_cast(3 * sizeof(float)), static_cast(sequential_view->current_position.data()))); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + found = true; + break; + } + } + } +#if ENABLE_SEAMS_USING_MODELS + } +#endif // ENABLE_SEAMS_USING_MODELS if (found) break; @@ -2230,6 +2327,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool // set sequential data to their final value sequential_view->endpoints = top_layer_only ? top_layer_endpoints : global_endpoints; sequential_view->current.first = !top_layer_only && keep_sequential_current_first ? std::clamp(sequential_view->current.first, sequential_view->endpoints.first, sequential_view->endpoints.last) : sequential_view->endpoints.first; +#if ENABLE_SEAMS_USING_MODELS + sequential_view->global = global_endpoints; +#endif // ENABLE_SEAMS_USING_MODELS // updates sequential range caps std::array* sequential_range_caps = const_cast*>(&m_sequential_range_caps); @@ -2351,6 +2451,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool statistics->render_paths_size += SLIC3R_STDVEC_MEMSIZE(path.sizes, unsigned int); statistics->render_paths_size += SLIC3R_STDVEC_MEMSIZE(path.offsets, size_t); } +#if ENABLE_SEAMS_USING_MODELS + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances, TBuffer::Model::Instance); +#endif // ENABLE_SEAMS_USING_MODELS } statistics->refresh_paths_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); #endif // ENABLE_GCODE_VIEWER_STATISTICS @@ -2439,17 +2542,49 @@ void GCodeViewer::render_toolpaths() } }; +#if ENABLE_SEAMS_USING_MODELS +#if ENABLE_GCODE_VIEWER_STATISTICS + auto render_as_instanced_model = [this] +#else + auto render_as_instanced_model = [] +#endif // ENABLE_GCODE_VIEWER_STATISTICS + (TBuffer & buffer, GLShaderProgram & shader) { + for (const TBuffer::Model::Instance& inst : buffer.model.instances) { + bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; + bool visible = top_layer_only ? + m_sequential_view.global.first <= inst.s_id && inst.s_id <= m_sequential_view.global.last : + m_sequential_view.current.first <= inst.s_id && inst.s_id <= m_sequential_view.current.last; + + if (visible) { + glsafe(::glPushMatrix()); + glsafe(::glTranslatef(inst.position.x(), inst.position.y(), inst.position.z() - 0.5f * inst.height)); + glsafe(::glScalef(inst.width, inst.width, inst.height)); + Color color = (top_layer_only && m_sequential_view.current.last != m_sequential_view.global.last && inst.s_id < m_sequential_view.endpoints.first) ? + Neutral_Color : buffer.model.color; + buffer.model.model.set_color(-1, color); + buffer.model.model.render(); + glsafe(::glPopMatrix()); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.gl_models_calls_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + } + } + }; +#endif // ENABLE_SEAMS_USING_MODELS + auto line_width = [](double zoom) { return (zoom < 5.0) ? 1.0 : (1.0 + 5.0 * (zoom - 5.0) / (100.0 - 5.0)); }; - glsafe(::glLineWidth(static_cast(line_width(zoom)))); - unsigned char begin_id = buffer_id(EMoveType::Retract); unsigned char end_id = buffer_id(EMoveType::Count); for (unsigned char i = begin_id; i < end_id; ++i) { +#if ENABLE_SEAMS_USING_MODELS + TBuffer& buffer = m_buffers[i]; +#else const TBuffer& buffer = m_buffers[i]; +#endif // ENABLE_SEAMS_USING_MODELS if (!buffer.visible || !buffer.has_data()) continue; @@ -2457,44 +2592,56 @@ void GCodeViewer::render_toolpaths() if (shader != nullptr) { shader->start_using(); - for (size_t j = 0; j < buffer.indices.size(); ++j) { - const IBuffer& i_buffer = buffer.indices[j]; - - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, i_buffer.vbo)); - glsafe(::glVertexPointer(buffer.vertices.position_size_floats(), GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.position_offset_bytes())); - glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); - bool has_normals = buffer.vertices.normal_size_floats() > 0; - if (has_normals) { - glsafe(::glNormalPointer(GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.normal_offset_bytes())); - glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); - } - - glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_buffer.ibo)); - - switch (buffer.render_primitive_type) - { - case TBuffer::ERenderPrimitiveType::Point: { - render_as_points(buffer, static_cast(j), *shader); - break; - } - case TBuffer::ERenderPrimitiveType::Line: { - render_as_lines(buffer, static_cast(j), *shader); - break; - } - case TBuffer::ERenderPrimitiveType::Triangle: { - render_as_triangles(buffer, static_cast(j), *shader); - break; - } - } - - glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); - - if (has_normals) - glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); - - glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); +#if ENABLE_SEAMS_USING_MODELS + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { + shader->set_uniform("emission_factor", 0.25f); + render_as_instanced_model(buffer, *shader); + shader->set_uniform("emission_factor", 0.0f); } + else { +#endif // ENABLE_SEAMS_USING_MODELS + for (size_t j = 0; j < buffer.indices.size(); ++j) { + const IBuffer& i_buffer = buffer.indices[j]; + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, i_buffer.vbo)); + glsafe(::glVertexPointer(buffer.vertices.position_size_floats(), GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.position_offset_bytes())); + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + bool has_normals = buffer.vertices.normal_size_floats() > 0; + if (has_normals) { + glsafe(::glNormalPointer(GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.normal_offset_bytes())); + glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_buffer.ibo)); + + switch (buffer.render_primitive_type) + { + case TBuffer::ERenderPrimitiveType::Point: { + render_as_points(buffer, static_cast(j), *shader); + break; + } + case TBuffer::ERenderPrimitiveType::Line: { + glsafe(::glLineWidth(static_cast(line_width(zoom)))); + render_as_lines(buffer, static_cast(j), *shader); + break; + } + case TBuffer::ERenderPrimitiveType::Triangle: { + render_as_triangles(buffer, static_cast(j), *shader); + break; + } + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + if (has_normals) + glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); + + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + } +#if ENABLE_SEAMS_USING_MODELS + } +#endif // ENABLE_SEAMS_USING_MODELS shader->stop_using(); } @@ -3488,6 +3635,9 @@ void GCodeViewer::render_statistics() add_counter(std::string("Multi GL_LINES:"), m_statistics.gl_multi_lines_calls_count); add_counter(std::string("Multi GL_TRIANGLES:"), m_statistics.gl_multi_triangles_calls_count); add_counter(std::string("GL_TRIANGLES:"), m_statistics.gl_triangles_calls_count); +#if ENABLE_SEAMS_USING_MODELS + add_counter(std::string("Models:"), m_statistics.gl_models_calls_count); +#endif // ENABLE_SEAMS_USING_MODELS } if (ImGui::CollapsingHeader("CPU memory")) { @@ -3496,6 +3646,9 @@ void GCodeViewer::render_statistics() ImGui::Separator(); add_memory(std::string("Paths:"), m_statistics.paths_size); add_memory(std::string("Render paths:"), m_statistics.render_paths_size); +#if ENABLE_SEAMS_USING_MODELS + add_memory(std::string("Models instances:"), m_statistics.models_instances_size); +#endif // ENABLE_SEAMS_USING_MODELS } if (ImGui::CollapsingHeader("GPU memory")) { diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 3d9b9bb698..53bc66def4 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -81,7 +81,10 @@ class GCodeViewer size_t position_size_floats() const { return 3; } size_t position_size_bytes() const { return position_size_floats() * sizeof(float); } - size_t normal_offset_floats() const { return position_size_floats(); } + size_t normal_offset_floats() const { + assert(format == EFormat::PositionNormal1 || format == EFormat::PositionNormal3); + return position_size_floats(); + } size_t normal_offset_bytes() const { return normal_offset_floats() * sizeof(float); } size_t normal_size_floats() const { @@ -230,13 +233,43 @@ class GCodeViewer { Point, Line, +#if ENABLE_SEAMS_USING_MODELS + Triangle, + Model +#else Triangle +#endif // ENABLE_SEAMS_USING_MODELS }; ERenderPrimitiveType render_primitive_type; + + // buffers for point, line and triangle primitive types VBuffer vertices; std::vector indices; +#if ENABLE_SEAMS_USING_MODELS + struct Model + { + struct Instance + { + Vec3f position; + float width; + float height; + size_t s_id; + }; + using Instances = std::vector; + + GLModel model; + Color color; + Instances instances; + + void reset(); + }; + + // contain the buffer for model primitive types + Model model; +#endif // ENABLE_SEAMS_USING_MODELS + std::string shader; std::vector paths; // std::set seems to perform significantly better, at least on Windows. @@ -284,9 +317,24 @@ class GCodeViewer } size_t max_indices_per_segment_size_bytes() const { return max_indices_per_segment() * sizeof(IBufferType); } +#if ENABLE_SEAMS_USING_MODELS + bool has_data() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Point: + case ERenderPrimitiveType::Line: + case ERenderPrimitiveType::Triangle: { + return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; + } + case ERenderPrimitiveType::Model: { return model.model.is_initialized() && !model.instances.empty(); } + default: { return false; } + } + } +#else bool has_data() const { return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; } +#endif // ENABLE_SEAMS_USING_MODELS }; // helper to render shells @@ -434,6 +482,9 @@ class GCodeViewer int64_t gl_multi_lines_calls_count{ 0 }; int64_t gl_multi_triangles_calls_count{ 0 }; int64_t gl_triangles_calls_count{ 0 }; +#if ENABLE_SEAMS_USING_MODELS + int64_t gl_models_calls_count{ 0 }; +#endif // ENABLE_SEAMS_USING_MODELS // memory int64_t results_size{ 0 }; int64_t total_vertices_gpu_size{ 0 }; @@ -442,6 +493,9 @@ class GCodeViewer int64_t max_ibuffer_gpu_size{ 0 }; int64_t paths_size{ 0 }; int64_t render_paths_size{ 0 }; +#if ENABLE_SEAMS_USING_MODELS + int64_t models_instances_size{ 0 }; +#endif // ENABLE_SEAMS_USING_MODELS // other int64_t travel_segments_count{ 0 }; int64_t wipe_segments_count{ 0 }; @@ -471,6 +525,9 @@ class GCodeViewer gl_multi_lines_calls_count = 0; gl_multi_triangles_calls_count = 0; gl_triangles_calls_count = 0; +#if ENABLE_SEAMS_USING_MODELS + gl_models_calls_count = 0; +#endif // ENABLE_SEAMS_USING_MODELS } void reset_sizes() { @@ -481,6 +538,9 @@ class GCodeViewer max_ibuffer_gpu_size = 0; paths_size = 0; render_paths_size = 0; +#if ENABLE_SEAMS_USING_MODELS + models_instances_size = 0; +#endif // ENABLE_SEAMS_USING_MODELS } void reset_others() { @@ -564,6 +624,9 @@ public: Endpoints endpoints; Endpoints current; Endpoints last_current; +#if ENABLE_SEAMS_USING_MODELS + Endpoints global; +#endif // ENABLE_SEAMS_USING_MODELS Vec3f current_position{ Vec3f::Zero() }; Marker marker; GCodeWindow gcode_window; From 1b03eec234089149ec779374e5b5978a0f445a0e Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Aug 2021 15:23:37 +0200 Subject: [PATCH 007/151] Fixed build of tech ENABLE_SEAMS_USING_MODELS when tech ENABLE_GCODE_VIEWER_STATISTICS is disabled --- src/slic3r/GUI/GCodeViewer.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 418d009720..cc25c31d8e 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2543,11 +2543,7 @@ void GCodeViewer::render_toolpaths() }; #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_GCODE_VIEWER_STATISTICS auto render_as_instanced_model = [this] -#else - auto render_as_instanced_model = [] -#endif // ENABLE_GCODE_VIEWER_STATISTICS (TBuffer & buffer, GLShaderProgram & shader) { for (const TBuffer::Model::Instance& inst : buffer.model.instances) { bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; From fd81041adb8e637c4b9422a1c97d723a410cb69c Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Aug 2021 08:10:42 +0200 Subject: [PATCH 008/151] Fixed build warnings on non-Windows OS --- src/slic3r/GUI/GCodeViewer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index cc25c31d8e..87e24c957c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1818,6 +1818,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) add_indices_as_solid(prev, curr, next, t_buffer, curr_vertex_buffer.second, static_cast(i_multibuffer.size()) - 1, i_buffer, i); break; } + default: { break; } } } @@ -2286,6 +2287,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool size_in_indices = buffer.indices_per_segment() * segments_count; break; } + default: { break; } } if (size_in_indices == 0) @@ -2625,6 +2627,7 @@ void GCodeViewer::render_toolpaths() render_as_triangles(buffer, static_cast(j), *shader); break; } + default: { break; } } glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); From 8c4e203e1d90b0003fe584edb522c1ae12048120 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Aug 2021 09:09:07 +0200 Subject: [PATCH 009/151] Tech ENABLE_SEAMS_USING_MODELS -> Use new options visualization only if OpenGL 3.1 or greater is detected --- src/slic3r/GUI/GCodeViewer.cpp | 44 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 87e24c957c..42c34b59b0 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -545,6 +545,7 @@ const GCodeViewer::Color GCodeViewer::Neutral_Color = { 0.25f, 0.25f, 0.25f, 1.0 GCodeViewer::GCodeViewer() { +#if !ENABLE_SEAMS_USING_MODELS // initializes non OpenGL data of TBuffers // OpenGL data are initialized into render().init_gl_data() for (size_t i = 0; i < m_buffers.size(); ++i) { @@ -552,18 +553,6 @@ GCodeViewer::GCodeViewer() switch (buffer_type(i)) { default: { break; } -#if ENABLE_SEAMS_USING_MODELS - case EMoveType::Tool_change: - case EMoveType::Color_change: - case EMoveType::Pause_Print: - case EMoveType::Custom_GCode: - case EMoveType::Retract: - case EMoveType::Unretract: - case EMoveType::Seam: { - buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; - break; - } -#else case EMoveType::Tool_change: case EMoveType::Color_change: case EMoveType::Pause_Print: @@ -575,7 +564,6 @@ GCodeViewer::GCodeViewer() buffer.vertices.format = VBuffer::EFormat::Position; break; } -#endif // ENABLE_SEAMS_USING_MODELS case EMoveType::Wipe: case EMoveType::Extrude: { buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Triangle; @@ -591,6 +579,8 @@ GCodeViewer::GCodeViewer() } set_toolpath_move_type_visible(EMoveType::Extrude, true); +#endif // !ENABLE_SEAMS_USING_MODELS + // m_sequential_view.skip_invisible_moves = true; } @@ -797,25 +787,47 @@ void GCodeViewer::render() case EMoveType::Unretract: case EMoveType::Seam: { #if ENABLE_SEAMS_USING_MODELS - buffer.shader = "gouraud_light"; - buffer.model.model.init_from(diamond(16)); - buffer.model.color = option_color(type); + if (wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; + buffer.shader = "gouraud_light"; + buffer.model.model.init_from(diamond(16)); + buffer.model.color = option_color(type); + } + else { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Point; + buffer.vertices.format = VBuffer::EFormat::Position; + buffer.shader = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20) ? "options_120" : "options_110"; + } break; #else + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Point; + buffer.vertices.format = VBuffer::EFormat::Position; buffer.shader = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20) ? "options_120" : "options_110"; break; #endif // ENABLE_SEAMS_USING_MODELS } case EMoveType::Wipe: case EMoveType::Extrude: { +#if ENABLE_SEAMS_USING_MODELS + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Triangle; + buffer.vertices.format = VBuffer::EFormat::PositionNormal3; +#endif // ENABLE_SEAMS_USING_MODELS buffer.shader = "gouraud_light"; break; } case EMoveType::Travel: { +#if ENABLE_SEAMS_USING_MODELS + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Line; + buffer.vertices.format = VBuffer::EFormat::PositionNormal1; +#endif // ENABLE_SEAMS_USING_MODELS buffer.shader = "toolpaths_lines"; break; } } + +#if ENABLE_SEAMS_USING_MODELS + set_toolpath_move_type_visible(EMoveType::Extrude, true); +#endif // ENABLE_SEAMS_USING_MODELS } // initializes tool marker From 73464c76b9cb36f81e1710e9b0a4404094e1ee38 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Aug 2021 09:35:12 +0200 Subject: [PATCH 010/151] Techs ENABLE_SEAMS_USING_MODELS + ENABLE_GCODE_VIEWER_STATISTICS -> Fixed detection of used memory to store instances data --- src/slic3r/GUI/GCodeViewer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 42c34b59b0..860d693cc3 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2096,6 +2096,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_GCODE_VIEWER_STATISTICS Statistics* statistics = const_cast(&m_statistics); statistics->render_paths_size = 0; +#if ENABLE_SEAMS_USING_MODELS + statistics->models_instances_size = 0; +#endif // ENABLE_SEAMS_USING_MODELS #endif // ENABLE_GCODE_VIEWER_STATISTICS const bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; From 252935dde74c18c5ee837c4b3a32039105411c24 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Aug 2021 10:19:02 +0200 Subject: [PATCH 011/151] Techs ENABLE_SEAMS_USING_MODELS + ENABLE_GCODE_VIEWER_STATISTICS -> Added instances counter --- src/slic3r/GUI/GCodeViewer.cpp | 21 ++++++++++++++++++++- src/slic3r/GUI/GCodeViewer.hpp | 6 ++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 860d693cc3..8730493cbd 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1514,7 +1514,14 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) case TBuffer::ERenderPrimitiveType::Line: { add_vertices_as_line(prev, curr, v_buffer); break; } case TBuffer::ERenderPrimitiveType::Triangle: { add_vertices_as_solid(prev, curr, t_buffer, static_cast(v_multibuffer.size()) - 1, v_buffer, i); break; } #if ENABLE_SEAMS_USING_MODELS - case TBuffer::ERenderPrimitiveType::Model: { add_model_instance(curr, t_buffer.model.instances, i); break; } + case TBuffer::ERenderPrimitiveType::Model: + { + add_model_instance(curr, t_buffer.model.instances, i); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.instances_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + break; + } #endif // ENABLE_SEAMS_USING_MODELS } @@ -1686,6 +1693,15 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } } +#if ENABLE_SEAMS_USING_MODELS + for (size_t i = 0; i < m_buffers.size(); ++i) { + TBuffer& t_buffer = m_buffers[i]; + if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { + t_buffer.model.instances.shrink_to_fit(); + } + } +#endif // ENABLE_SEAMS_USING_MODELS + // move the wipe toolpaths half height up to render them on proper position MultiVertexBuffer& wipe_vertices = vertices[buffer_id(EMoveType::Wipe)]; for (VertexBuffer& v_buffer : wipe_vertices) { @@ -3677,6 +3693,9 @@ void GCodeViewer::render_statistics() add_counter(std::string("Travel segments count:"), m_statistics.travel_segments_count); add_counter(std::string("Wipe segments count:"), m_statistics.wipe_segments_count); add_counter(std::string("Extrude segments count:"), m_statistics.extrude_segments_count); +#if ENABLE_SEAMS_USING_MODELS + add_counter(std::string("Instances count:"), m_statistics.instances_count); +#endif // ENABLE_SEAMS_USING_MODELS ImGui::Separator(); add_counter(std::string("VBuffers count:"), m_statistics.vbuffers_count); add_counter(std::string("IBuffers count:"), m_statistics.ibuffers_count); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 53bc66def4..6103f9722f 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -500,6 +500,9 @@ class GCodeViewer int64_t travel_segments_count{ 0 }; int64_t wipe_segments_count{ 0 }; int64_t extrude_segments_count{ 0 }; +#if ENABLE_SEAMS_USING_MODELS + int64_t instances_count{ 0 }; +#endif // ENABLE_SEAMS_USING_MODELS int64_t vbuffers_count{ 0 }; int64_t ibuffers_count{ 0 }; @@ -547,6 +550,9 @@ class GCodeViewer travel_segments_count = 0; wipe_segments_count = 0; extrude_segments_count = 0; +#if ENABLE_SEAMS_USING_MODELS + instances_count = 0; +#endif // ENABLE_SEAMS_USING_MODELS vbuffers_count = 0; ibuffers_count = 0; } From 34da899c66271262f6c9e8c402d7b2c49decca0c Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Aug 2021 15:08:49 +0200 Subject: [PATCH 012/151] Temporary tech ENABLE_SEAMS_USING_INSTANCED_MODELS -> WIP (still missing opengl calls) --- src/libslic3r/Technologies.hpp | 6 ++ src/slic3r/GUI/GCodeViewer.cpp | 140 +++++++++++++++++++++++++++++++++ src/slic3r/GUI/GCodeViewer.hpp | 47 +++++++++++ 3 files changed, 193 insertions(+) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 768b0cb6ab..79976d4a08 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -49,6 +49,12 @@ #define ENABLE_SINKING_CONTOURS (1 && ENABLE_2_4_0_ALPHA0) // Enable rendering seams (and other options) in preview using models #define ENABLE_SEAMS_USING_MODELS (1 && ENABLE_2_4_0_ALPHA0) +// Enable rendering seams (and other options) in preview using instanced models +// references: +// https://ogldev.org/www/tutorial33/tutorial33.html +// https://docs.gl/gl3/glDrawElementsInstanced +// https://www.khronos.org/opengl/wiki/Vertex_Rendering#Instancing +#define ENABLE_SEAMS_USING_INSTANCED_MODELS (1 && ENABLE_SEAMS_USING_MODELS) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 8730493cbd..b8388f82fa 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -95,6 +95,17 @@ void GCodeViewer::VBuffer::reset() count = 0; } +#if ENABLE_SEAMS_USING_INSTANCED_MODELS +void GCodeViewer::InstanceVBuffer::reset() +{ + // release gpu memory + if (vbo > 0) + glsafe(::glDeleteBuffers(1, &vbo)); + s_ids.clear(); + render_range = { 0, 0 }; +} +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS + void GCodeViewer::IBuffer::reset() { // release gpu memory @@ -140,6 +151,9 @@ bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const void GCodeViewer::TBuffer::Model::reset() { instances.clear(); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + instances2.reset(); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } #endif // ENABLE_SEAMS_USING_MODELS @@ -1422,6 +1436,23 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) instance.s_id = move_id; instances.emplace_back(instance); }; + +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + // format data into the buffers to be rendered as model + auto add_model_instance_2 = [](const GCodeProcessor::MoveVertex& curr, InstanceBuffer& instances, InstanceIdBuffer& instances_ids, size_t move_id) { + // append position + instances.push_back(curr.position.x()); + instances.push_back(curr.position.y()); + instances.push_back(curr.position.z()); + // append width + instances.push_back(1.2f * curr.width); + // append height + instances.push_back(1.2f * curr.height); + + // append id + instances_ids.push_back(move_id); + }; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS #if ENABLE_GCODE_VIEWER_STATISTICS @@ -1466,6 +1497,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) std::vector vertices(m_buffers.size()); std::vector indices(m_buffers.size()); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + std::vector instances(m_buffers.size()); + std::vector instances_ids(m_buffers.size()); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS std::vector options_zs; // toolpaths data -> extract vertices from result @@ -1490,6 +1525,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) const unsigned char id = buffer_id(curr.type); TBuffer& t_buffer = m_buffers[id]; MultiVertexBuffer& v_multibuffer = vertices[id]; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + InstanceBuffer& inst_buffer = instances[id]; + InstanceIdBuffer& inst_id_buffer = instances_ids[id]; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS // ensure there is at least one vertex buffer if (v_multibuffer.empty()) @@ -1517,6 +1556,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) case TBuffer::ERenderPrimitiveType::Model: { add_model_instance(curr, t_buffer.model.instances, i); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + add_model_instance_2(curr, inst_buffer, inst_id_buffer, i); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.instances_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS @@ -1714,7 +1756,31 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { + const InstanceBuffer& inst_buffer = instances[i]; + if (!inst_buffer.empty()) { + const size_t size_bytes = inst_buffer.size() * sizeof(float); + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.total_instances_gpu_size += static_cast(size_bytes); + m_statistics.max_instance_vbuffer_gpu_size = std::max(m_statistics.max_instance_vbuffer_gpu_size, static_cast(size_bytes)); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + GLuint id = 0; + glsafe(::glGenBuffers(1, &id)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, id)); + glsafe(::glBufferData(GL_ARRAY_BUFFER, size_bytes, inst_buffer.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + t_buffer.model.instances2.vbo = id; + t_buffer.model.instances2.s_ids = instances_ids[i]; + } + } + else { +#else if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS const MultiVertexBuffer& v_multibuffer = vertices[i]; for (const VertexBuffer& v_buffer : v_multibuffer) { @@ -1751,6 +1817,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // dismiss vertices data, no more needed std::vector().swap(vertices); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + std::vector().swap(instances); + std::vector().swap(instances_ids); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS // toolpaths data -> extract indices from result // paths may have been filled while extracting vertices, @@ -2137,6 +2207,23 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_SEAMS_USING_MODELS if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + for (size_t id : buffer.model.instances2.s_ids) { + if (id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < id) + continue; + + global_endpoints.first = std::min(global_endpoints.first, id); + global_endpoints.last = std::max(global_endpoints.last, id); + + if (top_layer_only) { + if (id < m_layers.get_endpoints_at(m_layers_z_range[1]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < id) + continue; + + top_layer_endpoints.first = std::min(top_layer_endpoints.first, id); + top_layer_endpoints.last = std::max(top_layer_endpoints.last, id); + } + } +#else for (const TBuffer::Model::Instance& inst : buffer.model.instances) { if (inst.s_id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < inst.s_id) continue; @@ -2152,6 +2239,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool top_layer_endpoints.last = std::max(top_layer_endpoints.last, inst.s_id); } } +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } else { #endif // ENABLE_SEAMS_USING_MODELS @@ -2202,6 +2290,20 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool for (const TBuffer& buffer : m_buffers) { #if ENABLE_SEAMS_USING_MODELS if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + for (size_t i = 0; i < buffer.model.instances2.s_ids.size(); ++i) { + if (buffer.model.instances2.s_ids[i] == m_sequential_view.current.last) { + + // gets the position from the vertices buffer on gpu + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.model.instances2.vbo)); + glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(i * buffer.model.instances2.instance_size_bytes()), static_cast(3 * sizeof(float)), static_cast(sequential_view->current_position.data()))); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + found = true; + break; + } + } +#else for (const TBuffer::Model::Instance& inst : buffer.model.instances) { if (inst.s_id == m_sequential_view.current.last) { sequential_view->current_position = inst.position; @@ -2209,6 +2311,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool break; } } +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } else { #endif // ENABLE_SEAMS_USING_MODELS @@ -2357,6 +2460,33 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #endif } +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + // second pass: for buffers using instanced models, update the instances render ranges + for (size_t b = 0; b < m_buffers.size(); ++b) { + TBuffer& buffer = const_cast(m_buffers[b]); + if (buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) + continue; + + buffer.model.instances2.render_range = { 0, 0 }; + + if (!buffer.visible) + continue; + + if (m_sequential_view.current.first <= buffer.model.instances2.s_ids.back() && buffer.model.instances2.s_ids.front() <= m_sequential_view.current.last) { + for (size_t id : buffer.model.instances2.s_ids) { + if (id <= m_sequential_view.current.first) { + buffer.model.instances2.render_range.offset += buffer.model.instances2.instance_size_bytes(); + buffer.model.instances2.render_range.count = 0; + } + else if (id <= m_sequential_view.current.last) + ++buffer.model.instances2.render_range.count; + else + break; + } + } + } +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS + // set sequential data to their final value sequential_view->endpoints = top_layer_only ? top_layer_endpoints : global_endpoints; sequential_view->current.first = !top_layer_only && keep_sequential_current_first ? std::clamp(sequential_view->current.first, sequential_view->endpoints.first, sequential_view->endpoints.last) : sequential_view->endpoints.first; @@ -2485,7 +2615,11 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool statistics->render_paths_size += SLIC3R_STDVEC_MEMSIZE(path.offsets, size_t); } #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.s_ids, size_t); +#else statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances, TBuffer::Model::Instance); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } statistics->refresh_paths_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); @@ -3684,9 +3818,15 @@ void GCodeViewer::render_statistics() if (ImGui::CollapsingHeader("GPU memory")) { add_memory(std::string("Vertices:"), m_statistics.total_vertices_gpu_size); add_memory(std::string("Indices:"), m_statistics.total_indices_gpu_size); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + add_memory(std::string("Instances:"), m_statistics.total_instances_gpu_size); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS ImGui::Separator(); add_memory(std::string("Max VBuffer:"), m_statistics.max_vbuffer_gpu_size); add_memory(std::string("Max IBuffer:"), m_statistics.max_ibuffer_gpu_size); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + add_memory(std::string("Max instance VBuffer:"), m_statistics.max_instance_vbuffer_gpu_size); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } if (ImGui::CollapsingHeader("Other")) { diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 6103f9722f..7bf341a68d 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -27,6 +27,10 @@ class GCodeViewer using MultiVertexBuffer = std::vector; using IndexBuffer = std::vector; using MultiIndexBuffer = std::vector; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + using InstanceBuffer = std::vector; + using InstanceIdBuffer = std::vector; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS static const std::vector Extrusion_Role_Colors; static const std::vector Options_Colors; @@ -100,6 +104,34 @@ class GCodeViewer void reset(); }; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + // vbo buffer containing instances data used to render a toolpaths using instanced models + // record format: 5 floats -> position.x|position.y|position.z|width|height + struct InstanceVBuffer + { + struct RenderRange + { + // offset in bytes of the 1st instance to render + unsigned int offset; + // count of instances to render + unsigned int count; + }; + + // vbo id + unsigned int vbo{ 0 }; + // move ids + std::vector s_ids; + RenderRange render_range; + + size_t data_size_bytes() const { return s_ids.size() * instance_size_bytes(); } + + size_t instance_size_floats() const { return 5; } + size_t instance_size_bytes() const { return instance_size_floats() * sizeof(float); } + + void reset(); + }; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS + // ibo buffer containing indices data (for lines/triangles) used to render a specific toolpath type struct IBuffer { @@ -262,6 +294,9 @@ class GCodeViewer GLModel model; Color color; Instances instances; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + InstanceVBuffer instances2; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS void reset(); }; @@ -489,8 +524,14 @@ class GCodeViewer int64_t results_size{ 0 }; int64_t total_vertices_gpu_size{ 0 }; int64_t total_indices_gpu_size{ 0 }; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + int64_t total_instances_gpu_size{ 0 }; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS int64_t max_vbuffer_gpu_size{ 0 }; int64_t max_ibuffer_gpu_size{ 0 }; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + int64_t max_instance_vbuffer_gpu_size{ 0 }; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS int64_t paths_size{ 0 }; int64_t render_paths_size{ 0 }; #if ENABLE_SEAMS_USING_MODELS @@ -537,8 +578,14 @@ class GCodeViewer results_size = 0; total_vertices_gpu_size = 0; total_indices_gpu_size = 0; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + total_instances_gpu_size = 0; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS max_vbuffer_gpu_size = 0; max_ibuffer_gpu_size = 0; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + max_instance_vbuffer_gpu_size = 0; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS paths_size = 0; render_paths_size = 0; #if ENABLE_SEAMS_USING_MODELS From 79e8e8627f403fc442d831b8a087528b7951c427 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 20 Aug 2021 11:24:48 +0200 Subject: [PATCH 013/151] Tech ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT - fixed export/import to/from 3mf file of mirrored volumes --- src/libslic3r/Format/3mf.cpp | 27 +++++++++++++++++++++++++++ src/libslic3r/Technologies.hpp | 2 ++ 2 files changed, 29 insertions(+) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 2a76f218fb..febeacdbe3 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1839,11 +1839,17 @@ namespace Slic3r { Transform3d volume_matrix_to_object = Transform3d::Identity(); bool has_transform = false; +#if ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + bool is_left_handed = false; +#endif // ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT // extract the volume transformation from the volume's metadata, if present for (const Metadata& metadata : volume_data.metadata) { if (metadata.key == MATRIX_KEY) { volume_matrix_to_object = Slic3r::Geometry::transform3d_from_string(metadata.value); has_transform = ! volume_matrix_to_object.isApprox(Transform3d::Identity(), 1e-10); +#if ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + is_left_handed = Slic3r::Geometry::Transformation(volume_matrix_to_object).is_left_handed(); +#endif // ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT break; } } @@ -1875,6 +1881,13 @@ namespace Slic3r { stl_get_size(&stl); triangle_mesh.repair(); +#if ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + // PrusaSlicer older than 2.4.0 saved mirrored volumes with reversed winding of the triangles + // This caused the call to TriangleMesh::repair() to reverse all the facets because the calculated volume was negative + if (is_left_handed && stl.stats.facets_reversed > 0 && stl.stats.facets_reversed == stl.stats.original_num_facets) + stl.stats.facets_reversed = 0; +#endif // ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + if (m_version == 0) { // if the 3mf was not produced by PrusaSlicer and there is only one instance, // bake the transformation into the geometry to allow the reload from disk command @@ -2499,6 +2512,10 @@ namespace Slic3r { if (volume == nullptr) continue; +#if ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + bool is_left_handed = volume->is_left_handed(); +#endif // ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + VolumeToOffsetsMap::iterator volume_it = volumes_offsets.find(volume); assert(volume_it != volumes_offsets.end()); @@ -2513,6 +2530,15 @@ namespace Slic3r { { const Vec3i &idx = its.indices[i]; char *ptr = buf; +#if ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT + boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << TRIANGLE_TAG << + " v1=\"" << boost::spirit::int_ << + "\" v2=\"" << boost::spirit::int_ << + "\" v3=\"" << boost::spirit::int_ << "\"", + idx[is_left_handed ? 2 : 0] + volume_it->second.first_vertex_id, + idx[1] + volume_it->second.first_vertex_id, + idx[is_left_handed ? 0 : 2] + volume_it->second.first_vertex_id); +#else boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << TRIANGLE_TAG << " v1=\"" << boost::spirit::int_ << "\" v2=\"" << boost::spirit::int_ << @@ -2520,6 +2546,7 @@ namespace Slic3r { idx[0] + volume_it->second.first_vertex_id, idx[1] + volume_it->second.first_vertex_id, idx[2] + volume_it->second.first_vertex_id); +#endif // ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT *ptr = '\0'; output_buffer += buf; } diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index f6366c18e7..d337fdc2d7 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -47,6 +47,8 @@ #define ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER (1 && ENABLE_2_4_0_ALPHA0) // Enable drawing contours, at cut level, for sinking volumes #define ENABLE_SINKING_CONTOURS (1 && ENABLE_2_4_0_ALPHA0) +// Enable the fix for exporting and importing to/from 3mf file of mirrored volumes +#define ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT (1 && ENABLE_2_4_0_ALPHA0) #endif // _prusaslicer_technologies_h_ From dbaaa35534c54d0d5ab8c858b29be7712fff222b Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 26 Aug 2021 10:38:14 +0200 Subject: [PATCH 014/151] Tech ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED -> 1st installment: Allow to save/load empty projects --- src/libslic3r/Format/3mf.cpp | 7 +++++++ src/libslic3r/Model.cpp | 2 ++ src/libslic3r/Technologies.hpp | 2 ++ src/slic3r/GUI/GLCanvas3D.cpp | 15 +++++++++++++-- src/slic3r/GUI/MainFrame.cpp | 11 +++++++++++ src/slic3r/GUI/Plater.cpp | 8 ++++++++ 6 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index ed2d5d9c36..d6f14f643b 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -2577,9 +2577,16 @@ namespace Slic3r { bool _3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) { +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + // This happens for empty projects +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED if (build_items.size() == 0) { add_error("No build item found"); +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + return true; +#else return false; +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED } stream << " <" << BUILD_TAG << ">\n"; diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 6654d3a130..7d551e2c31 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -161,8 +161,10 @@ Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig if (!result) throw Slic3r::RuntimeError("Loading of a model file failed."); +#if !ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED if (model.objects.empty()) throw Slic3r::RuntimeError("The supplied file couldn't be read because it's empty"); +#endif // !ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED for (ModelObject *o : model.objects) { diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 02d8fccd45..57f55bb3b7 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -49,6 +49,8 @@ #define ENABLE_SINKING_CONTOURS (1 && ENABLE_2_4_0_ALPHA0) // Enable implementation of retract acceleration in gcode processor #define ENABLE_RETRACT_ACCELERATION (1 && ENABLE_2_4_0_ALPHA0) +// Enable save and save as commands to be enabled also when the plater is empty and allow to load empty projects +#define ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED (1 && ENABLE_2_4_0_ALPHA0) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index ee1d3b14a4..2cbf3f6c82 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4108,13 +4108,24 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const } } +#if !ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED if (visible_volumes.empty()) return; +#endif // !ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED BoundingBoxf3 volumes_box; - for (const GLVolume* vol : visible_volumes) { - volumes_box.merge(vol->transformed_bounding_box()); +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + if (!visible_volumes.empty()) { +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + for (const GLVolume* vol : visible_volumes) { + volumes_box.merge(vol->transformed_bounding_box()); + } +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED } + else + // This happens for empty projects + volumes_box = wxGetApp().plater()->get_bed().get_bounding_box(true); +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED Camera camera; camera.set_type(camera_type); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 329a9a62a7..b15bab5796 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -824,15 +824,26 @@ bool MainFrame::can_start_new_project() const bool MainFrame::can_save() const { +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + return (m_plater != nullptr) && + !m_plater->canvas3D()->get_gizmos_manager().is_in_editing_mode(false) && + !m_plater->get_project_filename().empty() && m_plater->is_project_dirty(); +#else return (m_plater != nullptr) && !m_plater->model().objects.empty() && !m_plater->canvas3D()->get_gizmos_manager().is_in_editing_mode(false) && !m_plater->get_project_filename().empty() && m_plater->is_project_dirty(); +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED } bool MainFrame::can_save_as() const { +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + return (m_plater != nullptr) && + !m_plater->canvas3D()->get_gizmos_manager().is_in_editing_mode(false); +#else return (m_plater != nullptr) && !m_plater->model().objects.empty() && !m_plater->canvas3D()->get_gizmos_manager().is_in_editing_mode(false); +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED } void MainFrame::save_project() diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1841220d51..d145aeb75e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5587,8 +5587,16 @@ void Plater::export_amf() bool Plater::export_3mf(const boost::filesystem::path& output_path) { +#if ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED + if (p->model.objects.empty()) { + MessageDialog dialog(nullptr, _L("The plater is empty.\nConfirm you want to save the project ?"), _L("Save project"), wxYES_NO); + if (dialog.ShowModal() != wxID_YES) + return false; + } +#else if (p->model.objects.empty()) return false; +#endif // ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED wxString path; bool export_config = true; From 29804187060529c3fbc55ea737e728fd36be9826 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 26 Aug 2021 12:37:55 +0200 Subject: [PATCH 015/151] ENABLE_SEAMS_USING_INSTANCED_MODELS -> WIP: Render models using glDrawElementsInstanced --- resources/shaders/gouraud_light_instanced.fs | 12 +++ resources/shaders/gouraud_light_instanced.vs | 57 ++++++++++++++ src/libslic3r/Technologies.hpp | 4 - src/slic3r/GUI/GCodeViewer.cpp | 63 ++++++++++++---- src/slic3r/GUI/GCodeViewer.hpp | 28 ++++--- src/slic3r/GUI/GLModel.cpp | 79 ++++++++++++++++++++ src/slic3r/GUI/GLModel.hpp | 3 + src/slic3r/GUI/GLShadersManager.cpp | 14 +++- 8 files changed, 229 insertions(+), 31 deletions(-) create mode 100644 resources/shaders/gouraud_light_instanced.fs create mode 100644 resources/shaders/gouraud_light_instanced.vs diff --git a/resources/shaders/gouraud_light_instanced.fs b/resources/shaders/gouraud_light_instanced.fs new file mode 100644 index 0000000000..970185a00e --- /dev/null +++ b/resources/shaders/gouraud_light_instanced.fs @@ -0,0 +1,12 @@ +#version 110 + +uniform vec4 uniform_color; +uniform float emission_factor; + +// x = tainted, y = specular; +varying vec2 intensity; + +void main() +{ + gl_FragColor = vec4(vec3(intensity.y) + uniform_color.rgb * (intensity.x + emission_factor), uniform_color.a); +} diff --git a/resources/shaders/gouraud_light_instanced.vs b/resources/shaders/gouraud_light_instanced.vs new file mode 100644 index 0000000000..5d6a05a6f0 --- /dev/null +++ b/resources/shaders/gouraud_light_instanced.vs @@ -0,0 +1,57 @@ +#version 110 + +#define INTENSITY_CORRECTION 0.6 + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +// normalized values for (1./1.43, 0.2/1.43, 1./1.43) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +// vertex attributes +in vec3 v_position; +in vec3 v_normal; +// instance attributes +in vec3 i_offset; +in vec2 i_scales; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +// x = tainted, y = specular; +varying vec2 intensity; + +void main() +{ + // First transform the normal into camera space and normalize the result. +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + vec3 eye_normal = normalize(gl_NormalMatrix * v_normal); +// vec3 eye_normal = normalize(gl_NormalMatrix * gl_Normal); +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + + // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. + // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + vec4 world_position = vec4(v_position * vec3(vec2(i_scales.x), i_scales.y) + i_offset, 1.0); + vec3 eye_position = (gl_ModelViewMatrix * world_position).xyz; +// vec3 eye_position = (gl_ModelViewMatrix * gl_Vertex).xyz; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(eye_position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + // Perform the same lighting calculation for the 2nd light source (no specular applied). + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + gl_Position = gl_ProjectionMatrix * vec4(eye_position, 1.0); +// gl_Position = ftransform(); +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +} diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 79976d4a08..0a6b47397f 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -50,10 +50,6 @@ // Enable rendering seams (and other options) in preview using models #define ENABLE_SEAMS_USING_MODELS (1 && ENABLE_2_4_0_ALPHA0) // Enable rendering seams (and other options) in preview using instanced models -// references: -// https://ogldev.org/www/tutorial33/tutorial33.html -// https://docs.gl/gl3/glDrawElementsInstanced -// https://www.khronos.org/opengl/wiki/Vertex_Rendering#Instancing #define ENABLE_SEAMS_USING_INSTANCED_MODELS (1 && ENABLE_SEAMS_USING_MODELS) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b8388f82fa..f045cd72aa 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -102,7 +102,8 @@ void GCodeViewer::InstanceVBuffer::reset() if (vbo > 0) glsafe(::glDeleteBuffers(1, &vbo)); s_ids.clear(); - render_range = { 0, 0 }; + buffer.clear(); + render_ranges.clear(); } #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS @@ -803,7 +804,11 @@ void GCodeViewer::render() #if ENABLE_SEAMS_USING_MODELS if (wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) { buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + buffer.shader = "gouraud_light_instanced"; +#else buffer.shader = "gouraud_light"; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS buffer.model.model.init_from(diamond(16)); buffer.model.color = option_color(type); } @@ -1764,7 +1769,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) #if ENABLE_GCODE_VIEWER_STATISTICS m_statistics.total_instances_gpu_size += static_cast(size_bytes); - m_statistics.max_instance_vbuffer_gpu_size = std::max(m_statistics.max_instance_vbuffer_gpu_size, static_cast(size_bytes)); #endif // ENABLE_GCODE_VIEWER_STATISTICS GLuint id = 0; @@ -1774,6 +1778,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); t_buffer.model.instances2.vbo = id; + t_buffer.model.instances2.buffer = inst_buffer; t_buffer.model.instances2.s_ids = instances_ids[i]; } } @@ -2467,21 +2472,36 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) continue; - buffer.model.instances2.render_range = { 0, 0 }; + buffer.model.instances2.render_ranges.clear(); if (!buffer.visible) continue; + buffer.model.instances2.render_ranges.push_back({ 0, 0, buffer.model.color }); + bool has_second_range = top_layer_only && m_sequential_view.current.last != m_sequential_view.global.last; + if (has_second_range) + buffer.model.instances2.render_ranges.push_back({ 0, 0, Neutral_Color }); + if (m_sequential_view.current.first <= buffer.model.instances2.s_ids.back() && buffer.model.instances2.s_ids.front() <= m_sequential_view.current.last) { for (size_t id : buffer.model.instances2.s_ids) { - if (id <= m_sequential_view.current.first) { - buffer.model.instances2.render_range.offset += buffer.model.instances2.instance_size_bytes(); - buffer.model.instances2.render_range.count = 0; + if (has_second_range) { + if (id <= m_sequential_view.endpoints.first) { + buffer.model.instances2.render_ranges.front().offset += buffer.model.instances2.instance_size_bytes(); + ++buffer.model.instances2.render_ranges.back().count; + } + else if (id <= m_sequential_view.current.last) + ++buffer.model.instances2.render_ranges.front().count; + else + break; + } + else { + if (id <= m_sequential_view.current.first) + buffer.model.instances2.render_ranges.front().offset += buffer.model.instances2.instance_size_bytes(); + else if (id <= m_sequential_view.current.last) + ++buffer.model.instances2.render_ranges.front().count; + else + break; } - else if (id <= m_sequential_view.current.last) - ++buffer.model.instances2.render_range.count; - else - break; } } } @@ -2616,7 +2636,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool } #if ENABLE_SEAMS_USING_MODELS #if ENABLE_SEAMS_USING_INSTANCED_MODELS + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.buffer, float); statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.s_ids, size_t); + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.render_ranges, InstanceVBuffer::RenderRange); #else statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances, TBuffer::Model::Instance); #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS @@ -2711,7 +2733,18 @@ void GCodeViewer::render_toolpaths() #if ENABLE_SEAMS_USING_MODELS auto render_as_instanced_model = [this] - (TBuffer & buffer, GLShaderProgram & shader) { + (TBuffer& buffer, GLShaderProgram & shader) { +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + if (buffer.model.instances2.vbo > 0) { + for (const InstanceVBuffer::RenderRange& range : buffer.model.instances2.render_ranges) { + buffer.model.model.set_color(-1, range.color); + buffer.model.model.render_instanced(buffer.model.instances2.vbo, range.count); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.gl_instanced_models_calls_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + } + } +#else for (const TBuffer::Model::Instance& inst : buffer.model.instances) { bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; bool visible = top_layer_only ? @@ -2732,6 +2765,7 @@ void GCodeViewer::render_toolpaths() #endif // ENABLE_GCODE_VIEWER_STATISTICS } } +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS }; #endif // ENABLE_SEAMS_USING_MODELS @@ -3800,7 +3834,11 @@ void GCodeViewer::render_statistics() add_counter(std::string("Multi GL_TRIANGLES:"), m_statistics.gl_multi_triangles_calls_count); add_counter(std::string("GL_TRIANGLES:"), m_statistics.gl_triangles_calls_count); #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + add_counter(std::string("Instanced models:"), m_statistics.gl_instanced_models_calls_count); +#else add_counter(std::string("Models:"), m_statistics.gl_models_calls_count); +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } @@ -3824,9 +3862,6 @@ void GCodeViewer::render_statistics() ImGui::Separator(); add_memory(std::string("Max VBuffer:"), m_statistics.max_vbuffer_gpu_size); add_memory(std::string("Max IBuffer:"), m_statistics.max_ibuffer_gpu_size); -#if ENABLE_SEAMS_USING_INSTANCED_MODELS - add_memory(std::string("Max instance VBuffer:"), m_statistics.max_instance_vbuffer_gpu_size); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } if (ImGui::CollapsingHeader("Other")) { diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 7bf341a68d..bda0491de2 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -105,8 +105,9 @@ class GCodeViewer }; #if ENABLE_SEAMS_USING_INSTANCED_MODELS - // vbo buffer containing instances data used to render a toolpaths using instanced models - // record format: 5 floats -> position.x|position.y|position.z|width|height + // buffer containing instances data used to render a toolpaths using instanced models + // instance record format: 5 floats -> position.x|position.y|position.z|width|height + // which is sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced() struct InstanceVBuffer { struct RenderRange @@ -115,13 +116,18 @@ class GCodeViewer unsigned int offset; // count of instances to render unsigned int count; + // Color to apply to the instances + Color color; }; // vbo id unsigned int vbo{ 0 }; - // move ids + // cpu-side buffer containing all instances data + InstanceBuffer buffer; + // indices of the moves for all instances std::vector s_ids; - RenderRange render_range; + // ranges used to render only subparts of the intances + std::vector render_ranges; size_t data_size_bytes() const { return s_ids.size() * instance_size_bytes(); } @@ -518,7 +524,11 @@ class GCodeViewer int64_t gl_multi_triangles_calls_count{ 0 }; int64_t gl_triangles_calls_count{ 0 }; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + int64_t gl_instanced_models_calls_count{ 0 }; +#else int64_t gl_models_calls_count{ 0 }; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS // memory int64_t results_size{ 0 }; @@ -529,9 +539,6 @@ class GCodeViewer #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS int64_t max_vbuffer_gpu_size{ 0 }; int64_t max_ibuffer_gpu_size{ 0 }; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS - int64_t max_instance_vbuffer_gpu_size{ 0 }; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS int64_t paths_size{ 0 }; int64_t render_paths_size{ 0 }; #if ENABLE_SEAMS_USING_MODELS @@ -570,7 +577,11 @@ class GCodeViewer gl_multi_triangles_calls_count = 0; gl_triangles_calls_count = 0; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + gl_instanced_models_calls_count = 0; +#else gl_models_calls_count = 0; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } @@ -583,9 +594,6 @@ class GCodeViewer #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS max_vbuffer_gpu_size = 0; max_ibuffer_gpu_size = 0; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS - max_instance_vbuffer_gpu_size = 0; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS paths_size = 0; render_paths_size = 0; #if ENABLE_SEAMS_USING_MODELS diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index c4437a87ff..58dfcb68b8 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -207,6 +207,85 @@ void GLModel::render() const } } +#if ENABLE_SEAMS_USING_INSTANCED_MODELS +void GLModel::render_instanced(unsigned int instances_vbo, unsigned int instances_count) const +{ + if (instances_vbo == 0) + return; + + GLShaderProgram* shader = wxGetApp().get_current_shader(); + assert(shader == nullptr || boost::algorithm::iends_with(shader->get_name(), "_instanced")); + + // vertex attributes + GLint position_id = (shader != nullptr) ? shader->get_attrib_location("v_position") : -1; + GLint normal_id = (shader != nullptr) ? shader->get_attrib_location("v_normal") : -1; + assert(position_id != -1 && normal_id != -1); + + // instance attributes + GLint offset_id = (shader != nullptr) ? shader->get_attrib_location("i_offset") : -1; + GLint scales_id = (shader != nullptr) ? shader->get_attrib_location("i_scales") : -1; + assert(offset_id != -1 && scales_id != -1); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, instances_vbo)); + if (offset_id != -1) { + glsafe(::glVertexAttribPointer(offset_id, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (GLvoid*)0)); + glsafe(::glEnableVertexAttribArray(offset_id)); + glsafe(::glVertexAttribDivisor(offset_id, 1)); + } + if (scales_id != -1) { + glsafe(::glVertexAttribPointer(scales_id, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (GLvoid*)(3 * sizeof(float)))); + glsafe(::glEnableVertexAttribArray(scales_id)); + glsafe(::glVertexAttribDivisor(scales_id, 1)); + } + + for (const RenderData& data : m_render_data) { + if (data.vbo_id == 0 || data.ibo_id == 0) + continue; + + GLenum mode; + switch (data.type) + { + default: + case PrimitiveType::Triangles: { mode = GL_TRIANGLES; break; } + case PrimitiveType::Lines: { mode = GL_LINES; break; } + case PrimitiveType::LineStrip: { mode = GL_LINE_STRIP; break; } + case PrimitiveType::LineLoop: { mode = GL_LINE_LOOP; break; } + } + + if (shader != nullptr) + shader->set_uniform("uniform_color", data.color); + else + glsafe(::glColor4fv(data.color.data())); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, data.vbo_id)); + if (position_id != -1) { + glsafe(::glVertexAttribPointer(position_id, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)0)); + glsafe(::glEnableVertexAttribArray(position_id)); + } + if (normal_id != -1) { + glsafe(::glVertexAttribPointer(normal_id, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)(3 * sizeof(float)))); + glsafe(::glEnableVertexAttribArray(normal_id)); + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, data.ibo_id)); + glsafe(::glDrawElementsInstanced(mode, static_cast(data.indices_count), GL_UNSIGNED_INT, (const void*)0, instances_count)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + if (normal_id != -1) + glsafe(::glDisableVertexAttribArray(normal_id)); + if (position_id != -1) + glsafe(::glDisableVertexAttribArray(position_id)); + } + + if (scales_id != -1) + glsafe(::glDisableVertexAttribArray(scales_id)); + if (offset_id != -1) + glsafe(::glDisableVertexAttribArray(offset_id)); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); +} +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS + void GLModel::send_to_gpu(RenderData& data, const std::vector& vertices, const std::vector& indices) { assert(data.vbo_id == 0); diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index 99c4bf4425..73fb908271 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -72,6 +72,9 @@ namespace GUI { void reset(); void render() const; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + void render_instanced(unsigned int instances_vbo, unsigned int instances_count) const; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS bool is_initialized() const { return !m_render_data.empty(); } diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index 788fe90c0c..7bb0a50a6f 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -38,9 +38,17 @@ std::pair GLShadersManager::init() // used to render printbed valid &= append_shader("printbed", { "printbed.vs", "printbed.fs" }); // used to render options in gcode preview - valid &= append_shader("options_110", { "options_110.vs", "options_110.fs" }); - if (GUI::wxGetApp().is_glsl_version_greater_or_equal_to(1, 20)) - valid &= append_shader("options_120", { "options_120.vs", "options_120.fs" }); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) + valid &= append_shader("gouraud_light_instanced", { "gouraud_light_instanced.vs", "gouraud_light_instanced.fs" }); + else { +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS + valid &= append_shader("options_110", { "options_110.vs", "options_110.fs" }); + if (GUI::wxGetApp().is_glsl_version_greater_or_equal_to(1, 20)) + valid &= append_shader("options_120", { "options_120.vs", "options_120.fs" }); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + } +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS // used to render extrusion and travel paths as lines in gcode preview valid &= append_shader("toolpaths_lines", { "toolpaths_lines.vs", "toolpaths_lines.fs" }); // used to render objects in 3d editor From c471c3c7c41b3163a276e3fd2aaa2a312d9185f3 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 26 Aug 2021 14:37:09 +0200 Subject: [PATCH 016/151] Code cleanup and small refactoring --- src/slic3r/GUI/GCodeViewer.cpp | 81 ++++++++++++++++++---------------- src/slic3r/GUI/GCodeViewer.hpp | 11 ++++- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index f045cd72aa..b6cac40591 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -151,9 +151,10 @@ bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const #if ENABLE_SEAMS_USING_MODELS void GCodeViewer::TBuffer::Model::reset() { - instances.clear(); #if ENABLE_SEAMS_USING_INSTANCED_MODELS - instances2.reset(); + instances.reset(); +#else + instances.clear(); #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } #endif // ENABLE_SEAMS_USING_MODELS @@ -1432,19 +1433,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) }; #if ENABLE_SEAMS_USING_MODELS - // format data into the buffers to be rendered as model - auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, TBuffer::Model::Instances& instances, size_t move_id) { - TBuffer::Model::Instance instance; - instance.position = curr.position; - instance.width = 1.2f * curr.width; - instance.height = 1.2f * curr.height; - instance.s_id = move_id; - instances.emplace_back(instance); - }; - #if ENABLE_SEAMS_USING_INSTANCED_MODELS // format data into the buffers to be rendered as model - auto add_model_instance_2 = [](const GCodeProcessor::MoveVertex& curr, InstanceBuffer& instances, InstanceIdBuffer& instances_ids, size_t move_id) { + auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, InstanceBuffer& instances, InstanceIdBuffer& instances_ids, size_t move_id) { // append position instances.push_back(curr.position.x()); instances.push_back(curr.position.y()); @@ -1457,6 +1448,15 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // append id instances_ids.push_back(move_id); }; +#else + auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, TBuffer::Model::Instances& instances, size_t move_id) { + TBuffer::Model::Instance instance; + instance.position = curr.position; + instance.width = 1.2f * curr.width; + instance.height = 1.2f * curr.height; + instance.s_id = move_id; + instances.emplace_back(instance); + }; #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS @@ -1560,9 +1560,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) #if ENABLE_SEAMS_USING_MODELS case TBuffer::ERenderPrimitiveType::Model: { - add_model_instance(curr, t_buffer.model.instances, i); #if ENABLE_SEAMS_USING_INSTANCED_MODELS - add_model_instance_2(curr, inst_buffer, inst_id_buffer, i); + add_model_instance(curr, inst_buffer, inst_id_buffer, i); +#else + add_model_instance(curr, t_buffer.model.instances, i); #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.instances_count; @@ -1741,12 +1742,14 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } #if ENABLE_SEAMS_USING_MODELS +#if !ENABLE_SEAMS_USING_INSTANCED_MODELS for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { t_buffer.model.instances.shrink_to_fit(); } } +#endif // !ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS // move the wipe toolpaths half height up to render them on proper position @@ -1777,9 +1780,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) glsafe(::glBufferData(GL_ARRAY_BUFFER, size_bytes, inst_buffer.data(), GL_STATIC_DRAW)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); - t_buffer.model.instances2.vbo = id; - t_buffer.model.instances2.buffer = inst_buffer; - t_buffer.model.instances2.s_ids = instances_ids[i]; + t_buffer.model.instances.vbo = id; + t_buffer.model.instances.buffer = inst_buffer; + t_buffer.model.instances.s_ids = instances_ids[i]; } } else { @@ -2213,7 +2216,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_SEAMS_USING_MODELS if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { #if ENABLE_SEAMS_USING_INSTANCED_MODELS - for (size_t id : buffer.model.instances2.s_ids) { + for (size_t id : buffer.model.instances.s_ids) { if (id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < id) continue; @@ -2296,12 +2299,12 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_SEAMS_USING_MODELS if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { #if ENABLE_SEAMS_USING_INSTANCED_MODELS - for (size_t i = 0; i < buffer.model.instances2.s_ids.size(); ++i) { - if (buffer.model.instances2.s_ids[i] == m_sequential_view.current.last) { + for (size_t i = 0; i < buffer.model.instances.s_ids.size(); ++i) { + if (buffer.model.instances.s_ids[i] == m_sequential_view.current.last) { // gets the position from the vertices buffer on gpu - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.model.instances2.vbo)); - glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(i * buffer.model.instances2.instance_size_bytes()), static_cast(3 * sizeof(float)), static_cast(sequential_view->current_position.data()))); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.model.instances.vbo)); + glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(i * buffer.model.instances.instance_size_bytes()), static_cast(3 * sizeof(float)), static_cast(sequential_view->current_position.data()))); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); found = true; @@ -2472,33 +2475,33 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) continue; - buffer.model.instances2.render_ranges.clear(); + buffer.model.instances.render_ranges.clear(); if (!buffer.visible) continue; - buffer.model.instances2.render_ranges.push_back({ 0, 0, buffer.model.color }); + buffer.model.instances.render_ranges.push_back({ 0, 0, buffer.model.color }); bool has_second_range = top_layer_only && m_sequential_view.current.last != m_sequential_view.global.last; if (has_second_range) - buffer.model.instances2.render_ranges.push_back({ 0, 0, Neutral_Color }); + buffer.model.instances.render_ranges.push_back({ 0, 0, Neutral_Color }); - if (m_sequential_view.current.first <= buffer.model.instances2.s_ids.back() && buffer.model.instances2.s_ids.front() <= m_sequential_view.current.last) { - for (size_t id : buffer.model.instances2.s_ids) { + if (m_sequential_view.current.first <= buffer.model.instances.s_ids.back() && buffer.model.instances.s_ids.front() <= m_sequential_view.current.last) { + for (size_t id : buffer.model.instances.s_ids) { if (has_second_range) { if (id <= m_sequential_view.endpoints.first) { - buffer.model.instances2.render_ranges.front().offset += buffer.model.instances2.instance_size_bytes(); - ++buffer.model.instances2.render_ranges.back().count; + buffer.model.instances.render_ranges.front().offset += buffer.model.instances.instance_size_bytes(); + ++buffer.model.instances.render_ranges.back().count; } else if (id <= m_sequential_view.current.last) - ++buffer.model.instances2.render_ranges.front().count; + ++buffer.model.instances.render_ranges.front().count; else break; } else { if (id <= m_sequential_view.current.first) - buffer.model.instances2.render_ranges.front().offset += buffer.model.instances2.instance_size_bytes(); + buffer.model.instances.render_ranges.front().offset += buffer.model.instances.instance_size_bytes(); else if (id <= m_sequential_view.current.last) - ++buffer.model.instances2.render_ranges.front().count; + ++buffer.model.instances.render_ranges.front().count; else break; } @@ -2636,9 +2639,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool } #if ENABLE_SEAMS_USING_MODELS #if ENABLE_SEAMS_USING_INSTANCED_MODELS - statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.buffer, float); - statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.s_ids, size_t); - statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances2.render_ranges, InstanceVBuffer::RenderRange); + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.buffer, float); + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.s_ids, size_t); + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.render_ranges, InstanceVBuffer::RenderRange); #else statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances, TBuffer::Model::Instance); #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS @@ -2735,10 +2738,10 @@ void GCodeViewer::render_toolpaths() auto render_as_instanced_model = [this] (TBuffer& buffer, GLShaderProgram & shader) { #if ENABLE_SEAMS_USING_INSTANCED_MODELS - if (buffer.model.instances2.vbo > 0) { - for (const InstanceVBuffer::RenderRange& range : buffer.model.instances2.render_ranges) { + if (buffer.model.instances.vbo > 0) { + for (const InstanceVBuffer::RenderRange& range : buffer.model.instances.render_ranges) { buffer.model.model.set_color(-1, range.color); - buffer.model.model.render_instanced(buffer.model.instances2.vbo, range.count); + buffer.model.model.render_instanced(buffer.model.instances.vbo, range.count); #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.gl_instanced_models_calls_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index bda0491de2..3e5e50c82d 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -288,6 +288,7 @@ class GCodeViewer #if ENABLE_SEAMS_USING_MODELS struct Model { +#if !ENABLE_SEAMS_USING_INSTANCED_MODELS struct Instance { Vec3f position; @@ -296,12 +297,14 @@ class GCodeViewer size_t s_id; }; using Instances = std::vector; +#endif // !ENABLE_SEAMS_USING_INSTANCED_MODELS GLModel model; Color color; - Instances instances; #if ENABLE_SEAMS_USING_INSTANCED_MODELS - InstanceVBuffer instances2; + InstanceVBuffer instances; +#else + Instances instances; #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS void reset(); @@ -367,7 +370,11 @@ class GCodeViewer case ERenderPrimitiveType::Triangle: { return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; } +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + case ERenderPrimitiveType::Model: { return model.model.is_initialized() && !model.instances.buffer.empty(); } +#else case ERenderPrimitiveType::Model: { return model.model.is_initialized() && !model.instances.empty(); } +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS default: { return false; } } } From 03dc2f7c11b128cbef4610f0f70063bf930eff35 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 27 Aug 2021 10:16:50 +0200 Subject: [PATCH 017/151] ENABLE_SEAMS_USING_INSTANCED_MODELS -> Completed implementation of rendering models using instancing --- src/slic3r/GUI/GCodeViewer.cpp | 83 ++++++++++++++++++++-------------- src/slic3r/GUI/GCodeViewer.hpp | 29 +++++++----- 2 files changed, 67 insertions(+), 45 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b6cac40591..c672f90609 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -96,14 +96,22 @@ void GCodeViewer::VBuffer::reset() } #if ENABLE_SEAMS_USING_INSTANCED_MODELS +void GCodeViewer::InstanceVBuffer::Ranges::reset() +{ + for (Range& range : ranges) { + // release gpu memory + if (range.vbo > 0) + glsafe(::glDeleteBuffers(1, &range.vbo)); + } + + ranges.clear(); +} + void GCodeViewer::InstanceVBuffer::reset() { - // release gpu memory - if (vbo > 0) - glsafe(::glDeleteBuffers(1, &vbo)); s_ids.clear(); buffer.clear(); - render_ranges.clear(); + render_ranges.reset(); } #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS @@ -862,6 +870,9 @@ void GCodeViewer::render() #if ENABLE_GCODE_VIEWER_STATISTICS m_statistics.reset_opengl(); +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + m_statistics.total_instances_gpu_size = 0; +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_GCODE_VIEWER_STATISTICS // OpenGL data must be initialized after the glContext has been created. @@ -1760,7 +1771,11 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } } +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + // send vertices data to gpu, where needed +#else // send vertices data to gpu +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; #if ENABLE_SEAMS_USING_MODELS @@ -1768,19 +1783,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { const InstanceBuffer& inst_buffer = instances[i]; if (!inst_buffer.empty()) { - const size_t size_bytes = inst_buffer.size() * sizeof(float); - -#if ENABLE_GCODE_VIEWER_STATISTICS - m_statistics.total_instances_gpu_size += static_cast(size_bytes); -#endif // ENABLE_GCODE_VIEWER_STATISTICS - - GLuint id = 0; - glsafe(::glGenBuffers(1, &id)); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, id)); - glsafe(::glBufferData(GL_ARRAY_BUFFER, size_bytes, inst_buffer.data(), GL_STATIC_DRAW)); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); - - t_buffer.model.instances.vbo = id; t_buffer.model.instances.buffer = inst_buffer; t_buffer.model.instances.s_ids = instances_ids[i]; } @@ -2293,7 +2295,11 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool sequential_view->current.first = !top_layer_only && keep_sequential_current_first ? std::clamp(sequential_view->current.first, global_endpoints.first, global_endpoints.last) : global_endpoints.first; sequential_view->current.last = keep_sequential_current_last ? std::clamp(sequential_view->current.last, global_endpoints.first, global_endpoints.last) : global_endpoints.last; +#if ENABLE_SEAMS_USING_INSTANCED_MODELS + // get the world position from the vertex buffer +#else // get the world position from gpu +#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS bool found = false; for (const TBuffer& buffer : m_buffers) { #if ENABLE_SEAMS_USING_MODELS @@ -2301,11 +2307,10 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_SEAMS_USING_INSTANCED_MODELS for (size_t i = 0; i < buffer.model.instances.s_ids.size(); ++i) { if (buffer.model.instances.s_ids[i] == m_sequential_view.current.last) { - - // gets the position from the vertices buffer on gpu - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.model.instances.vbo)); - glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(i * buffer.model.instances.instance_size_bytes()), static_cast(3 * sizeof(float)), static_cast(sequential_view->current_position.data()))); - glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + size_t offset = i * buffer.model.instances.instance_size_floats(); + sequential_view->current_position.x() = buffer.model.instances.buffer[offset + 0]; + sequential_view->current_position.y() = buffer.model.instances.buffer[offset + 1]; + sequential_view->current_position.z() = buffer.model.instances.buffer[offset + 2]; found = true; break; @@ -2475,33 +2480,33 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) continue; - buffer.model.instances.render_ranges.clear(); + buffer.model.instances.render_ranges.reset(); if (!buffer.visible) continue; - buffer.model.instances.render_ranges.push_back({ 0, 0, buffer.model.color }); + buffer.model.instances.render_ranges.ranges.push_back({ 0, 0, 0, buffer.model.color }); bool has_second_range = top_layer_only && m_sequential_view.current.last != m_sequential_view.global.last; if (has_second_range) - buffer.model.instances.render_ranges.push_back({ 0, 0, Neutral_Color }); + buffer.model.instances.render_ranges.ranges.push_back({ 0, 0, 0, Neutral_Color }); if (m_sequential_view.current.first <= buffer.model.instances.s_ids.back() && buffer.model.instances.s_ids.front() <= m_sequential_view.current.last) { for (size_t id : buffer.model.instances.s_ids) { if (has_second_range) { if (id <= m_sequential_view.endpoints.first) { - buffer.model.instances.render_ranges.front().offset += buffer.model.instances.instance_size_bytes(); - ++buffer.model.instances.render_ranges.back().count; + ++buffer.model.instances.render_ranges.ranges.front().offset; + ++buffer.model.instances.render_ranges.ranges.back().count; } else if (id <= m_sequential_view.current.last) - ++buffer.model.instances.render_ranges.front().count; + ++buffer.model.instances.render_ranges.ranges.front().count; else break; } else { if (id <= m_sequential_view.current.first) - buffer.model.instances.render_ranges.front().offset += buffer.model.instances.instance_size_bytes(); + ++buffer.model.instances.render_ranges.ranges.front().offset; else if (id <= m_sequential_view.current.last) - ++buffer.model.instances.render_ranges.front().count; + ++buffer.model.instances.render_ranges.ranges.front().count; else break; } @@ -2641,7 +2646,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_SEAMS_USING_INSTANCED_MODELS statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.buffer, float); statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.s_ids, size_t); - statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.render_ranges, InstanceVBuffer::RenderRange); + statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.render_ranges.ranges, InstanceVBuffer::Ranges::Range); #else statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances, TBuffer::Model::Instance); #endif // ENABLE_SEAMS_USING_INSTANCED_MODELS @@ -2738,12 +2743,22 @@ void GCodeViewer::render_toolpaths() auto render_as_instanced_model = [this] (TBuffer& buffer, GLShaderProgram & shader) { #if ENABLE_SEAMS_USING_INSTANCED_MODELS - if (buffer.model.instances.vbo > 0) { - for (const InstanceVBuffer::RenderRange& range : buffer.model.instances.render_ranges) { + for (auto& range : buffer.model.instances.render_ranges.ranges) { + if (range.vbo == 0 && range.count > 0) { + std::cout << range.offset * buffer.model.instances.instance_size_bytes() << " + " << range.count * buffer.model.instances.instance_size_bytes() << " = " << buffer.model.instances.buffer.size() * sizeof(float) << "\n"; + + glsafe(::glGenBuffers(1, &range.vbo)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, range.vbo)); + glsafe(::glBufferData(GL_ARRAY_BUFFER, range.count * buffer.model.instances.instance_size_bytes(), (const void*)&buffer.model.instances.buffer[range.offset * buffer.model.instances.instance_size_floats()], GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + } + + if (range.vbo > 0) { buffer.model.model.set_color(-1, range.color); - buffer.model.model.render_instanced(buffer.model.instances.vbo, range.count); + buffer.model.model.render_instanced(range.vbo, range.count); #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.gl_instanced_models_calls_count; + m_statistics.total_instances_gpu_size += static_cast(range.count * buffer.model.instances.instance_size_bytes()); #endif // ENABLE_GCODE_VIEWER_STATISTICS } } diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 3e5e50c82d..46f6558dda 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -110,24 +110,31 @@ class GCodeViewer // which is sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced() struct InstanceVBuffer { - struct RenderRange + // ranges used to render only subparts of the intances + struct Ranges { - // offset in bytes of the 1st instance to render - unsigned int offset; - // count of instances to render - unsigned int count; - // Color to apply to the instances - Color color; + struct Range + { + // offset in bytes of the 1st instance to render + unsigned int offset; + // count of instances to render + unsigned int count; + // vbo id + unsigned int vbo{ 0 }; + // Color to apply to the instances + Color color; + }; + + std::vector ranges; + + void reset(); }; - // vbo id - unsigned int vbo{ 0 }; // cpu-side buffer containing all instances data InstanceBuffer buffer; // indices of the moves for all instances std::vector s_ids; - // ranges used to render only subparts of the intances - std::vector render_ranges; + Ranges render_ranges; size_t data_size_bytes() const { return s_ids.size() * instance_size_bytes(); } From ae8e0311d796f82b701083677a53962f8af9487c Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 27 Aug 2021 11:25:50 +0200 Subject: [PATCH 018/151] debugging function debug_output_path() moved to utils.cpp/hpp and it now prints to console the default path when called for the first time. Fixed compilation of debugging output in SupportMaterial. --- src/libslic3r/SupportMaterial.cpp | 105 +++++++++++++++--------------- src/libslic3r/Utils.hpp | 5 ++ src/libslic3r/libslic3r.h | 12 ---- src/libslic3r/utils.cpp | 17 +++++ 4 files changed, 75 insertions(+), 64 deletions(-) diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index e2d7bf1995..c9794a6c88 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -37,6 +37,7 @@ #define DEBUG #define _DEBUG #undef NDEBUG + #include "utils.hpp" #include "SVG.hpp" #endif @@ -429,7 +430,7 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) for (const MyLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( debug_out_path("support-top-contacts-%d-%lf.svg", iRun, layer->print_z), - union_ex(layer->polygons, false)); + union_ex(layer->polygons)); #endif /* SLIC3R_DEBUG */ BOOST_LOG_TRIVIAL(info) << "Support generator - Creating bottom contacts"; @@ -447,7 +448,7 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) for (size_t layer_id = 0; layer_id < object.layers().size(); ++ layer_id) Slic3r::SVG::export_expolygons( debug_out_path("support-areas-%d-%lf.svg", iRun, object.layers()[layer_id]->print_z), - union_ex(layer_support_areas[layer_id], false)); + union_ex(layer_support_areas[layer_id])); #endif /* SLIC3R_DEBUG */ BOOST_LOG_TRIVIAL(info) << "Support generator - Creating intermediate layers - indices"; @@ -466,7 +467,7 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) for (const MyLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), - union_ex(layer->polygons, false)); + union_ex(layer->polygons)); #endif BOOST_LOG_TRIVIAL(info) << "Support generator - Creating base layers"; @@ -478,7 +479,7 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) for (MyLayersPtr::const_iterator it = intermediate_layers.begin(); it != intermediate_layers.end(); ++ it) Slic3r::SVG::export_expolygons( debug_out_path("support-base-layers-%d-%lf.svg", iRun, (*it)->print_z), - union_ex((*it)->polygons, false)); + union_ex((*it)->polygons)); #endif /* SLIC3R_DEBUG */ BOOST_LOG_TRIVIAL(info) << "Support generator - Trimming top contacts by bottom contacts"; @@ -507,11 +508,11 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) for (const MyLayer *l : interface_layers) Slic3r::SVG::export_expolygons( debug_out_path("support-interface-layers-%d-%lf.svg", iRun, l->print_z), - union_ex(l->polygons, false)); + union_ex(l->polygons)); for (const MyLayer *l : base_interface_layers) Slic3r::SVG::export_expolygons( debug_out_path("support-base-interface-layers-%d-%lf.svg", iRun, l->print_z), - union_ex(l->polygons, false)); + union_ex(l->polygons)); #endif // SLIC3R_DEBUG /* @@ -1308,9 +1309,9 @@ namespace SupportMaterialInternal { #ifdef SLIC3R_DEBUG static int iRun = 0; SVG::export_expolygons(debug_out_path("support-top-contacts-remove-bridges-run%d.svg", iRun ++), - { { { union_ex(offset(layerm->unsupported_bridge_edges, scale_(SUPPORT_MATERIAL_MARGIN), SUPPORT_SURFACES_OFFSET_PARAMETERS), false) }, { "unsupported_bridge_edges", "orange", 0.5f } }, - { { union_ex(contact_polygons, false) }, { "contact_polygons", "blue", 0.5f } }, - { { union_ex(bridges, false) }, { "bridges", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(offset(layerm->unsupported_bridge_edges, scale_(SUPPORT_MATERIAL_MARGIN), SUPPORT_SURFACES_OFFSET_PARAMETERS)) }, { "unsupported_bridge_edges", "orange", 0.5f } }, + { { union_ex(contact_polygons) }, { "contact_polygons", "blue", 0.5f } }, + { { union_ex(bridges) }, { "bridges", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ } } @@ -1494,7 +1495,7 @@ static inline std::tuple detect_overhangs( iRun, layer_id, std::find_if(layer.regions().begin(), layer.regions().end(), [layerm](const LayerRegion* other){return other == layerm;}) - layer.regions().begin()), get_extents(diff_polygons)); - Slic3r::ExPolygons expolys = union_ex(diff_polygons, false); + Slic3r::ExPolygons expolys = union_ex(diff_polygons); svg.draw(expolys); } #endif /* SLIC3R_DEBUG */ @@ -1512,7 +1513,7 @@ static inline std::tuple detect_overhangs( iRun, layer_id, std::find_if(layer.regions().begin(), layer.regions().end(), [layerm](const LayerRegion* other){return other == layerm;}) - layer.regions().begin(), layer.print_z), - union_ex(diff_polygons, false)); + union_ex(diff_polygons)); #endif /* SLIC3R_DEBUG */ //FIXME the overhang_polygons are used to construct the support towers as well. @@ -1572,10 +1573,10 @@ static inline std::tuple detect_overhangs( offset(lower_layer_polygons, 0.05f * fw, SUPPORT_SURFACES_OFFSET_PARAMETERS)); #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-top-contacts-enforcers-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), - { { layer.lslices, { "layer.lslices", "gray", 0.2f } }, - { { union_ex(lower_layer_polygons, false) }, { "lower_layer_polygons", "green", 0.5f } }, - { enforcers_united, { "enforcers", "blue", 0.5f } }, - { { union_ex(enforcer_polygons, true) }, { "new_contacts", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { layer.lslices, { "layer.lslices", "gray", 0.2f } }, + { { union_ex(lower_layer_polygons) }, { "lower_layer_polygons", "green", 0.5f } }, + { enforcers_united, { "enforcers", "blue", 0.5f } }, + { { union_safety_offset_ex(enforcer_polygons) }, { "new_contacts", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ polygons_append(overhang_polygons, enforcer_polygons); polygons_append(contact_polygons, diff(enforcer_polygons, slices_margin.all_polygons.empty() ? slices_margin.polygons : slices_margin.all_polygons)); @@ -1739,18 +1740,18 @@ static inline void fill_contact_layer( ); #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-top-contacts-final0-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), - { { { union_ex(lower_layer_polygons, false) }, { "lower_layer_polygons", "gray", 0.2f } }, - { { union_ex(*new_layer.contact_polygons, false) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, - { { union_ex(slices_margin.polygons, false) }, { "slices_margin_cached", "blue", 0.5f } }, - { { union_ex(dense_interface_polygons, false) }, { "dense_interface_polygons", "green", 0.5f } }, - { { union_ex(new_layer.polygons, true) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(lower_layer_polygons) }, { "lower_layer_polygons", "gray", 0.2f } }, + { { union_ex(*new_layer.contact_polygons) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, + { { union_ex(slices_margin.polygons) }, { "slices_margin_cached", "blue", 0.5f } }, + { { union_ex(dense_interface_polygons) }, { "dense_interface_polygons", "green", 0.5f } }, + { { union_safety_offset_ex(new_layer.polygons) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); //support_grid_pattern.serialize(debug_out_path("support-top-contacts-final-run%d-layer%d-z%f.bin", iRun, layer_id, layer.print_z)); SVG::export_expolygons(debug_out_path("support-top-contacts-final0-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), - { { { union_ex(lower_layer_polygons, false) }, { "lower_layer_polygons", "gray", 0.2f } }, - { { union_ex(*new_layer.contact_polygons, false) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, - { { union_ex(contact_polygons, false) }, { "contact_polygons", "blue", 0.5f } }, - { { union_ex(dense_interface_polygons, false) }, { "dense_interface_polygons", "green", 0.5f } }, - { { union_ex(new_layer.polygons, true) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(lower_layer_polygons) }, { "lower_layer_polygons", "gray", 0.2f } }, + { { union_ex(*new_layer.contact_polygons) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, + { { union_ex(contact_polygons) }, { "contact_polygons", "blue", 0.5f } }, + { { union_ex(dense_interface_polygons) }, { "dense_interface_polygons", "green", 0.5f } }, + { { union_safety_offset_ex(new_layer.polygons) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ } } @@ -1796,11 +1797,11 @@ static inline void fill_contact_layer( #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-top-contacts-final0-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), - { { { union_ex(lower_layer_polygons, false) }, { "lower_layer_polygons", "gray", 0.2f } }, - { { union_ex(*new_layer.contact_polygons, false) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, - { { union_ex(contact_polygons, false) }, { "contact_polygons", "blue", 0.5f } }, - { { union_ex(overhang_polygons, false) }, { "overhang_polygons", "green", 0.5f } }, - { { union_ex(new_layer.polygons, true) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(lower_layer_polygons) }, { "lower_layer_polygons", "gray", 0.2f } }, + { { union_ex(*new_layer.contact_polygons) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, + { { union_ex(contact_polygons) }, { "contact_polygons", "blue", 0.5f } }, + { { union_ex(overhang_polygons) }, { "overhang_polygons", "green", 0.5f } }, + { { union_safety_offset_ex(new_layer.polygons) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ // Even after the contact layer was expanded into a grid, some of the contact islands may be too tiny to be extruded. @@ -1964,10 +1965,10 @@ static inline PrintObjectSupportMaterial::MyLayer* detect_bottom_contacts( Polygons top = collect_region_slices_by_type(layer, stTop); #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-bottom-layers-raw-%d-%lf.svg", iRun, layer.print_z), - { { { union_ex(top, false) }, { "top", "blue", 0.5f } }, - { { union_ex(supports_projected, true) }, { "overhangs", "magenta", 0.5f } }, - { layer.lslices, { "layer.lslices", "green", 0.5f } }, - { { union_ex(polygons_new, true) }, { "polygons_new", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(top) }, { "top", "blue", 0.5f } }, + { { union_safety_offset_ex(supports_projected) }, { "overhangs", "magenta", 0.5f } }, + { layer.lslices, { "layer.lslices", "green", 0.5f } }, + { { union_safety_offset_ex(polygons_new) }, { "polygons_new", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ // Now find whether any projection of the contact surfaces above layer.print_z not yet supported by any @@ -2037,7 +2038,7 @@ static inline PrintObjectSupportMaterial::MyLayer* detect_bottom_contacts( #ifdef SLIC3R_DEBUG Slic3r::SVG::export_expolygons( debug_out_path("support-bottom-contacts-%d-%lf.svg", iRun, layer_new.print_z), - union_ex(layer_new.polygons, false)); + union_ex(layer_new.polygons)); #endif /* SLIC3R_DEBUG */ // Trim the already created base layers above the current layer intersecting with the new bottom contacts layer. @@ -2050,14 +2051,14 @@ static inline PrintObjectSupportMaterial::MyLayer* detect_bottom_contacts( if (! layer_support_areas[layer_id_above].empty()) { #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-support-areas-raw-before-trimming-%d-with-%f-%lf.svg", iRun, layer.print_z, layer_above.print_z), - { { { union_ex(touching, false) }, { "touching", "blue", 0.5f } }, - { { union_ex(layer_support_areas[layer_id_above], true) }, { "above", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(touching) }, { "touching", "blue", 0.5f } }, + { { union_safety_offset_ex(layer_support_areas[layer_id_above]) }, { "above", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ layer_support_areas[layer_id_above] = diff(layer_support_areas[layer_id_above], touching); #ifdef SLIC3R_DEBUG Slic3r::SVG::export_expolygons( debug_out_path("support-support-areas-raw-after-trimming-%d-with-%f-%lf.svg", iRun, layer.print_z, layer_above.print_z), - union_ex(layer_support_areas[layer_id_above], false)); + union_ex(layer_support_areas[layer_id_above])); #endif /* SLIC3R_DEBUG */ } } @@ -2080,8 +2081,8 @@ static inline std::pair project_support_to_grid(const Layer #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-support-areas-%s-raw-%d-%lf.svg", debug_name, iRun, layer.print_z), - { { { union_ex(trimming, false) }, { "trimming", "blue", 0.5f } }, - { { union_ex(overhangs_projection, true) }, { "overhangs_projection", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(trimming) }, { "trimming", "blue", 0.5f } }, + { { union_safety_offset_ex(overhangs_projection) }, { "overhangs_projection", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ remove_sticks(overhangs_projection); @@ -2089,8 +2090,8 @@ static inline std::pair project_support_to_grid(const Layer #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-support-areas-%s-raw-cleaned-%d-%lf.svg", debug_name, iRun, layer.print_z), - { { { union_ex(trimming, false) }, { "trimming", "blue", 0.5f } }, - { { union_ex(overhangs_projection, false) }, { "overhangs_projection", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(trimming) }, { "trimming", "blue", 0.5f } }, + { { union_ex(overhangs_projection) }, { "overhangs_projection", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ SupportGridPattern support_grid_pattern(&overhangs_projection, &trimming, grid_params); @@ -2113,7 +2114,7 @@ static inline std::pair project_support_to_grid(const Layer #ifdef SLIC3R_DEBUG Slic3r::SVG::export_expolygons( debug_out_path("support-layer_support_area-gridded-%s-%d-%lf.svg", debug_name, iRun, layer.print_z), - union_ex(out.first, false)); + union_ex(out.first)); #endif /* SLIC3R_DEBUG */ }); @@ -2131,13 +2132,13 @@ static inline std::pair project_support_to_grid(const Layer #ifdef SLIC3R_DEBUG Slic3r::SVG::export_expolygons( debug_out_path("support-projection_new-gridded-%d-%lf.svg", iRun, layer.print_z), - union_ex(out.second, false)); + union_ex(out.second)); #endif /* SLIC3R_DEBUG */ #ifdef SLIC3R_DEBUG SVG::export_expolygons(debug_out_path("support-projection_new-gridded-%d-%lf.svg", iRun, layer.print_z), - { { { union_ex(trimming, false) }, { "trimming", "gray", 0.5f } }, - { { union_ex(overhangs_projection, true) }, { "overhangs_projection", "blue", 0.5f } }, - { { union_ex(out.second, true) }, { "projection_new", "red", "black", "", scaled(0.1f), 0.5f } } }); + { { { union_ex(trimming) }, { "trimming", "gray", 0.5f } }, + { { union_safety_offset_ex(overhangs_projection) }, { "overhangs_projection", "blue", 0.5f } }, + { { union_safety_offset_ex(out.second) }, { "projection_new", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ }); @@ -2667,10 +2668,10 @@ void PrintObjectSupportMaterial::generate_base_layers( BoundingBox bbox = get_extents(polygons_new); bbox.merge(get_extents(polygons_trimming)); ::Slic3r::SVG svg(debug_out_path("support-intermediate-layers-raw-%d-%lf.svg", iRun, layer_intermediate.print_z), bbox); - svg.draw(union_ex(polygons_new, false), "blue", 0.5f); - svg.draw(to_polylines(polygons_new), "blue"); - svg.draw(union_ex(polygons_trimming, true), "red", 0.5f); - svg.draw(to_polylines(polygons_trimming), "red"); + svg.draw(union_ex(polygons_new), "blue", 0.5f); + svg.draw(to_polylines(polygons_new), "blue"); + svg.draw(union_safety_offset_ex(polygons_trimming), "red", 0.5f); + svg.draw(to_polylines(polygons_trimming), "red"); } #endif /* SLIC3R_DEBUG */ @@ -2706,7 +2707,7 @@ void PrintObjectSupportMaterial::generate_base_layers( for (MyLayersPtr::const_iterator it = intermediate_layers.begin(); it != intermediate_layers.end(); ++it) ::Slic3r::SVG::export_expolygons( debug_out_path("support-intermediate-layers-untrimmed-%d-%lf.svg", iRun, (*it)->print_z), - union_ex((*it)->polygons, false)); + union_ex((*it)->polygons)); ++ iRun; #endif /* SLIC3R_DEBUG */ diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 1b02e03310..a01e63166b 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -58,6 +58,11 @@ void set_data_dir(const std::string &path); // Return a full path to the GUI resource files. const std::string& data_dir(); +// Format an output path for debugging purposes. +// Writes out the output path prefix to the console for the first time the function is called, +// so the user knows where to search for the debugging output. +std::string debug_out_path(const char *name, ...); + // A special type for strings encoded in the local Windows 8-bit code page. // This type is only needed for Perl bindings to relay to Perl that the string is raw, not UTF-8 encoded. typedef std::string local_encoded_string; diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index 8d8d15df9d..a294361893 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -65,18 +65,6 @@ static constexpr double EXTERNAL_INFILL_MARGIN = 3.; #define SCALED_EPSILON scale_(EPSILON) -#define SLIC3R_DEBUG_OUT_PATH_PREFIX "out/" - -inline std::string debug_out_path(const char *name, ...) -{ - char buffer[2048]; - va_list args; - va_start(args, name); - std::vsprintf(buffer, name, args); - va_end(args); - return std::string(SLIC3R_DEBUG_OUT_PATH_PREFIX) + std::string(buffer); -} - #ifndef UNUSED #define UNUSED(x) (void)(x) #endif /* UNUSED */ diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 29f955e924..78125079ad 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -207,6 +207,23 @@ std::string custom_shapes_dir() return (boost::filesystem::path(g_data_dir) / "shapes").string(); } +static std::atomic debug_out_path_called(false); + +std::string debug_out_path(const char *name, ...) +{ + static constexpr const char *SLIC3R_DEBUG_OUT_PATH_PREFIX = "out/"; + if (! debug_out_path_called.exchange(true)) { + std::string path = boost::filesystem::system_complete(SLIC3R_DEBUG_OUT_PATH_PREFIX).string(); + printf("Debugging output files will be written to %s\n", path.c_str()); + } + char buffer[2048]; + va_list args; + va_start(args, name); + std::vsprintf(buffer, name, args); + va_end(args); + return std::string(SLIC3R_DEBUG_OUT_PATH_PREFIX) + std::string(buffer); +} + #ifdef _WIN32 // The following helpers are borrowed from the LLVM project https://github.com/llvm namespace WindowsSupport From fbe46959589a0b51df0584ffc83e1a1d7222a159 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 27 Aug 2021 11:30:37 +0200 Subject: [PATCH 019/151] Slight optimization of GLGizmoSimplify::process(): Moved a static variable from inside a lambda outside as the static inner variable initialization & access has to be made thread safe. --- src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index d4e14c6f4f..b9e5d111b0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -283,11 +283,11 @@ void GLGizmoSimplify::process() } }; - std::function statusfn = [this](int percent) { + int64_t last = 0; + std::function statusfn = [this, &last](int percent) { m_progress = percent; // check max 4fps - static int64_t last = 0; int64_t now = m_parent.timestamp_now(); if ((now - last) < 250) return; last = now; From 9c4eeeaa189d5051c3e8e9acc98f91765e0e6068 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 27 Aug 2021 11:41:34 +0200 Subject: [PATCH 020/151] Tech ENABLE_SEAMS_USING_INSTANCED_MODELS merged into tech ENABLE_SEAMS_USING_MODELS --- src/libslic3r/Technologies.hpp | 2 - src/slic3r/GUI/GCodeViewer.cpp | 135 +++------------------------- src/slic3r/GUI/GCodeViewer.hpp | 43 ++------- src/slic3r/GUI/GLModel.cpp | 4 +- src/slic3r/GUI/GLModel.hpp | 4 +- src/slic3r/GUI/GLShadersManager.cpp | 8 +- 6 files changed, 30 insertions(+), 166 deletions(-) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index f11ad4a175..e03887d261 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -51,8 +51,6 @@ #define ENABLE_RETRACT_ACCELERATION (1 && ENABLE_2_4_0_ALPHA0) // Enable rendering seams (and other options) in preview using models #define ENABLE_SEAMS_USING_MODELS (1 && ENABLE_2_4_0_ALPHA0) -// Enable rendering seams (and other options) in preview using instanced models -#define ENABLE_SEAMS_USING_INSTANCED_MODELS (1 && ENABLE_SEAMS_USING_MODELS) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index c672f90609..382b78ca3a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -95,7 +95,7 @@ void GCodeViewer::VBuffer::reset() count = 0; } -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS void GCodeViewer::InstanceVBuffer::Ranges::reset() { for (Range& range : ranges) { @@ -113,7 +113,7 @@ void GCodeViewer::InstanceVBuffer::reset() buffer.clear(); render_ranges.reset(); } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS void GCodeViewer::IBuffer::reset() { @@ -159,11 +159,7 @@ bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const #if ENABLE_SEAMS_USING_MODELS void GCodeViewer::TBuffer::Model::reset() { -#if ENABLE_SEAMS_USING_INSTANCED_MODELS instances.reset(); -#else - instances.clear(); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } #endif // ENABLE_SEAMS_USING_MODELS @@ -813,11 +809,7 @@ void GCodeViewer::render() #if ENABLE_SEAMS_USING_MODELS if (wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) { buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS buffer.shader = "gouraud_light_instanced"; -#else - buffer.shader = "gouraud_light"; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS buffer.model.model.init_from(diamond(16)); buffer.model.color = option_color(type); } @@ -870,9 +862,9 @@ void GCodeViewer::render() #if ENABLE_GCODE_VIEWER_STATISTICS m_statistics.reset_opengl(); -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS m_statistics.total_instances_gpu_size = 0; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS #endif // ENABLE_GCODE_VIEWER_STATISTICS // OpenGL data must be initialized after the glContext has been created. @@ -1444,7 +1436,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) }; #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_SEAMS_USING_INSTANCED_MODELS // format data into the buffers to be rendered as model auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, InstanceBuffer& instances, InstanceIdBuffer& instances_ids, size_t move_id) { // append position @@ -1459,16 +1450,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // append id instances_ids.push_back(move_id); }; -#else - auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, TBuffer::Model::Instances& instances, size_t move_id) { - TBuffer::Model::Instance instance; - instance.position = curr.position; - instance.width = 1.2f * curr.width; - instance.height = 1.2f * curr.height; - instance.s_id = move_id; - instances.emplace_back(instance); - }; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS #if ENABLE_GCODE_VIEWER_STATISTICS @@ -1513,10 +1494,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) std::vector vertices(m_buffers.size()); std::vector indices(m_buffers.size()); -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS std::vector instances(m_buffers.size()); std::vector instances_ids(m_buffers.size()); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS std::vector options_zs; // toolpaths data -> extract vertices from result @@ -1541,10 +1522,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) const unsigned char id = buffer_id(curr.type); TBuffer& t_buffer = m_buffers[id]; MultiVertexBuffer& v_multibuffer = vertices[id]; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS InstanceBuffer& inst_buffer = instances[id]; InstanceIdBuffer& inst_id_buffer = instances_ids[id]; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS // ensure there is at least one vertex buffer if (v_multibuffer.empty()) @@ -1571,11 +1552,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) #if ENABLE_SEAMS_USING_MODELS case TBuffer::ERenderPrimitiveType::Model: { -#if ENABLE_SEAMS_USING_INSTANCED_MODELS add_model_instance(curr, inst_buffer, inst_id_buffer, i); -#else - add_model_instance(curr, t_buffer.model.instances, i); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #if ENABLE_GCODE_VIEWER_STATISTICS ++m_statistics.instances_count; #endif // ENABLE_GCODE_VIEWER_STATISTICS @@ -1752,17 +1729,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } } -#if ENABLE_SEAMS_USING_MODELS -#if !ENABLE_SEAMS_USING_INSTANCED_MODELS - for (size_t i = 0; i < m_buffers.size(); ++i) { - TBuffer& t_buffer = m_buffers[i]; - if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { - t_buffer.model.instances.shrink_to_fit(); - } - } -#endif // !ENABLE_SEAMS_USING_INSTANCED_MODELS -#endif // ENABLE_SEAMS_USING_MODELS - // move the wipe toolpaths half height up to render them on proper position MultiVertexBuffer& wipe_vertices = vertices[buffer_id(EMoveType::Wipe)]; for (VertexBuffer& v_buffer : wipe_vertices) { @@ -1771,15 +1737,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } } -#if ENABLE_SEAMS_USING_INSTANCED_MODELS // send vertices data to gpu, where needed -#else - // send vertices data to gpu -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_SEAMS_USING_INSTANCED_MODELS if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { const InstanceBuffer& inst_buffer = instances[i]; if (!inst_buffer.empty()) { @@ -1788,9 +1749,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } } else { -#else - if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) { -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS const MultiVertexBuffer& v_multibuffer = vertices[i]; for (const VertexBuffer& v_buffer : v_multibuffer) { @@ -1827,10 +1785,10 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // dismiss vertices data, no more needed std::vector().swap(vertices); -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS std::vector().swap(instances); std::vector().swap(instances_ids); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS // toolpaths data -> extract indices from result // paths may have been filled while extracting vertices, @@ -2217,7 +2175,6 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #if ENABLE_SEAMS_USING_MODELS if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { -#if ENABLE_SEAMS_USING_INSTANCED_MODELS for (size_t id : buffer.model.instances.s_ids) { if (id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < id) continue; @@ -2233,23 +2190,6 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool top_layer_endpoints.last = std::max(top_layer_endpoints.last, id); } } -#else - for (const TBuffer::Model::Instance& inst : buffer.model.instances) { - if (inst.s_id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < inst.s_id) - continue; - - global_endpoints.first = std::min(global_endpoints.first, inst.s_id); - global_endpoints.last = std::max(global_endpoints.last, inst.s_id); - - if (top_layer_only) { - if (inst.s_id < m_layers.get_endpoints_at(m_layers_z_range[1]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < inst.s_id) - continue; - - top_layer_endpoints.first = std::min(top_layer_endpoints.first, inst.s_id); - top_layer_endpoints.last = std::max(top_layer_endpoints.last, inst.s_id); - } - } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } else { #endif // ENABLE_SEAMS_USING_MODELS @@ -2295,16 +2235,11 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool sequential_view->current.first = !top_layer_only && keep_sequential_current_first ? std::clamp(sequential_view->current.first, global_endpoints.first, global_endpoints.last) : global_endpoints.first; sequential_view->current.last = keep_sequential_current_last ? std::clamp(sequential_view->current.last, global_endpoints.first, global_endpoints.last) : global_endpoints.last; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS // get the world position from the vertex buffer -#else - // get the world position from gpu -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS bool found = false; for (const TBuffer& buffer : m_buffers) { #if ENABLE_SEAMS_USING_MODELS if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { -#if ENABLE_SEAMS_USING_INSTANCED_MODELS for (size_t i = 0; i < buffer.model.instances.s_ids.size(); ++i) { if (buffer.model.instances.s_ids[i] == m_sequential_view.current.last) { size_t offset = i * buffer.model.instances.instance_size_floats(); @@ -2316,15 +2251,6 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool break; } } -#else - for (const TBuffer::Model::Instance& inst : buffer.model.instances) { - if (inst.s_id == m_sequential_view.current.last) { - sequential_view->current_position = inst.position; - found = true; - break; - } - } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS } else { #endif // ENABLE_SEAMS_USING_MODELS @@ -2473,7 +2399,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool #endif } -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS // second pass: for buffers using instanced models, update the instances render ranges for (size_t b = 0; b < m_buffers.size(); ++b) { TBuffer& buffer = const_cast(m_buffers[b]); @@ -2513,7 +2439,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool } } } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS // set sequential data to their final value sequential_view->endpoints = top_layer_only ? top_layer_endpoints : global_endpoints; @@ -2643,13 +2569,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool statistics->render_paths_size += SLIC3R_STDVEC_MEMSIZE(path.offsets, size_t); } #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_SEAMS_USING_INSTANCED_MODELS statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.buffer, float); statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.s_ids, size_t); statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances.render_ranges.ranges, InstanceVBuffer::Ranges::Range); -#else - statistics->models_instances_size += SLIC3R_STDVEC_MEMSIZE(buffer.model.instances, TBuffer::Model::Instance); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } statistics->refresh_paths_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); @@ -2742,11 +2664,8 @@ void GCodeViewer::render_toolpaths() #if ENABLE_SEAMS_USING_MODELS auto render_as_instanced_model = [this] (TBuffer& buffer, GLShaderProgram & shader) { -#if ENABLE_SEAMS_USING_INSTANCED_MODELS for (auto& range : buffer.model.instances.render_ranges.ranges) { if (range.vbo == 0 && range.count > 0) { - std::cout << range.offset * buffer.model.instances.instance_size_bytes() << " + " << range.count * buffer.model.instances.instance_size_bytes() << " = " << buffer.model.instances.buffer.size() * sizeof(float) << "\n"; - glsafe(::glGenBuffers(1, &range.vbo)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, range.vbo)); glsafe(::glBufferData(GL_ARRAY_BUFFER, range.count * buffer.model.instances.instance_size_bytes(), (const void*)&buffer.model.instances.buffer[range.offset * buffer.model.instances.instance_size_floats()], GL_STATIC_DRAW)); @@ -2762,28 +2681,6 @@ void GCodeViewer::render_toolpaths() #endif // ENABLE_GCODE_VIEWER_STATISTICS } } -#else - for (const TBuffer::Model::Instance& inst : buffer.model.instances) { - bool top_layer_only = get_app_config()->get("seq_top_layer_only") == "1"; - bool visible = top_layer_only ? - m_sequential_view.global.first <= inst.s_id && inst.s_id <= m_sequential_view.global.last : - m_sequential_view.current.first <= inst.s_id && inst.s_id <= m_sequential_view.current.last; - - if (visible) { - glsafe(::glPushMatrix()); - glsafe(::glTranslatef(inst.position.x(), inst.position.y(), inst.position.z() - 0.5f * inst.height)); - glsafe(::glScalef(inst.width, inst.width, inst.height)); - Color color = (top_layer_only && m_sequential_view.current.last != m_sequential_view.global.last && inst.s_id < m_sequential_view.endpoints.first) ? - Neutral_Color : buffer.model.color; - buffer.model.model.set_color(-1, color); - buffer.model.model.render(); - glsafe(::glPopMatrix()); -#if ENABLE_GCODE_VIEWER_STATISTICS - ++m_statistics.gl_models_calls_count; -#endif // ENABLE_GCODE_VIEWER_STATISTICS - } - } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS }; #endif // ENABLE_SEAMS_USING_MODELS @@ -3852,11 +3749,7 @@ void GCodeViewer::render_statistics() add_counter(std::string("Multi GL_TRIANGLES:"), m_statistics.gl_multi_triangles_calls_count); add_counter(std::string("GL_TRIANGLES:"), m_statistics.gl_triangles_calls_count); #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_SEAMS_USING_INSTANCED_MODELS add_counter(std::string("Instanced models:"), m_statistics.gl_instanced_models_calls_count); -#else - add_counter(std::string("Models:"), m_statistics.gl_models_calls_count); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } @@ -3874,9 +3767,9 @@ void GCodeViewer::render_statistics() if (ImGui::CollapsingHeader("GPU memory")) { add_memory(std::string("Vertices:"), m_statistics.total_vertices_gpu_size); add_memory(std::string("Indices:"), m_statistics.total_indices_gpu_size); -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS add_memory(std::string("Instances:"), m_statistics.total_instances_gpu_size); -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS ImGui::Separator(); add_memory(std::string("Max VBuffer:"), m_statistics.max_vbuffer_gpu_size); add_memory(std::string("Max IBuffer:"), m_statistics.max_ibuffer_gpu_size); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 46f6558dda..429175fe68 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -27,10 +27,10 @@ class GCodeViewer using MultiVertexBuffer = std::vector; using IndexBuffer = std::vector; using MultiIndexBuffer = std::vector; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS using InstanceBuffer = std::vector; using InstanceIdBuffer = std::vector; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS static const std::vector Extrusion_Role_Colors; static const std::vector Options_Colors; @@ -104,7 +104,7 @@ class GCodeViewer void reset(); }; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS // buffer containing instances data used to render a toolpaths using instanced models // instance record format: 5 floats -> position.x|position.y|position.z|width|height // which is sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced() @@ -143,7 +143,7 @@ class GCodeViewer void reset(); }; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS // ibo buffer containing indices data (for lines/triangles) used to render a specific toolpath type struct IBuffer @@ -295,24 +295,9 @@ class GCodeViewer #if ENABLE_SEAMS_USING_MODELS struct Model { -#if !ENABLE_SEAMS_USING_INSTANCED_MODELS - struct Instance - { - Vec3f position; - float width; - float height; - size_t s_id; - }; - using Instances = std::vector; -#endif // !ENABLE_SEAMS_USING_INSTANCED_MODELS - GLModel model; Color color; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS InstanceVBuffer instances; -#else - Instances instances; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS void reset(); }; @@ -377,11 +362,7 @@ class GCodeViewer case ERenderPrimitiveType::Triangle: { return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; } -#if ENABLE_SEAMS_USING_INSTANCED_MODELS case ERenderPrimitiveType::Model: { return model.model.is_initialized() && !model.instances.buffer.empty(); } -#else - case ERenderPrimitiveType::Model: { return model.model.is_initialized() && !model.instances.empty(); } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS default: { return false; } } } @@ -538,19 +519,15 @@ class GCodeViewer int64_t gl_multi_triangles_calls_count{ 0 }; int64_t gl_triangles_calls_count{ 0 }; #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_SEAMS_USING_INSTANCED_MODELS int64_t gl_instanced_models_calls_count{ 0 }; -#else - int64_t gl_models_calls_count{ 0 }; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS // memory int64_t results_size{ 0 }; int64_t total_vertices_gpu_size{ 0 }; int64_t total_indices_gpu_size{ 0 }; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS int64_t total_instances_gpu_size{ 0 }; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS int64_t max_vbuffer_gpu_size{ 0 }; int64_t max_ibuffer_gpu_size{ 0 }; int64_t paths_size{ 0 }; @@ -591,11 +568,7 @@ class GCodeViewer gl_multi_triangles_calls_count = 0; gl_triangles_calls_count = 0; #if ENABLE_SEAMS_USING_MODELS -#if ENABLE_SEAMS_USING_INSTANCED_MODELS gl_instanced_models_calls_count = 0; -#else - gl_models_calls_count = 0; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } @@ -603,9 +576,9 @@ class GCodeViewer results_size = 0; total_vertices_gpu_size = 0; total_indices_gpu_size = 0; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS total_instances_gpu_size = 0; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS max_vbuffer_gpu_size = 0; max_ibuffer_gpu_size = 0; paths_size = 0; diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index ce44c4649f..5cddd7fa3b 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -208,7 +208,7 @@ void GLModel::render() const } } -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS void GLModel::render_instanced(unsigned int instances_vbo, unsigned int instances_count) const { if (instances_vbo == 0) @@ -285,7 +285,7 @@ void GLModel::render_instanced(unsigned int instances_vbo, unsigned int instance glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS void GLModel::send_to_gpu(RenderData& data, const std::vector& vertices, const std::vector& indices) { diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index 73fb908271..8e0039b083 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -72,9 +72,9 @@ namespace GUI { void reset(); void render() const; -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS void render_instanced(unsigned int instances_vbo, unsigned int instances_count) const; -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS bool is_initialized() const { return !m_render_data.empty(); } diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index 418fe8d1e9..c93f22f469 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -38,17 +38,17 @@ std::pair GLShadersManager::init() // used to render printbed valid &= append_shader("printbed", { "printbed.vs", "printbed.fs" }); // used to render options in gcode preview -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) valid &= append_shader("gouraud_light_instanced", { "gouraud_light_instanced.vs", "gouraud_light_instanced.fs" }); else { -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS valid &= append_shader("options_110", { "options_110.vs", "options_110.fs" }); if (GUI::wxGetApp().is_glsl_version_greater_or_equal_to(1, 20)) valid &= append_shader("options_120", { "options_120.vs", "options_120.fs" }); -#if ENABLE_SEAMS_USING_INSTANCED_MODELS +#if ENABLE_SEAMS_USING_MODELS } -#endif // ENABLE_SEAMS_USING_INSTANCED_MODELS +#endif // ENABLE_SEAMS_USING_MODELS // used to render extrusion and travel paths as lines in gcode preview valid &= append_shader("toolpaths_lines", { "toolpaths_lines.vs", "toolpaths_lines.fs" }); // used to render objects in 3d editor From 7852ba061c7bbfba6431335305e18b772f3ebd94 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 27 Aug 2021 12:10:25 +0200 Subject: [PATCH 021/151] Slightly reworded the 'Empty layers detected' warning, it mentions the layers between which the problem occurs, not just the upper one (which may be unclear). --- src/libslic3r/GCode.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 254f1d4fd9..85e6f810b7 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -13,6 +13,7 @@ #include "ClipperUtils.hpp" #include "libslic3r.h" #include "LocalesUtils.hpp" +#include "libslic3r/format.hpp" #include #include @@ -512,7 +513,8 @@ std::vector GCode::collect_layers_to_print(const PrintObjec bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions()) || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()); - // Check that there are extrusions on the very first layer. + // Check that there are extrusions on the very first layer. The case with empty + // first layer may result in skirt/brim in the air and maybe other issues. if (layers_to_print.size() == 1u) { if (!has_extrusions) throw Slic3r::SlicingError(_(L("There is an object with no extrusions in the first layer.")) + "\n" + @@ -534,11 +536,12 @@ std::vector GCode::collect_layers_to_print(const PrintObjec if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) { const_cast(object.print())->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - _(L("Empty layers detected. Make sure the object is printable.")) + "\n" + - _(L("Object name")) + ": " + object.model_object()->name + "\n" + _(L("Print z")) + ": " + - std::to_string(layers_to_print.back().print_z()) + "\n\n" + _(L("This is " - "usually caused by negligibly small extrusions or by a faulty model. Try to repair " - "the model or change its orientation on the bed."))); + Slic3r::format(_(L("Empty layer detected between heights %1% and %2%. Make sure the object is printable.")), + (last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), + layers_to_print.back().print_z()) + + "\n" + Slic3r::format(_(L("Object name: %1%")), object.model_object()->name) + "\n\n" + + _(L("This is usually caused by negligibly small extrusions or by a faulty model. " + "Try to repair the model or change its orientation on the bed."))); } // Remember last layer with extrusions. From 2844bee60bdb3aa6b84d0b251ba635ebc811f6d3 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 27 Aug 2021 14:01:29 +0200 Subject: [PATCH 022/151] Added missing include (gcc 8.4) --- src/libslic3r/utils.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 78125079ad..c330f34b2b 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1,6 +1,7 @@ #include "Utils.hpp" #include "I18N.hpp" +#include #include #include #include From 03fce23570bf6a276022639f19fe5964c01b7cb5 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 27 Aug 2021 15:02:43 +0200 Subject: [PATCH 023/151] Fixed Perl bindings --- xs/xsp/XS.xsp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/xs/xsp/XS.xsp b/xs/xsp/XS.xsp index 083d21d388..68ea282bc0 100644 --- a/xs/xsp/XS.xsp +++ b/xs/xsp/XS.xsp @@ -23,12 +23,6 @@ BUILD() RETVAL = newSVpv(SLIC3R_BUILD_ID, 0); OUTPUT: RETVAL -SV* -DEBUG_OUT_PATH_PREFIX() - CODE: - RETVAL = newSVpv(SLIC3R_DEBUG_OUT_PATH_PREFIX, 0); - OUTPUT: RETVAL - SV* FORK_NAME() CODE: From 306bd0a1988a6753b8c81b771bd3319104b9cd88 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 27 Aug 2021 15:04:25 +0200 Subject: [PATCH 024/151] Fixed leakage of paint-on supports through thin objects. This is a bug introduced during 2.4.0 refactoring. Fixes To much support #6067 --- src/libslic3r/SupportMaterial.cpp | 46 +++++++++++++++++++------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index c9794a6c88..2e4a479543 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -1417,13 +1417,35 @@ static inline std::tuple detect_overhangs( // Generate overhang / contact_polygons for non-raft layers. const Layer &lower_layer = *layer.lower_layer; const bool has_enforcer = ! annotations.enforcers_layers.empty() && ! annotations.enforcers_layers[layer_id].empty(); - float fw = 0; + + // Cache support trimming polygons derived from lower layer polygons, possible merged with "on build plate only" trimming polygons. + auto slices_margin_update = + [&slices_margin, &lower_layer, &lower_layer_polygons, buildplate_only, has_enforcer, &annotations, layer_id] + (float slices_margin_offset, float no_interface_offset) { + if (slices_margin.offset != slices_margin_offset) { + slices_margin.offset = slices_margin_offset; + slices_margin.polygons = (slices_margin_offset == 0.f) ? + lower_layer_polygons : + offset2(lower_layer.lslices, -no_interface_offset * 0.5f, slices_margin_offset + no_interface_offset * 0.5f, SUPPORT_SURFACES_OFFSET_PARAMETERS); + if (buildplate_only && !annotations.buildplate_covered[layer_id].empty()) { + if (has_enforcer) + // Make a backup of trimming polygons before enforcing "on build plate only". + slices_margin.all_polygons = slices_margin.polygons; + // Trim the inflated contact surfaces by the top surfaces as well. + slices_margin.polygons = union_(slices_margin.polygons, annotations.buildplate_covered[layer_id]); + } + } + }; + + float fw = 0; + float lower_layer_offset = 0; + float no_interface_offset = 0; for (LayerRegion *layerm : layer.regions()) { // Extrusion width accounts for the roundings of the extrudates. // It is the maximum widh of the extrudate. fw = float(layerm->flow(frExternalPerimeter).scaled_width()); no_interface_offset = (no_interface_offset == 0.f) ? fw : std::min(no_interface_offset, fw); - float lower_layer_offset = + lower_layer_offset = (layer_id < (size_t)object_config.support_material_enforce_layers.value) ? // Enforce a full possible support, ignore the overhang angle. 0.f : @@ -1530,20 +1552,7 @@ static inline std::tuple detect_overhangs( //FIXME one should trim with the layer span colliding with the support layer, this layer // may be lower than lower_layer, so the support area needed may need to be actually bigger! // For the same reason, the non-bridging support area may be smaller than the bridging support area! - float slices_margin_offset = std::min(lower_layer_offset, float(scale_(gap_xy))); - if (slices_margin.offset != slices_margin_offset) { - slices_margin.offset = slices_margin_offset; - slices_margin.polygons = (slices_margin_offset == 0.f) ? - lower_layer_polygons : - offset2(lower_layer.lslices, - no_interface_offset * 0.5f, slices_margin_offset + no_interface_offset * 0.5f, SUPPORT_SURFACES_OFFSET_PARAMETERS); - if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { - if (has_enforcer) - // Make a backup of trimming polygons before enforcing "on build plate only". - slices_margin.all_polygons = slices_margin.polygons; - // Trim the inflated contact surfaces by the top surfaces as well. - slices_margin.polygons = union_(slices_margin.polygons, annotations.buildplate_covered[layer_id]); - } - } + slices_margin_update(std::min(lower_layer_offset, float(scale_(gap_xy))), no_interface_offset); // Offset the contact polygons outside. #if 0 for (size_t i = 0; i < NUM_MARGIN_STEPS; ++ i) { @@ -1579,6 +1588,7 @@ static inline std::tuple detect_overhangs( { { union_safety_offset_ex(enforcer_polygons) }, { "new_contacts", "red", "black", "", scaled(0.1f), 0.5f } } }); #endif /* SLIC3R_DEBUG */ polygons_append(overhang_polygons, enforcer_polygons); + slices_margin_update(std::min(lower_layer_offset, float(scale_(gap_xy))), no_interface_offset); polygons_append(contact_polygons, diff(enforcer_polygons, slices_margin.all_polygons.empty() ? slices_margin.polygons : slices_margin.all_polygons)); } } @@ -1739,14 +1749,14 @@ static inline void fill_contact_layer( #endif // SLIC3R_DEBUG ); #ifdef SLIC3R_DEBUG - SVG::export_expolygons(debug_out_path("support-top-contacts-final0-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), + SVG::export_expolygons(debug_out_path("support-top-contacts-final1-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), { { { union_ex(lower_layer_polygons) }, { "lower_layer_polygons", "gray", 0.2f } }, { { union_ex(*new_layer.contact_polygons) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, { { union_ex(slices_margin.polygons) }, { "slices_margin_cached", "blue", 0.5f } }, { { union_ex(dense_interface_polygons) }, { "dense_interface_polygons", "green", 0.5f } }, { { union_safety_offset_ex(new_layer.polygons) }, { "new_layer.polygons", "red", "black", "", scaled(0.1f), 0.5f } } }); //support_grid_pattern.serialize(debug_out_path("support-top-contacts-final-run%d-layer%d-z%f.bin", iRun, layer_id, layer.print_z)); - SVG::export_expolygons(debug_out_path("support-top-contacts-final0-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), + SVG::export_expolygons(debug_out_path("support-top-contacts-final2-run%d-layer%d-z%f.svg", iRun, layer_id, layer.print_z), { { { union_ex(lower_layer_polygons) }, { "lower_layer_polygons", "gray", 0.2f } }, { { union_ex(*new_layer.contact_polygons) }, { "new_layer.contact_polygons", "yellow", 0.5f } }, { { union_ex(contact_polygons) }, { "contact_polygons", "blue", 0.5f } }, From 8dfc0422a878c5e44d4233c6ce522c77a0c3280f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 27 Aug 2021 15:05:08 +0200 Subject: [PATCH 025/151] Faster and hopefully more reliable projection of paint-on support blockers and enforcers on a sliced mesh. --- src/libslic3r/PrintObject.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ee09e0f5b7..d8d26baa61 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2294,9 +2294,13 @@ void PrintObject::project_and_append_custom_facets( ? mv->seam_facets.get_facets_strict(*mv, type) : mv->supported_facets.get_facets_strict(*mv, type); if (! custom_facets.indices.empty()) +#if 0 project_triangles_to_slabs(this->layers(), custom_facets, (this->trafo_centered() * mv->get_matrix()).cast(), seam, out); +#else + slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &out, [](){}); +#endif } } From 7bd14bfaf7fef96719a37e5d632c1f09cfebf348 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 27 Aug 2021 15:23:27 +0200 Subject: [PATCH 026/151] ENABLE_SEAMS_USING_MODELS -> Fixed instances when horizontal slider is enabled for top layer only --- src/slic3r/GUI/GCodeViewer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 382b78ca3a..9ac0536a41 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2421,7 +2421,10 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (has_second_range) { if (id <= m_sequential_view.endpoints.first) { ++buffer.model.instances.render_ranges.ranges.front().offset; - ++buffer.model.instances.render_ranges.ranges.back().count; + if (id <= m_sequential_view.current.first) + ++buffer.model.instances.render_ranges.ranges.back().offset; + else + ++buffer.model.instances.render_ranges.ranges.back().count; } else if (id <= m_sequential_view.current.last) ++buffer.model.instances.render_ranges.ranges.front().count; From d9f2fd7501274141b55fad7f5f4a982e7286d7d3 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 27 Aug 2021 19:46:44 +0200 Subject: [PATCH 027/151] Fixed shifted clippimg plane triangulation on scaled meshes, fighting z-fighting has to be done in world coords. --- src/slic3r/GUI/MeshUtils.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index ba83bdbff7..75232c9301 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -106,7 +106,6 @@ void MeshClipper::recalculate_triangles() Transform3d tr = Transform3d::Identity(); tr.rotate(q); tr = m_trafo.get_matrix() * tr; - height_mesh += 0.001f; // to avoid z-fighting if (m_limiting_plane != ClippingPlane::ClipsNothing()) { @@ -165,6 +164,8 @@ void MeshClipper::recalculate_triangles() m_triangles2d = triangulate_expolygons_2f(expolys, m_trafo.get_matrix().matrix().determinant() < 0.); + tr.pretranslate(0.001 * m_plane.get_normal().normalized()); // to avoid z-fighting + m_vertex_array.release_geometry(); for (auto it=m_triangles2d.cbegin(); it != m_triangles2d.cend(); it=it+3) { m_vertex_array.push_geometry(tr * Vec3d((*(it+0))(0), (*(it+0))(1), height_mesh), up); From 1c76df89eaf9dcbad50601a8a8aabd9b6a7e2f84 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 27 Aug 2021 21:04:11 +0200 Subject: [PATCH 028/151] Fix of paint on supports don't work for object that has been scaled up #6718 The triangle-ray intersection function used a hard coded epsilon, which did not work for triangle meshes, that were either too small or too large. Newly the epsilon may be provided to the AABBTreeIndirect search functions externally and IndexedMesh calculates a suitable epsilon on demand from an average triangle mesh edge length. --- src/libslic3r/AABBTreeIndirect.hpp | 125 ++++++++++++++++++++--------- src/libslic3r/SLA/IndexedMesh.cpp | 26 +++--- src/libslic3r/SLA/IndexedMesh.hpp | 8 +- src/libslic3r/TriangleMesh.cpp | 15 ++++ src/libslic3r/TriangleMesh.hpp | 1 + src/slic3r/GUI/MeshUtils.hpp | 2 +- 6 files changed, 127 insertions(+), 50 deletions(-) diff --git a/src/libslic3r/AABBTreeIndirect.hpp b/src/libslic3r/AABBTreeIndirect.hpp index 76aa36194e..217166f8c7 100644 --- a/src/libslic3r/AABBTreeIndirect.hpp +++ b/src/libslic3r/AABBTreeIndirect.hpp @@ -15,11 +15,6 @@ #include "Utils.hpp" // for next_highest_power_of_2() -extern "C" -{ -// Ray-Triangle Intersection Test Routines by Tomas Moller, May 2000 -#include -} // Definition of the ray intersection hit structure. #include @@ -231,6 +226,9 @@ namespace detail { const VectorType origin; const VectorType dir; const VectorType invdir; + + // epsilon for ray-triangle intersection, see intersect_triangle1() + const double eps; }; template @@ -283,44 +281,91 @@ namespace detail { return tmin < t1 && tmax > t0; } + // The following intersect_triangle() is derived from raytri.c routine intersect_triangle1() + // Ray-Triangle Intersection Test Routines + // Different optimizations of my and Ben Trumbore's + // code from journals of graphics tools (JGT) + // http://www.acm.org/jgt/ + // by Tomas Moller, May 2000 template - std::enable_if_t::value && std::is_same::value, bool> - intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v) { - return intersect_triangle1(const_cast(origin.data()), const_cast(dir.data()), - const_cast(v0.data()), const_cast(v1.data()), const_cast(v2.data()), - &t, &u, &v); + std::enable_if_t::value&& std::is_same::value, bool> + intersect_triangle(const V &orig, const V &dir, const W &vert0, const W &vert1, const W &vert2, double &t, double &u, double &v, double eps) + { + // find vectors for two edges sharing vert0 + const V edge1 = vert1 - vert0; + const V edge2 = vert2 - vert0; + // begin calculating determinant - also used to calculate U parameter + const V pvec = dir.cross(edge2); + // if determinant is near zero, ray lies in plane of triangle + const double det = edge1.dot(pvec); + V qvec; + + if (det > eps) { + // calculate distance from vert0 to ray origin + V tvec = orig - vert0; + // calculate U parameter and test bounds + u = tvec.dot(pvec); + if (u < 0.0 || u > det) + return false; + // prepare to test V parameter + qvec = tvec.cross(edge1); + // calculate V parameter and test bounds + v = dir.dot(qvec); + if (v < 0.0 || u + v > det) + return false; + } else if (det < -eps) { + // calculate distance from vert0 to ray origin + V tvec = orig - vert0; + // calculate U parameter and test bounds + u = tvec.dot(pvec); + if (u > 0.0 || u < det) + return false; + // prepare to test V parameter + qvec = tvec.cross(edge1); + // calculate V parameter and test bounds + v = dir.dot(qvec); + if (v > 0.0 || u + v < det) + return false; + } else + // ray is parallel to the plane of the triangle + return false; + + double inv_det = 1.0 / det; + // calculate t, ray intersects triangle + t = edge2.dot(qvec) * inv_det; + u *= inv_det; + v *= inv_det; + return true; } template std::enable_if_t::value && !std::is_same::value, bool> - intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v) { - using Vector = Eigen::Matrix; - Vector w0 = v0.template cast(); - Vector w1 = v1.template cast(); - Vector w2 = v2.template cast(); - return intersect_triangle1(const_cast(origin.data()), const_cast(dir.data()), - w0.data(), w1.data(), w2.data(), &t, &u, &v); + intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v, double eps) { + return intersect_triangle(origin, dir, v0.template cast(), v1.template cast(), v2.template cast(), t, u, v, eps); } template std::enable_if_t::value && std::is_same::value, bool> - intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v) { - using Vector = Eigen::Matrix; - Vector o = origin.template cast(); - Vector d = dir.template cast(); - return intersect_triangle1(o.data(), d.data(), const_cast(v0.data()), const_cast(v1.data()), const_cast(v2.data()), &t, &u, &v); + intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v, double eps) { + return intersect_triangle(origin.template cast(), dir.template cast(), v0, v1, v2, t, u, v, eps); } template std::enable_if_t::value && ! std::is_same::value, bool> - intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v) { - using Vector = Eigen::Matrix; - Vector o = origin.template cast(); - Vector d = dir.template cast(); - Vector w0 = v0.template cast(); - Vector w1 = v1.template cast(); - Vector w2 = v2.template cast(); - return intersect_triangle1(o.data(), d.data(), w0.data(), w1.data(), w2.data(), &t, &u, &v); + intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v, double eps) { + return intersect_triangle(origin.template cast(), dir.template cast(), v0.template cast(), v1.template cast(), v2.template cast(), t, u, v, eps); + } + + template + double intersect_triangle_epsilon(const Tree &tree) { + double eps = 0.000001; + if (! tree.empty()) { + const typename Tree::BoundingBox &bbox = tree.nodes().front().bbox; + double l = (bbox.max() - bbox.min()).cwiseMax(); + if (l > 0) + eps /= (l * l); + } + return eps; } template @@ -343,7 +388,7 @@ namespace detail { if (intersect_triangle( ray_intersector.origin, ray_intersector.dir, ray_intersector.vertices[face(0)], ray_intersector.vertices[face(1)], ray_intersector.vertices[face(2)], - t, u, v) + t, u, v, ray_intersector.eps) && t > 0.) { hit = igl::Hit { int(node.idx), -1, float(u), float(v), float(t) }; return true; @@ -388,7 +433,7 @@ namespace detail { if (intersect_triangle( ray_intersector.origin, ray_intersector.dir, ray_intersector.vertices[face(0)], ray_intersector.vertices[face(1)], ray_intersector.vertices[face(2)], - t, u, v) + t, u, v, ray_intersector.eps) && t > 0.) { ray_intersector.hits.emplace_back(igl::Hit{ int(node.idx), -1, float(u), float(v), float(t) }); } @@ -623,12 +668,15 @@ inline bool intersect_ray_first_hit( // Direction of the ray. const VectorType &dir, // First intersection of the ray with the indexed triangle set. - igl::Hit &hit) + igl::Hit &hit, + // Epsilon for the ray-triangle intersection, it should be proportional to an average triangle edge length. + const double eps = 0.000001) { using Scalar = typename VectorType::Scalar; - auto ray_intersector = detail::RayIntersector { + auto ray_intersector = detail::RayIntersector { vertices, faces, tree, - origin, dir, VectorType(dir.cwiseInverse()) + origin, dir, VectorType(dir.cwiseInverse()), + eps }; return ! tree.empty() && detail::intersect_ray_recursive_first_hit( ray_intersector, size_t(0), std::numeric_limits::infinity(), hit); @@ -652,11 +700,14 @@ inline bool intersect_ray_all_hits( // Direction of the ray. const VectorType &dir, // All intersections of the ray with the indexed triangle set, sorted by parameter t. - std::vector &hits) + std::vector &hits, + // Epsilon for the ray-triangle intersection, it should be proportional to an average triangle edge length. + const double eps = 0.000001) { auto ray_intersector = detail::RayIntersectorHits { { vertices, faces, {tree}, - origin, dir, VectorType(dir.cwiseInverse()) } + origin, dir, VectorType(dir.cwiseInverse()), + eps } }; if (! tree.empty()) { ray_intersector.hits.reserve(8); diff --git a/src/libslic3r/SLA/IndexedMesh.cpp b/src/libslic3r/SLA/IndexedMesh.cpp index 887ef1555d..07c4203ab2 100644 --- a/src/libslic3r/SLA/IndexedMesh.cpp +++ b/src/libslic3r/SLA/IndexedMesh.cpp @@ -17,10 +17,18 @@ namespace sla { class IndexedMesh::AABBImpl { private: AABBTreeIndirect::Tree3f m_tree; + double m_triangle_ray_epsilon; public: - void init(const indexed_triangle_set &its) + void init(const indexed_triangle_set &its, bool calculate_epsilon) { + m_triangle_ray_epsilon = 0.000001; + if (calculate_epsilon) { + // Calculate epsilon from average triangle edge length. + double l = its_average_edge_length(its); + if (l > 0) + m_triangle_ray_epsilon = 0.000001 * l * l; + } m_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( its.vertices, its.indices); } @@ -31,7 +39,7 @@ public: igl::Hit & hit) { AABBTreeIndirect::intersect_ray_first_hit(its.vertices, its.indices, - m_tree, s, dir, hit); + m_tree, s, dir, hit, m_triangle_ray_epsilon); } void intersect_ray(const indexed_triangle_set &its, @@ -40,7 +48,7 @@ public: std::vector & hits) { AABBTreeIndirect::intersect_ray_all_hits(its.vertices, its.indices, - m_tree, s, dir, hits); + m_tree, s, dir, hits, m_triangle_ray_epsilon); } double squared_distance(const indexed_triangle_set & its, @@ -60,25 +68,25 @@ public: } }; -template void IndexedMesh::init(const M &mesh) +template void IndexedMesh::init(const M &mesh, bool calculate_epsilon) { BoundingBoxf3 bb = bounding_box(mesh); m_ground_level += bb.min(Z); // Build the AABB accelaration tree - m_aabb->init(*m_tm); + m_aabb->init(*m_tm, calculate_epsilon); } -IndexedMesh::IndexedMesh(const indexed_triangle_set& tmesh) +IndexedMesh::IndexedMesh(const indexed_triangle_set& tmesh, bool calculate_epsilon) : m_aabb(new AABBImpl()), m_tm(&tmesh) { - init(tmesh); + init(tmesh, calculate_epsilon); } -IndexedMesh::IndexedMesh(const TriangleMesh &mesh) +IndexedMesh::IndexedMesh(const TriangleMesh &mesh, bool calculate_epsilon) : m_aabb(new AABBImpl()), m_tm(&mesh.its) { - init(mesh); + init(mesh, calculate_epsilon); } IndexedMesh::~IndexedMesh() {} diff --git a/src/libslic3r/SLA/IndexedMesh.hpp b/src/libslic3r/SLA/IndexedMesh.hpp index 25ab75b836..9348a97c9b 100644 --- a/src/libslic3r/SLA/IndexedMesh.hpp +++ b/src/libslic3r/SLA/IndexedMesh.hpp @@ -42,12 +42,14 @@ class IndexedMesh { std::vector m_holes; #endif - template void init(const M &mesh); + template void init(const M &mesh, bool calculate_epsilon); public: - explicit IndexedMesh(const indexed_triangle_set&); - explicit IndexedMesh(const TriangleMesh &mesh); + // calculate_epsilon ... calculate epsilon for triangle-ray intersection from an average triangle edge length. + // If set to false, a default epsilon is used, which works for "reasonable" meshes. + explicit IndexedMesh(const indexed_triangle_set &tmesh, bool calculate_epsilon = false); + explicit IndexedMesh(const TriangleMesh &mesh, bool calculate_epsilon = false); IndexedMesh(const IndexedMesh& other); IndexedMesh& operator=(const IndexedMesh&); diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index d4baabc977..fa8f8bce64 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1275,6 +1275,21 @@ float its_volume(const indexed_triangle_set &its) return volume; } +float its_average_edge_length(const indexed_triangle_set &its) +{ + if (its.indices.empty()) + return 0.f; + + double edge_length = 0.f; + for (size_t i = 0; i < its.indices.size(); ++ i) { + const its_triangle v = its_triangle_vertices(its, i); + edge_length += (v[1] - v[0]).cast().norm() + + (v[2] - v[0]).cast().norm() + + (v[1] - v[2]).cast().norm(); + } + return float(edge_length / (3 * its.indices.size())); +} + std::vector its_split(const indexed_triangle_set &its) { return its_split<>(its); diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index b7a1bebb15..c463af5a22 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -199,6 +199,7 @@ inline stl_normal its_unnormalized_normal(const indexed_triangle_set &its, } float its_volume(const indexed_triangle_set &its); +float its_average_edge_length(const indexed_triangle_set &its); void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B); void its_merge(indexed_triangle_set &A, const std::vector &triangles); diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index ec6c337c06..65c3261168 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -112,7 +112,7 @@ public: // The class references extern TriangleMesh, which must stay alive // during MeshRaycaster existence. MeshRaycaster(const TriangleMesh& mesh) - : m_emesh(mesh) + : m_emesh(mesh, true) // calculate epsilon for triangle-ray intersection from an average edge length { m_normals.reserve(mesh.stl.facet_start.size()); for (const stl_facet& facet : mesh.stl.facet_start) From ae3478c6c59ce541f4ee93ebf42c2ba778c98d8e Mon Sep 17 00:00:00 2001 From: David Kocik Date: Sat, 28 Aug 2021 14:35:12 +0200 Subject: [PATCH 029/151] Desktop integration escaping path --- src/slic3r/GUI/DesktopIntegrationDialog.cpp | 98 +++++++++++++++++++-- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp index fb4a62f918..d34d0730e1 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.cpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp @@ -22,6 +22,93 @@ namespace Slic3r { namespace GUI { namespace { + +// escaping of path string according to +// https://cgit.freedesktop.org/xdg/xdg-specs/tree/desktop-entry/desktop-entry-spec.xml +std::string escape_string(const std::string& str) +{ + // The buffer needs to be bigger if escaping <,>,& + std::vector out(str.size() * 2, 0); + char *outptr = out.data(); + for (size_t i = 0; i < str.size(); ++ i) { + char c = str[i]; + // must be escaped + if (c == '\"') { //double quote + (*outptr ++) = '\\'; + (*outptr ++) = '\"'; + } else if (c == '`') { // backtick character + (*outptr ++) = '\\'; + (*outptr ++) = '`'; + } else if (c == '$') { // dollar sign + (*outptr ++) = '\\'; + (*outptr ++) = '$'; + } else if (c == '\\') { // backslash character + (*outptr ++) = '\\'; + (*outptr ++) = '\\'; + // Reserved characters + // At Ubuntu, all these characters must NOT be escaped for desktop integration to work + /* + } else if (c == ' ') { // space + (*outptr ++) = '\\'; + (*outptr ++) = ' '; + } else if (c == '\t') { // tab + (*outptr ++) = '\\'; + (*outptr ++) = '\t'; + } else if (c == '\n') { // newline + (*outptr ++) = '\\'; + (*outptr ++) = '\n'; + } else if (c == '\'') { // single quote + (*outptr ++) = '\\'; + (*outptr ++) = '\''; + } else if (c == '>') { // greater-than sign + (*outptr ++) = '\\'; + (*outptr ++) = '&'; + (*outptr ++) = 'g'; + (*outptr ++) = 't'; + (*outptr ++) = ';'; + } else if (c == '<') { //less-than sign + (*outptr ++) = '\\'; + (*outptr ++) = '&'; + (*outptr ++) = 'l'; + (*outptr ++) = 't'; + (*outptr ++) = ';'; + } else if (c == '~') { // tilde + (*outptr ++) = '\\'; + (*outptr ++) = '~'; + } else if (c == '|') { // vertical bar + (*outptr ++) = '\\'; + (*outptr ++) = '|'; + } else if (c == '&') { // ampersand + (*outptr ++) = '\\'; + (*outptr ++) = '&'; + (*outptr ++) = 'a'; + (*outptr ++) = 'm'; + (*outptr ++) = 'p'; + (*outptr ++) = ';'; + } else if (c == ';') { // semicolon + (*outptr ++) = '\\'; + (*outptr ++) = ';'; + } else if (c == '*') { //asterisk + (*outptr ++) = '\\'; + (*outptr ++) = '*'; + } else if (c == '?') { // question mark + (*outptr ++) = '\\'; + (*outptr ++) = '?'; + } else if (c == '#') { // hash mark + (*outptr ++) = '\\'; + (*outptr ++) = '#'; + } else if (c == '(') { // parenthesis + (*outptr ++) = '\\'; + (*outptr ++) = '('; + } else if (c == ')') { + (*outptr ++) = '\\'; + (*outptr ++) = ')'; + */ + } else + (*outptr ++) = c; + } + return std::string(out.data(), outptr - out.data()); +} // Disects path strings stored in system variable divided by ':' and adds into vector void resolve_path_from_var(const std::string& var, std::vector& paths) { @@ -157,7 +244,8 @@ void DesktopIntegrationDialog::perform_desktop_integration() } // Escape ' characters in appimage, other special symbols will be esacaped in desktop file by 'excutable_path' - boost::replace_all(excutable_path, "'", "'\\''"); + //boost::replace_all(excutable_path, "'", "'\\''"); + excutable_path = escape_string(excutable_path); // Find directories icons and applications // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. @@ -243,14 +331,14 @@ void DesktopIntegrationDialog::perform_desktop_integration() "Name=PrusaSlicer%1%\n" "GenericName=3D Printing Software\n" "Icon=PrusaSlicer%2%\n" - "Exec=\'%3%\' %%F\n" + "Exec=\"%3%\" %%F\n" "Terminal=false\n" "Type=Application\n" "MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n" "Categories=Graphics;3DGraphics;Engineering;\n" "Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n" "StartupNotify=false\n" - "StartupWMClass=prusa-slicer", name_suffix, version_suffix, excutable_path); + "StartupWMClass=prusa-slicer\n", name_suffix, version_suffix, excutable_path); std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix); if (create_desktop_file(path, desktop_file)){ @@ -310,13 +398,13 @@ void DesktopIntegrationDialog::perform_desktop_integration() "Name=Prusa Gcode Viewer%1%\n" "GenericName=3D Printing Software\n" "Icon=PrusaSlicer-gcodeviewer%2%\n" - "Exec=\'%3%\' --gcodeviwer %%F\n" + "Exec=\"%3%\" --gcodeviewer %%F\n" "Terminal=false\n" "Type=Application\n" "MimeType=text/x.gcode;\n" "Categories=Graphics;3DGraphics;\n" "Keywords=3D;Printing;Slicer;\n" - "StartupNotify=false", name_suffix, version_suffix, excutable_path); + "StartupNotify=false\n", name_suffix, version_suffix, excutable_path); std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix); if (create_desktop_file(desktop_path, desktop_file)) From 14659cf760acf5d2b34b6287c26f601518c202b1 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 30 Aug 2021 08:29:50 +0200 Subject: [PATCH 030/151] Revert of d701dfe436b353f4847b7a78605847f85e50e4eb --- src/slic3r/GUI/GUI_ObjectList.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index d4030d41f2..b85219c0b4 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1049,7 +1049,7 @@ void ObjectList::key_event(wxKeyEvent& event) || event.GetKeyCode() == WXK_BACK #endif //__WXOSX__ ) { - wxGetApp().plater()->remove_selected(); + remove(); } else if (event.GetKeyCode() == WXK_F5) wxGetApp().plater()->reload_all_from_disk(); From 876f9d5a990404b568a67cc088497627072e88ed Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 30 Aug 2021 08:37:41 +0200 Subject: [PATCH 031/151] Cleanup in shader gouraud_light_instanced.vs --- resources/shaders/gouraud_light_instanced.vs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/resources/shaders/gouraud_light_instanced.vs b/resources/shaders/gouraud_light_instanced.vs index 5d6a05a6f0..69f58d6e14 100644 --- a/resources/shaders/gouraud_light_instanced.vs +++ b/resources/shaders/gouraud_light_instanced.vs @@ -14,14 +14,12 @@ const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); #define INTENSITY_AMBIENT 0.3 -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // vertex attributes in vec3 v_position; in vec3 v_normal; // instance attributes in vec3 i_offset; in vec2 i_scales; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // x = tainted, y = specular; varying vec2 intensity; @@ -29,29 +27,20 @@ varying vec2 intensity; void main() { // First transform the normal into camera space and normalize the result. -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ vec3 eye_normal = normalize(gl_NormalMatrix * v_normal); -// vec3 eye_normal = normalize(gl_NormalMatrix * gl_Normal); -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ vec4 world_position = vec4(v_position * vec3(vec2(i_scales.x), i_scales.y) + i_offset, 1.0); vec3 eye_position = (gl_ModelViewMatrix * world_position).xyz; -// vec3 eye_position = (gl_ModelViewMatrix * gl_Vertex).xyz; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(eye_position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); // Perform the same lighting calculation for the 2nd light source (no specular applied). NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ gl_Position = gl_ProjectionMatrix * vec4(eye_position, 1.0); -// gl_Position = ftransform(); -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ } From 6be99f941dcaaf477961ca740ebeae66046d2341 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 30 Aug 2021 10:25:21 +0200 Subject: [PATCH 032/151] Escaping of backslash --- src/slic3r/GUI/DesktopIntegrationDialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp index d34d0730e1..9351219355 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.cpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp @@ -28,7 +28,7 @@ namespace { std::string escape_string(const std::string& str) { // The buffer needs to be bigger if escaping <,>,& - std::vector out(str.size() * 2, 0); + std::vector out(str.size() * 4, 0); char *outptr = out.data(); for (size_t i = 0; i < str.size(); ++ i) { char c = str[i]; @@ -45,6 +45,8 @@ std::string escape_string(const std::string& str) } else if (c == '\\') { // backslash character (*outptr ++) = '\\'; (*outptr ++) = '\\'; + (*outptr ++) = '\\'; + (*outptr ++) = '\\'; // Reserved characters // At Ubuntu, all these characters must NOT be escaped for desktop integration to work /* From 57f7b49223d231d7f160a13c55f6bc8d02c16b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Mon, 30 Aug 2021 10:41:01 +0200 Subject: [PATCH 033/151] Corrected undo/redo snapshot name when entering or leaving multi-material painting gizmo. --- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 3 +++ src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 3 +++ src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 10 ++-------- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 3 +++ src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp | 3 +++ 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index 019ed15f40..f2c19ba5c8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -19,6 +19,9 @@ protected: wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + std::string get_gizmo_entering_text() const override { return _u8L("Entering Paint-on supports"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Paint-on supports"); } + private: bool on_init() override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index afd5854a09..ab58ba1866 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -128,6 +128,9 @@ protected: wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + std::string get_gizmo_entering_text() const override { return _u8L("Entering Multimaterial painting"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Multimaterial painting"); } + size_t m_first_selected_extruder_idx = 0; size_t m_second_selected_extruder_idx = 1; std::vector m_original_extruders_names; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index ac9d7adcf1..9477b89c7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -47,20 +47,14 @@ void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate) plater->undo_redo_topmost_string_getter(plater->can_undo(), last_snapshot_name); if (activate && !m_internal_stack_active) { - std::string str = get_painter_type() == PainterGizmoType::FDM_SUPPORTS - ? _u8L("Entering Paint-on supports") - : _u8L("Entering Seam painting"); - if (last_snapshot_name != str) + if (std::string str = this->get_gizmo_entering_text(); last_snapshot_name != str) Plater::TakeSnapshot(plater, str); plater->enter_gizmos_stack(); m_internal_stack_active = true; } if (!activate && m_internal_stack_active) { plater->leave_gizmos_stack(); - std::string str = get_painter_type() == PainterGizmoType::SEAM - ? _u8L("Leaving Seam painting") - : _u8L("Leaving Paint-on supports"); - if (last_snapshot_name != str) + if (std::string str = this->get_gizmo_leaving_text(); last_snapshot_name != str) Plater::TakeSnapshot(plater, str); m_internal_stack_active = false; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 4db17d5974..c20c8140d0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -173,6 +173,9 @@ protected: virtual wxString handle_snapshot_action_name(bool shift_down, Button button_down) const = 0; + virtual std::string get_gizmo_entering_text() const = 0; + virtual std::string get_gizmo_leaving_text() const = 0; + friend class ::Slic3r::GUI::GLGizmoMmuSegmentation; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp index 196fe5023a..7a00a9d8e6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp @@ -20,6 +20,9 @@ protected: wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + std::string get_gizmo_entering_text() const override { return _u8L("Entering Seam painting"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Seam painting"); } + private: bool on_init() override; From 9c1c72325305455bbf73ad9b76d4b2cc7ed900e9 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 30 Aug 2021 11:46:26 +0200 Subject: [PATCH 034/151] Fixed volume name when loading a 3mf, saved with 3rd part software, as project --- src/libslic3r/Format/3mf.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index e5007a3f4f..57b0d81159 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1837,6 +1837,7 @@ namespace Slic3r { } unsigned int geo_tri_count = (unsigned int)geometry.triangles.size() / 3; + unsigned int renamed_volumes_count = 0; for (const ObjectMetadata::VolumeMetadata& volume_data : volumes) { if (geo_tri_count <= volume_data.first_triangle_id || geo_tri_count <= volume_data.last_triangle_id || volume_data.last_triangle_id < volume_data.first_triangle_id) { @@ -1958,6 +1959,14 @@ namespace Slic3r { else volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); } + + // this may happen for 3mf saved by 3rd part softwares + if (volume->name.empty()) { + volume->name = object.name; + if (renamed_volumes_count > 0) + volume->name += "_" + std::to_string(renamed_volumes_count + 1); + ++renamed_volumes_count; + } } return true; From c32f2fb7659cc0ac9923e6774bb94382f186ec39 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 30 Aug 2021 12:14:45 +0200 Subject: [PATCH 035/151] New project command enabled when the plater is empty but the project state is marked as dirty --- src/slic3r/GUI/MainFrame.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 329a9a62a7..900b9e59f8 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -620,7 +620,10 @@ void MainFrame::update_title() wxString dirty_marker = (!m_plater->model().objects.empty() && m_plater->is_project_dirty()) ? "*" : ""; if (!dirty_marker.empty() || !project.empty()) { if (!dirty_marker.empty() && project.empty()) - project = _("Untitled"); +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + project = _L("Untitled"); +// project = _("Untitled"); +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ title = dirty_marker + project + " - "; } } @@ -819,7 +822,7 @@ bool MainFrame::is_active_and_shown_tab(Tab* tab) bool MainFrame::can_start_new_project() const { - return (m_plater != nullptr) && (!m_plater->get_project_filename(".3mf").IsEmpty() || !m_plater->model().objects.empty()); + return (m_plater != nullptr) && (!m_plater->get_project_filename(".3mf").IsEmpty() || GetTitle().StartsWith('*') || !m_plater->model().objects.empty()); } bool MainFrame::can_save() const From ce3f51379aad8cffb78bac09e235f0cad74de60b Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 30 Aug 2021 14:13:41 +0200 Subject: [PATCH 036/151] ENABLE_SEAMS_USING_MODELS -> Increased size of instances --- src/slic3r/GUI/GCodeViewer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9ac0536a41..aaea44c7e8 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1443,9 +1443,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) instances.push_back(curr.position.y()); instances.push_back(curr.position.z()); // append width - instances.push_back(1.2f * curr.width); + instances.push_back(1.5f * curr.width); // append height - instances.push_back(1.2f * curr.height); + instances.push_back(1.5f * curr.height); // append id instances_ids.push_back(move_id); From dcbaf87048543e01289972a9df8b6e4ac669bf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Bubn=C3=ADk?= <5830947+bubnikv@users.noreply.github.com> Date: Mon, 30 Aug 2021 14:32:05 +0200 Subject: [PATCH 037/151] Note on desktop integration (PrusaSlicer 2.4) --- doc/How to build - Linux et al.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/How to build - Linux et al.md b/doc/How to build - Linux et al.md index cf47c93921..4a48147dcd 100644 --- a/doc/How to build - Linux et al.md +++ b/doc/How to build - Linux et al.md @@ -87,3 +87,13 @@ If you instead want PrusaSlicer installed in a structure according to the File S This will make PrusaSlicer look for a fixed-location `share/slic3r-prusa3d` directory instead (note that the location becomes hardcoded). You can then use the `make install` target to install PrusaSlicer. + +### Desktop Integration (PrusaSlicer 2.4 and newer) + +If PrusaSlicer is to be distributed as an AppImage or a binary blob (.tar.gz and similar), then a desktop integration support is compiled in by default: PrusaSlicer will offer to integrate with desktop by manually copying the desktop file and application icon into user's desktop configuration. The built-in desktop integration is also handy on Crosstini (Linux on Chrome OS). + +If PrusaSlicer is compiled with `SLIC3R_FHS` enabled, then a desktop integration support will not be integrated. One may want to disable desktop integration by running + + cmake .. -DSLIC3R_DESKTOP_INTEGRATION=0 + +when building PrusaSlicer for flatpack or snap, where the desktop integration is performed by the installer. From 87b542597e7a0490bf9ea757865eb2a8afa0cc23 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 30 Aug 2021 14:52:47 +0200 Subject: [PATCH 038/151] Referenced https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b93a47b01..7fdf11bbb4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # PrusaSlicer You may want to check the [PrusaSlicer project page](https://www.prusa3d.com/prusaslicer/). -Prebuilt Windows, OSX and Linux binaries are available through the [git releases page](https://github.com/prusa3d/PrusaSlicer/releases) or from the [Prusa3D downloads page](https://www.prusa3d.com/drivers/). +Prebuilt Windows, OSX and Linux binaries are available through the [git releases page](https://github.com/prusa3d/PrusaSlicer/releases) or from the [Prusa3D downloads page](https://www.prusa3d.com/drivers/). There are also [3rd party Linux builds available](https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions). PrusaSlicer takes 3D models (STL, OBJ, AMF) and converts them into G-code instructions for FFF printers or PNG layers for mSLA 3D printers. It's From a571e0f9c46e3b1b59b9a8c32dcc6c26972accec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Bubn=C3=ADk?= <5830947+bubnikv@users.noreply.github.com> Date: Mon, 30 Aug 2021 15:00:34 +0200 Subject: [PATCH 039/151] Update How to build - Linux et al.md --- doc/How to build - Linux et al.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/How to build - Linux et al.md b/doc/How to build - Linux et al.md index 4a48147dcd..299d82381f 100644 --- a/doc/How to build - Linux et al.md +++ b/doc/How to build - Linux et al.md @@ -1,6 +1,12 @@ # Building PrusaSlicer on UNIX/Linux +Please understand that PrusaSlicer team cannot support compilation issues on all possible Linux distros. Namely, we cannot help trouble shooting OpenGL driver issues or dependency issues if compiled against distro provided libraries. We can only support PrusaSlicer compiled the same way we do compile PrusaSlicer for our [binary builds](https://github.com/prusa3d/PrusaSlicer/releases), that is linked statically agains the dependencies compiled with the `deps` scripts. + +Instead of compiling PrusaSlicer from source code, you may consider to install PrusaSlicer [pre-compiled by contributors](https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions). + +### How to build + PrusaSlicer uses the CMake build system and requires several dependencies. The dependencies can be listed in the `deps` directory in individual subdirectories, although they don't necessarily need to be as recent as the versions listed - generally versions available on conservative Linux distros such as Debian stable, Ubuntu LTS releases or Fedora are likely sufficient. From 1e3100d295530bf41d6f1f5b57a70c3904054611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Bubn=C3=ADk?= <5830947+bubnikv@users.noreply.github.com> Date: Mon, 30 Aug 2021 15:02:30 +0200 Subject: [PATCH 040/151] Update How to build - Linux et al.md --- doc/How to build - Linux et al.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/How to build - Linux et al.md b/doc/How to build - Linux et al.md index 299d82381f..c9ad62dc21 100644 --- a/doc/How to build - Linux et al.md +++ b/doc/How to build - Linux et al.md @@ -1,9 +1,9 @@ # Building PrusaSlicer on UNIX/Linux -Please understand that PrusaSlicer team cannot support compilation issues on all possible Linux distros. Namely, we cannot help trouble shooting OpenGL driver issues or dependency issues if compiled against distro provided libraries. We can only support PrusaSlicer compiled the same way we do compile PrusaSlicer for our [binary builds](https://github.com/prusa3d/PrusaSlicer/releases), that is linked statically agains the dependencies compiled with the `deps` scripts. +Please understand that PrusaSlicer team cannot support compilation on all possible Linux distros. Namely, we cannot help trouble shooting OpenGL driver issues or dependency issues if compiled against distro provided libraries. We can only support PrusaSlicer compiled the same way we do compile PrusaSlicer for our [binary builds](https://github.com/prusa3d/PrusaSlicer/releases), that means linked statically agains the dependencies compiled with the `deps` scripts. -Instead of compiling PrusaSlicer from source code, you may consider to install PrusaSlicer [pre-compiled by contributors](https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions). +Instead of compiling PrusaSlicer from source code, one may consider to install PrusaSlicer [pre-compiled by contributors](https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions). ### How to build From 9b97414a946401cef4a845345d9683da72b6b582 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Mon, 30 Aug 2021 15:52:24 +0200 Subject: [PATCH 041/151] Mention DEP_DOWNLOAD_DIR in docs Also update dependency report, add CGAL --- doc/Dependencies.md | 3 +++ doc/How to build - Linux et al.md | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/Dependencies.md b/doc/Dependencies.md index 137aaf17ba..f37f454668 100644 --- a/doc/Dependencies.md +++ b/doc/Dependencies.md @@ -11,6 +11,9 @@ * openssl * nlopt * openvdb: This library depends on other libs, namely boost, zlib, openexr, blosc (not strictly), etc... +* CGAL: Needs additional dependencies + * MPFR + * GMP ## External libraries in source tree * ad-mesh: Lots of customization, have to be bundled in the source tree. diff --git a/doc/How to build - Linux et al.md b/doc/How to build - Linux et al.md index c9ad62dc21..559536f143 100644 --- a/doc/How to build - Linux et al.md +++ b/doc/How to build - Linux et al.md @@ -38,10 +38,11 @@ Note: We say _mostly independent_ because it's still expected the system will pr To do this, go to the `deps` directory, create a `build` subdirectory (or the like) and use: - cmake .. -DDESTDIR= + cmake .. -DDESTDIR= -DDEP_DOWNLOAD_DIR= where the target destdir is a directory of your choosing where the dependencies will be installed. -You can also omit the `DESTDIR` option to use the default, in that case the `destdir` will be created inside the `build` directory where `cmake` is run. +You can also omit the `DESTDIR` option to use the default, in that case the `destdir` will be created inside the `build` directory where `cmake` is run. The optional `DEP_DOWNLOAD_DIR` argument specifies a directory to cache the downloaded +source packages for each dependent library. Can be useful for repeated builds, to avoid unnecessary network traffic. Once the dependencies have been built, in order to pass the destdir path to the **top-level** PrusaSlicer `CMakeLists.txt` script, use the `CMAKE_PREFIX_PATH` option along with turning on `SLIC3R_STATIC`: From b5742eabe07ffa58e32bed9d8b2c3232b402b602 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 30 Aug 2021 16:38:51 +0200 Subject: [PATCH 042/151] Delayed notifications list Hint notification is delayed until empty notifications --- src/slic3r/GUI/NotificationManager.cpp | 45 +++++++++++++++++++++++++- src/slic3r/GUI/NotificationManager.hpp | 19 +++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 3278a5a6e3..9b01cb2fc1 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1369,7 +1369,21 @@ void NotificationManager::push_hint_notification(bool open_next) } NotificationData data{ NotificationType::DidYouKnowHint, NotificationLevel::RegularNotification, 300, "" }; - push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, open_next), 0); + // from user + if (!open_next) { + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, open_next), 0); + // delete from delayed list + for (auto it = m_waiting_notifications.begin(); it != m_waiting_notifications.end();) { + if ((*it).notification->get_type() == NotificationType::DidYouKnowHint) { + it = m_waiting_notifications.erase(it); + } else { + ++it; + } + } + // at startup + } else { + push_delayed_notification(std::make_unique(data, m_id_provider, m_evt_handler, open_next), 500, 30000); + } } bool NotificationManager::is_hint_notification_open() @@ -1425,6 +1439,16 @@ bool NotificationManager::push_notification_data(std::unique_ptr notification, int64_t initial_delay, int64_t delay_interval) +{ + if (initial_delay == 0 && m_pop_notifications.empty()) { + push_notification_data(std::move(notification), 0); + } else { + m_waiting_notifications.emplace_back(std::move(notification), initial_delay, delay_interval); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(initial_delay); + } +} + void NotificationManager::render_notifications(GLCanvas3D& canvas, float overlay_width) { sort_notifications(); @@ -1477,6 +1501,25 @@ bool NotificationManager::update_notifications(GLCanvas3D& canvas) ++it; } + // delayed notifications + for (auto it = m_waiting_notifications.begin(); it != m_waiting_notifications.end();) { + // substract time + if ((*it).remaining_time > 0) + (*it).remaining_time -= time_since_render; + if ((*it).remaining_time <= 0) { + if (m_pop_notifications.empty()) { // push notification, erase it from waiting list (frame is scheduled by push) + (*it).notification->reset_timer(); + push_notification_data(std::move((*it).notification), 0); + it = m_waiting_notifications.erase(it); + continue; + } else { // not possible to push, delay for delay_interval + (*it).remaining_time = (*it).delay_interval; + } + } + next_render = std::min(next_render, (*it).remaining_time); + ++it; + } + // request next frame in future if (next_render < max) canvas.schedule_extra_frame(int(next_render)); diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 038290f823..d2a2eb5a78 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -262,6 +262,8 @@ private: EState get_state() const { return m_state; } bool is_hovered() const { return m_state == EState::Hovered; } void set_hovered() { if (m_state != EState::Finished && m_state != EState::ClosePending && m_state != EState::Hidden && m_state != EState::Unknown) m_state = EState::Hovered; } + // set start of notification to now. Used by delayed notifications + void reset_timer() { m_notification_start = GLCanvas3D::timestamp_now(); m_state = EState::Shown; } protected: // Call after every size change virtual void init(); @@ -515,10 +517,25 @@ private: // in HintNotification.hpp class HintNotification; + // Data of waiting notifications + struct DelayedNotification + { + std::unique_ptr notification; + int64_t remaining_time; + int64_t delay_interval; + DelayedNotification(std::unique_ptr n, int64_t r, int64_t d) + : notification(std::move(n)) + , remaining_time(r) + , delay_interval(d) + {} + }; + //pushes notification into the queue of notifications that are rendered //can be used to create custom notification bool push_notification_data(const NotificationData& notification_data, int timestamp); bool push_notification_data(std::unique_ptr notification, int timestamp); + // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 and m_pop_notifications are empty, notification is regular pushed. Otherwise another delay interval waiting. Timestamp is 0. + void push_delayed_notification(std::unique_ptr notification, int64_t initial_delay, int64_t delay_interval); //finds older notification of same type and moves it to the end of queue. returns true if found bool activate_existing(const NotificationManager::PopNotification* notification); // Put the more important notifications to the bottom of the list. @@ -531,6 +548,8 @@ private: // Cache of IDs to identify and reuse ImGUI windows. NotificationIDProvider m_id_provider; std::deque> m_pop_notifications; + // delayed waiting notifications, first is remaining time + std::deque m_waiting_notifications; //timestamps used for slicing finished - notification could be gone so it needs to be stored here std::unordered_set m_used_timestamps; // True if G-code preview is active. False if the Plater is active. From f92312b597e198ba20fb5b91bb3ceaddee178479 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 30 Aug 2021 17:56:58 +0200 Subject: [PATCH 043/151] Fixed parsing of Config from AMF / 3MF if it started with an empty line. This bug was introduced with e947a29fc88f098febd2d93a8d9acf8ccedd4229 --- src/libslic3r/Config.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index c8a3835dde..ab58a43aa0 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -696,10 +696,8 @@ ConfigSubstitutions ConfigBase::load_from_ini_string_commented(std::string &&dat for (size_t i = 0; i < data.size();) if (i == 0 || data[i] == '\n') { // Start of a line. - if (i != 0) { - // Consume LF. - assert(data[i] == '\n'); - // Don't keep empty lines. + if (data[i] == '\n') { + // Consume LF, don't keep empty lines. if (j > 0 && data[j - 1] != '\n') data[j ++] = data[i]; ++ i; From 368e9d7f4e8894284a490f97f9aaa119cad2dec2 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 31 Aug 2021 08:07:38 +0200 Subject: [PATCH 044/151] Do not add an undo/redo snapshot when hitting Del key while nothing is selected --- src/slic3r/GUI/Plater.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d315e7734d..4e9adab62b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5159,8 +5159,11 @@ void Plater::delete_object_from_model(size_t obj_idx) { p->delete_object_from_mo void Plater::remove_selected() { + if (p->get_selection().is_empty()) + return; + Plater::TakeSnapshot snapshot(this, _L("Delete Selected Objects")); - this->p->view3D->delete_selected(); + p->view3D->delete_selected(); } void Plater::increase_instances(size_t num) From 9a5f61c306ce07739579b8ee67a5bfbc8be5f909 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 31 Aug 2021 08:53:40 +0200 Subject: [PATCH 045/151] Added missing vertical alignments of labels in imgui dialogs --- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp | 9 ++++++++- src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp | 5 ++++- src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 3 +-- src/slic3r/GUI/MainFrame.cpp | 3 --- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 4f3e31beaa..99c010bf1d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -131,6 +131,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l m_imgui->text(""); ImGui::Separator(); + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc["highlight_by_angle"] + ":"); ImGui::AlignTextToFramePadding(); std::string format_str = std::string("%.f") + I18N::translate_utf8("°", diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 46608b4c73..05e0be1410 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -542,6 +542,7 @@ RENDER_AGAIN: m_imgui->disabled_begin(! m_enable_hollowing); float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("offset")); ImGui::SameLine(settings_sliders_left); ImGui::PushItemWidth(window_width - settings_sliders_left); @@ -558,6 +559,7 @@ RENDER_AGAIN: bool slider_released = ImGui::IsItemDeactivatedAfterEdit(); // someone has just released the slider if (current_mode >= quality_mode) { + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("quality")); ImGui::SameLine(settings_sliders_left); m_imgui->slider_float(" ", &quality, quality_min, quality_max, "%.1f"); @@ -574,6 +576,7 @@ RENDER_AGAIN: } if (current_mode >= closing_d_mode) { + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("closing_distance")); ImGui::SameLine(settings_sliders_left); m_imgui->slider_float(" ", &closing_d, closing_d_min, closing_d_max, "%.1f mm"); @@ -621,6 +624,7 @@ RENDER_AGAIN: float diameter_upper_cap = 60.; if (m_new_hole_radius * 2.f > diameter_upper_cap) m_new_hole_radius = diameter_upper_cap / 2.f; + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("hole_diameter")); ImGui::SameLine(diameter_slider_left); ImGui::PushItemWidth(window_width - diameter_slider_left); @@ -636,6 +640,7 @@ RENDER_AGAIN: bool edited = ImGui::IsItemEdited(); bool deactivated = ImGui::IsItemDeactivatedAfterEdit(); + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc["hole_depth"]); ImGui::SameLine(diameter_slider_left); m_imgui->slider_float(" ", &m_new_hole_height, 0.f, 10.f, "%.1f mm", 1.f, false); @@ -692,8 +697,10 @@ RENDER_AGAIN: // Following is rendered in both editing and non-editing mode: // m_imgui->text(""); ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("clipping_of_view")); + } else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index ff9888e199..5b84bbaba4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -138,6 +138,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("cursor_size")); ImGui::SameLine(cursor_size_slider_left); ImGui::PushItemWidth(window_width - cursor_size_slider_left); @@ -188,8 +189,10 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("clipping_of_view")); + } else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 40b8d436d6..8c90d20d31 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -786,8 +786,7 @@ RENDER_AGAIN: // Following is rendered in both editing and non-editing mode: ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) - { + if (m_c->object_clipper()->get_position() == 0.f) { ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("clipping_of_view")); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 900b9e59f8..fa83f4bdf8 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -620,10 +620,7 @@ void MainFrame::update_title() wxString dirty_marker = (!m_plater->model().objects.empty() && m_plater->is_project_dirty()) ? "*" : ""; if (!dirty_marker.empty() || !project.empty()) { if (!dirty_marker.empty() && project.empty()) -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ project = _L("Untitled"); -// project = _("Untitled"); -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ title = dirty_marker + project + " - "; } } From 270c076e77663895e69d975e276e3edf6643cc1a Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Mon, 30 Aug 2021 17:23:44 +0200 Subject: [PATCH 046/151] Fixed undo/redo issue when clear method of FacetsAnnotation and ModelConfig reset timestamp to 1. This led to a bug where e.g. deleting painted facets through the respective item in object list followed by possible other actions and undo restored the painted facets from the time when the project was loaded. I'm not sure if there was any other situation where this problem manifested. --- src/libslic3r/Model.cpp | 10 +++++----- src/libslic3r/Model.hpp | 5 ++++- src/libslic3r/ObjectID.hpp | 3 --- src/libslic3r/PrintConfig.hpp | 6 ++++-- src/slic3r/GUI/GUI_ObjectList.cpp | 12 ++++++------ src/slic3r/GUI/Plater.cpp | 6 +++--- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 6654d3a130..6f9e4fd4c1 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1198,9 +1198,9 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr for (ModelVolume *volume : volumes) { const auto volume_matrix = volume->get_matrix(); - volume->supported_facets.clear(); - volume->seam_facets.clear(); - volume->mmu_segmentation_facets.clear(); + volume->supported_facets.reset(); + volume->seam_facets.reset(); + volume->mmu_segmentation_facets.reset(); if (! volume->is_model_part()) { // Modifiers are not cut, but we still need to add the instance transformation @@ -2019,11 +2019,11 @@ bool FacetsAnnotation::set(const TriangleSelector& selector) return false; } -void FacetsAnnotation::clear() +void FacetsAnnotation::reset() { m_data.first.clear(); m_data.second.clear(); - this->reset_timestamp(); + this->touch(); } // Following function takes data from a triangle and encodes it as string diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index b89dd5aa1e..ec6fac8214 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -541,7 +541,10 @@ public: indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; bool empty() const { return m_data.first.empty(); } - void clear(); + + // Following method clears the config and increases its timestamp, so the deleted + // state is considered changed from perspective of the undo/redo stack. + void reset(); // Serialize triangle into string, for serialization into 3MF/AMF. std::string get_triangle_as_string(int i) const; diff --git a/src/libslic3r/ObjectID.hpp b/src/libslic3r/ObjectID.hpp index ea7c748a50..1030171e7f 100644 --- a/src/libslic3r/ObjectID.hpp +++ b/src/libslic3r/ObjectID.hpp @@ -105,9 +105,6 @@ protected: // The class tree will have virtual tables and type information. virtual ~ObjectWithTimestamp() = default; - // Resetting timestamp to 1 indicates the object is in its initial (cleared) state. - // To be called by the derived class's clear() method. - void reset_timestamp() { m_timestamp = 1; } // The timestamp uniquely identifies content of the derived class' data, therefore it makes sense to copy the timestamp if the content data was copied. void copy_timestamp(const ObjectWithTimestamp& rhs) { m_timestamp = rhs.m_timestamp; } diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index b15651ba70..64c8445f8c 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1064,7 +1064,9 @@ Points get_bed_shape(const SLAPrinterConfig &cfg); class ModelConfig { public: - void clear() { m_data.clear(); m_timestamp = 1; } + // Following method clears the config and increases its timestamp, so the deleted + // state is considered changed from perspective of the undo/redo stack. + void reset() { m_data.clear(); touch(); } void assign_config(const ModelConfig &rhs) { if (m_timestamp != rhs.m_timestamp) { @@ -1076,7 +1078,7 @@ public: if (m_timestamp != rhs.m_timestamp) { m_data = std::move(rhs.m_data); m_timestamp = rhs.m_timestamp; - rhs.clear(); + rhs.reset(); } } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b85219c0b4..6be9a7accf 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1803,21 +1803,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) cnv->get_gizmos_manager().reset_all_states(); Plater::TakeSnapshot(plater, _L("Remove paint-on supports")); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) - mv->supported_facets.clear(); + mv->supported_facets.reset(); break; case InfoItemType::CustomSeam: cnv->get_gizmos_manager().reset_all_states(); Plater::TakeSnapshot(plater, _L("Remove paint-on seam")); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) - mv->seam_facets.clear(); + mv->seam_facets.reset(); break; case InfoItemType::MmuSegmentation: cnv->get_gizmos_manager().reset_all_states(); Plater::TakeSnapshot(plater, _L("Remove Multi Material painting")); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) - mv->mmu_segmentation_facets.clear(); + mv->mmu_segmentation_facets.reset(); break; case InfoItemType::Sinking: @@ -1856,7 +1856,7 @@ void ObjectList::del_settings_from_config(const wxDataViewItem& parent_item) if (is_layer_settings) layer_height = m_config->opt_float("layer_height"); - m_config->clear(); + m_config->reset(); if (extruder >= 0) m_config->set_key_value("extruder", new ConfigOptionInt(extruder)); @@ -1932,7 +1932,7 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con const auto last_volume = object->volumes[0]; if (!last_volume->config.empty()) { object->config.apply(last_volume->config); - last_volume->config.clear(); + last_volume->config.reset(); // update extruder color in ObjectList wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx); @@ -3769,7 +3769,7 @@ void ObjectList::last_volume_is_deleted(const int obj_idx) auto volume = (*m_objects)[obj_idx]->volumes.front(); // clear volume's config values - volume->config.clear(); + volume->config.reset(); // set a default extruder value, since user can't add it manually volume->config.set_key_value("extruder", new ConfigOptionInt(0)); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4e9adab62b..e8ce905161 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6217,9 +6217,9 @@ void Plater::clear_before_change_mesh(int obj_idx) bool paint_removed = false; for (ModelVolume* mv : mo->volumes) { paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mmu_segmentation_facets.empty(); - mv->supported_facets.clear(); - mv->seam_facets.clear(); - mv->mmu_segmentation_facets.clear(); + mv->supported_facets.reset(); + mv->seam_facets.reset(); + mv->mmu_segmentation_facets.reset(); } if (paint_removed) { // snapshot_time is captured by copy so the lambda knows where to undo/redo to. From 86ddac7b1ec4e9a1ff33b11363e55607b2a09f55 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 31 Aug 2021 09:17:52 +0200 Subject: [PATCH 047/151] Fixed warning --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index aaea44c7e8..732403b03e 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2665,7 +2665,7 @@ void GCodeViewer::render_toolpaths() }; #if ENABLE_SEAMS_USING_MODELS - auto render_as_instanced_model = [this] + auto render_as_instanced_model = [] (TBuffer& buffer, GLShaderProgram & shader) { for (auto& range : buffer.model.instances.render_ranges.ranges) { if (range.vbo == 0 && range.count > 0) { From 04778e0fa55dee620637f2e7a5615711655cc182 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Tue, 31 Aug 2021 10:34:39 +0200 Subject: [PATCH 048/151] delayed notifications: custom condition function for showing and refactoring --- src/slic3r/GUI/NotificationManager.cpp | 65 +++++++++++++++++--------- src/slic3r/GUI/NotificationManager.hpp | 17 +++++-- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 9b01cb2fc1..219faf42ef 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1369,20 +1369,17 @@ void NotificationManager::push_hint_notification(bool open_next) } NotificationData data{ NotificationType::DidYouKnowHint, NotificationLevel::RegularNotification, 300, "" }; - // from user + // from user - open now if (!open_next) { push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, open_next), 0); - // delete from delayed list - for (auto it = m_waiting_notifications.begin(); it != m_waiting_notifications.end();) { - if ((*it).notification->get_type() == NotificationType::DidYouKnowHint) { - it = m_waiting_notifications.erase(it); - } else { - ++it; - } - } - // at startup + stop_delayed_notifications_of_type(NotificationType::DidYouKnowHint); + // at startup - delay for half a second to let other notification pop up, than try every 30 seconds + // show only if no notifications are shown } else { - push_delayed_notification(std::make_unique(data, m_id_provider, m_evt_handler, open_next), 500, 30000); + auto condition = [this]() { + return this->get_notification_count() == 0; + }; + push_delayed_notification(std::make_unique(data, m_id_provider, m_evt_handler, open_next), condition, 500, 30000); } } @@ -1439,13 +1436,25 @@ bool NotificationManager::push_notification_data(std::unique_ptr notification, int64_t initial_delay, int64_t delay_interval) +void NotificationManager::push_delayed_notification(std::unique_ptr notification, std::function condition_callback, int64_t initial_delay, int64_t delay_interval) { - if (initial_delay == 0 && m_pop_notifications.empty()) { - push_notification_data(std::move(notification), 0); - } else { - m_waiting_notifications.emplace_back(std::move(notification), initial_delay, delay_interval); - wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(initial_delay); + if (initial_delay == 0 && condition_callback()) { + if( push_notification_data(std::move(notification), 0)) + return; + } + m_waiting_notifications.emplace_back(std::move(notification), condition_callback, initial_delay == 0 ? delay_interval : initial_delay, delay_interval); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(initial_delay == 0 ? delay_interval : initial_delay); +} + +void NotificationManager::stop_delayed_notifications_of_type(const NotificationType type) +{ + for (auto it = m_waiting_notifications.begin(); it != m_waiting_notifications.end();) { + if ((*it).notification->get_type() == type) { + it = m_waiting_notifications.erase(it); + } + else { + ++it; + } } } @@ -1507,14 +1516,15 @@ bool NotificationManager::update_notifications(GLCanvas3D& canvas) if ((*it).remaining_time > 0) (*it).remaining_time -= time_since_render; if ((*it).remaining_time <= 0) { - if (m_pop_notifications.empty()) { // push notification, erase it from waiting list (frame is scheduled by push) + if ((*it).condition_callback()) { // push notification, erase it from waiting list (frame is scheduled by push) (*it).notification->reset_timer(); - push_notification_data(std::move((*it).notification), 0); - it = m_waiting_notifications.erase(it); - continue; - } else { // not possible to push, delay for delay_interval - (*it).remaining_time = (*it).delay_interval; + if (push_notification_data(std::move((*it).notification), 0)) { + it = m_waiting_notifications.erase(it); + continue; + } } + // not possible to push, delay for delay_interval + (*it).remaining_time = (*it).delay_interval; } next_render = std::min(next_render, (*it).remaining_time); ++it; @@ -1612,6 +1622,15 @@ void NotificationManager::device_ejected() notification->close(); } } +size_t NotificationManager::get_notification_count() const +{ + size_t ret = 0; + for (const std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_state() != PopNotification::EState::Hidden) + ret++; + } + return ret; +} }//namespace GUI }//namespace Slic3r diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index d2a2eb5a78..4aba35f4cb 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -190,6 +190,8 @@ public: void set_move_from_overlay(bool move) { m_move_from_overlay = move; } // perform update_state on each notification and ask for more frames if needed, return true for render needed bool update_notifications(GLCanvas3D& canvas); + // returns number of all notifications shown + size_t get_notification_count() const; private: // duration 0 means not disapearing struct NotificationData { @@ -521,10 +523,13 @@ private: struct DelayedNotification { std::unique_ptr notification; + std::function condition_callback; int64_t remaining_time; int64_t delay_interval; - DelayedNotification(std::unique_ptr n, int64_t r, int64_t d) + + DelayedNotification(std::unique_ptr n, std::function cb, int64_t r, int64_t d) : notification(std::move(n)) + , condition_callback(cb) , remaining_time(r) , delay_interval(d) {} @@ -534,8 +539,14 @@ private: //can be used to create custom notification bool push_notification_data(const NotificationData& notification_data, int timestamp); bool push_notification_data(std::unique_ptr notification, int timestamp); - // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 and m_pop_notifications are empty, notification is regular pushed. Otherwise another delay interval waiting. Timestamp is 0. - void push_delayed_notification(std::unique_ptr notification, int64_t initial_delay, int64_t delay_interval); + // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 + // and condition callback is success, notification is regular pushed from update function. + // Otherwise another delay interval waiting. Timestamp is 0. + // Note that notification object is constructed when being added to the waiting list, but there are no updates called on it and its timer is reset at regular push. + // Also note that no control of same notification is done during push_delayed_notification but if waiting notif fails to push, it continues waiting. + void push_delayed_notification(std::unique_ptr notification, std::function condition_callback, int64_t initial_delay, int64_t delay_interval); + // Removes all notifications of type from m_waiting_notifications + void stop_delayed_notifications_of_type(const NotificationType type); //finds older notification of same type and moves it to the end of queue. returns true if found bool activate_existing(const NotificationManager::PopNotification* notification); // Put the more important notifications to the bottom of the list. From 788d114a2fe727015b6622d4699d04a1ce2221c7 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Tue, 31 Aug 2021 11:26:00 +0200 Subject: [PATCH 049/151] Passing call_selection_changed to update_info_items. Helps to decide correctly if object was added or only undo / redo operation. --- src/slic3r/GUI/GUI_ObjectList.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 6be9a7accf..82f03f51ac 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2637,7 +2637,7 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed) model_object->config.has("extruder") ? model_object->config.extruder() : 0, get_mesh_errors_count(obj_idx) > 0); - update_info_items(obj_idx, nullptr, true); + update_info_items(obj_idx, nullptr, call_selection_changed); // add volumes to the object if (model_object->volumes.size() > 1) { From 4cc729b312533e8b2001b93b51f0f15c69229a78 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Tue, 31 Aug 2021 12:05:06 +0200 Subject: [PATCH 050/151] Fixed encoding of undo/redo snapshot names created through TakeSnapshot class (implicit std::string/wxString conversion) --- src/slic3r/GUI/Plater.cpp | 5 +++++ src/slic3r/GUI/Plater.hpp | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e8ce905161..14350bddf1 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6581,6 +6581,11 @@ bool Plater::is_render_statistic_dialog_visible() const return p->show_render_statistic_dialog; } + +Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name) +: TakeSnapshot(plater, from_u8(snapshot_name)) {} + + // Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu. bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos) { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 3556756e23..99f7e3cdaa 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -383,10 +383,11 @@ public: Plater *m_plater; }; - // ROII wrapper for taking an Undo / Redo snapshot while disabling the snapshot taking by the methods called from inside this snapshot. + // RAII wrapper for taking an Undo / Redo snapshot while disabling the snapshot taking by the methods called from inside this snapshot. class TakeSnapshot { public: + TakeSnapshot(Plater *plater, const std::string &snapshot_name); TakeSnapshot(Plater *plater, const wxString &snapshot_name) : m_plater(plater) { m_plater->take_snapshot(snapshot_name); From 5f26bfd3974c006f3aa0e419c2f7f866225d50de Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 31 Aug 2021 12:22:38 +0200 Subject: [PATCH 051/151] Brim separation from object, follow up to 82373334bcdaaaf10ed860fd2887b635f815d81f 1) Changed the name of the variable "brim_offset" to "brim_separation" for clarity. 2) Added legacy conversion after loading an old 3MF that does not define then new "brim_separation" variable: The "brim_separation" is being filled in with the "elefant_foot_compensation" value to produce equal brim separation to the old PrusaSlicer that saved that 3MF file. --- resources/profiles/PrusaResearch.idx | 2 +- resources/profiles/PrusaResearch.ini | 2 +- src/libslic3r/Brim.cpp | 26 +++++++++++++------------- src/libslic3r/Format/3mf.cpp | 15 +++++++++++++++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 6 +++--- src/libslic3r/PrintConfig.hpp | 2 +- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportMaterial.cpp | 14 +++++++------- src/slic3r/GUI/ConfigManipulation.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 2 +- 12 files changed, 46 insertions(+), 31 deletions(-) diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index 37d144ff0f..8a34a9a113 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,5 +1,5 @@ min_slic3r_version = 2.4.0-alpha0 -1.4.0-alpha7 Updated brim_offset value. Updated Prusa MINI end g-code. Added Filamentworld filament profiles. +1.4.0-alpha7 Updated brim_separation value. Updated Prusa MINI end g-code. Added Filamentworld filament profiles. 1.4.0-alpha6 Added nozzle priming after M600. Added nozzle diameter checks for 0.8 nozzle printer profiles. Updated FW version. Increased number of top solid infill layers (0.2 layer height). 1.4.0-alpha5 Added multiple add:north and Extrudr filament profiles. Updated support head settings (SL1S). 1.4.0-alpha4 Decreased Area Fill (SL1S). diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 0ed5b59a6d..835e552a18 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -144,7 +144,7 @@ bridge_angle = 0 bridge_flow_ratio = 1 bridge_speed = 25 brim_width = 0 -brim_offset = 0.1 +brim_separation = 0.1 clip_multipart_objects = 1 compatible_printers = complete_objects = 0 diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index f4455fdd55..f20404bc9f 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -134,10 +134,10 @@ static Polygons top_level_outer_brim_islands(const ConstPrintObjectPtrs &top_lev Polygons islands; for (const PrintObject *object : top_level_objects_with_brim) { //FIXME how about the brim type? - auto brim_offset = float(scale_(object->config().brim_offset.value)); + auto brim_separation = float(scale_(object->config().brim_separation.value)); Polygons islands_object; for (const ExPolygon &ex_poly : get_print_object_bottom_layer_expolygons(*object)) { - Polygons contour_offset = offset(ex_poly.contour, brim_offset); + Polygons contour_offset = offset(ex_poly.contour, brim_separation); for (Polygon &poly : contour_offset) poly.douglas_peucker(SCALED_RESOLUTION); @@ -166,7 +166,7 @@ static ExPolygons top_level_outer_brim_area(const Print &print for(size_t print_object_idx = 0; print_object_idx < print.objects().size(); ++print_object_idx) { const PrintObject *object = print.objects()[print_object_idx]; const BrimType brim_type = object->config().brim_type.value; - const float brim_offset = scale_(object->config().brim_offset.value); + const float brim_separation = scale_(object->config().brim_separation.value); const float brim_width = scale_(object->config().brim_width.value); const bool is_top_outer_brim = top_level_objects_idx.find(object->id().id) != top_level_objects_idx.end(); @@ -174,7 +174,7 @@ static ExPolygons top_level_outer_brim_area(const Print &print ExPolygons no_brim_area_object; for (const ExPolygon &ex_poly : bottom_layers_expolygons[print_object_idx]) { if ((brim_type == BrimType::btOuterOnly || brim_type == BrimType::btOuterAndInner) && is_top_outer_brim) - append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_offset), offset(ex_poly.contour, brim_offset))); + append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_separation), offset(ex_poly.contour, brim_separation))); if (brim_type == BrimType::btOuterOnly || brim_type == BrimType::btNoBrim) append(no_brim_area_object, offset_ex(ex_poly.holes, -no_brim_offset)); @@ -183,7 +183,7 @@ static ExPolygons top_level_outer_brim_area(const Print &print append(no_brim_area_object, diff_ex(offset(ex_poly.contour, no_brim_offset), ex_poly.holes)); if (brim_type != BrimType::btNoBrim) - append(no_brim_area_object, offset_ex(ExPolygon(ex_poly.contour), brim_offset)); + append(no_brim_area_object, offset_ex(ExPolygon(ex_poly.contour), brim_separation)); no_brim_area_object.emplace_back(ex_poly.contour); } @@ -212,11 +212,11 @@ static ExPolygons inner_brim_area(const Print &print, ExPolygons no_brim_area; Polygons holes; for(size_t print_object_idx = 0; print_object_idx < print.objects().size(); ++print_object_idx) { - const PrintObject *object = print.objects()[print_object_idx]; - const BrimType brim_type = object->config().brim_type.value; - const float brim_offset = scale_(object->config().brim_offset.value); - const float brim_width = scale_(object->config().brim_width.value); - const bool top_outer_brim = top_level_objects_idx.find(object->id().id) != top_level_objects_idx.end(); + const PrintObject *object = print.objects()[print_object_idx]; + const BrimType brim_type = object->config().brim_type.value; + const float brim_separation = scale_(object->config().brim_separation.value); + const float brim_width = scale_(object->config().brim_width.value); + const bool top_outer_brim = top_level_objects_idx.find(object->id().id) != top_level_objects_idx.end(); ExPolygons brim_area_object; ExPolygons no_brim_area_object; @@ -226,11 +226,11 @@ static ExPolygons inner_brim_area(const Print &print, if (top_outer_brim) no_brim_area_object.emplace_back(ex_poly); else - append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_offset), offset(ex_poly.contour, brim_offset))); + append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_separation), offset(ex_poly.contour, brim_separation))); } if (brim_type == BrimType::btInnerOnly || brim_type == BrimType::btOuterAndInner) - append(brim_area_object, diff_ex(offset_ex(ex_poly.holes, -brim_offset), offset_ex(ex_poly.holes, -brim_width - brim_offset))); + append(brim_area_object, diff_ex(offset_ex(ex_poly.holes, -brim_separation), offset_ex(ex_poly.holes, -brim_width - brim_separation))); if (brim_type == BrimType::btInnerOnly || brim_type == BrimType::btNoBrim) append(no_brim_area_object, diff_ex(offset(ex_poly.contour, no_brim_offset), ex_poly.holes)); @@ -240,7 +240,7 @@ static ExPolygons inner_brim_area(const Print &print, append(holes_object, ex_poly.holes); } - append(no_brim_area_object, offset_ex(bottom_layers_expolygons[print_object_idx], brim_offset)); + append(no_brim_area_object, offset_ex(bottom_layers_expolygons[print_object_idx], brim_separation)); for (const PrintInstance &instance : object->instances()) { append_and_translate(brim_area, brim_area_object, instance); diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 57b0d81159..74969b5239 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -420,6 +420,7 @@ namespace Slic3r { ~_3MF_Importer(); bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); + unsigned int version() const { return m_version; } private: void _destroy_xml_parser(); @@ -2990,6 +2991,19 @@ bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archiv return true; } +// Perform conversions based on the config values available. +//FIXME provide a version of PrusaSlicer that stored the project file (3MF). +static void handle_legacy_project_loaded(unsigned int version_project_file, DynamicPrintConfig& config) +{ + if (! config.has("brim_separation")) { + if (auto *opt_elephant_foot = config.option("elefant_foot_compensation", false); opt_elephant_foot) { + // Conversion from older PrusaSlicer which applied brim separation equal to elephant foot compensation. + auto *opt_brim_separation = config.option("brim_separation", true); + opt_brim_separation->value = opt_elephant_foot->value; + } + } +} + bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) { if (path == nullptr || model == nullptr) @@ -3000,6 +3014,7 @@ bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionCo _3MF_Importer importer; bool res = importer.load_model_from_file(path, *model, config, config_substitutions, check_version); importer.log_errors(); + handle_legacy_project_loaded(importer.version(), config); return res; } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3883c4980b..564300baf3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -430,7 +430,7 @@ static std::vector s_Preset_print_options { "top_solid_infill_speed", "support_material_speed", "support_material_xy_spacing", "support_material_interface_speed", "bridge_speed", "gap_fill_speed", "gap_fill_enabled", "travel_speed", "travel_speed_z", "first_layer_speed", "perimeter_acceleration", "infill_acceleration", "bridge_acceleration", "first_layer_acceleration", "default_acceleration", "skirts", "skirt_distance", "skirt_height", "draft_shield", - "min_skirt_length", "brim_width", "brim_offset", "brim_type", "support_material", "support_material_auto", "support_material_threshold", "support_material_enforce_layers", + "min_skirt_length", "brim_width", "brim_separation", "brim_type", "support_material", "support_material_auto", "support_material_threshold", "support_material_enforce_layers", "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", "support_material_pattern", "support_material_with_sheath", "support_material_spacing", "support_material_closing_radius", "support_material_style", "support_material_synchronize_layers", "support_material_angle", "support_material_interface_layers", "support_material_bottom_interface_layers", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f010bad399..b5c1cbeb33 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -505,10 +505,10 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(btOuterOnly)); - def = this->add("brim_offset", coFloat); - def->label = L("Brim offset"); + def = this->add("brim_separation", coFloat); + def->label = L("Brim separation gap"); def->category = L("Skirt and brim"); - def->tooltip = L("The offset of the brim from the printed object. The offset is applied after the elephant foot compensation."); + def->tooltip = L("Offset of brim from the printed object. The offset is applied after the elephant foot compensation."); def->sidetext = L("mm"); def->min = 0; def->mode = comAdvanced; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 64c8445f8c..f5070c54d9 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -449,7 +449,7 @@ protected: \ PRINT_CONFIG_CLASS_DEFINE( PrintObjectConfig, - ((ConfigOptionFloat, brim_offset)) + ((ConfigOptionFloat, brim_separation)) ((ConfigOptionEnum, brim_type)) ((ConfigOptionFloat, brim_width)) ((ConfigOptionBool, clip_multipart_objects)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index d8d26baa61..f0eaa982ab 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -500,7 +500,7 @@ bool PrintObject::invalidate_state_by_config_options( bool invalidated = false; for (const t_config_option_key &opt_key : opt_keys) { if ( opt_key == "brim_width" - || opt_key == "brim_offset" + || opt_key == "brim_separation" || opt_key == "brim_type") { // Brim is printed below supports, support invalidates brim and skirt. steps.emplace_back(posSupportMaterial); diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index 2e4a479543..1b66bcc533 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -2810,22 +2810,22 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::generate_raf Polygons brim; if (object.has_brim()) { // Calculate the area covered by the brim. - const BrimType brim_type = object.config().brim_type; - const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner; - const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner; - const auto brim_offset = scaled(object.config().brim_offset.value + object.config().brim_width.value); + const BrimType brim_type = object.config().brim_type; + const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner; + const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner; + const auto brim_separation = scaled(object.config().brim_separation.value + object.config().brim_width.value); for (const ExPolygon &ex : object.layers().front()->lslices) { if (brim_outer && brim_inner) - polygons_append(brim, offset(ex, brim_offset)); + polygons_append(brim, offset(ex, brim_separation)); else { if (brim_outer) - polygons_append(brim, offset(ex.contour, brim_offset, ClipperLib::jtRound, float(scale_(0.1)))); + polygons_append(brim, offset(ex.contour, brim_separation, ClipperLib::jtRound, float(scale_(0.1)))); else brim.emplace_back(ex.contour); if (brim_inner) { Polygons holes = ex.holes; polygons_reverse(holes); - holes = offset(holes, - brim_offset, ClipperLib::jtRound, float(scale_(0.1))); + holes = offset(holes, - brim_separation, ClipperLib::jtRound, float(scale_(0.1))); polygons_reverse(holes); polygons_append(brim, std::move(holes)); } else diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 54b87786a5..72584e9389 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -281,7 +281,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field(el, have_skirt); bool have_brim = config->opt_enum("brim_type") != btNoBrim; - for (auto el : { "brim_width", "brim_offset" }) + for (auto el : { "brim_width", "brim_separation" }) toggle_field(el, have_brim); // perimeter_extruder uses the same logic as in Print::extruders() toggle_field("perimeter_extruder", have_perimeters || have_brim); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 14350bddf1..743f275782 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1834,7 +1834,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) , main_frame(main_frame) , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({ "bed_shape", "bed_custom_texture", "bed_custom_model", "complete_objects", "duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance", - "brim_width", "brim_offset", "brim_type", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material", + "brim_width", "brim_separation", "brim_type", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material", "wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "extruder_colour", "filament_colour", "max_print_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bc4c6b7289..6bd295fdc9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1509,7 +1509,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Brim")); optgroup->append_single_option_line("brim_type", category_path + "brim"); optgroup->append_single_option_line("brim_width", category_path + "brim"); - optgroup->append_single_option_line("brim_offset", category_path + "brim"); + optgroup->append_single_option_line("brim_separation", category_path + "brim"); page = add_options_page(L("Support material"), "support"); category_path = "support-material_1698#"; From c797bed67d0db1281fb0820238aa811a4b66fcdf Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 31 Aug 2021 14:35:32 +0200 Subject: [PATCH 052/151] Changed vertex attributes syntax in gouraud_light_instanced.vs --- resources/shaders/gouraud_light_instanced.vs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/shaders/gouraud_light_instanced.vs b/resources/shaders/gouraud_light_instanced.vs index 69f58d6e14..a8931361d0 100644 --- a/resources/shaders/gouraud_light_instanced.vs +++ b/resources/shaders/gouraud_light_instanced.vs @@ -15,11 +15,11 @@ const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); #define INTENSITY_AMBIENT 0.3 // vertex attributes -in vec3 v_position; -in vec3 v_normal; +attribute vec3 v_position; +attribute vec3 v_normal; // instance attributes -in vec3 i_offset; -in vec2 i_scales; +attribute vec3 i_offset; +attribute vec2 i_scales; // x = tainted, y = specular; varying vec2 intensity; From 1835dae296f52f6704fbaee25f504d18dae589dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Mon, 30 Aug 2021 15:51:16 +0200 Subject: [PATCH 053/151] Fixed a bug that the skirt was generated too far from the brim for some objects. --- src/libslic3r/Brim.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index f20404bc9f..db31975e35 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -137,7 +137,7 @@ static Polygons top_level_outer_brim_islands(const ConstPrintObjectPtrs &top_lev auto brim_separation = float(scale_(object->config().brim_separation.value)); Polygons islands_object; for (const ExPolygon &ex_poly : get_print_object_bottom_layer_expolygons(*object)) { - Polygons contour_offset = offset(ex_poly.contour, brim_separation); + Polygons contour_offset = offset(ex_poly.contour, brim_separation, ClipperLib::jtSquare); for (Polygon &poly : contour_offset) poly.douglas_peucker(SCALED_RESOLUTION); @@ -174,16 +174,16 @@ static ExPolygons top_level_outer_brim_area(const Print &print ExPolygons no_brim_area_object; for (const ExPolygon &ex_poly : bottom_layers_expolygons[print_object_idx]) { if ((brim_type == BrimType::btOuterOnly || brim_type == BrimType::btOuterAndInner) && is_top_outer_brim) - append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_separation), offset(ex_poly.contour, brim_separation))); + append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_separation, ClipperLib::jtSquare), offset(ex_poly.contour, brim_separation, ClipperLib::jtSquare))); if (brim_type == BrimType::btOuterOnly || brim_type == BrimType::btNoBrim) - append(no_brim_area_object, offset_ex(ex_poly.holes, -no_brim_offset)); + append(no_brim_area_object, offset_ex(ex_poly.holes, -no_brim_offset, ClipperLib::jtSquare)); if (brim_type == BrimType::btInnerOnly || brim_type == BrimType::btNoBrim) - append(no_brim_area_object, diff_ex(offset(ex_poly.contour, no_brim_offset), ex_poly.holes)); + append(no_brim_area_object, diff_ex(offset(ex_poly.contour, no_brim_offset, ClipperLib::jtSquare), ex_poly.holes)); if (brim_type != BrimType::btNoBrim) - append(no_brim_area_object, offset_ex(ExPolygon(ex_poly.contour), brim_separation)); + append(no_brim_area_object, offset_ex(ExPolygon(ex_poly.contour), brim_separation, ClipperLib::jtSquare)); no_brim_area_object.emplace_back(ex_poly.contour); } @@ -226,21 +226,21 @@ static ExPolygons inner_brim_area(const Print &print, if (top_outer_brim) no_brim_area_object.emplace_back(ex_poly); else - append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_separation), offset(ex_poly.contour, brim_separation))); + append(brim_area_object, diff_ex(offset(ex_poly.contour, brim_width + brim_separation, ClipperLib::jtSquare), offset(ex_poly.contour, brim_separation, ClipperLib::jtSquare))); } if (brim_type == BrimType::btInnerOnly || brim_type == BrimType::btOuterAndInner) - append(brim_area_object, diff_ex(offset_ex(ex_poly.holes, -brim_separation), offset_ex(ex_poly.holes, -brim_width - brim_separation))); + append(brim_area_object, diff_ex(offset_ex(ex_poly.holes, -brim_separation, ClipperLib::jtSquare), offset_ex(ex_poly.holes, -brim_width - brim_separation, ClipperLib::jtSquare))); if (brim_type == BrimType::btInnerOnly || brim_type == BrimType::btNoBrim) - append(no_brim_area_object, diff_ex(offset(ex_poly.contour, no_brim_offset), ex_poly.holes)); + append(no_brim_area_object, diff_ex(offset(ex_poly.contour, no_brim_offset, ClipperLib::jtSquare), ex_poly.holes)); if (brim_type == BrimType::btOuterOnly || brim_type == BrimType::btNoBrim) - append(no_brim_area_object, offset_ex(ex_poly.holes, -no_brim_offset)); + append(no_brim_area_object, offset_ex(ex_poly.holes, -no_brim_offset, ClipperLib::jtSquare)); append(holes_object, ex_poly.holes); } - append(no_brim_area_object, offset_ex(bottom_layers_expolygons[print_object_idx], brim_separation)); + append(no_brim_area_object, offset_ex(bottom_layers_expolygons[print_object_idx], brim_separation, ClipperLib::jtSquare)); for (const PrintInstance &instance : object->instances()) { append_and_translate(brim_area, brim_area_object, instance); @@ -356,12 +356,12 @@ static void make_inner_brim(const Print &print, Flow flow = print.brim_flow(); ExPolygons islands_ex = inner_brim_area(print, top_level_objects_with_brim, bottom_layers_expolygons, float(flow.scaled_spacing())); Polygons loops; - islands_ex = offset_ex(islands_ex, -0.5f * float(flow.scaled_spacing()), jtSquare); + islands_ex = offset_ex(islands_ex, -0.5f * float(flow.scaled_spacing()), ClipperLib::jtSquare); for (size_t i = 0; !islands_ex.empty(); ++i) { for (ExPolygon &poly_ex : islands_ex) poly_ex.douglas_peucker(SCALED_RESOLUTION); polygons_append(loops, to_polygons(islands_ex)); - islands_ex = offset_ex(islands_ex, -float(flow.scaled_spacing()), jtSquare); + islands_ex = offset_ex(islands_ex, -float(flow.scaled_spacing()), ClipperLib::jtSquare); } loops = union_pt_chained_outside_in(loops); @@ -385,7 +385,7 @@ ExtrusionEntityCollection make_brim(const Print &print, PrintTryCancel try_cance size_t num_loops = size_t(floor(max_brim_width(print.objects()) / flow.spacing())); for (size_t i = 0; i < num_loops; ++i) { try_cancel(); - islands = offset(islands, float(flow.scaled_spacing()), jtSquare); + islands = offset(islands, float(flow.scaled_spacing()), ClipperLib::jtSquare); for (Polygon &poly : islands) poly.douglas_peucker(SCALED_RESOLUTION); polygons_append(loops, offset(islands, -0.5f * float(flow.scaled_spacing()))); From ccf671dcc983b63627fa61f3db6f594e0cb772ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Tue, 31 Aug 2021 08:03:09 +0200 Subject: [PATCH 054/151] Added versioning of the structure of stored data for all three painting gizmos (fdm supports, seam and multi-material). --- src/libslic3r/Format/3mf.cpp | 52 ++++++++++++++++++++++++++++++++++++ src/libslic3r/Model.cpp | 25 +++++++++++++++++ src/libslic3r/Model.hpp | 13 +++++++++ 3 files changed, 90 insertions(+) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 74969b5239..cde7d0a439 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -49,6 +49,17 @@ const unsigned int VERSION_3MF = 1; const unsigned int VERSION_3MF_COMPATIBLE = 2; const char* SLIC3RPE_3MF_VERSION = "slic3rpe:Version3mf"; // definition of the metadata name saved into .model file +// Painting gizmos data version numbers +// 0 : 3MF files saved by older PrusaSlicer or the painting gizmo wasn't used. No version definition in them. +// 1 : Introduction of painting gizmos data versioning. No other changes in painting gizmos data. +const unsigned int FDM_SUPPORTS_PAINTING_VERSION = 1; +const unsigned int SEAM_PAINTING_VERSION = 1; +const unsigned int MM_PAINTING_VERSION = 1; + +const std::string SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION = "slic3rpe:FdmSupportsPaintingVersion"; +const std::string SLIC3RPE_SEAM_PAINTING_VERSION = "slic3rpe:SeamPaintingVersion"; +const std::string SLIC3RPE_MM_PAINTING_VERSION = "slic3rpe:MmPaintingVersion"; + const std::string MODEL_FOLDER = "3D/"; const std::string MODEL_EXTENSION = ".model"; const std::string MODEL_FILE = "3D/3dmodel.model"; // << this is the only format of the string which works with CURA @@ -393,6 +404,10 @@ namespace Slic3r { unsigned int m_version; bool m_check_version; + unsigned int m_fdm_supports_painting_version = 0; + unsigned int m_seam_painting_version = 0; + unsigned int m_mm_painting_version = 0; + XML_Parser m_xml_parser; // Error code returned by the application side of the parser. In that case the expat may not reliably deliver the error state // after returning from XML_Parse() function, thus we keep the error state here. @@ -543,6 +558,9 @@ namespace Slic3r { bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) { m_version = 0; + m_fdm_supports_painting_version = 0; + m_seam_painting_version = 0; + m_mm_painting_version = 0; m_check_version = check_version; m_model = &model; m_unit_factor = 1.0f; @@ -1669,6 +1687,12 @@ namespace Slic3r { return true; } + inline static void check_painting_version(unsigned int loaded_version, unsigned int highest_supported_version, const std::string &error_msg) + { + if (loaded_version > highest_supported_version) + throw version_error(error_msg); + } + bool _3MF_Importer::_handle_end_metadata() { if (m_curr_metadata_name == SLIC3RPE_3MF_VERSION) { @@ -1681,6 +1705,24 @@ namespace Slic3r { } } + if (m_curr_metadata_name == SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION) { + m_fdm_supports_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_fdm_supports_painting_version, FDM_SUPPORTS_PAINTING_VERSION, + _(L("The selected 3MF contains FDM supports painted object using a newer version of PrusaSlicer and is not compatible."))); + } + + if (m_curr_metadata_name == SLIC3RPE_SEAM_PAINTING_VERSION) { + m_seam_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_seam_painting_version, SEAM_PAINTING_VERSION, + _(L("The selected 3MF contains seam painted object using a newer version of PrusaSlicer and is not compatible."))); + } + + if (m_curr_metadata_name == SLIC3RPE_MM_PAINTING_VERSION) { + m_mm_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_mm_painting_version, MM_PAINTING_VERSION, + _(L("The selected 3MF contains multi-material painted object using a newer version of PrusaSlicer and is not compatible."))); + } + return true; } @@ -2294,6 +2336,16 @@ namespace Slic3r { stream << "\n"; stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:slic3rpe=\"http://schemas.slic3r.org/3mf/2017/06\">\n"; stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_3MF_VERSION << "\">" << VERSION_3MF << "\n"; + + if (model.is_fdm_support_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION << "\">" << FDM_SUPPORTS_PAINTING_VERSION << "\n"; + + if (model.is_seam_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_SEAM_PAINTING_VERSION << "\">" << SEAM_PAINTING_VERSION << "\n"; + + if (model.is_mm_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_MM_PAINTING_VERSION << "\">" << MM_PAINTING_VERSION << "\n"; + std::string name = xml_escape(boost::filesystem::path(filename).stem().string()); stream << " <" << METADATA_TAG << " name=\"Title\">" << name << "\n"; stream << " <" << METADATA_TAG << " name=\"Designer\">" << "\n"; diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 6f9e4fd4c1..dcffbb1f3a 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -557,6 +557,21 @@ std::string Model::propose_export_file_name_and_path(const std::string &new_exte return boost::filesystem::path(this->propose_export_file_name_and_path()).replace_extension(new_extension).string(); } +bool Model::is_fdm_support_painted() const +{ + return std::any_of(this->objects.cbegin(), this->objects.cend(), [](const ModelObject *mo) { return mo->is_fdm_support_painted(); }); +} + +bool Model::is_seam_painted() const +{ + return std::any_of(this->objects.cbegin(), this->objects.cend(), [](const ModelObject *mo) { return mo->is_seam_painted(); }); +} + +bool Model::is_mm_painted() const +{ + return std::any_of(this->objects.cbegin(), this->objects.cend(), [](const ModelObject *mo) { return mo->is_mm_painted(); }); +} + ModelObject::~ModelObject() { this->clear_volumes(); @@ -733,6 +748,16 @@ void ModelObject::clear_volumes() this->invalidate_bounding_box(); } +bool ModelObject::is_fdm_support_painted() const +{ + return std::any_of(this->volumes.cbegin(), this->volumes.cend(), [](const ModelVolume *mv) { return mv->is_fdm_support_painted(); }); +} + +bool ModelObject::is_seam_painted() const +{ + return std::any_of(this->volumes.cbegin(), this->volumes.cend(), [](const ModelVolume *mv) { return mv->is_seam_painted(); }); +} + bool ModelObject::is_mm_painted() const { return std::any_of(this->volumes.cbegin(), this->volumes.cend(), [](const ModelVolume *mv) { return mv->is_mm_painted(); }); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index ec6fac8214..ba3156139a 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -285,6 +285,10 @@ public: void clear_volumes(); void sort_volumes(bool full_sort); bool is_multiparts() const { return volumes.size() > 1; } + // Checks if any of object volume is painted using the fdm support painting gizmo. + bool is_fdm_support_painted() const; + // Checks if any of object volume is painted using the seam painting gizmo. + bool is_seam_painted() const; // Checks if any of object volume is painted using the multi-material painting gizmo. bool is_mm_painted() const; @@ -723,6 +727,8 @@ public: this->mmu_segmentation_facets.set_new_unique_id(); } + bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } + bool is_seam_painted() const { return !this->seam_facets.empty(); } bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } protected: @@ -1127,6 +1133,13 @@ public: // Propose an output path, replace extension. The new_extension shall contain the initial dot. std::string propose_export_file_name_and_path(const std::string &new_extension) const; + // Checks if any of objects is painted using the fdm support painting gizmo. + bool is_fdm_support_painted() const; + // Checks if any of objects is painted using the seam painting gizmo. + bool is_seam_painted() const; + // Checks if any of objects is painted using the multi-material painting gizmo. + bool is_mm_painted() const; + private: explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); } void assign_new_unique_ids_recursive(); From b29c0ead7dfc1a997cad012b9f04e84cec1beb9a Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 31 Aug 2021 16:03:07 +0200 Subject: [PATCH 055/151] Implemented configurable speed and acceleration settings for the first object layer over raft interface: "first_layer_speed_over_raft", "first_layer_acceleration_over_raft". Fixes I have a question about the speed of the first layer after the raft. #6623 Fixes Layer After Raft Is Not Considered First Layer! #6166 --- src/libslic3r/GCode.cpp | 18 +++++++++++++++--- src/libslic3r/GCode.hpp | 11 ++++++++--- src/libslic3r/Preset.cpp | 4 ++-- src/libslic3r/Print.cpp | 2 ++ src/libslic3r/PrintConfig.cpp | 19 +++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 2 ++ src/slic3r/GUI/ConfigManipulation.cpp | 3 ++- src/slic3r/GUI/Tab.cpp | 2 ++ 8 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 85e6f810b7..19909b2bcf 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1977,6 +1977,7 @@ void GCode::process_layer( } gcode += this->change_layer(print_z); // this will increase m_layer_index m_layer = &layer; + m_object_layer_over_raft = false; if (! print.config().layer_gcode.value.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); @@ -2235,8 +2236,13 @@ void GCode::process_layer( gcode+="; PURGING FINISHED\n"; for (InstanceToPrint &instance_to_print : instances_to_print) { + const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id]; + // To control print speed of the 1st object layer printed over raft interface. + bool object_layer_over_raft = layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 && + instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id(); m_config.apply(instance_to_print.print_object.config(), true); - m_layer = layers[instance_to_print.layer_id].layer(); + m_layer = layer_to_print.layer(); + m_object_layer_over_raft = object_layer_over_raft; if (m_config.avoid_crossing_perimeters) m_avoid_crossing_perimeters.init_layer(*m_layer); if (this->config().gcode_label_objects) @@ -2249,11 +2255,13 @@ void GCode::process_layer( m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); if (instance_to_print.object_by_extruder.support != nullptr && !print_wipe_extrusions) { - m_layer = layers[instance_to_print.layer_id].support_layer; + m_layer = layer_to_print.support_layer; + m_object_layer_over_raft = false; gcode += this->extrude_support( // support_extrusion_role is erSupportMaterial, erSupportMaterialInterface or erMixed for all extrusion paths. instance_to_print.object_by_extruder.support->chained_path_from(m_last_pos, instance_to_print.object_by_extruder.support_extrusion_role)); - m_layer = layers[instance_to_print.layer_id].layer(); + m_layer = layer_to_print.layer(); + m_object_layer_over_raft = object_layer_over_raft; } //FIXME order islands? // Sequential tool path ordering of multiple parts within the same object, aka. perimeter tracking (#5511) @@ -2705,6 +2713,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double acceleration; if (this->on_first_layer() && m_config.first_layer_acceleration.value > 0) { acceleration = m_config.first_layer_acceleration.value; + } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { + acceleration = m_config.first_layer_acceleration_over_raft.value; } else if (m_config.perimeter_acceleration.value > 0 && is_perimeter(path.role())) { acceleration = m_config.perimeter_acceleration.value; } else if (m_config.bridge_acceleration.value > 0 && is_bridge(path.role())) { @@ -2749,6 +2759,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, speed = m_volumetric_speed / path.mm3_per_mm; if (this->on_first_layer()) speed = m_config.get_abs_value("first_layer_speed", speed); + else if (this->object_layer_over_raft()) + speed = m_config.get_abs_value("first_layer_speed_over_raft", speed); if (m_config.max_volumetric_speed.value > 0) { // cap speed with max_volumetric_speed anyway (even if user is not using autospeed) speed = std::min( diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 08ab830024..d2d241054f 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -125,7 +125,8 @@ public: m_last_processor_extrusion_role(erNone), m_layer_count(0), m_layer_index(-1), - m_layer(nullptr), + m_layer(nullptr), + m_object_layer_over_raft(false), m_volumetric_speed(0), m_last_pos_defined(false), m_last_extrusion_role(erNone), @@ -138,7 +139,7 @@ public: m_silent_time_estimator_enabled(false), m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())) {} - ~GCode() {} + ~GCode() = default; // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). @@ -316,9 +317,11 @@ private: unsigned int m_layer_count; // Progress bar indicator. Increments from -1 up to layer_count. int m_layer_index; - // Current layer processed. Insequential printing mode, only a single copy will be printed. + // Current layer processed. In sequential printing mode, only a single copy will be printed. // In non-sequential mode, all its copies will be printed. const Layer* m_layer; + // m_layer is an object layer and it is being printed over raft surface. + bool m_object_layer_over_raft; double m_volumetric_speed; // Support for the extrusion role markers. Which marker is active? ExtrusionRole m_last_extrusion_role; @@ -373,6 +376,8 @@ private: void _print_first_layer_extruder_temperatures(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); // On the first printing layer. This flag triggers first layer speeds. bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0; } + // To control print speed of 1st object layer over raft interface. + bool object_layer_over_raft() const { return m_object_layer_over_raft; } friend ObjectByExtruder& object_by_extruder( std::map> &by_extruder, diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 564300baf3..f5c8235ed5 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -428,8 +428,8 @@ static std::vector s_Preset_print_options { #endif /* HAS_PRESSURE_EQUALIZER */ "perimeter_speed", "small_perimeter_speed", "external_perimeter_speed", "infill_speed", "solid_infill_speed", "top_solid_infill_speed", "support_material_speed", "support_material_xy_spacing", "support_material_interface_speed", - "bridge_speed", "gap_fill_speed", "gap_fill_enabled", "travel_speed", "travel_speed_z", "first_layer_speed", "perimeter_acceleration", "infill_acceleration", - "bridge_acceleration", "first_layer_acceleration", "default_acceleration", "skirts", "skirt_distance", "skirt_height", "draft_shield", + "bridge_speed", "gap_fill_speed", "gap_fill_enabled", "travel_speed", "travel_speed_z", "first_layer_speed", "first_layer_speed_over_raft", "perimeter_acceleration", "infill_acceleration", + "bridge_acceleration", "first_layer_acceleration", "first_layer_acceleration_over_raft", "default_acceleration", "skirts", "skirt_distance", "skirt_height", "draft_shield", "min_skirt_length", "brim_width", "brim_separation", "brim_type", "support_material", "support_material_auto", "support_material_threshold", "support_material_enforce_layers", "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", "support_material_pattern", "support_material_with_sheath", "support_material_spacing", "support_material_closing_radius", "support_material_style", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 48737f8302..06052a62f3 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -88,7 +88,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "filament_cost", "filament_spool_weight", "first_layer_acceleration", + "first_layer_acceleration_over_raft", "first_layer_bed_temperature", + "first_layer_speed_over_raft", "gcode_comments", "gcode_label_objects", "infill_acceleration", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index b5c1cbeb33..2917a9a19d 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1152,6 +1152,15 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0)); + def = this->add("first_layer_acceleration_over_raft", coFloat); + def->label = L("First object layer over raft interface"); + def->tooltip = L("This is the acceleration your printer will use for first layer of object above raft interface. Set zero " + "to disable acceleration control for first layer of object above raft interface."); + def->sidetext = L("mm/s²"); + def->min = 0; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0)); + def = this->add("first_layer_bed_temperature", coInts); def->label = L("First layer"); def->full_label = L("First layer bed temperature"); @@ -1194,6 +1203,16 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(30, false)); + def = this->add("first_layer_speed_over_raft", coFloatOrPercent); + def->label = L("Speed of object first layer over raft interface"); + def->tooltip = L("If expressed as absolute value in mm/s, this speed will be applied to all the print moves " + "of the first object layer above raft interface, regardless of their type. If expressed as a percentage " + "(for example: 40%) it will scale the default speeds."); + def->sidetext = L("mm/s or %"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloatOrPercent(30, false)); + def = this->add("first_layer_temperature", coInts); def->label = L("First layer"); def->full_label = L("First layer nozzle temperature"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f5070c54d9..d7409d12ce 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -456,6 +456,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, dont_support_bridges)) ((ConfigOptionFloat, elefant_foot_compensation)) ((ConfigOptionFloatOrPercent, extrusion_width)) + ((ConfigOptionFloat, first_layer_acceleration_over_raft)) + ((ConfigOptionFloatOrPercent, first_layer_speed_over_raft)) ((ConfigOptionBool, infill_only_where_needed)) // Force the generation of solid shells between adjacent materials/volumes. ((ConfigOptionBool, interface_shells)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 72584e9389..d4920d8364 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -312,7 +312,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("support_material_speed", have_support_material || have_brim || have_skirt); toggle_field("raft_contact_distance", have_raft && !have_support_soluble); - toggle_field("raft_expansion", have_raft); + for (auto el : { "raft_expansion", "first_layer_acceleration_over_raft", "first_layer_speed_over_raft" }) + toggle_field(el, have_raft); bool has_ironing = config->opt_bool("ironing"); for (auto el : { "ironing_type", "ironing_flowrate", "ironing_spacing", "ironing_speed" }) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6bd295fdc9..86089f729e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1565,12 +1565,14 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Modifiers")); optgroup->append_single_option_line("first_layer_speed"); + optgroup->append_single_option_line("first_layer_speed_over_raft"); optgroup = page->new_optgroup(L("Acceleration control (advanced)")); optgroup->append_single_option_line("perimeter_acceleration"); optgroup->append_single_option_line("infill_acceleration"); optgroup->append_single_option_line("bridge_acceleration"); optgroup->append_single_option_line("first_layer_acceleration"); + optgroup->append_single_option_line("first_layer_acceleration_over_raft"); optgroup->append_single_option_line("default_acceleration"); optgroup = page->new_optgroup(L("Autospeed (advanced)")); From d7bb4c36f58dfb46834919a6625ea7637f374954 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 1 Sep 2021 09:34:07 +0200 Subject: [PATCH 056/151] Requires OpenGL 3.3 as a minimum to enable instanced rendering of seams and other options in preview --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/GLShadersManager.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 732403b03e..ad2e6e9737 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -807,7 +807,7 @@ void GCodeViewer::render() case EMoveType::Unretract: case EMoveType::Seam: { #if ENABLE_SEAMS_USING_MODELS - if (wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) { + if (wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) { buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; buffer.shader = "gouraud_light_instanced"; buffer.model.model.init_from(diamond(16)); diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index c93f22f469..edbe2bb46d 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -39,7 +39,7 @@ std::pair GLShadersManager::init() valid &= append_shader("printbed", { "printbed.vs", "printbed.fs" }); // used to render options in gcode preview #if ENABLE_SEAMS_USING_MODELS - if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 1)) + if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) valid &= append_shader("gouraud_light_instanced", { "gouraud_light_instanced.vs", "gouraud_light_instanced.fs" }); else { #endif // ENABLE_SEAMS_USING_MODELS From 95c05aae3bb85b9da51e466578a0061c23e7881d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 1 Sep 2021 09:54:59 +0200 Subject: [PATCH 057/151] Fixed typo which broke export of toolpaths to obj file --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index ad2e6e9737..e6e391d370 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1100,7 +1100,7 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const // save vertices to file fprintf(fp, "\n# vertices\n"); for (const Vec3f& v : out_vertices) { - fprintf(fp, "v %g %g %g\n", v.x(), v.y(), v.x()); + fprintf(fp, "v %g %g %g\n", v.x(), v.y(), v.z()); } // save normals to file From ab84da6c56767d9894eea925263966bdefdf0556 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 1 Sep 2021 11:08:08 +0200 Subject: [PATCH 058/151] Fix for #6803 - Illegal character in filename when STL opened direct from CAD app Follow-up to ea4e9b35a3e2084325fbaed4c5bedf310bcdef3e Win32 specific: Substitute slashes to back slashes in file paths when loading model files (STLs, 3MFS ...) --- src/slic3r/GUI/GUI_ObjectList.cpp | 3 +-- src/slic3r/GUI/Plater.cpp | 36 +++++++++++++------------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 82f03f51ac..737c840834 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1702,8 +1702,7 @@ void ObjectList::load_shape_object_from_gallery(const wxArrayString& input_files snapshot_label += ", " + wxString::FromUTF8(paths[i].filename().string().c_str()); take_snapshot(snapshot_label); - std::vector res = wxGetApp().plater()->load_files(paths, true, false); - if (!res.empty()) + if (! wxGetApp().plater()->load_files(paths, true, false).empty()) wxGetApp().mainframe->update_title(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 743f275782..98eb73c896 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2221,7 +2221,14 @@ std::vector Plater::priv::load_files(const std::vector& input_ std::vector obj_idxs; for (size_t i = 0; i < input_files.size(); ++i) { +#ifdef _WIN32 + auto path = input_files[i]; + // On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it assignes full path to names of loaded objects. + path.make_preferred(); +#else // _WIN32 + // Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute. const auto &path = input_files[i]; +#endif // _WIN32 const auto filename = path.filename(); dlg.Update(static_cast(100.0f * static_cast(i) / static_cast(input_files.size())), _L("Loading file") + ": " + from_path(filename)); dlg.Fit(); @@ -2336,9 +2343,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ else { model = Slic3r::Model::read_from_file(path.string(), nullptr, nullptr, only_if(load_config, Model::LoadAttribute::CheckVersion)); for (auto obj : model.objects) - if (obj->name.empty() || - obj->name.find_first_of("/") != std::string::npos) // When file is imported from Fusion360 the path containes "/" instead of "\\" (see https://github.com/prusa3d/PrusaSlicer/issues/6803) - // But read_from_file doesn't support that direction separator and as a result object name containes full path + if (obj->name.empty()) obj->name = fs::path(obj->input_file).filename().string(); } } catch (const ConfigurationError &e) { @@ -2457,7 +2462,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (load_model) { - wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().string()); + wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); // XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames... statusbar()->set_status_text(_L("Loaded")); } @@ -4787,10 +4792,7 @@ void Plater::load_project(const wxString& filename) p->reset(); - std::vector input_paths; - input_paths.push_back(into_path(filename)); - - if (! load_files(input_paths).empty()) { + if (! load_files({ into_path(filename) }).empty()) { // At least one file was loaded. p->set_project_filename(filename); reset_project_dirty_initial_presets(); @@ -4807,7 +4809,7 @@ void Plater::add_model(bool imperial_units/* = false*/) std::vector paths; for (const auto &file : input_files) - paths.push_back(into_path(file)); + paths.emplace_back(into_path(file)); wxString snapshot_label; assert(! paths.empty()); @@ -4840,12 +4842,8 @@ void Plater::extract_config_from_project() wxString input_file; wxGetApp().load_project(this, input_file); - if (input_file.empty()) - return; - - std::vector input_paths; - input_paths.push_back(into_path(input_file)); - load_files(input_paths, false, true); + if (! input_file.empty()) + load_files({ into_path(input_file) }, false, true); } void Plater::load_gcode() @@ -5076,15 +5074,11 @@ bool Plater::load_files(const wxArrayString& filenames) } case LoadType::LoadGeometry: { Plater::TakeSnapshot snapshot(this, _L("Import Object")); - std::vector in_paths; - in_paths.emplace_back(*it); - load_files(in_paths, true, false); + load_files({ *it }, true, false); break; } case LoadType::LoadConfig: { - std::vector in_paths; - in_paths.emplace_back(*it); - load_files(in_paths, false, true); + load_files({ *it }, false, true); break; } case LoadType::Unknown : { From 73e8f5aed2cccb17c6dee29bd91f6636882be7a4 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 1 Sep 2021 12:58:25 +0200 Subject: [PATCH 059/151] Supress desktop integration of gcode viewer at ChromeOS --- src/slic3r/GUI/DesktopIntegrationDialog.cpp | 91 +++++++++++---------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp index 9351219355..24552ec585 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.cpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp @@ -382,40 +382,44 @@ void DesktopIntegrationDialog::perform_desktop_integration() app_config->set("desktop_integration_app_path", GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix)); // Repeat for Gcode viewer - use same paths as for slicer files - // Icon - if (!target_dir_icons.empty()) - { - std::string icon_path = GUI::format("%1%/icons/PrusaSlicer-gcodeviewer_192px.png",resources_dir()); - std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer-gcodeviewer%3%.png", target_dir_icons, icon_theme_path, version_suffix); - if (copy_icon(icon_path, dest_path)) - // save path to icon - app_config->set("desktop_integration_icon_viewer_path", dest_path); - else - BOOST_LOG_TRIVIAL(error) << "Copying Gcode Viewer icon to icons directory failed."; - } + // Do NOT add gcode viewer desktop file on ChromeOS + if (platform_flavor() != PlatformFlavor::LinuxOnChromium) { + // Icon + if (!target_dir_icons.empty()) + { + std::string icon_path = GUI::format("%1%/icons/PrusaSlicer-gcodeviewer_192px.png",resources_dir()); + std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer-gcodeviewer%3%.png", target_dir_icons, icon_theme_path, version_suffix); + if (copy_icon(icon_path, dest_path)) + // save path to icon + app_config->set("desktop_integration_icon_viewer_path", dest_path); + else + BOOST_LOG_TRIVIAL(error) << "Copying Gcode Viewer icon to icons directory failed."; + } - // Desktop file - std::string desktop_file = GUI::format( - "[Desktop Entry]\n" - "Name=Prusa Gcode Viewer%1%\n" - "GenericName=3D Printing Software\n" - "Icon=PrusaSlicer-gcodeviewer%2%\n" - "Exec=\"%3%\" --gcodeviewer %%F\n" - "Terminal=false\n" - "Type=Application\n" - "MimeType=text/x.gcode;\n" - "Categories=Graphics;3DGraphics;\n" - "Keywords=3D;Printing;Slicer;\n" - "StartupNotify=false\n", name_suffix, version_suffix, excutable_path); + // Desktop file + std::string desktop_file = GUI::format( + "[Desktop Entry]\n" + "Name=Prusa Gcode Viewer%1%\n" + "GenericName=3D Printing Software\n" + "Icon=PrusaSlicer-gcodeviewer%2%\n" + "Exec=\"%3%\" --gcodeviewer %%F\n" + "Terminal=false\n" + "Type=Application\n" + "MimeType=text/x.gcode;\n" + "Categories=Graphics;3DGraphics;\n" + "Keywords=3D;Printing;Slicer;\n" + "StartupNotify=false\n", name_suffix, version_suffix, excutable_path); - std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix); - if (create_desktop_file(desktop_path, desktop_file)) - // save path to desktop file - app_config->set("desktop_integration_app_viewer_path", desktop_path); - else { - BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create Gcodeviewer desktop file"; - show_error(nullptr, _L("Performing desktop integration failed - could not create Gcodeviewer desktop file. PrusaSlicer desktop file was probably created successfully.")); + std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix); + if (create_desktop_file(desktop_path, desktop_file)) + // save path to desktop file + app_config->set("desktop_integration_app_viewer_path", desktop_path); + else { + BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create Gcodeviewer desktop file"; + show_error(nullptr, _L("Performing desktop integration failed - could not create Gcodeviewer desktop file. PrusaSlicer desktop file was probably created successfully.")); + } } + wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess); } void DesktopIntegrationDialog::undo_desktop_intgration() @@ -433,17 +437,20 @@ void DesktopIntegrationDialog::undo_desktop_intgration() BOOST_LOG_TRIVIAL(debug) << "removing " << path; std::remove(path.c_str()); } - // gcode viwer .desktop - path = std::string(app_config->get("desktop_integration_app_viewer_path")); - if (!path.empty()) { - BOOST_LOG_TRIVIAL(debug) << "removing " << path; - std::remove(path.c_str()); - } - // gcode viewer icon - path = std::string(app_config->get("desktop_integration_icon_viewer_path")); - if (!path.empty()) { - BOOST_LOG_TRIVIAL(debug) << "removing " << path; - std::remove(path.c_str()); + // No gcode viewer at ChromeOS + if (platform_flavor() != PlatformFlavor::LinuxOnChromium) { + // gcode viewer .desktop + path = std::string(app_config->get("desktop_integration_app_viewer_path")); + if (!path.empty()) { + BOOST_LOG_TRIVIAL(debug) << "removing " << path; + std::remove(path.c_str()); + } + // gcode viewer icon + path = std::string(app_config->get("desktop_integration_icon_viewer_path")); + if (!path.empty()) { + BOOST_LOG_TRIVIAL(debug) << "removing " << path; + std::remove(path.c_str()); + } } wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationSuccess); } From 229cf4b8b6aa341ca6ba2602b4cdc1e69d93d9ad Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 1 Sep 2021 10:25:46 +0200 Subject: [PATCH 060/151] Open menubar item by item index. Possible to call as hint notification hyperlink --- resources/data/hints.ini | 18 ++++++++---------- src/slic3r/GUI/HintNotification.cpp | 13 ++++++++----- src/slic3r/GUI/MainFrame.cpp | 21 +++++++++++++++++++++ src/slic3r/GUI/MainFrame.hpp | 2 ++ 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/resources/data/hints.ini b/resources/data/hints.ini index 95060f9ec6..af2e09209b 100644 --- a/resources/data/hints.ini +++ b/resources/data/hints.ini @@ -39,6 +39,10 @@ # Open gallery (no aditional var) # hypertext_type = gallery # +#Open top menubar item +#hypertext_menubar_menu_id = 4 (0 - 5, index of menu from left: File = 0, Edit = 1...) +#hypertext_menubar_item_id = 1 (index of item, starting at 0, Including separators! In Window: 0 = Plater Tab, 1 = SEPARATOR, 2 = Print Setting Tab...) +# # # Each notification can have disabled and enabled modes and techs - divided by ; and space # enabled_tags = ... @@ -77,18 +81,15 @@ hypertext_plater_item = arrange [hint:Negative volume] text = Negative volume\nDid you know that you can subtract one mesh from another using the Negative volume modifier? That way you can, for example, create easily resizable holes directly in PrusaSlicer. Read more in the documentation. (Requires Advanced or Expert mode.) -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/negative-volume_238503 disabled_tags = SLA; simple [hint:Simplify mesh] text = Simplify mesh\nDid you know that you can reduce the number of triangles in a mesh using the Simplify mesh feature? Right-click the model and select Simplify model. Read more in the documentation. -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/simplify-mesh_238941 [hint:Reload from disk] text = Reload from disk\nDid you know that if you created a newer version of your model, you can simply reload it in PrusaSlicer? Right-click the model in the 3D view and choose Reload from disk. Read more in the documentation. -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/reload-from-disk_120427 [hint:Hiding sidebar] @@ -131,7 +132,6 @@ hypertext_plater_item = undo [hint:Different layer height for each model] text = Different layer height for each model\nDid you know that you can print each model on the plater with a different layer height? Right-click the model in the 3D view, choose Layers and Perimeters and adjust the values in the right panel. Read more in the documentation. -hypertext_type = link documentation_link= https://help.prusa3d.com/en/article/per-model-settings_1674 disabled_tags = SLA @@ -168,7 +168,6 @@ text = Load config from G-code\nDid you know that you can use File-Import Config [hint:Ironing] text = Ironing\nDid you know that you can smooth top surfaces of prints using Ironing? The nozzle will run a special second infill phase at the same layer to fill in holes and flatten any lifted plastic. Read more in the documentation. (Requires Advanced or Expert mode.) -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/ironing_177488 disabled_tags = SLA; simple @@ -186,20 +185,20 @@ disabled_tags = SLA; simple [hint:Insert Pause] text = Insert Pause\nDid you know that you can schedule the print to pause at a specific layer? Right-click the layer slider in the Preview and select Add pause print (M601). This can be used to insert magnets, weights or nuts into your prints. Read more in the documentation. -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/insert-pause-or-custom-g-code-at-layer_120490#insert-pause-at-layer disabled_tags = SLA [hint:Insert Custom G-code] text = Insert Custom G-code\nDid you know that you can insert a custom G-code at a specific layer? Right-click the layer in the Preview and select Add custom G-code. With this function you can, for example, create a temperature tower. Read more in the documentation. -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/insert-pause-or-custom-g-code-at-layer_120490#insert-custom-g-code-at-layer disabled_tags = SLA [hint:Configuration snapshots] -text = Configuration snapshots\nDid you know that roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - Configuration snapshots menu. Read more in the documentation. -hypertext_type = link +text = Configuration snapshots\nDid you know that roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - Configuration snapshots menu. documentation_link = https://help.prusa3d.com/en/article/configuration-snapshots_1776 +hypertext_type = menubar +hypertext_menubar_menu_id = 4 +hypertext_menubar_item_id = 1 [hint:Minimum wall thickness] text = Minimum wall thickness\nDid you know that instead of the number of top and bottom layers, you can define theMinimum shell thicknessin millimeters? This feature is especially useful when using the variable layer height function. @@ -216,7 +215,6 @@ hypertext_preferences_page = 2 [hint:Adaptive infills] text = Adaptive infills\nDid you know that you can use the Adaptive cubic and Support cubic infills to decrease the print time and lower the filament consumption? Read more in the documentation. -hypertext_type = link documentation_link = https://help.prusa3d.com/en/article/infill-patterns_177130 disabled_tags = SLA diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index ead7a8f29a..0cdf441c03 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -4,6 +4,7 @@ #include "I18N.hpp" #include "GUI_ObjectList.hpp" #include "GLCanvas3D.hpp" +#include "MainFrame.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Config.hpp" @@ -57,10 +58,6 @@ inline void push_style_color(ImGuiCol idx, const ImVec4& col, bool fading_out, f ImGui::PushStyleColor(idx, col); } - - - - void write_used_binary(const std::vector& ids) { boost::filesystem::ofstream file((boost::filesystem::path(data_dir()) / "cache" / "hints.cereal"), std::ios::binary); @@ -379,7 +376,13 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path) HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, []() { // Deselect all objects, otherwise gallery wont show. wxGetApp().plater()->canvas3D()->deselect_all(); - wxGetApp().obj_list()->load_shape_object_from_gallery(); } }; + wxGetApp().obj_list()->load_shape_object_from_gallery(); } + }; + m_loaded_hints.emplace_back(hint_data); + } else if (dict["hypertext_type"] == "menubar") { + int menu = std::atoi(dict["hypertext_menubar_menu_id"].c_str()); + int item = std::atoi(dict["hypertext_menubar_item_id"].c_str()); + HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [menu, item]() { wxGetApp().mainframe->open_menubar_item(menu, item); } }; m_loaded_hints.emplace_back(hint_data); } } else { diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c6d176fd65..d7c0a25b2b 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1479,6 +1479,27 @@ void MainFrame::init_menubar_as_editor() update_menubar(); } +void MainFrame::open_menubar_item(int menu_index, int item_index) +{ + if (m_menubar == nullptr) + return; + wxMenu* menu = m_menubar->GetMenu(menu_index); + if (menu == nullptr) { + BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find menu: " << menu_index; + return; + } + wxMenuItemList items = menu->GetMenuItems(); + if (items.size() <= item_index) { + BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find item: " << item_index; + return; + } + wxMenuItem* item = items[item_index]; + if (item == nullptr) { + BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find item: " << item_index; + return; + } + wxPostEvent((wxEvtHandler*)menu, wxCommandEvent(wxEVT_MENU, item->GetId())); +} void MainFrame::init_menubar_as_gcodeviewer() { wxMenu* fileMenu = new wxMenu; diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index aced5efbe4..e8d3c67a72 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -158,6 +158,8 @@ public: void init_menubar_as_editor(); void init_menubar_as_gcodeviewer(); void update_menubar(); + // Open item in menu by menu and item index (visible order of items including separators) + void open_menubar_item(int menu_index, int item_index); #ifdef _WIN32 void show_tabs_menu(bool show); #endif From d0e37aada876785aae029c46d6e08566241af850 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:36:13 +0200 Subject: [PATCH 061/151] Updated raft_contact_distance values. --- resources/profiles/PrusaResearch.ini | 33 ++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 835e552a18..47a3883b4b 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -213,6 +213,7 @@ support_material_angle = 0 support_material_buildplate_only = 0 support_material_enforce_layers = 0 support_material_contact_distance = 0.2 +raft_contact_distance = 0.2 support_material_interface_contact_loops = 0 support_material_interface_layers = 2 support_material_interface_spacing = 0.2 @@ -432,6 +433,7 @@ solid_infill_speed = 30 support_material_extrusion_width = 0.33 support_material_spacing = 1.5 support_material_contact_distance = 0.15 +raft_contact_distance = 0.15 perimeter_acceleration = 300 perimeter_speed = 30 perimeters = 3 @@ -461,6 +463,7 @@ layer_height = 0.1 perimeter_acceleration = 800 top_solid_layers = 9 support_material_contact_distance = 0.17 +raft_contact_distance = 0.17 [print:*0.15mm*] inherits = *common* @@ -613,6 +616,7 @@ solid_infill_extrusion_width = 0.7 top_infill_extrusion_width = 0.45 support_material_extrusion_width = 0.37 support_material_contact_distance = 0.1 +raft_contact_distance = 0.2 top_solid_infill_speed = 40 thick_bridges = 1 @@ -669,6 +673,7 @@ small_perimeter_speed = 15 solid_infill_speed = 20 support_material_speed = 20 support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.10mm DETAIL @0.25 nozzle] inherits = *0.10mm*; *0.25nozzle* @@ -683,6 +688,7 @@ small_perimeter_speed = 15 solid_infill_speed = 40 top_solid_infill_speed = 30 support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.15mm OPTIMAL @0.25 nozzle] inherits = *0.15mm*; *0.25nozzle* @@ -698,6 +704,7 @@ small_perimeter_speed = 15 solid_infill_speed = 40 top_solid_infill_speed = 30 support_material_contact_distance = 0.08 +raft_contact_distance = 0.08 ## MK2 - 0.6mm nozzle @@ -923,6 +930,7 @@ solid_infill_extrusion_width = 0.5 top_infill_extrusion_width = 0.45 support_material_extrusion_width = 0.38 support_material_contact_distance = 0.2 +raft_contact_distance = 0.2 ## MK3 - MMU2 specific [print:0.15mm SOLUBLE FULL @MK3] @@ -977,6 +985,7 @@ compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.07mm ULTRADETAIL @0.25 nozzle MK3] inherits = *0.07mm*; *0.25nozzle*; *MK3* @@ -988,6 +997,7 @@ top_solid_infill_speed = 20 fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.10mm DETAIL @0.25 nozzle MK3] inherits = *0.10mm*; *0.25nozzleMK3*; *MK3* @@ -995,6 +1005,7 @@ compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.15mm QUALITY @0.25 nozzle MK3] inherits = *0.15mm*; *0.25nozzleMK3*; *MK3* @@ -1002,6 +1013,7 @@ compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.08 +raft_contact_distance = 0.08 ## MK3 - 0.6mm nozzle @@ -1016,6 +1028,7 @@ perimeter_speed = 45 solid_infill_speed = 70 top_solid_infill_speed = 45 support_material_contact_distance = 0.22 +raft_contact_distance = 0.22 bridge_flow_ratio = 1 [print:0.20mm DETAIL @0.6 nozzle MK3] @@ -1029,6 +1042,7 @@ perimeter_speed = 45 solid_infill_speed = 70 top_solid_infill_speed = 45 support_material_contact_distance = 0.22 +raft_contact_distance = 0.22 bridge_flow_ratio = 1 [print:0.30mm QUALITY @0.6 nozzle MK3] @@ -1042,6 +1056,7 @@ perimeter_speed = 45 solid_infill_speed = 70 top_solid_infill_speed = 45 support_material_contact_distance = 0.25 +raft_contact_distance = 0.25 bridge_flow_ratio = 1 [print:0.35mm SPEED @0.6 nozzle MK3] @@ -1059,6 +1074,7 @@ perimeter_extrusion_width = 0.68 infill_extrusion_width = 0.68 solid_infill_extrusion_width = 0.68 support_material_contact_distance = 0.25 +raft_contact_distance = 0.25 bridge_flow_ratio = 0.95 [print:0.40mm DRAFT @0.6 nozzle MK3] @@ -1076,6 +1092,7 @@ perimeter_extrusion_width = 0.68 infill_extrusion_width = 0.68 solid_infill_extrusion_width = 0.68 support_material_contact_distance = 0.25 +raft_contact_distance = 0.25 bridge_flow_ratio = 0.95 ## MK3 - MMU2 specific @@ -1255,6 +1272,7 @@ solid_infill_extrusion_width = 0.45 top_infill_extrusion_width = 0.4 support_material_xy_spacing = 60% support_material_contact_distance = 0.2 +raft_contact_distance = 0.2 # MINI - 0.25mm nozzle @@ -1265,6 +1283,7 @@ fill_pattern = grid fill_density = 20% support_material_speed = 30 support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.07mm ULTRADETAIL @0.25 nozzle MINI] inherits = *0.07mm*; *0.25nozzle*; *MINI* @@ -1276,6 +1295,7 @@ top_solid_infill_speed = 20 fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.10mm DETAIL @0.25 nozzle MINI] inherits = *0.10mm*; *0.25nozzleMINI*; *MINI* @@ -1283,6 +1303,7 @@ compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.07 +raft_contact_distance = 0.07 [print:0.15mm QUALITY @0.25 nozzle MINI] inherits = *0.15mm*; *0.25nozzleMINI*; *MINI* @@ -1290,6 +1311,7 @@ compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and fill_pattern = grid fill_density = 20% support_material_contact_distance = 0.08 +raft_contact_distance = 0.08 # MINI - 0.6mm nozzle @@ -1305,6 +1327,7 @@ top_solid_infill_speed = 45 infill_extrusion_width = 0.65 solid_infill_extrusion_width = 0.65 support_material_contact_distance = 0.22 +raft_contact_distance = 0.22 bridge_flow_ratio = 1 [print:0.20mm DETAIL @0.6 nozzle MINI] @@ -1319,6 +1342,7 @@ top_solid_infill_speed = 45 infill_extrusion_width = 0.65 solid_infill_extrusion_width = 0.65 support_material_contact_distance = 0.22 +raft_contact_distance = 0.22 bridge_flow_ratio = 1 [print:0.30mm QUALITY @0.6 nozzle MINI] @@ -1333,6 +1357,7 @@ top_solid_infill_speed = 45 external_perimeter_extrusion_width = 0.68 perimeter_extrusion_width = 0.68 support_material_contact_distance = 0.25 +raft_contact_distance = 0.25 bridge_flow_ratio = 1 [print:0.35mm SPEED @0.6 nozzle MINI] @@ -1347,6 +1372,7 @@ top_solid_infill_speed = 45 external_perimeter_extrusion_width = 0.68 perimeter_extrusion_width = 0.68 support_material_contact_distance = 0.25 +raft_contact_distance = 0.25 bridge_flow_ratio = 0.95 [print:0.40mm DRAFT @0.6 nozzle MINI] @@ -1363,6 +1389,7 @@ perimeter_extrusion_width = 0.68 infill_extrusion_width = 0.68 solid_infill_extrusion_width = 0.68 support_material_contact_distance = 0.25 +raft_contact_distance = 0.25 bridge_flow_ratio = 0.95 # MINI - 0.8mm nozzle @@ -1398,6 +1425,7 @@ infill_speed = 40 solid_infill_speed = 40 support_material_speed = 35 support_material_contact_distance = 0.25 +raft_contact_distance = 0.2 top_solid_infill_speed = 28 external_perimeter_extrusion_width = 1 perimeter_extrusion_width = 1 @@ -6522,7 +6550,7 @@ start_gcode = G90 ; use absolute coordinates\nM83 ; extruder relative mode\nM104 end_gcode = G1 E-1 F2100 ; retract\n{if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+2, max_print_height)} F720 ; Move print head up{endif}\nG1 X178 Y178 F4200 ; park print head\n{if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+30, max_print_height)} F720 ; Move print head further up{endif}\nG4 ; wait\nM104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nM221 S100 ; reset flow\nM900 K0 ; reset LA\nM84 ; disable motors printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_MINI\n extruder_colour = -color_change_gcode = M600\nG1 E0.8 F1500 ; prime after color change +color_change_gcode = M600 [printer:Original Prusa MINI & MINI+ 0.25 nozzle] inherits = Original Prusa MINI & MINI+ @@ -6536,7 +6564,6 @@ retract_length = 3 retract_lift = 0.15 retract_before_travel = 1 start_gcode = G90 ; use absolute coordinates\nM83 ; extruder relative mode\nM104 S170 ; set extruder temp for bed leveling\nM140 S[first_layer_bed_temperature] ; set bed temp\nM109 R170 ; wait for bed leveling temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM204 T1250 ; set travel acceleration\nG28 ; home all without mesh bed level\nG29 ; mesh bed leveling \nM204 T[machine_max_acceleration_travel] ; restore travel acceleration\nM104 S[first_layer_temperature] ; set extruder temp\nG92 E0\nG1 Y-2 X179 F2400\nG1 Z3 F720\nM109 S[first_layer_temperature] ; wait for extruder temp\n\n; intro line\nG1 X170 F1000\nG1 Z0.2 F720\nG1 X110 E8 F600\nG1 X40 E10 F400\nG92 E0\n\nM221 S95 ; set flow -color_change_gcode = M600\nG1 E0.6 F1500 ; prime after color change [printer:Original Prusa MINI & MINI+ 0.6 nozzle] inherits = Original Prusa MINI & MINI+ @@ -6548,7 +6575,6 @@ min_layer_height = 0.15 default_print_profile = 0.30mm QUALITY @0.6 nozzle MINI retract_length = 3.5 retract_before_travel = 1.5 -color_change_gcode = M600\nG1 E1 F1500 ; prime after color change [printer:Original Prusa MINI & MINI+ 0.8 nozzle] inherits = Original Prusa MINI & MINI+ @@ -6560,7 +6586,6 @@ default_print_profile = 0.40mm QUALITY @0.8 nozzle MINI default_filament_profile = Prusament PLA @0.8 nozzle retract_length = 3.5 retract_before_travel = 1.5 -color_change_gcode = M600\nG1 E1.2 F1500 ; prime after color change [printer:Original Prusa SL1] printer_technology = SLA From 20453f412b557ba9e75388077dca6bf6f3497176 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 1 Sep 2021 13:48:07 +0200 Subject: [PATCH 062/151] Check unsaved changes before opening of the ConfigWizard --- src/libslic3r/PresetBundle.cpp | 1 - src/slic3r/GUI/GUI_App.cpp | 13 ++++++++----- src/slic3r/GUI/GUI_App.hpp | 2 +- src/slic3r/GUI/PresetComboBoxes.cpp | 3 +-- src/slic3r/GUI/UnsavedChangesDialog.cpp | 4 ++-- src/slic3r/GUI/UnsavedChangesDialog.hpp | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 9ac8864f91..63f9f27f22 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -240,7 +240,6 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward if (! errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); - // ysToDo : set prefered filament or sla_material (relates to print technology) and force o use of preffered printer model if it was added this->load_selections(config, preferred_selection); return substitutions; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 102026a0cb..3d5e56b462 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2082,10 +2082,10 @@ std::vector> GUI_App::get_selected_presets( // This is called when closing the application, when loading a config file or when starting the config wizard // to notify the user whether he is aware that some preset changes will be lost. -bool GUI_App::check_and_save_current_preset_changes(const wxString& header) +bool GUI_App::check_and_save_current_preset_changes(const wxString& header, const wxString& caption) { if (/*this->plater()->model().objects.empty() && */has_current_preset_changes()) { - UnsavedChangesDialog dlg(header); + UnsavedChangesDialog dlg(header, caption); if (wxGetApp().app_config->get("default_action_on_close_application") == "none" && dlg.ShowModal() == wxID_CANCEL) return false; @@ -2334,10 +2334,13 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage { wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - if (reason == ConfigWizard::RR_USER) - if (PresetUpdater::UpdateResult result = preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD); - result == PresetUpdater::R_ALL_CANCELED) + if (reason == ConfigWizard::RR_USER) { + wxString header = _L("Updates to Configuration Wizard may cause an another preset selection and lost of preset modification as a result.\n" + "So, check unsaved changes and save them if necessary.") + "\n"; + if (!check_and_save_current_preset_changes(header, _L("ConfigWizard is opening")) || + preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) return false; + } if (! m_wizard) { wxBusyCursor wait; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 1d8c2fcf75..a088e10d4c 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -247,7 +247,7 @@ public: bool has_current_preset_changes() const; void update_saved_preset_from_current_preset(); std::vector> get_selected_presets() const; - bool check_and_save_current_preset_changes(const wxString& header = wxString()); + bool check_and_save_current_preset_changes(const wxString& header = wxString(), const wxString& caption = wxString()); bool check_print_host_queue(); bool checked_tab(Tab* tab); void load_current_presets(bool check_printer_presets = true); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index b8d4ead509..5da3594582 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -636,8 +636,7 @@ PlaterPresetComboBox::~PlaterPresetComboBox() static void run_wizard(ConfigWizard::StartPage sp) { - if (wxGetApp().check_and_save_current_preset_changes()) - wxGetApp().run_wizard(ConfigWizard::RR_USER, sp); + wxGetApp().run_wizard(ConfigWizard::RR_USER, sp); } void PlaterPresetComboBox::OnSelect(wxCommandEvent &evt) diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 9fb8a1e642..2b63bc79b6 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -752,8 +752,8 @@ std::vector DiffViewCtrl::selected_options() // UnsavedChangesDialog //------------------------------------------ -UnsavedChangesDialog::UnsavedChangesDialog(const wxString& header) - : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, _L("PrusaSlicer is closing: Unsaved Changes"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +UnsavedChangesDialog::UnsavedChangesDialog(const wxString& header, const wxString& caption/* = wxString()*/) + : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, (caption.IsEmpty() ? _L("PrusaSlicer is closing") : caption) + ": " + _L("Unsaved Changes"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { m_app_config_key = "default_action_on_close_application"; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index 06838d9361..9667430334 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -263,7 +263,7 @@ class UnsavedChangesDialog : public DPIDialog std::vector> names_and_types; public: - UnsavedChangesDialog(const wxString& header); + UnsavedChangesDialog(const wxString& header, const wxString& caption = wxString()); UnsavedChangesDialog(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset); ~UnsavedChangesDialog() {} From 8ad109235b86ca6d3f8e65be03f4b26edaa198d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Wed, 1 Sep 2021 14:28:42 +0200 Subject: [PATCH 063/151] Added a missing include (GCC 11.1). --- src/slic3r/GUI/MainFrame.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index d7c0a25b2b..7d51985c57 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "libslic3r/Print.hpp" #include "libslic3r/Polygon.hpp" From 3b4d10656cb8626b539551c74cd50e0c925f2b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Wed, 1 Sep 2021 14:32:34 +0200 Subject: [PATCH 064/151] Renamed 'Seed fill' to 'Smart fill'. --- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 37 +++++++++---------- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 20 +++++----- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 10 ++--- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index a69319d3b6..a30ac377e8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -124,13 +124,12 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["sphere"] = _L("Sphere"); m_desc["pointer"] = _L("Pointer"); - m_desc["tool_type"] = _L("Tool type"); + m_desc["tool_type"] = _L("Tool type"); m_desc["tool_brush"] = _L("Brush"); - m_desc["tool_seed_fill"] = _L("Seed fill"); + m_desc["tool_smart_fill"] = _L("Smart fill"); m_desc["tool_bucket_fill"] = _L("Bucket fill"); - m_desc["seed_fill"] = _L("Seed fill"); - m_desc["seed_fill_angle"] = _L("Seed fill angle"); + m_desc["smart_fill_angle"] = _L("Smart fill angle"); init_extruders_data(); @@ -246,10 +245,10 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); - const float seed_fill_slider_left = m_imgui->calc_text_size(m_desc.at("seed_fill_angle")).x + m_imgui->scaled(1.f); + const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); - const float cursor_type_radio_circle = m_imgui->calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); - const float cursor_type_radio_sphere = m_imgui->calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_circle = m_imgui->calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_sphere = m_imgui->calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); const float cursor_type_radio_pointer = m_imgui->calc_text_size(m_desc["pointer"]).x + m_imgui->scaled(2.5f); const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); @@ -259,9 +258,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float combo_label_width = std::max(m_imgui->calc_text_size(m_desc.at("first_color")).x, m_imgui->calc_text_size(m_desc.at("second_color")).x) + m_imgui->scaled(1.f); - const float tool_type_radio_brush = m_imgui->calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_brush = m_imgui->calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); const float tool_type_radio_bucket_fill = m_imgui->calc_text_size(m_desc["tool_bucket_fill"]).x + m_imgui->scaled(2.5f); - const float tool_type_radio_seed_fill = m_imgui->calc_text_size(m_desc["tool_seed_fill"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_smart_fill = m_imgui->calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); float caption_max = 0.f; float total_text_max = 0.; @@ -272,12 +271,12 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott caption_max += m_imgui->scaled(1.f); total_text_max += m_imgui->scaled(1.f); - float sliders_width = std::max(seed_fill_slider_left, std::max(cursor_slider_left, clipping_slider_left)); + float sliders_width = std::max(smart_fill_slider_left, std::max(cursor_slider_left, clipping_slider_left)); float window_width = minimal_slider_width + sliders_width; window_width = std::max(window_width, total_text_max); window_width = std::max(window_width, button_width); window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); - window_width = std::max(window_width, tool_type_radio_brush + tool_type_radio_bucket_fill + tool_type_radio_seed_fill); + window_width = std::max(window_width, tool_type_radio_brush + tool_type_radio_bucket_fill + tool_type_radio_smart_fill); window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); auto draw_text_with_caption = [this, &caption_max](const wxString &caption, const wxString &text) { @@ -321,7 +320,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("tool_type")); - float tool_type_offset = (window_width - tool_type_radio_brush - tool_type_radio_bucket_fill - tool_type_radio_seed_fill + m_imgui->scaled(2.f)) / 2.f; + float tool_type_offset = (window_width - tool_type_radio_brush - tool_type_radio_bucket_fill - tool_type_radio_smart_fill + m_imgui->scaled(2.f)) / 2.f; ImGui::NewLine(); @@ -345,9 +344,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } ImGui::SameLine(tool_type_offset + tool_type_radio_brush + m_imgui->scaled(0.f)); - ImGui::PushItemWidth(tool_type_radio_seed_fill); - if (m_imgui->radio_button(m_desc["tool_seed_fill"], m_tool_type == GLGizmoMmuSegmentation::ToolType::SEED_FILL)) { - m_tool_type = GLGizmoMmuSegmentation::ToolType::SEED_FILL; + ImGui::PushItemWidth(tool_type_radio_smart_fill); + if (m_imgui->radio_button(m_desc["tool_smart_fill"], m_tool_type == GLGizmoMmuSegmentation::ToolType::SMART_FILL)) { + m_tool_type = GLGizmoMmuSegmentation::ToolType::SMART_FILL; for (auto &triangle_selector : m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); @@ -362,7 +361,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::EndTooltip(); } - ImGui::SameLine(tool_type_offset + tool_type_radio_brush + tool_type_radio_seed_fill + m_imgui->scaled(0.f)); + ImGui::SameLine(tool_type_offset + tool_type_radio_brush + tool_type_radio_smart_fill + m_imgui->scaled(0.f)); ImGui::PushItemWidth(tool_type_radio_bucket_fill); if (m_imgui->radio_button(m_desc["tool_bucket_fill"], m_tool_type == GLGizmoMmuSegmentation::ToolType::BUCKET_FILL)) { m_tool_type = GLGizmoMmuSegmentation::ToolType::BUCKET_FILL; @@ -458,14 +457,14 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->disabled_end(); ImGui::Separator(); - } else if(m_tool_type == ToolType::SEED_FILL) { - m_imgui->text(m_desc["seed_fill_angle"] + ":"); + } else if(m_tool_type == ToolType::SMART_FILL) { + m_imgui->text(m_desc["smart_fill_angle"] + ":"); ImGui::AlignTextToFramePadding(); std::string format_str = std::string("%.f") + I18N::translate_utf8("°", "Degree sign to use in the respective slider in MMU gizmo," "placed after the number with no whitespace in between."); ImGui::SameLine(sliders_width); ImGui::PushItemWidth(window_width - sliders_width); - if(m_imgui->slider_float("##seed_fill_angle", &m_seed_fill_angle, SeedFillAngleMin, SeedFillAngleMax, format_str.data())) + if(m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data())) for (auto &triangle_selector : m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 9477b89c7e..155738b327 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -282,12 +282,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous : std::min(m_cursor_radius + CursorRadiusStep, CursorRadiusMax); m_parent.set_as_dirty(); return true; - } else if (m_tool_type == ToolType::SEED_FILL) { - m_seed_fill_angle = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_seed_fill_angle - SeedFillAngleStep, SeedFillAngleMin) - : std::min(m_seed_fill_angle + SeedFillAngleStep, SeedFillAngleMax); + } else if (m_tool_type == ToolType::SMART_FILL) { + m_smart_fill_angle = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_smart_fill_angle - SmartFillAngleStep, SmartFillAngleMin) + : std::min(m_smart_fill_angle + SmartFillAngleStep, SmartFillAngleMax); m_parent.set_as_dirty(); if (m_rr.mesh_id != -1) { - m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), m_seed_fill_angle, true); + m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), m_smart_fill_angle, true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -379,10 +379,10 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous Vec3f camera_pos = (trafo_matrix.inverse() * camera.get_position()).cast(); assert(m_rr.mesh_id < int(m_triangle_selectors.size())); - if (m_tool_type == ToolType::SEED_FILL || m_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER)) { + 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::SEED_FILL) - m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), m_seed_fill_angle, true); + 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), m_smart_fill_angle, 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), false, true); else if (m_tool_type == ToolType::BUCKET_FILL) @@ -400,7 +400,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous return true; } - if (action == SLAGizmoEventType::Moving && (m_tool_type == ToolType::SEED_FILL || m_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER))) { + if (action == SLAGizmoEventType::Moving && (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER))) { if (m_triangle_selectors.empty()) return false; @@ -440,8 +440,8 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous seed_fill_unselect_all(); assert(m_rr.mesh_id < int(m_triangle_selectors.size())); - if (m_tool_type == ToolType::SEED_FILL) - m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), m_seed_fill_angle); + 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), m_smart_fill_angle); 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), false); else if (m_tool_type == ToolType::BUCKET_FILL) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index c20c8140d0..6a15ab2a52 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -109,16 +109,16 @@ protected: enum class ToolType { BRUSH, BUCKET_FILL, - SEED_FILL + SMART_FILL }; bool m_triangle_splitting_enabled = true; ToolType m_tool_type = ToolType::BRUSH; - float m_seed_fill_angle = 30.f; + float m_smart_fill_angle = 30.f; - static constexpr float SeedFillAngleMin = 0.0f; - static constexpr float SeedFillAngleMax = 90.f; - static constexpr float SeedFillAngleStep = 1.f; + static constexpr float SmartFillAngleMin = 0.0f; + static constexpr float SmartFillAngleMax = 90.f; + static constexpr float SmartFillAngleStep = 1.f; // It stores the value of the previous mesh_id to which the seed fill was applied. // It is used to detect when the mouse has moved from one volume to another one. From 61583473792222221295c3ac0c8e1fd5a72049af Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 1 Sep 2021 14:37:14 +0200 Subject: [PATCH 065/151] Follow-up to #6817 1) Activate installed filament or SLA material profile after update_compatible(), so that the compatiblity and visibility flags of presets are updated. 2) Only activate the first newly installed filament / SLA material profile if the active printer did not change. This also means that if no filament profile was active before Wizard was open or it became incompatible with the newly installed Printer profile, the default filament profile assigned to the activated Printer is activated preferably, which may or may not be one of the newly installed filament profiles. --- src/libslic3r/PresetBundle.cpp | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 63f9f27f22..cb9f04e456 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -465,20 +465,9 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // will be selected by the following call of this->update_compatible(PresetSelectCompatibleType::Always). const Preset *initial_printer = printers.find_preset(initial_printer_profile_name); + // If executed due to a Config Wizard update, preferred_printer contains the first newly installed printer, otherwise nullptr. const Preset *preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, preferred_selection.printer_variant); - printers.select_preset_by_name( - (preferred_printer != nullptr /*&& (initial_printer == nullptr || !initial_printer->is_visible)*/) ? - preferred_printer->name : - initial_printer_profile_name, - true); - - // select preferred filament/sla_material profile if any exists and is visible - if (!preferred_selection.filament.empty()) - if (auto it = filaments.find_preset_internal(preferred_selection.filament); it != filaments.end() && it->is_visible) - initial_filament_profile_name = it->name; - if (!preferred_selection.sla_material.empty()) - if (auto it = sla_materials.find_preset_internal(preferred_selection.sla_material); it != sla_materials.end() && it->is_visible) - initial_sla_material_profile_name = it->name; + printers.select_preset_by_name(preferred_printer ? preferred_printer->name : initial_printer_profile_name, true); // Selects the profile, leaves it to -1 if the initial profile name is empty or if it was not found. prints.select_preset_by_name_strict(initial_print_profile_name); @@ -506,6 +495,21 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p this->update_compatible(PresetSelectCompatibleType::Always); this->update_multi_material_filament_presets(); + if (initial_printer != nullptr && (preferred_printer == nullptr || initial_printer == preferred_printer)) { + // Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new printer. + // Only run this code if just a filament / SLA material was installed by Config Wizard for an active Printer. + auto printer_technology = printers.get_selected_preset().printer_technology(); + if (printer_technology == ptFFF && ! preferred_selection.filament.empty()) { + if (auto it = filaments.find_preset_internal(preferred_selection.filament); it != filaments.end() && it->is_visible) { + filaments.select_preset_by_name_strict(preferred_selection.filament); + this->filament_presets.front() = filaments.get_selected_preset_name(); + } + } else if (printer_technology == ptSLA && ! preferred_selection.sla_material.empty()) { + if (auto it = sla_materials.find_preset_internal(preferred_selection.sla_material); it != sla_materials.end() && it->is_visible) + sla_materials.select_preset_by_name_strict(preferred_selection.sla_material); + } + } + // Parse the initial physical printer name. std::string initial_physical_printer_name = remove_ini_suffix(config.get("presets", "physical_printer")); From 950c216239866a0ed917e8d4a680264109778646 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 1 Sep 2021 15:11:26 +0200 Subject: [PATCH 066/151] Commented out AMF export. The format never took off, we don't want to keep supporting it. Let's wait for user feedback. --- src/slic3r/GUI/MainFrame.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7d51985c57..469c3b0f65 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1239,9 +1239,10 @@ void MainFrame::init_menubar_as_editor() append_menu_item(export_menu, wxID_ANY, _L("Export plate as STL &including supports") + dots, _L("Export current plate as STL including supports"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(true); }, "export_plater", nullptr, [this](){return can_export_supports(); }, this); - append_menu_item(export_menu, wxID_ANY, _L("Export plate as &AMF") + dots, _L("Export current plate as AMF"), - [this](wxCommandEvent&) { if (m_plater) m_plater->export_amf(); }, "export_plater", nullptr, - [this](){return can_export_model(); }, this); +// Deprecating AMF export. Let's wait for user feedback. +// append_menu_item(export_menu, wxID_ANY, _L("Export plate as &AMF") + dots, _L("Export current plate as AMF"), +// [this](wxCommandEvent&) { if (m_plater) m_plater->export_amf(); }, "export_plater", nullptr, +// [this](){return can_export_model(); }, this); export_menu->AppendSeparator(); append_menu_item(export_menu, wxID_ANY, _L("Export &toolpaths as OBJ") + dots, _L("Export toolpaths as OBJ"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_toolpaths_to_obj(); }, "export_plater", nullptr, From 876e6fdddd8e3d1f78cd3c7e4e07be28430154b2 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 1 Sep 2021 15:12:36 +0200 Subject: [PATCH 067/151] Commented opening menubar item until its properly implemented. --- resources/data/hints.ini | 8 ++++---- src/slic3r/GUI/HintNotification.cpp | 4 ++-- src/slic3r/GUI/MainFrame.cpp | 3 ++- src/slic3r/GUI/MainFrame.hpp | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/resources/data/hints.ini b/resources/data/hints.ini index af2e09209b..5c89e93e07 100644 --- a/resources/data/hints.ini +++ b/resources/data/hints.ini @@ -194,11 +194,11 @@ documentation_link = https://help.prusa3d.com/en/article/insert-pause-or-custom- disabled_tags = SLA [hint:Configuration snapshots] -text = Configuration snapshots\nDid you know that roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - Configuration snapshots menu. +text = Configuration snapshots\nDid you know that roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - Configuration snapshots menu. documentation_link = https://help.prusa3d.com/en/article/configuration-snapshots_1776 -hypertext_type = menubar -hypertext_menubar_menu_id = 4 -hypertext_menubar_item_id = 1 +#hypertext_type = menubar +#hypertext_menubar_menu_id = 4 +#hypertext_menubar_item_id = 1 [hint:Minimum wall thickness] text = Minimum wall thickness\nDid you know that instead of the number of top and bottom layers, you can define theMinimum shell thicknessin millimeters? This feature is especially useful when using the variable layer height function. diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index 0cdf441c03..cf35022c2a 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -379,12 +379,12 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path) wxGetApp().obj_list()->load_shape_object_from_gallery(); } }; m_loaded_hints.emplace_back(hint_data); - } else if (dict["hypertext_type"] == "menubar") { + } /*else if (dict["hypertext_type"] == "menubar") { int menu = std::atoi(dict["hypertext_menubar_menu_id"].c_str()); int item = std::atoi(dict["hypertext_menubar_item_id"].c_str()); HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [menu, item]() { wxGetApp().mainframe->open_menubar_item(menu, item); } }; m_loaded_hints.emplace_back(hint_data); - } + }*/ } else { // plain text without hypertext HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link }; diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 469c3b0f65..3d486f052d 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1480,7 +1480,7 @@ void MainFrame::init_menubar_as_editor() if (plater()->printer_technology() == ptSLA) update_menubar(); } - +/* void MainFrame::open_menubar_item(int menu_index, int item_index) { if (m_menubar == nullptr) @@ -1502,6 +1502,7 @@ void MainFrame::open_menubar_item(int menu_index, int item_index) } wxPostEvent((wxEvtHandler*)menu, wxCommandEvent(wxEVT_MENU, item->GetId())); } +*/ void MainFrame::init_menubar_as_gcodeviewer() { wxMenu* fileMenu = new wxMenu; diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index e8d3c67a72..093e9e5c47 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -159,7 +159,7 @@ public: void init_menubar_as_gcodeviewer(); void update_menubar(); // Open item in menu by menu and item index (visible order of items including separators) - void open_menubar_item(int menu_index, int item_index); + //void open_menubar_item(int menu_index, int item_index); #ifdef _WIN32 void show_tabs_menu(bool show); #endif From c25ca3015ab3701574c39ddb94e82fa6eaba64d6 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 1 Sep 2021 15:15:23 +0200 Subject: [PATCH 068/151] Bumped up version number --- version.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.inc b/version.inc index 4e7aab2355..d981d620ab 100644 --- a/version.inc +++ b/version.inc @@ -3,7 +3,7 @@ set(SLIC3R_APP_NAME "PrusaSlicer") set(SLIC3R_APP_KEY "PrusaSlicer") -set(SLIC3R_VERSION "2.4.0-alpha0") +set(SLIC3R_VERSION "2.4.0-alpha1") set(SLIC3R_BUILD_ID "PrusaSlicer-${SLIC3R_VERSION}+UNKNOWN") set(SLIC3R_RC_VERSION "2,4,0,0") set(SLIC3R_RC_VERSION_DOTS "2.4.0.0") From e6eae6258487ad15d05b621557968d8c49239f86 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 1 Sep 2021 16:08:03 +0200 Subject: [PATCH 069/151] MSW specific: Fixed a typo caused extension of PresetComboBox's height on SettingsTabs, added comment for workaround --- src/slic3r/GUI/Tab.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 86089f729e..5effa35990 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -492,17 +492,15 @@ void Tab::OnActivate() activate_selected_page([](){}); m_hsizer->Layout(); - // Workaroud for Menu instead of NoteBook #ifdef _MSW_DARK_MODE -// if (wxGetApp().tabs_as_menu()) - { - wxSize sz = m_presets_choice->GetSize(); - wxSize ok_sz = wxSize(35 * m_em_unit, m_presets_choice->GetBestSize().y+1); - if (sz != ok_sz) { - m_presets_choice->SetMinSize(ok_sz); - m_presets_choice->SetSize(ok_sz); - GetSizer()->GetItem(size_t(0))->GetSizer()->Layout(); - } + // Because of DarkMode we use our own Notebook (inherited from wxSiplebook) instead of wxNotebook + // And it looks like first Layout of the page doesn't update a size of the m_presets_choice + // So we have to set correct size explicitely + if (wxSize ok_sz = wxSize(35 * m_em_unit, m_presets_choice->GetBestSize().y); + ok_sz != m_presets_choice->GetSize()) { + m_presets_choice->SetMinSize(ok_sz); + m_presets_choice->SetSize(ok_sz); + GetSizer()->GetItem(size_t(0))->GetSizer()->Layout(); } #endif // _MSW_DARK_MODE Refresh(); From 25feacfd9583d5629656dee162c0ab99457470b0 Mon Sep 17 00:00:00 2001 From: Filip Sykala Date: Wed, 1 Sep 2021 19:15:16 +0200 Subject: [PATCH 070/151] FIX Creation of non volume place in mode during simplification --- src/libslic3r/QuadricEdgeCollapse.cpp | 130 ++++++++++++++---- tests/libslic3r/test_indexed_triangle_set.cpp | 9 ++ 2 files changed, 114 insertions(+), 25 deletions(-) diff --git a/src/libslic3r/QuadricEdgeCollapse.cpp b/src/libslic3r/QuadricEdgeCollapse.cpp index 6efc5f4a9d..2e1b7089be 100644 --- a/src/libslic3r/QuadricEdgeCollapse.cpp +++ b/src/libslic3r/QuadricEdgeCollapse.cpp @@ -7,6 +7,8 @@ using namespace Slic3r; +#define NDEBUG + // only private namespace not neccessary be in .hpp namespace QuadricEdgeCollapse { using Vertices = std::vector; @@ -79,10 +81,13 @@ namespace QuadricEdgeCollapse { init(const indexed_triangle_set &its, ThrowOnCancel& throw_on_cancel, StatusFn& status_fn); std::optional find_triangle_index1(uint32_t vi, const VertexInfo& v_info, uint32_t ti, const EdgeInfos& e_infos, const Indices& indices); + void reorder_edges(EdgeInfos &e_infos, const VertexInfo &v_info, uint32_t ti0, uint32_t ti1); bool is_flipped(const Vec3f &new_vertex, uint32_t ti0, uint32_t ti1, const VertexInfo& v_info, const TriangleInfos &t_infos, const EdgeInfos &e_infos, const indexed_triangle_set &its); bool degenerate(uint32_t vi, uint32_t ti0, uint32_t ti1, const VertexInfo &v_info, const EdgeInfos &e_infos, const Indices &indices); + bool create_no_volume(uint32_t vi0, uint32_t vi1, uint32_t ti0, uint32_t ti1, + const VertexInfo &v_info0, const VertexInfo &v_info1, const EdgeInfos &e_infos, const Indices &indices); // find edge with smallest error in triangle Vec3d calculate_3errors(const Triangle &t, const Vertices &vertices, const VertexInfos &v_infos); Error calculate_error(uint32_t ti, const Triangle& t,const Vertices &vertices, const VertexInfos& v_infos, unsigned char& min_index); @@ -99,6 +104,10 @@ namespace QuadricEdgeCollapse { const VertexInfos &v_infos, const EdgeInfos &e_infos); #endif /* NDEBUG */ + // constants --> may be move to config + const int status_init_size = 10; // in percents + const uint32_t check_cancel_period = 16; // how many edge to reduce before call throw_on_cancel + const size_t max_triangle_count_for_one_vertex = 50; } // namespace QuadricEdgeCollapse using namespace QuadricEdgeCollapse; @@ -110,10 +119,6 @@ void Slic3r::its_quadric_edge_collapse( std::function throw_on_cancel, std::function status_fn) { - // constants --> may be move to config - const int status_init_size = 10; // in percents - const int check_cancel_period = 16; // how many edge to reduce before call throw_on_cancel - // check input if (triangle_count >= its.indices.size()) return; float maximal_error = (max_error == nullptr)? std::numeric_limits::max() : *max_error; @@ -122,7 +127,8 @@ void Slic3r::its_quadric_edge_collapse( if (status_fn == nullptr) status_fn = [](int) {}; StatusFn init_status_fn = [&](int percent) { - status_fn(std::round((percent * status_init_size) / 100.)); + float n_percent = percent * status_init_size / 100.f; + status_fn(static_cast(std::round(n_percent))); }; TriangleInfos t_infos; // only normals with information about deleted triangle @@ -145,7 +151,6 @@ void Slic3r::its_quadric_edge_collapse( mpq.reserve(its.indices.size()); for (Error &error :errors) mpq.push(error); - const size_t max_triangle_count_for_one_vertex = 50; CopyEdgeInfos ceis; ceis.reserve(max_triangle_count_for_one_vertex); EdgeInfos e_infos_swap; @@ -162,8 +167,9 @@ void Slic3r::its_quadric_edge_collapse( (1. - reduced); status_fn(static_cast(std::round(status))); }; - // modulo for update status - uint32_t status_mod = std::max(uint32_t(16), count_triangle_to_reduce / 100); + // modulo for update status, call each percent only once + uint32_t status_mod = std::max(uint32_t(16), + count_triangle_to_reduce / (100 - status_init_size)); uint32_t iteration_number = 0; float last_collapsed_error = 0.f; @@ -195,14 +201,21 @@ void Slic3r::its_quadric_edge_collapse( q += v_info1.q; Vec3f new_vertex0 = calculate_vertex(vi0, vi1, q, its.vertices); // set of triangle indices that change quadric + uint32_t ti1 = -1; // triangle 1 index auto ti1_opt = (v_info0.count < v_info1.count)? find_triangle_index1(vi1, v_info0, ti0, e_infos, its.indices) : find_triangle_index1(vi0, v_info1, ti0, e_infos, its.indices) ; + if (ti1_opt.has_value()) { + ti1 = *ti1_opt; + reorder_edges(e_infos, v_info0, ti0, ti1); + reorder_edges(e_infos, v_info1, ti0, ti1); + } if (!ti1_opt.has_value() || // edge has only one triangle - degenerate(vi0, ti0, *ti1_opt, v_info1, e_infos, its.indices) || - degenerate(vi1, ti0, *ti1_opt, v_info0, e_infos, its.indices) || - is_flipped(new_vertex0, ti0, *ti1_opt, v_info0, t_infos, e_infos, its) || - is_flipped(new_vertex0, ti0, *ti1_opt, v_info1, t_infos, e_infos, its)) { + degenerate(vi0, ti0, ti1, v_info1, e_infos, its.indices) || + degenerate(vi1, ti0, ti1, v_info0, e_infos, its.indices) || + create_no_volume(vi0, vi1, ti0, ti1, v_info0, v_info1, e_infos, its.indices) || + is_flipped(new_vertex0, ti0, ti1, v_info0, t_infos, e_infos, its) || + is_flipped(new_vertex0, ti0, ti1, v_info1, t_infos, e_infos, its)) { // try other triangle's edge Vec3d errors = calculate_3errors(t0, its.vertices, v_infos); Vec3i ord = (errors[0] < errors[1]) ? @@ -227,29 +240,25 @@ void Slic3r::its_quadric_edge_collapse( mpq.push(e); continue; } - uint32_t ti1 = *ti1_opt; + last_collapsed_error = e.value; changed_triangle_indices.clear(); changed_triangle_indices.reserve(v_info0.count + v_info1.count - 4); // for each vertex0 triangles - uint32_t v_info0_end = v_info0.start + v_info0.count; + uint32_t v_info0_end = v_info0.start + v_info0.count - 2; for (uint32_t di = v_info0.start; di < v_info0_end; ++di) { assert(di < e_infos.size()); uint32_t ti = e_infos[di].t_index; - if (ti == ti0) continue; // ti0 will be deleted - if (ti == ti1) continue; // ti1 will be deleted changed_triangle_indices.emplace_back(ti); } // for each vertex1 triangles - uint32_t v_info1_end = v_info1.start + v_info1.count; + uint32_t v_info1_end = v_info1.start + v_info1.count - 2; for (uint32_t di = v_info1.start; di < v_info1_end; ++di) { assert(di < e_infos.size()); EdgeInfo &e_info = e_infos[di]; uint32_t ti = e_info.t_index; - if (ti == ti0) continue; // ti0 will be deleted - if (ti == ti1) continue; // ti1 will be deleted Triangle &t = its.indices[ti]; t[e_info.edge] = vi0; // change index changed_triangle_indices.emplace_back(ti); @@ -282,7 +291,9 @@ void Slic3r::its_quadric_edge_collapse( t_info1.set_deleted(); // triangle counter decrementation actual_triangle_count-=2; +#ifndef NDEBUG assert(check_neighbors(its, t_infos, v_infos, e_infos)); +#endif // NDEBUG } // compact triangle @@ -506,6 +517,38 @@ std::optional QuadricEdgeCollapse::find_triangle_index1(uint32_t return {}; } +void QuadricEdgeCollapse::reorder_edges(EdgeInfos & e_infos, + const VertexInfo &v_info, + uint32_t ti0, + uint32_t ti1) +{ + // swap edge info of ti0 and ti1 to end(last one and one before) + size_t v_info_end = v_info.start + v_info.count - 2; + EdgeInfo &e_info_ti0 = e_infos[v_info_end]; + EdgeInfo &e_info_ti1 = e_infos[v_info_end+1]; + bool is_swaped = false; + for (size_t ei = v_info.start; ei < v_info_end; ++ei) { + EdgeInfo &e_info = e_infos[ei]; + if (e_info.t_index == ti0) { + std::swap(e_info, e_info_ti0); + if (is_swaped) return; + if (e_info.t_index == ti1) { + std::swap(e_info, e_info_ti1); + return; + } + is_swaped = true; + } else if (e_info.t_index == ti1) { + std::swap(e_info, e_info_ti1); + if (is_swaped) return; + if (e_info.t_index == ti0) { + std::swap(e_info, e_info_ti0); + return; + } + is_swaped = true; + } + } +} + bool QuadricEdgeCollapse::is_flipped(const Vec3f & new_vertex, uint32_t ti0, uint32_t ti1, @@ -519,12 +562,10 @@ bool QuadricEdgeCollapse::is_flipped(const Vec3f & new_vertex, static const float dot_thr = 0.2f; // Value from simplify mesh cca 80 DEG // for each vertex triangles - size_t v_info_end = v_info.start + v_info.count; + size_t v_info_end = v_info.start + v_info.count-2; for (size_t ei = v_info.start; ei < v_info_end; ++ei) { assert(ei < e_infos.size()); const EdgeInfo &e_info = e_infos[ei]; - if (e_info.t_index == ti0) continue; // ti0 will be deleted - if (e_info.t_index == ti1) continue; // ti1 will be deleted const Triangle &t = its.indices[e_info.t_index]; const Vec3f &normal = t_infos[e_info.t_index].n; const Vec3f &vf = its.vertices[t[(e_info.edge + 1) % 3]]; @@ -554,12 +595,10 @@ bool QuadricEdgeCollapse::degenerate(uint32_t vi, { // check surround triangle do not contain vertex index // protect from creation of triangle with two same vertices inside - size_t v_info_end = v_info.start + v_info.count; + size_t v_info_end = v_info.start + v_info.count - 2; for (size_t ei = v_info.start; ei < v_info_end; ++ei) { assert(ei < e_infos.size()); const EdgeInfo &e_info = e_infos[ei]; - if (e_info.t_index == ti0) continue; // ti0 will be deleted - if (e_info.t_index == ti1) continue; // ti1 will be deleted const Triangle &t = indices[e_info.t_index]; for (size_t i = 0; i < 3; ++i) if (static_cast(t[i]) == vi) return true; @@ -567,6 +606,47 @@ bool QuadricEdgeCollapse::degenerate(uint32_t vi, return false; } +bool QuadricEdgeCollapse::create_no_volume( + uint32_t vi0 , uint32_t vi1, + uint32_t ti0 , uint32_t ti1, + const VertexInfo &v_info0, const VertexInfo &v_info1, + const EdgeInfos & e_infos, const Indices &indices) +{ + // check that triangles around vertex0 doesn't have half edge + // with opposit order in set of triangles around vertex1 + // protect from creation of two triangles with oposit order - no volume space + size_t v_info0_end = v_info0.start + v_info0.count - 2; + size_t v_info1_end = v_info1.start + v_info1.count - 2; + for (size_t ei0 = v_info0.start; ei0 < v_info0_end; ++ei0) { + const EdgeInfo &e_info0 = e_infos[ei0]; + const Triangle &t0 = indices[e_info0.t_index]; + // edge CCW vertex indices are t0vi0, t0vi1 + size_t t0i = 0; + if (t0[t0i] == vi0) ++t0i; + uint32_t t0vi0 = t0[t0i]; + ++t0i; + if (t0[t0i] == vi0) ++t0i; + uint32_t t0vi1 = t0[t0i]; + + for (size_t ei1 = v_info1.start; ei1 < v_info1_end; ++ei1) { + const EdgeInfo &e_info1 = e_infos[ei1]; + const Triangle &t1 = indices[e_info1.t_index]; + size_t t1i = 0; + for (; t1i < 3; ++t1i) if (t1[t1i] == t0vi1) break; + if (t1i >= 3) continue; // without vertex index from triangle 0 + // check if second index is same too + ++t1i; + if (t1i == 3) t1i = 0; // triangle loop(modulo 3) + if (t1[t1i] == vi1) { + ++t1i; + if (t1i == 3) t1i = 0; // triangle loop(modulo 3) + } + if (t1[t1i] == t0vi0) return true; + } + } + return false; +} + Vec3d QuadricEdgeCollapse::calculate_3errors(const Triangle & t, const Vertices & vertices, const VertexInfos &v_infos) diff --git a/tests/libslic3r/test_indexed_triangle_set.cpp b/tests/libslic3r/test_indexed_triangle_set.cpp index a3996e6519..bbb22e778c 100644 --- a/tests/libslic3r/test_indexed_triangle_set.cpp +++ b/tests/libslic3r/test_indexed_triangle_set.cpp @@ -288,3 +288,12 @@ TEST_CASE("Simplify trouble case", "[its]") its_quadric_edge_collapse(tm.its, wanted_count, &max_error); CHECK(tm.its.indices.size() <= 8); } + +TEST_CASE("Simplified cube should not be empty.", "[its]") +{ + auto its = its_make_cube(1, 2, 3); + float max_error = std::numeric_limits::max(); + uint32_t wanted_count = 0; + its_quadric_edge_collapse(its, wanted_count, &max_error); + CHECK(!its.indices.empty()); +} From f6f70f6fd4115b205d462ca14234810157d1bbb5 Mon Sep 17 00:00:00 2001 From: Filip Sykala Date: Wed, 1 Sep 2021 20:19:55 +0200 Subject: [PATCH 071/151] Fix Crash when delete model during simplification --- src/libslic3r/QuadricEdgeCollapse.cpp | 21 ++++++++++++--------- src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp | 21 +++++++++++++++++++-- src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp | 1 + 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/QuadricEdgeCollapse.cpp b/src/libslic3r/QuadricEdgeCollapse.cpp index 2e1b7089be..6609879cd5 100644 --- a/src/libslic3r/QuadricEdgeCollapse.cpp +++ b/src/libslic3r/QuadricEdgeCollapse.cpp @@ -7,7 +7,10 @@ using namespace Slic3r; +// Faster debug, comment when you want deep check +#ifndef NDEBUG #define NDEBUG +#endif // !NDEBUG // only private namespace not neccessary be in .hpp namespace QuadricEdgeCollapse { @@ -105,10 +108,17 @@ namespace QuadricEdgeCollapse { #endif /* NDEBUG */ // constants --> may be move to config - const int status_init_size = 10; // in percents const uint32_t check_cancel_period = 16; // how many edge to reduce before call throw_on_cancel const size_t max_triangle_count_for_one_vertex = 50; -} // namespace QuadricEdgeCollapse + // change speed of progress bargraph + const int status_init_size = 10; // in percents + // parts of init size + const int status_normal_size = 25; + const int status_sum_quadric = 25; + const int status_set_offsets = 10; + const int status_calc_errors = 30; + const int status_create_refs = 10; + } // namespace QuadricEdgeCollapse using namespace QuadricEdgeCollapse; @@ -395,13 +405,6 @@ SymMat QuadricEdgeCollapse::create_quadric(const Triangle &t, std::tuple QuadricEdgeCollapse::init(const indexed_triangle_set &its, ThrowOnCancel& throw_on_cancel, StatusFn& status_fn) { - // change speed of progress bargraph - const int status_normal_size = 25; - const int status_sum_quadric = 25; - const int status_set_offsets = 10; - const int status_calc_errors = 30; - const int status_create_refs = 10; - int status_offset = 0; TriangleInfos t_infos(its.indices.size()); VertexInfos v_infos(its.vertices.size()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index b9e5d111b0..f70e3f93e2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -55,6 +55,7 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi create_gui_cfg(); const Selection &selection = m_parent.get_selection(); int object_idx = selection.get_object_idx(); + if (!is_selected_object(&object_idx)) return; ModelObject *obj = wxGetApp().plater()->model().objects[object_idx]; ModelVolume *act_volume = obj->volumes.front(); @@ -329,9 +330,9 @@ void GLGizmoSimplify::on_set_state() { // Closing gizmo. e.g. selecting another one if (GLGizmoBase::m_state == GLGizmoBase::Off) { - // refuse outgoing during simlification - if (m_state != State::settings) { + // object is not selected when it is deleted(cancel and close gizmo) + if (m_state != State::settings && is_selected_object()) { GLGizmoBase::m_state = GLGizmoBase::On; auto notification_manager = wxGetApp().plater()->get_notification_manager(); notification_manager->push_notification( @@ -383,4 +384,20 @@ void GLGizmoSimplify::request_rerender() { }); } +bool GLGizmoSimplify::is_selected_object(int *object_idx) +{ + int index = (object_idx != nullptr) ? *object_idx : + m_parent.get_selection().get_object_idx(); + // no selected object --> can appear after delete model + if (index < 0) { + switch (m_state) { + case State::settings: close(); break; + case State::canceling: break; + default: m_state = State::canceling; + } + return false; + } + return true; +} + } // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp index 0f5bd5991f..856b6298d0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp @@ -38,6 +38,7 @@ private: void set_its(indexed_triangle_set &its); void create_gui_cfg(); void request_rerender(); + bool is_selected_object(int *object_idx = nullptr); std::atomic_bool m_is_valid_result; // differ what to do in apply std::atomic_bool m_exist_preview; // set when process end From 2bb14849f46dc0bc49b9b9ee8bc05c7aea509c39 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 2 Sep 2021 08:32:05 +0200 Subject: [PATCH 072/151] #6866 - Do not clear plater when pressing Cancel button in the New Project confirmation dialog --- src/slic3r/GUI/Plater.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f8bcafdd8b..f80f2cc3b5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4759,7 +4759,7 @@ SLAPrint& Plater::sla_print() { return p->sla_print; } void Plater::new_project() { - if (!p->save_project_if_dirty()) + if (p->save_project_if_dirty() == wxID_CANCEL) return; p->select_view_3D("3D"); From 5f7a4982f6ab241a5576df1ced11fddbe6ccdafb Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 1 Sep 2021 16:49:23 +0200 Subject: [PATCH 073/151] Open menubar item by name with translations --- resources/data/hints.ini | 10 ++++----- src/slic3r/GUI/HintNotification.cpp | 6 +++--- src/slic3r/GUI/MainFrame.cpp | 33 +++++++++++++++++------------ src/slic3r/GUI/MainFrame.hpp | 4 ++-- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/resources/data/hints.ini b/resources/data/hints.ini index 5c89e93e07..e7b1bb6814 100644 --- a/resources/data/hints.ini +++ b/resources/data/hints.ini @@ -40,8 +40,8 @@ # hypertext_type = gallery # #Open top menubar item -#hypertext_menubar_menu_id = 4 (0 - 5, index of menu from left: File = 0, Edit = 1...) -#hypertext_menubar_item_id = 1 (index of item, starting at 0, Including separators! In Window: 0 = Plater Tab, 1 = SEPARATOR, 2 = Print Setting Tab...) +#hypertext_menubar_menu_name = (Name in english visible as menu name: File, ) +#hypertext_menubar_item_name = (Name of item in english, if there are three dots at the end of name, put name without three dots) # # # Each notification can have disabled and enabled modes and techs - divided by ; and space @@ -196,9 +196,9 @@ disabled_tags = SLA [hint:Configuration snapshots] text = Configuration snapshots\nDid you know that roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - Configuration snapshots menu. documentation_link = https://help.prusa3d.com/en/article/configuration-snapshots_1776 -#hypertext_type = menubar -#hypertext_menubar_menu_id = 4 -#hypertext_menubar_item_id = 1 +hypertext_type = menubar +hypertext_menubar_menu_name = Configuration +hypertext_menubar_item_name = Configuration Snapshots [hint:Minimum wall thickness] text = Minimum wall thickness\nDid you know that instead of the number of top and bottom layers, you can define theMinimum shell thicknessin millimeters? This feature is especially useful when using the variable layer height function. diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index cf35022c2a..75bb69a656 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -379,9 +379,9 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path) wxGetApp().obj_list()->load_shape_object_from_gallery(); } }; m_loaded_hints.emplace_back(hint_data); - } /*else if (dict["hypertext_type"] == "menubar") { - int menu = std::atoi(dict["hypertext_menubar_menu_id"].c_str()); - int item = std::atoi(dict["hypertext_menubar_item_id"].c_str()); + } else if (dict["hypertext_type"] == "menubar") { + wxString menu(_L("&" + dict["hypertext_menubar_menu_name"])); + wxString item(_L(dict["hypertext_menubar_item_name"])); HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [menu, item]() { wxGetApp().mainframe->open_menubar_item(menu, item); } }; m_loaded_hints.emplace_back(hint_data); }*/ diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3d486f052d..eb309b1afb 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1480,29 +1480,34 @@ void MainFrame::init_menubar_as_editor() if (plater()->printer_technology() == ptSLA) update_menubar(); } -/* -void MainFrame::open_menubar_item(int menu_index, int item_index) + +void MainFrame::open_menubar_item(const wxString& menu_name,const wxString& item_name) { if (m_menubar == nullptr) return; - wxMenu* menu = m_menubar->GetMenu(menu_index); + // Get menu object from menubar + int menu_index = m_menubar->FindMenu(menu_name); + wxMenu* menu = m_menubar->GetMenu(menu_index); if (menu == nullptr) { - BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find menu: " << menu_index; + BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find menu: " << menu_name; return; } - wxMenuItemList items = menu->GetMenuItems(); - if (items.size() <= item_index) { - BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find item: " << item_index; - return; - } - wxMenuItem* item = items[item_index]; - if (item == nullptr) { - BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find item: " << item_index; + // Get item id from menu + int item_id = menu->FindItem(item_name); + if (item_id == wxNOT_FOUND) + { + // try adding three dots char + item_id = menu->FindItem(item_name + dots); + } + if (item_id == wxNOT_FOUND) + { + BOOST_LOG_TRIVIAL(error) << "Mainframe open_menubar_item function couldn't find item: " << item_name; return; } - wxPostEvent((wxEvtHandler*)menu, wxCommandEvent(wxEVT_MENU, item->GetId())); + // wxEVT_MENU will trigger item + wxPostEvent((wxEvtHandler*)menu, wxCommandEvent(wxEVT_MENU, item_id)); } -*/ + void MainFrame::init_menubar_as_gcodeviewer() { wxMenu* fileMenu = new wxMenu; diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 093e9e5c47..e87f94f650 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -158,8 +158,8 @@ public: void init_menubar_as_editor(); void init_menubar_as_gcodeviewer(); void update_menubar(); - // Open item in menu by menu and item index (visible order of items including separators) - //void open_menubar_item(int menu_index, int item_index); + // Open item in menu by menu and item name (in actual language) + void open_menubar_item(const wxString& menu_name,const wxString& item_name); #ifdef _WIN32 void show_tabs_menu(bool show); #endif From 42f4de94dfd416d8b4b5718eaba4f43225a55ada Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 1 Sep 2021 17:58:55 +0200 Subject: [PATCH 074/151] Calling destructor of notifications manager and hint database Ensures to translate hints after change of language --- src/slic3r/GUI/HintNotification.cpp | 7 +++++++ src/slic3r/GUI/HintNotification.hpp | 3 +++ src/slic3r/GUI/NotificationManager.cpp | 4 ++++ src/slic3r/GUI/NotificationManager.hpp | 1 + src/slic3r/GUI/Plater.cpp | 2 ++ 5 files changed, 17 insertions(+) diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index 75bb69a656..d1a2328e9a 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -242,6 +242,13 @@ HintDatabase::~HintDatabase() write_used_binary(m_used_ids); } } +void HintDatabase::uninit() +{ + if (m_initialized) { + write_used_binary(m_used_ids); + } + m_initialized = false; +} void HintDatabase::init() { load_hints_from_file(std::move(boost::filesystem::path(resources_dir()) / "data" / "hints.ini")); diff --git a/src/slic3r/GUI/HintNotification.hpp b/src/slic3r/GUI/HintNotification.hpp index c1e7c0ed3c..e5dd90bdbd 100644 --- a/src/slic3r/GUI/HintNotification.hpp +++ b/src/slic3r/GUI/HintNotification.hpp @@ -47,6 +47,9 @@ public: return 0; return m_loaded_hints.size(); } + // resets m_initiailized to false and writes used if was initialized + // used when reloading in runtime - like change language + void uninit(); private: void init(); void load_hints_from_file(const boost::filesystem::path& path); diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 219faf42ef..4e94d82608 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1107,6 +1107,10 @@ NotificationManager::NotificationManager(wxEvtHandler* evt_handler) : m_evt_handler(evt_handler) { } +NotificationManager::~NotificationManager() +{ + HintDatabase::get_instance().uninit(); +} void NotificationManager::push_notification(const NotificationType type, int timestamp) { auto it = std::find_if(std::begin(basic_notifications), std::end(basic_notifications), diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 4aba35f4cb..1e667e4525 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -121,6 +121,7 @@ public: }; NotificationManager(wxEvtHandler* evt_handler); + ~NotificationManager(); // Push a prefabricated notification from basic_notifications (see the table at the end of this file). void push_notification(const NotificationType type, int timestamp = 0); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f80f2cc3b5..b86092795d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2073,6 +2073,8 @@ Plater::priv::~priv() { if (config != nullptr) delete config; + if (notification_manager != nullptr) + delete notification_manager; } void Plater::priv::update(unsigned int flags) From 795ac7f297ecc339bec26c2eb3e704d15393e4d1 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 2 Sep 2021 09:49:04 +0200 Subject: [PATCH 075/151] Follow-up of 2bb14849f46dc0bc49b9b9ee8bc05c7aea509c39 - Similar fix for Open Project command --- src/slic3r/GUI/Plater.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f80f2cc3b5..0bdbae8f41 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4772,7 +4772,7 @@ void Plater::new_project() void Plater::load_project() { - if (!p->save_project_if_dirty()) + if (p->save_project_if_dirty() == wxID_CANCEL) return; // Ask user for a project file name. From 0cb08ebda401228251603d47527227cecb0d92b0 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 2 Sep 2021 09:54:20 +0200 Subject: [PATCH 076/151] fix of b45ae0170bebf6fd564bf12fede3013f3bf94e7a --- src/slic3r/GUI/HintNotification.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index d1a2328e9a..33ccdc4ae3 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -391,7 +391,7 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path) wxString item(_L(dict["hypertext_menubar_item_name"])); HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [menu, item]() { wxGetApp().mainframe->open_menubar_item(menu, item); } }; m_loaded_hints.emplace_back(hint_data); - }*/ + } } else { // plain text without hypertext HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link }; From d45ab1c484c9699a596bf40e21e04d25fe645bda Mon Sep 17 00:00:00 2001 From: Filip Sykala Date: Thu, 2 Sep 2021 11:17:07 +0200 Subject: [PATCH 077/151] Fix trouble case test --- tests/libslic3r/test_indexed_triangle_set.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/libslic3r/test_indexed_triangle_set.cpp b/tests/libslic3r/test_indexed_triangle_set.cpp index bbb22e778c..7e2d511c77 100644 --- a/tests/libslic3r/test_indexed_triangle_set.cpp +++ b/tests/libslic3r/test_indexed_triangle_set.cpp @@ -279,14 +279,23 @@ TEST_CASE("Simplify mesh by Quadric edge collapse to 5%", "[its]") CHECK(is_similar(its, mesh.its, cfg)); } +bool exist_triangle_with_twice_vertices(const std::vector& indices) +{ + for (const auto &face : indices) + if (face[0] == face[1] || + face[0] == face[2] || + face[1] == face[2]) return true; + return false; +} + TEST_CASE("Simplify trouble case", "[its]") { TriangleMesh tm = load_model("simplification.obj"); REQUIRE_FALSE(tm.empty()); float max_error = std::numeric_limits::max(); - uint32_t wanted_count = 8; + uint32_t wanted_count = 0; its_quadric_edge_collapse(tm.its, wanted_count, &max_error); - CHECK(tm.its.indices.size() <= 8); + CHECK(!exist_triangle_with_twice_vertices(tm.its.indices)); } TEST_CASE("Simplified cube should not be empty.", "[its]") From 746300eafd2a2655ca8d872cfdadd4d6577f9bb8 Mon Sep 17 00:00:00 2001 From: Filip Sykala Date: Thu, 2 Sep 2021 11:24:33 +0200 Subject: [PATCH 078/151] Fix @(Lukas Matena) note about using NDEBUG --- src/libslic3r/QuadricEdgeCollapse.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/QuadricEdgeCollapse.cpp b/src/libslic3r/QuadricEdgeCollapse.cpp index 6609879cd5..37cd6965de 100644 --- a/src/libslic3r/QuadricEdgeCollapse.cpp +++ b/src/libslic3r/QuadricEdgeCollapse.cpp @@ -7,10 +7,9 @@ using namespace Slic3r; -// Faster debug, comment when you want deep check #ifndef NDEBUG -#define NDEBUG -#endif // !NDEBUG +// #define EXPENSIVE_DEBUG_CHECKS +#endif // NDEBUG // only private namespace not neccessary be in .hpp namespace QuadricEdgeCollapse { @@ -100,12 +99,12 @@ namespace QuadricEdgeCollapse { const Triangle &t1, CopyEdgeInfos& infos, EdgeInfos &e_infos1); void compact(const VertexInfos &v_infos, const TriangleInfos &t_infos, const EdgeInfos &e_infos, indexed_triangle_set &its); -#ifndef NDEBUG +#ifdef EXPENSIVE_DEBUG_CHECKS void store_surround(const char *obj_filename, size_t triangle_index, int depth, const indexed_triangle_set &its, const VertexInfos &v_infos, const EdgeInfos &e_infos); bool check_neighbors(const indexed_triangle_set &its, const TriangleInfos &t_infos, const VertexInfos &v_infos, const EdgeInfos &e_infos); -#endif /* NDEBUG */ +#endif /* EXPENSIVE_DEBUG_CHECKS */ // constants --> may be move to config const uint32_t check_cancel_period = 16; // how many edge to reduce before call throw_on_cancel @@ -301,9 +300,9 @@ void Slic3r::its_quadric_edge_collapse( t_info1.set_deleted(); // triangle counter decrementation actual_triangle_count-=2; -#ifndef NDEBUG +#ifdef EXPENSIVE_DEBUG_CHECKS assert(check_neighbors(its, t_infos, v_infos, e_infos)); -#endif // NDEBUG +#endif // EXPENSIVE_DEBUG_CHECKS } // compact triangle @@ -815,7 +814,8 @@ void QuadricEdgeCollapse::compact(const VertexInfos & v_infos, its.indices.erase(its.indices.begin() + ti_new, its.indices.end()); } -#ifndef NDEBUG +#ifdef EXPENSIVE_DEBUG_CHECKS + // store triangle surrounding to file void QuadricEdgeCollapse::store_surround(const char *obj_filename, size_t triangle_index, @@ -925,4 +925,4 @@ bool QuadricEdgeCollapse::check_neighbors(const indexed_triangle_set &its, } return true; } -#endif /* NDEBUG */ +#endif /* EXPENSIVE_DEBUG_CHECKS */ From 3f923628aaf3a8700e2779e6ad4b7d80a42cc823 Mon Sep 17 00:00:00 2001 From: Filip Sykala Date: Thu, 2 Sep 2021 11:41:11 +0200 Subject: [PATCH 079/151] fix ../src/libslic3r/QuadricEdgeCollapse.cpp:628:21: warning: comparison of integer expressions of different signedness: 'const int' and 'uint32_t' {aka 'unsigned int'} [-Wsign-compare] ../src/libslic3r/QuadricEdgeCollapse.cpp:631:21: warning: comparison of integer expressions of different signedness: 'const int' and 'uint32_t' {aka 'unsigned int'} [-Wsign-compare] ../src/libslic3r/QuadricEdgeCollapse.cpp:638:48: warning: comparison of integer expressions of different signedness: 'const int' and 'uint32_t' {aka 'unsigned int'} [-Wsign-compare] ../src/libslic3r/QuadricEdgeCollapse.cpp:643:25: warning: comparison of integer expressions of different signedness: 'const int' and 'uint32_t' {aka 'unsigned int'} [-Wsign-compare] ../src/libslic3r/QuadricEdgeCollapse.cpp:647:25: warning: comparison of integer expressions of different signedness: 'const int' and 'uint32_t' {aka 'unsigned int'} [-Wsign-compare] --- src/libslic3r/QuadricEdgeCollapse.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/QuadricEdgeCollapse.cpp b/src/libslic3r/QuadricEdgeCollapse.cpp index 37cd6965de..7efe15bb24 100644 --- a/src/libslic3r/QuadricEdgeCollapse.cpp +++ b/src/libslic3r/QuadricEdgeCollapse.cpp @@ -624,26 +624,31 @@ bool QuadricEdgeCollapse::create_no_volume( const Triangle &t0 = indices[e_info0.t_index]; // edge CCW vertex indices are t0vi0, t0vi1 size_t t0i = 0; - if (t0[t0i] == vi0) ++t0i; - uint32_t t0vi0 = t0[t0i]; + uint32_t t0vi0 = static_cast(t0[t0i]); + if (t0vi0 == vi0) { + ++t0i; + t0vi0 = static_cast(t0[t0i]); + } ++t0i; - if (t0[t0i] == vi0) ++t0i; - uint32_t t0vi1 = t0[t0i]; - + uint32_t t0vi1 = static_cast(t0[t0i]); + if (t0vi1 == vi0) { + ++t0i; + t0vi1 = static_cast(t0[t0i]); + } for (size_t ei1 = v_info1.start; ei1 < v_info1_end; ++ei1) { const EdgeInfo &e_info1 = e_infos[ei1]; const Triangle &t1 = indices[e_info1.t_index]; size_t t1i = 0; - for (; t1i < 3; ++t1i) if (t1[t1i] == t0vi1) break; + for (; t1i < 3; ++t1i) if (static_cast(t1[t1i]) == t0vi1) break; if (t1i >= 3) continue; // without vertex index from triangle 0 // check if second index is same too ++t1i; if (t1i == 3) t1i = 0; // triangle loop(modulo 3) - if (t1[t1i] == vi1) { + if (static_cast(t1[t1i]) == vi1) { ++t1i; if (t1i == 3) t1i = 0; // triangle loop(modulo 3) } - if (t1[t1i] == t0vi0) return true; + if (static_cast(t1[t1i]) == t0vi0) return true; } } return false; From b20bd26e84f9a31a56a14c5372d9c285908ca667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Mat=C4=9Bna?= <33929324+lukasmatena@users.noreply.github.com> Date: Thu, 2 Sep 2021 12:27:07 +0200 Subject: [PATCH 080/151] Update How to build - Linux et al.md --- doc/How to build - Linux et al.md | 120 +++++++++++++++++------------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/doc/How to build - Linux et al.md b/doc/How to build - Linux et al.md index 559536f143..704ceb46aa 100644 --- a/doc/How to build - Linux et al.md +++ b/doc/How to build - Linux et al.md @@ -1,87 +1,101 @@ # Building PrusaSlicer on UNIX/Linux -Please understand that PrusaSlicer team cannot support compilation on all possible Linux distros. Namely, we cannot help trouble shooting OpenGL driver issues or dependency issues if compiled against distro provided libraries. We can only support PrusaSlicer compiled the same way we do compile PrusaSlicer for our [binary builds](https://github.com/prusa3d/PrusaSlicer/releases), that means linked statically agains the dependencies compiled with the `deps` scripts. +Please understand that PrusaSlicer team cannot support compilation on all possible Linux distros. Namely, we cannot help troubleshoot OpenGL driver issues or dependency issues if compiled against distro provided libraries. **We can only support PrusaSlicer statically linked against the dependencies compiled with the `deps` scripts**, the same way we compile PrusaSlicer for our [binary builds](https://github.com/prusa3d/PrusaSlicer/releases). -Instead of compiling PrusaSlicer from source code, one may consider to install PrusaSlicer [pre-compiled by contributors](https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions). +If you have some reason to link dynamically to your system libraries, you are free to do so, but we can not and will not troubleshoot any issues you possibly run into. -### How to build +Instead of compiling PrusaSlicer from source code, one may also consider to install PrusaSlicer [pre-compiled by contributors](https://github.com/prusa3d/PrusaSlicer/wiki/PrusaSlicer-on-Linux---binary-distributions). -PrusaSlicer uses the CMake build system and requires several dependencies. -The dependencies can be listed in the `deps` directory in individual subdirectories, although they don't necessarily need to be as recent -as the versions listed - generally versions available on conservative Linux distros such as Debian stable, Ubuntu LTS releases or Fedora are likely sufficient. +## Step by step guide -Perl is not required anymore. +This guide describes building PrusaSlicer statically against dependencies pulled by our `deps` script. Running all the listed commands in order should result in successful build. -In a typical situation, one would open a command line, go to the PrusaSlicer sources (**the root directory of the repository**), create a directory called `build` or similar, -`cd` into it and call: +#### 0. Prerequisities +You must have CMake, GNU build tools and git. If you don't already have them, install them as usual from your distribution packages (e.g. on Ubuntu, you would run `sudo apt-get install cmake build-essential git`, etc.) + +#### 1. Cloning the repository + + +Cloning the repository is simple thanks to git and Github. Simply `cd` into wherever you want to clone PrusaSlicer code base and run +``` +git clone https://www.github.com/prusa3d/PrusaSlicer +cd PrusaSlicer +``` +This will download the source code into a new directory and `cd` into it. You can now optionally select a tag/branch/commit to build using `git checkout`. Otherwise, `master` branch will be built. + + +#### 2. Building dependencies + +PrusaSlicer uses CMake and the build is quite simple, the only tricky part is resolution of dependencies. The supported and recommended way is to build the dependencies first and link to them statically. The source base contains a CMake script that automatically downloads and builds the required dependencies. All that is needed is to run the following (from the top of the cloned repository): + + cd deps + mkdir build + cd build cmake .. - make -jN + make + cd ../.. -where `N` is the number of CPU cores available. -Additional CMake flags may be applicable as explained below. +**Warning**: Once the dependency bundle is installed in a destdir, the destdir cannot be moved elsewhere. This is because wxWidgets hardcode the installation path. -### Dependency resolution -By default PrusaSlicer looks for dependencies the default way CMake looks for them, i.e. in default system locations. -On Linux this will typically make PrusaSlicer depend on dynamically loaded libraries from the system, however, PrusaSlicer can be told -to specifically look for static libraries with the `SLIC3R_STATIC` flag passed to cmake: +#### 3. Building PrusaSlicer - cmake .. -DSLIC3R_STATIC=1 +Now when you have the dependencies compiled, all that is needed is to tell CMake that we are interested in static build and point it to the dependencies. From the top of the repository, run -Additionally, PrusaSlicer can be built in a static manner mostly independent of the system libraries with a dependencies bundle -created using CMake script in the `deps` directory (these are not interconnected with the rest of the CMake scripts). + mkdir build + cd build + cmake .. -DSLIC3R_STATIC=1 -DSLIC3R_PCH=OFF -DCMAKE_PREFIX_PATH=$(pwd)/../deps/build/destdir/usr/local + make -j4 -Note: We say _mostly independent_ because it's still expected the system will provide some transitive dependencies, such as GTK for wxWidgets. +And that's it. You can now run the freshly built PrusaSlicer binary: -To do this, go to the `deps` directory, create a `build` subdirectory (or the like) and use: + cd src + ./prusa-slicer - cmake .. -DDESTDIR= -DDEP_DOWNLOAD_DIR= -where the target destdir is a directory of your choosing where the dependencies will be installed. -You can also omit the `DESTDIR` option to use the default, in that case the `destdir` will be created inside the `build` directory where `cmake` is run. The optional `DEP_DOWNLOAD_DIR` argument specifies a directory to cache the downloaded -source packages for each dependent library. Can be useful for repeated builds, to avoid unnecessary network traffic. -Once the dependencies have been built, in order to pass the destdir path to the **top-level** PrusaSlicer `CMakeLists.txt` script, use the `CMAKE_PREFIX_PATH` option along with turning on `SLIC3R_STATIC`: +#### Troubleshooting - cmake .. -DSLIC3R_STATIC=1 -DCMAKE_PREFIX_PATH=/usr/local +Although most of the dependencies are handled by the build script, we still rely on some system libraries (such as GTK, GL, etc). It is quite likely that you have them already installed, but in case that CMake reports any library missing, install the respective package from your distribution and run CMake again. -Note that `/usr/local` needs to be appended to the destdir path and also the prefix path should be absolute. -**Warning**: Once the dependency bundle is installed in a destdir, the destdir cannot be moved elsewhere. -This is because wxWidgets hardcode the installation path. +## Useful CMake flags when building dependencies -### wxWidgets version +- `-DDESTDIR=` allows to specify a directory where the dependencies will be installed. When not provided, the script creates and uses `destdir` directory where cmake is run. -By default, PrusaSlicer looks for wxWidgets 3.1, this is because the 3.1 version has -a number of bugfixes and improvements not found in 3.0. However, it can also be built with wxWidgets 3.0. -This is done by passing this option to CMake: +- `-DDEP_DOWNLOAD_DIR=` specifies a directory to cache the downloaded source packages for each library. Can be useful for repeated builds, to avoid unnecessary network traffic. - -DSLIC3R_WX_STABLE=1 -Note that PrusaSlicer is tested with wxWidgets 3.0 somewhat sporadically and so there may be bugs in bleeding edge releases. +## Useful CMake flags when building PrusaSlicer +- `-DSLIC3R_ASAN=ON` enables gcc/clang address sanitizer (defaults to `OFF`, requires gcc>4.8 or clang>3.1) +- `-DSLIC3R_GTK=3` to use GTK3 (defaults to `2`) +- `-DSLIC3R_STATIC=ON` for static build (defaults to `OFF`) +- `-DSLIC3R_WX_STABLE=ON` to look for wxWidgets 3.0 (defaults to `OFF`) +- `-DCMAKE_BUILD_TYPE=Debug` to build in debug mode (defaults to `Release`) + +See the CMake files to get the complete list. + + + +## Building dynamically + +As already mentioned above, dynamic linking of dependencies is possible, but PrusaSlicer team is unable to troubleshoot (Linux world is way too complex). Feel free to do so, but you are on your own. Several remarks though: + +The list of dependencies can be easily obtained by inspecting the CMake scripts in the `deps/` directory. Many don't necessarily need to be as recent +as the versions listed - generally versions available on conservative Linux distros such as Debian stable, Ubuntu LTS releases or Fedora are likely sufficient. If you decide to build this way, it is your responsibility to make sure that CMake finds all required dependencies. It is possible to look at your distribution PrusaSlicer package to see how the package maintainers solved the dependency issues. + +#### wxWidgets +By default, PrusaSlicer looks for wxWidgets 3.1. Our build script in fact downloads specific patched version of wxWidgets. If you want to link against wxWidgets 3.0 (which are still provided by most distributions because wxWidgets 3.1 have not yet been declared stable), you must set `-DSLIC3R_WX_STABLE=ON` when running CMake. Note that while PrusaSlicer can be linked against wWidgets 3.0, the combination is not well tested and there might be bugs in the resulting application. When building on ubuntu 20.04 focal fossa, the package libwxgtk3.0-gtk3-dev needs to be installed instead of libwxgtk3.0-dev and you should use: +``` +-DSLIC3R_WX_STABLE=1 -DSLIC3R_GTK=3 +``` - -DSLIC3R_WX_STABLE=1 -DSLIC3R_GTK=3 - -### Build variant - -By default PrusaSlicer builds the release variant. -To create a debug build, use the following CMake flag: - - -DCMAKE_BUILD_TYPE=Debug - -### Enabling address sanitizer - -If you're using GCC/Clang compiler, it is possible to build PrusaSlicer with the built-in address sanitizer enabled to help detect memory-corruption issues. -To enable it, simply use the following CMake flag: - - -DSLIC3R_ASAN=1 - -This requires GCC>4.8 or Clang>3.1. +## Miscellaneous ### Installation From b69d03ad4abbe324683f1e751a3caaef6a8d61f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Mat=C4=9Bna?= <33929324+lukasmatena@users.noreply.github.com> Date: Thu, 2 Sep 2021 12:56:24 +0200 Subject: [PATCH 081/151] Update How to build - Linux et al.md --- doc/How to build - Linux et al.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/How to build - Linux et al.md b/doc/How to build - Linux et al.md index 704ceb46aa..090a97e6c3 100644 --- a/doc/How to build - Linux et al.md +++ b/doc/How to build - Linux et al.md @@ -68,10 +68,12 @@ Although most of the dependencies are handled by the build script, we still rely - `-DDEP_DOWNLOAD_DIR=` specifies a directory to cache the downloaded source packages for each library. Can be useful for repeated builds, to avoid unnecessary network traffic. +- `-DDEP_WX_GTK3=ON` builds wxWidgets (one of the dependencies) against GTK3 (defaults to OFF) + ## Useful CMake flags when building PrusaSlicer - `-DSLIC3R_ASAN=ON` enables gcc/clang address sanitizer (defaults to `OFF`, requires gcc>4.8 or clang>3.1) -- `-DSLIC3R_GTK=3` to use GTK3 (defaults to `2`) +- `-DSLIC3R_GTK=3` to use GTK3 (defaults to `2`). Note that wxWidgets must be built against the same GTK version. - `-DSLIC3R_STATIC=ON` for static build (defaults to `OFF`) - `-DSLIC3R_WX_STABLE=ON` to look for wxWidgets 3.0 (defaults to `OFF`) - `-DCMAKE_BUILD_TYPE=Debug` to build in debug mode (defaults to `Release`) From cc480afe9b54b3126bafb37f2afa785a9cda0303 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 2 Sep 2021 13:42:37 +0200 Subject: [PATCH 082/151] Fixed build when tech ENABLE_GCODE_VIEWER_STATISTICS is enabled --- src/slic3r/GUI/GCodeViewer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index e6e391d370..3fc6242648 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2665,7 +2665,11 @@ void GCodeViewer::render_toolpaths() }; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_GCODE_VIEWER_STATISTICS + auto render_as_instanced_model = [this] +#else auto render_as_instanced_model = [] +#endif // ENABLE_GCODE_VIEWER_STATISTICS (TBuffer& buffer, GLShaderProgram & shader) { for (auto& range : buffer.model.instances.render_ranges.ranges) { if (range.vbo == 0 && range.count > 0) { From 0efa0fc512094d134fca71b1961ef2d6d9f638aa Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Thu, 2 Sep 2021 14:20:04 +0200 Subject: [PATCH 083/151] Fixed a crash when 'Repair by Netfabb' was called with multiple object selection, the bug was introduced with 1eebaa4 and possibly manifested in other scenarios as well. --- src/slic3r/GUI/GUI_ObjectList.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 737c840834..096836f23e 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2561,6 +2561,9 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selections/* = nullptr*/, bool added_object/* = false*/) { + if (obj_idx >= m_objects->size()) + return; + const ModelObject* model_object = (*m_objects)[obj_idx]; wxDataViewItem item_obj = m_objects_model->GetItemById(obj_idx); assert(item_obj.IsOk()); From 18c8204837502d3103f793e863c44c6bdc7eca93 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Thu, 2 Sep 2021 14:37:29 +0200 Subject: [PATCH 084/151] Fix of #6873: Bed texture located in a directory that user has no permission to (crashed) --- src/slic3r/GUI/3DBed.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 7b08b5f793..eaf75ba5b6 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -141,11 +141,13 @@ void Bed3D::Axes::render() const bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) { auto check_texture = [](const std::string& texture) { - return !texture.empty() && (boost::algorithm::iends_with(texture, ".png") || boost::algorithm::iends_with(texture, ".svg")) && boost::filesystem::exists(texture); + boost::system::error_code ec; // so the exists call does not throw (e.g. after a permission problem) + return !texture.empty() && (boost::algorithm::iends_with(texture, ".png") || boost::algorithm::iends_with(texture, ".svg")) && boost::filesystem::exists(texture, ec); }; auto check_model = [](const std::string& model) { - return !model.empty() && boost::algorithm::iends_with(model, ".stl") && boost::filesystem::exists(model); + boost::system::error_code ec; + return !model.empty() && boost::algorithm::iends_with(model, ".stl") && boost::filesystem::exists(model, ec); }; EType type; From 298825672bf985b7467fe4cb033db1835d9abd88 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Thu, 2 Sep 2021 15:18:05 +0200 Subject: [PATCH 085/151] Fix painted seam on vertical surfaces (broken since 8dfc042) --- src/libslic3r/PrintObject.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f0eaa982ab..29b2ece41e 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2294,13 +2294,12 @@ void PrintObject::project_and_append_custom_facets( ? mv->seam_facets.get_facets_strict(*mv, type) : mv->supported_facets.get_facets_strict(*mv, type); if (! custom_facets.indices.empty()) -#if 0 - project_triangles_to_slabs(this->layers(), custom_facets, - (this->trafo_centered() * mv->get_matrix()).cast(), - seam, out); -#else - slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &out, [](){}); -#endif + if (seam) + project_triangles_to_slabs(this->layers(), custom_facets, + (this->trafo_centered() * mv->get_matrix()).cast(), + seam, out); + else + slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &out, [](){}); } } From d89f01c71795e324f510939e2cece05b9586291c Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 2 Sep 2021 16:29:21 +0200 Subject: [PATCH 086/151] Fix of "Support on build plate only" no longer overridden by support painting (#6863) This is a regression to a late PrusaSlicer 2.4.0-alpha0 change 8dfc0422a878c5e44d4233c6ce522c77a0c3280f Faster and hopefully more reliable projection of paint-on support blockers and enforcers on a sliced mesh. --- src/libslic3r/PrintObject.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 29b2ece41e..526b7b874a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2298,8 +2298,11 @@ void PrintObject::project_and_append_custom_facets( project_triangles_to_slabs(this->layers(), custom_facets, (this->trafo_centered() * mv->get_matrix()).cast(), seam, out); - else - slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &out, [](){}); + else { + std::vector projected; + slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, [](){}); + append(out, std::move(projected)); + } } } From 7c074d5a99c762064634088bf945637eef441bda Mon Sep 17 00:00:00 2001 From: Jason Scurtu Date: Thu, 2 Sep 2021 16:40:17 +0200 Subject: [PATCH 087/151] update PrusaSlicer icon for macOS (#6874) --- resources/icons/PrusaSlicer.icns | Bin 225060 -> 299502 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/icons/PrusaSlicer.icns b/resources/icons/PrusaSlicer.icns index 051477dc37bf1556d700d89c58939b905ced40a0..5f6a7feb273e962489ed01e55d1be856c1c76a8c 100644 GIT binary patch literal 299502 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Ð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
qAKHr@(6HPqhhg=* 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 AmjD0& literal 225060 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 z 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 CVr Date: Thu, 2 Sep 2021 16:41:50 +0200 Subject: [PATCH 088/151] Flatpak changes for 2.4 (#6875) * add gcodeviewer desktop file * update metainfo * add actions for GCodeViewer + DE translation --- ...om.prusa3d.PrusaSlicer.GCodeViewer.desktop | 10 +++++ .../flatpak/com.prusa3d.PrusaSlicer.desktop | 10 ++++- .../com.prusa3d.PrusaSlicer.metainfo.xml | 44 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop diff --git a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop new file mode 100644 index 0000000000..3177854734 --- /dev/null +++ b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Prusa GCode viewer +Name[de]=Prusa GCode Vorschau +Exec=prusa-slicer --gcodeviewer %F +Icon=com.prusa3d.PrusaSlicer.GCodeViewer +Terminal=false +Type=Application +MimeType=text/x.gcode; +Categories=Graphics;3DGraphics; +Keywords=3D;Printing;Slicer; diff --git a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop index 8987222984..1ea3e157e7 100755 --- a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop +++ b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop @@ -1,5 +1,6 @@ [Desktop Entry] Name=PrusaSlicer +Name[de]=PrusaSlicer GenericName=3D Printing Software Icon=com.prusa3d.PrusaSlicer Exec=prusa-slicer %F @@ -9,4 +10,11 @@ MimeType=model/stl;model/x-wavefront-obj;model/3mf;model/x-geomview-off;applicat Categories=Graphics;3DGraphics;Engineering; Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA StartupNotify=false -StartupWMClass=prusa-slicer \ No newline at end of file +StartupWMClass=prusa-slicer +Actions=GCodeViewer; + +[Desktop Action GCodeViewer] +Exec=prusa-slicer --gcodeviewer %F +Name=G-Code Viewer +Name[de]=G-Code Vorschau +Icon=com.prusa3d.PrusaSlicer.GCodeViewer diff --git a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml index b62a57e480..c37f43d69f 100755 --- a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml +++ b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml @@ -50,6 +50,50 @@ + + +

This is the first alpha release of PrusaSlicer 2.4.0, introducing Multi-material painting tool, improved FDM supports and raft, + Windows dark mode, Fuzzy Skin, per object Brim configuration, Negative volumes, automatic Color Print for signs, Shape Gallery, + "Tip of the day" notifications, Model simplification, support for Marlin 2 acceleration control, support of 3DLabPrint airplane models, + new 3rd party printer profiles and many more new features, improvements and bugfixes.

+
+
+ + +

PrusaSlicer 2.3.3 is a patch release following PrusaSlicer 2.3.2 release, fixing an unfortunate bug in handling FDM multi-material + project and configuration files #6711.

+
+
+ + +

This is a final release of PrusaSlicer 2.3.2, following 2.3.2-beta and 2.3.2-rc. + For the new features in the 2.3.2 series, please read the change logs of the beta and release candidate. + The final release is functionally equal to the release candidate, with a single additional improvement: + Before installing profile updates, a configuration snapshot is taken. In rare circumstances when the current configuration is not consistent, + taking a configuration snapshot fails. PrusaSlicer 2.3.2 newly informs about the issue and offers either to install the configuration updates + even if taking the configuration snapshot failed or to abort update of the profiles.

+
+
+ + +

This a final release of PrusaSlicer 2.3.1, introducing native builds for the new Apple Silicon MacBooks, Chrome OS support, performance improvements + in G-code rendering, security fixes and new 3rd party printer profiles. For full changelog on PrusaSlicer 2.3.1 series, please check the PrusaSlicer + 2.3.1-rc change log.

+
+
+ + +

This a final release of PrusaSlicer 2.3.0, introducing paint-on supports, ironing, monotonic infill, seam painting, adaptive and support cubic infill, + print time per feature analysis, standalone G-code viewer application, better auto-arrange with a customizable gap and rotation, + search function for the settings, reworked "Avoid crossing perimeters" function, physical printers (network settings), + many new 3rd party printer profiles and much, much more.

+
+
+ + +

Development/Unstable Snapshot

+
+

This is final release of PrusaSlicer 2.2.0 introducing SLA hollowing and hole drilling, support for 3rd party printer vendors, 3Dconnexion support, From e630332e245b4773beea7c77b54c1062c7a3b3b9 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 3 Sep 2021 09:28:02 +0200 Subject: [PATCH 089/151] Added SS2OSD.dll to blacklist. Raises warning dialog for issue #6864 --- src/libslic3r/BlacklistedLibraryCheck.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/BlacklistedLibraryCheck.cpp b/src/libslic3r/BlacklistedLibraryCheck.cpp index 3d2ee34827..d9dea188f5 100644 --- a/src/libslic3r/BlacklistedLibraryCheck.cpp +++ b/src/libslic3r/BlacklistedLibraryCheck.cpp @@ -12,7 +12,7 @@ namespace Slic3r { #ifdef WIN32 //only dll name with .dll suffix - currently case sensitive -const std::vector BlacklistedLibraryCheck::blacklist({ L"NahimicOSD.dll" }); +const std::vector BlacklistedLibraryCheck::blacklist({ L"NahimicOSD.dll", L"SS2OSD.dll" }); bool BlacklistedLibraryCheck::get_blacklisted(std::vector& names) { From cd4155a890d6a69e749e0079fb0d86c4d5a7a397 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 3 Sep 2021 10:19:06 +0200 Subject: [PATCH 090/151] Hints.cereal file existence check --- src/slic3r/GUI/HintNotification.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index 33ccdc4ae3..fe5eeb67c2 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -74,7 +74,12 @@ void write_used_binary(const std::vector& ids) } void read_used_binary(std::vector& ids) { - boost::filesystem::ifstream file((boost::filesystem::path(data_dir()) / "cache" / "hints.cereal")); + boost::filesystem::path path(boost::filesystem::path(data_dir()) / "cache" / "hints.cereal"); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "Failed to load to hints.cereal. File does not exists. " << path.string(); + return; + } + boost::filesystem::ifstream file(path); cereal::BinaryInputArchive archive(file); HintsCerealData cd; try From 79784d1a2e3772b51c661a2dc144f7ea142acbb5 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Sep 2021 11:45:01 +0200 Subject: [PATCH 091/151] Fix of "Support on build plate only" no longer overridden by support painting (#6863) This is a regression to a late PrusaSlicer 2.4.0-alpha0 change 8dfc0422a878c5e44d4233c6ce522c77a0c3280f Faster and hopefully more reliable projection of paint-on support blockers and enforcers on a sliced mesh. Previous d89f01c71795e324f510939e2cece05b9586291c did not fix it. --- src/libslic3r/PrintObject.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 526b7b874a..808bdd8a1b 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2301,7 +2301,14 @@ void PrintObject::project_and_append_custom_facets( else { std::vector projected; slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, [](){}); - append(out, std::move(projected)); + // Merge these projections with the output, layer by layer. + assert(! projected.empty()); + assert(out.empty() || out.size() == projected.size()); + if (out.empty()) + out = std::move(projected); + else + for (size_t i = 0; i < out.size(); ++ i) + append(out[i], std::move(projected[i])); } } } From ed7ebf15d97970597d871ea31b464e6538354529 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 3 Sep 2021 12:13:01 +0200 Subject: [PATCH 092/151] Added logging when bed texture/model is not found --- src/libslic3r/PrintObject.cpp | 3 ++- src/slic3r/GUI/3DBed.cpp | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 808bdd8a1b..d9a4f2670a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2293,7 +2293,7 @@ void PrintObject::project_and_append_custom_facets( const indexed_triangle_set custom_facets = seam ? mv->seam_facets.get_facets_strict(*mv, type) : mv->supported_facets.get_facets_strict(*mv, type); - if (! custom_facets.indices.empty()) + if (! custom_facets.indices.empty()) { if (seam) project_triangles_to_slabs(this->layers(), custom_facets, (this->trafo_centered() * mv->get_matrix()).cast(), @@ -2310,6 +2310,7 @@ void PrintObject::project_and_append_custom_facets( for (size_t i = 0; i < out.size(); ++ i) append(out[i], std::move(projected[i])); } + } } } diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index eaf75ba5b6..5b7218c87e 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -163,12 +163,16 @@ bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, c } std::string texture_filename = custom_texture.empty() ? texture : custom_texture; - if (!check_texture(texture_filename)) + if (! texture_filename.empty() && ! check_texture(texture_filename)) { + BOOST_LOG_TRIVIAL(error) << "Unable to load bed texture: " << texture_filename; texture_filename.clear(); + } std::string model_filename = custom_model.empty() ? model : custom_model; - if (!check_model(model_filename)) + if (! model_filename.empty() && ! check_model(model_filename)) { + BOOST_LOG_TRIVIAL(error) << "Unable to load bed model: " << model_filename; model_filename.clear(); + } if (m_shape == shape && m_type == type && m_texture_filename == texture_filename && m_model_filename == model_filename) // No change, no need to update the UI. From 4c80d9ed01bc0fdcda9235224f50caeb77d86d77 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 27 Aug 2021 09:04:15 +0200 Subject: [PATCH 093/151] Fix taking entering/leaving snapshots in SLA gizmo: This was broken between 2.2.0 and 2.3.0. The 'entering' snapshot should be taken before the gizmo opens, not after. Otherwise it is in fact the same as the next snapshot. --- src/slic3r/GUI/Gizmos/GLGizmoBase.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 11 ------- src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp | 2 ++ src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 34 +++++++++++--------- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index 8b033ce732..1c6e00c070 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -135,6 +135,7 @@ public: bool is_activable() const { return on_is_activable(); } bool is_selectable() const { return on_is_selectable(); } CommonGizmosDataID get_requirements() const { return on_get_requirements(); } + virtual bool wants_enter_leave_snapshots() const { return false; } void set_common_data_pool(CommonGizmosDataPool* ptr) { m_c = ptr; } unsigned int get_sprite_id() const { return m_sprite_id; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 8c90d20d31..c7faab3bf5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -901,15 +901,6 @@ void GLGizmoSlaSupports::on_set_state() return; if (m_state == On && m_old_state != On) { // the gizmo was just turned on - if (! m_parent.get_gizmos_manager().is_serializing()) { - // Only take the snapshot when the USER opens the gizmo. Common gizmos - // data are not yet available, the CallAfter will postpone taking the - // snapshot until they are. No, it does not feel right. - wxGetApp().CallAfter([]() { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Entering SLA gizmo")); - }); - } - // Set default head diameter from config. const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; m_new_point_head_diameter = static_cast(cfg.option("support_head_front_diameter"))->value; @@ -925,8 +916,6 @@ void GLGizmoSlaSupports::on_set_state() else { // we are actually shutting down disable_editing_mode(); // so it is not active next time the gizmo opens - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Leaving SLA gizmo")); - m_normal_cache.clear(); m_old_mo_id = -1; } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index cb60c0e258..8e30aa6b85 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -67,6 +67,8 @@ public: bool has_backend_supports() const; void reslice_SLA_supports(bool postpone_error_messages = false) const; + bool wants_enter_leave_snapshots() const override { return true; } + private: bool on_init() override; void on_update(const UpdateData& data) override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 6d9a039772..080991859b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -22,6 +22,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" +#include "libslic3r/format.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -1198,26 +1199,29 @@ void GLGizmosManager::activate_gizmo(EType type) if (m_gizmos.empty() || m_current == type) return; - if (m_current != Undefined) { - m_gizmos[m_current]->set_state(GLGizmoBase::Off); - if (m_gizmos[m_current]->get_state() != GLGizmoBase::Off) + GLGizmoBase* old_gizmo = m_current == Undefined ? nullptr : m_gizmos[m_current].get(); + GLGizmoBase* new_gizmo = type == Undefined ? nullptr : m_gizmos[type].get(); + + if (old_gizmo) { + old_gizmo->set_state(GLGizmoBase::Off); + if (old_gizmo->get_state() != GLGizmoBase::Off) return; // gizmo refused to be turned off, do nothing. + + if (! m_parent.get_gizmos_manager().is_serializing() + && old_gizmo->wants_enter_leave_snapshots()) + Plater::TakeSnapshot snapshot(wxGetApp().plater(), + Slic3r::format(_utf8("Leaving %1%"), old_gizmo->get_name())); } + if (new_gizmo && ! m_parent.get_gizmos_manager().is_serializing() + && new_gizmo->wants_enter_leave_snapshots()) + Plater::TakeSnapshot snapshot(wxGetApp().plater(), + Slic3r::format(_utf8("Entering %1%"), new_gizmo->get_name())); + m_current = type; - // Updating common data should be left to the update_data function, which - // is always called after this one. activate_gizmo can be called by undo/redo, - // when selection is not yet deserialized, so the common data would update - // incorrectly (or crash if relying on unempty selection). Undo/redo stack - // will also call update_data, after selection is restored. - - //m_common_gizmos_data->update(get_current() - // ? get_current()->get_requirements() - // : CommonGizmosDataID(0)); - - if (type != Undefined) - m_gizmos[type]->set_state(GLGizmoBase::On); + if (new_gizmo) + new_gizmo->set_state(GLGizmoBase::On); } From a4300b8e6411d42ce73f3ff2cd23785a3f172d8f Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 27 Aug 2021 09:04:33 +0200 Subject: [PATCH 094/151] Naming of the entering/leaving snapshots is now more generic, it uses the actual name of the gizmo. Also, the keyboard shortcut is now appended to the name, instead of being duplicated in it. --- src/slic3r/GUI/Gizmos/GLGizmoBase.cpp | 14 ++++++++++++++ src/slic3r/GUI/Gizmos/GLGizmoBase.hpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoMove.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoScale.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 4 ++-- 14 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index cc089e26ef..64479a39e2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -232,6 +232,20 @@ void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) } } + + +std::string GLGizmoBase::get_name(bool include_shortcut) const +{ + int key = get_shortcut_key(); + assert( key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z); + std::string out = on_get_name(); + if (include_shortcut) + out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; + return out; +} + + + // Produce an alpha channel checksum for the red green blue components. The alpha channel may then be used to verify, whether the rgb components // were not interpolated by alpha blending or multi sampling. unsigned char picking_checksum_alpha_channel(unsigned char red, unsigned char green, unsigned char blue) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index 1c6e00c070..05f6adb5e8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -120,7 +120,7 @@ public: void load(cereal::BinaryInputArchive& ar) { m_state = On; on_load(ar); } void save(cereal::BinaryOutputArchive& ar) const { on_save(ar); } - std::string get_name() const { return on_get_name(); } + std::string get_name(bool include_shortcut = true) const; int get_group_id() const { return m_group_id; } void set_group_id(int id) { m_group_id = id; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 641258ca44..3dcb9e2b10 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -49,7 +49,7 @@ bool GLGizmoCut::on_init() std::string GLGizmoCut::on_get_name() const { - return (_L("Cut") + " [C]").ToUTF8().data(); + return _u8L("Cut"); } void GLGizmoCut::on_set_state() diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 99c010bf1d..1ebba4d11f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -28,7 +28,7 @@ void GLGizmoFdmSupports::on_shutdown() std::string GLGizmoFdmSupports::on_get_name() const { - return (_L("Paint-on supports") + " [L]").ToUTF8().data(); + return _u8L("Paint-on supports"); } @@ -85,7 +85,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp index 3e5c12eec4..b8c7d9f92e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -37,7 +37,7 @@ CommonGizmosDataID GLGizmoFlatten::on_get_requirements() const std::string GLGizmoFlatten::on_get_name() const { - return (_L("Place on face") + " [F]").ToUTF8().data(); + return _u8L("Place on face"); } bool GLGizmoFlatten::on_is_activable() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 05e0be1410..f405f457d1 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -505,7 +505,7 @@ RENDER_AGAIN: y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float settings_sliders_left = @@ -773,7 +773,7 @@ bool GLGizmoHollow::on_is_selectable() const std::string GLGizmoHollow::on_get_name() const { - return (_(L("Hollow and drill")) + " [H]").ToUTF8().data(); + return _u8L("Hollow and drill"); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index a30ac377e8..608e7bd1fa 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -42,7 +42,7 @@ void GLGizmoMmuSegmentation::on_shutdown() std::string GLGizmoMmuSegmentation::on_get_name() const { // FIXME Lukas H.: Discuss and change shortcut - return (_L("Multimaterial painting") + " [N]").ToUTF8().data(); + return _u8L("Multimaterial painting"); } bool GLGizmoMmuSegmentation::on_is_selectable() const @@ -239,7 +239,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 9a056adcb3..e7b6a0ad6e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -52,7 +52,7 @@ bool GLGizmoMove3D::on_init() std::string GLGizmoMove3D::on_get_name() const { - return (_L("Move") + " [M]").ToUTF8().data(); + return _u8L("Move"); } bool GLGizmoMove3D::on_is_activable() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 36759d2eca..54fcfc2c7c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -463,7 +463,7 @@ bool GLGizmoRotate3D::on_init() std::string GLGizmoRotate3D::on_get_name() const { - return (_L("Rotate") + " [R]").ToUTF8().data(); + return _u8L("Rotate"); } bool GLGizmoRotate3D::on_is_activable() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 894d844d83..b97507166b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -76,7 +76,7 @@ bool GLGizmoScale3D::on_init() std::string GLGizmoScale3D::on_get_name() const { - return (_L("Scale") + " [S]").ToUTF8().data(); + return _u8L("Scale"); } bool GLGizmoScale3D::on_is_activable() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index 5b84bbaba4..a2ee56d9ae 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -48,7 +48,7 @@ bool GLGizmoSeam::on_init() std::string GLGizmoSeam::on_get_name() const { - return (_L("Seam painting") + " [P]").ToUTF8().data(); + return _u8L("Seam painting"); } @@ -79,7 +79,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) const float approx_height = m_imgui->scaled(14.0f); y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index f70e3f93e2..9f49f9734a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -44,7 +44,7 @@ bool GLGizmoSimplify::on_init() std::string GLGizmoSimplify::on_get_name() const { - return (_L("Simplify")).ToUTF8().data(); + return _u8L("Simplify"); } void GLGizmoSimplify::on_render() {} @@ -97,7 +97,7 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi int flag = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; - m_imgui->begin(on_get_name(), flag); + m_imgui->begin(get_name(), flag); size_t triangle_count = m_volume->mesh().its.indices.size(); // already reduced mesh diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index c7faab3bf5..11446d6d78 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -625,7 +625,7 @@ RENDER_AGAIN: //ImGui::SetNextWindowPos(ImVec2(x, y - std::max(0.f, y+window_size.y-bottom_limit) )); //ImGui::SetNextWindowSize(ImVec2(window_size)); - m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // adjust window position to avoid overlap the view toolbar float win_h = ImGui::GetWindowHeight(); @@ -863,7 +863,7 @@ bool GLGizmoSlaSupports::on_is_selectable() const std::string GLGizmoSlaSupports::on_get_name() const { - return (_L("SLA Support Points") + " [L]").ToUTF8().data(); + return _u8L("SLA Support Points"); } CommonGizmosDataID GLGizmoSlaSupports::on_get_requirements() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 080991859b..dfe6de746c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -1210,13 +1210,13 @@ void GLGizmosManager::activate_gizmo(EType type) if (! m_parent.get_gizmos_manager().is_serializing() && old_gizmo->wants_enter_leave_snapshots()) Plater::TakeSnapshot snapshot(wxGetApp().plater(), - Slic3r::format(_utf8("Leaving %1%"), old_gizmo->get_name())); + Slic3r::format(_utf8("Leaving %1%"), old_gizmo->get_name(false))); } if (new_gizmo && ! m_parent.get_gizmos_manager().is_serializing() && new_gizmo->wants_enter_leave_snapshots()) Plater::TakeSnapshot snapshot(wxGetApp().plater(), - Slic3r::format(_utf8("Entering %1%"), new_gizmo->get_name())); + Slic3r::format(_utf8("Entering %1%"), new_gizmo->get_name(false))); m_current = type; From 315663980bf1f567e110cdcb72a2f2b694f4c5e5 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Tue, 31 Aug 2021 13:48:22 +0200 Subject: [PATCH 095/151] Do not allow 'replace by stl' when a gizmo is active, some of the gizmos might not cope well. ALso avoided code duplication in all such cases (currently fix by Netfabb, simplify, replace by stl). --- src/slic3r/GUI/GUI_ObjectList.cpp | 13 ++++------ src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 29 ++++++++++++++++------- src/slic3r/GUI/Gizmos/GLGizmosManager.hpp | 3 ++- src/slic3r/GUI/Plater.cpp | 11 ++++----- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 096836f23e..c99d9090cb 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -4030,17 +4030,12 @@ void ObjectList::simplify() // Do not simplify when a gizmo is open. There might be issues with updates // and what is worse, the snapshot time would refer to the internal stack. - auto current_type = gizmos_mgr.get_current_type(); - if (current_type == GLGizmosManager::Simplify) { + if (! gizmos_mgr.check_gizmos_closed_except(GLGizmosManager::EType::Simplify)) + return; + + if (gizmos_mgr.get_current_type() == GLGizmosManager::Simplify) { // close first gizmos_mgr.open_gizmo(GLGizmosManager::EType::Simplify); - }else if (current_type != GLGizmosManager::Undefined) { - plater->get_notification_manager()->push_notification( - NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::RegularNotification, - _u8L("ERROR: Please close all manipulators available from " - "the left toolbar before start simplify the mesh.")); - return; } gizmos_mgr.open_gizmo(GLGizmosManager::EType::Simplify); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index dfe6de746c..93eba3042c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -164,10 +164,8 @@ void GLGizmosManager::refresh_on_off_state() return; if (m_current != Undefined - && ! m_gizmos[m_current]->is_activable()) { - activate_gizmo(Undefined); + && ! m_gizmos[m_current]->is_activable() && activate_gizmo(Undefined)) update_data(); - } } void GLGizmosManager::reset_all_states() @@ -182,14 +180,28 @@ void GLGizmosManager::reset_all_states() bool GLGizmosManager::open_gizmo(EType type) { int idx = int(type); - if (m_gizmos[idx]->is_activable()) { - activate_gizmo(m_current == idx ? Undefined : (EType)idx); + if (m_gizmos[idx]->is_activable() + && activate_gizmo(m_current == idx ? Undefined : (EType)idx)) { update_data(); return true; } return false; } + +bool GLGizmosManager::check_gizmos_closed_except(EType type) const +{ + if (get_current_type() != type && get_current_type() != Undefined) { + wxGetApp().plater()->get_notification_manager()->push_notification( + NotificationType::CustomSupportsAndSeamRemovedAfterRepair, + NotificationManager::NotificationLevel::RegularNotification, + _u8L("ERROR: Please close all manipulators available from " + "the left toolbar first")); + return false; + } + return true; +} + void GLGizmosManager::set_hover_id(int id) { if (!m_enabled || m_current == Undefined) @@ -1194,10 +1206,10 @@ std::string GLGizmosManager::update_hover_state(const Vec2d& mouse_pos) return name; } -void GLGizmosManager::activate_gizmo(EType type) +bool GLGizmosManager::activate_gizmo(EType type) { if (m_gizmos.empty() || m_current == type) - return; + return true; GLGizmoBase* old_gizmo = m_current == Undefined ? nullptr : m_gizmos[m_current].get(); GLGizmoBase* new_gizmo = type == Undefined ? nullptr : m_gizmos[type].get(); @@ -1205,7 +1217,7 @@ void GLGizmosManager::activate_gizmo(EType type) if (old_gizmo) { old_gizmo->set_state(GLGizmoBase::Off); if (old_gizmo->get_state() != GLGizmoBase::Off) - return; // gizmo refused to be turned off, do nothing. + return false; // gizmo refused to be turned off, do nothing. if (! m_parent.get_gizmos_manager().is_serializing() && old_gizmo->wants_enter_leave_snapshots()) @@ -1222,6 +1234,7 @@ void GLGizmosManager::activate_gizmo(EType type) if (new_gizmo) new_gizmo->set_state(GLGizmoBase::On); + return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 188c9e9141..ed02574dfa 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -104,7 +104,7 @@ private: std::vector get_selectable_idxs() const; size_t get_gizmo_idx_from_mouse(const Vec2d& mouse_pos) const; - void activate_gizmo(EType type); + bool activate_gizmo(EType type); struct MouseCapture { @@ -176,6 +176,7 @@ public: void reset_all_states(); bool is_serializing() const { return m_serializing; } bool open_gizmo(EType type); + bool check_gizmos_closed_except(EType) const; void set_hover_id(int id); void enable_grabber(EType type, unsigned int id, bool enable); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4236ee4501..af8734184f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3186,6 +3186,9 @@ void Plater::priv::update_sla_scene() void Plater::priv::replace_with_stl() { + if (! q->canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + return; + const Selection& selection = get_selection(); if (selection.is_wipe_tower() || get_selection().get_volume_idxs().size() != 1) @@ -3530,14 +3533,8 @@ void Plater::priv::fix_through_netfabb(const int obj_idx, const int vol_idx/* = // Do not fix anything when a gizmo is open. There might be issues with updates // and what is worse, the snapshot time would refer to the internal stack. - if (q->canvas3D()->get_gizmos_manager().get_current_type() != GLGizmosManager::Undefined) { - notification_manager->push_notification( - NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::RegularNotification, - _u8L("ERROR: Please close all manipulators available from " - "the left toolbar before fixing the mesh.")); + if (! q->canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined)) return; - } // size_t snapshot_time = undo_redo_stack().active_snapshot_time(); Plater::TakeSnapshot snapshot(q, _L("Fix through NetFabb")); From 7f2d1522ce31f2467c238ca330d342d5eb37d872 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 3 Sep 2021 15:35:44 +0200 Subject: [PATCH 096/151] Rendering seams (and other options) in preview using batched models on systems not supporting OpenGL 3.3 --- src/libslic3r/Technologies.hpp | 27 ++-- src/slic3r/GUI/GCodeViewer.cpp | 243 +++++++++++++++++++++++++++- src/slic3r/GUI/GCodeViewer.hpp | 56 +++++++ src/slic3r/GUI/GLModel.cpp | 20 +++ src/slic3r/GUI/GLModel.hpp | 9 ++ src/slic3r/GUI/GLShadersManager.cpp | 9 ++ 6 files changed, 351 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 6132430f1a..d031b9527d 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -37,24 +37,33 @@ //==================== -// 2.4.0.alpha0 techs +// 2.4.0.alpha1 techs //==================== -#define ENABLE_2_4_0_ALPHA0 1 +#define ENABLE_2_4_0_ALPHA1 1 // Enable delayed rendering of transparent volumes -#define ENABLE_DELAYED_TRANSPARENT_VOLUMES_RENDERING (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_DELAYED_TRANSPARENT_VOLUMES_RENDERING (1 && ENABLE_2_4_0_ALPHA1) // Enable the fix of importing color print view from gcode files into GCodeViewer -#define ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER (1 && ENABLE_2_4_0_ALPHA1) // Enable drawing contours, at cut level, for sinking volumes -#define ENABLE_SINKING_CONTOURS (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_SINKING_CONTOURS (1 && ENABLE_2_4_0_ALPHA1) // Enable implementation of retract acceleration in gcode processor -#define ENABLE_RETRACT_ACCELERATION (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_RETRACT_ACCELERATION (1 && ENABLE_2_4_0_ALPHA1) // Enable the fix for exporting and importing to/from 3mf file of mirrored volumes -#define ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_FIX_MIRRORED_VOLUMES_3MF_IMPORT_EXPORT (1 && ENABLE_2_4_0_ALPHA1) // Enable rendering seams (and other options) in preview using models -#define ENABLE_SEAMS_USING_MODELS (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_SEAMS_USING_MODELS (1 && ENABLE_2_4_0_ALPHA1) // Enable save and save as commands to be enabled also when the plater is empty and allow to load empty projects -#define ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED (1 && ENABLE_2_4_0_ALPHA0) +#define ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED (1 && ENABLE_2_4_0_ALPHA1) + + +//==================== +// 2.4.0.alpha2 techs +//==================== +#define ENABLE_2_4_0_ALPHA2 1 + +// Enable rendering seams (and other options) in preview using batched models on systems not supporting OpenGL 3.3 +#define ENABLE_SEAMS_USING_BATCHED_MODELS (1 && ENABLE_SEAMS_USING_MODELS && ENABLE_2_4_0_ALPHA2) #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 3fc6242648..1e0b9b1242 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -807,6 +807,25 @@ void GCodeViewer::render() case EMoveType::Unretract: case EMoveType::Seam: { #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::InstancedModel; + buffer.shader = "gouraud_light_instanced"; + buffer.model.model.init_from(diamond(16)); + buffer.model.color = option_color(type); + buffer.model.instances.format = InstanceVBuffer::EFormat::InstancedModel; + } + else { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::BatchedModel; + buffer.vertices.format = VBuffer::EFormat::PositionNormal3; + buffer.shader = "gouraud_light"; + + buffer.model.data = diamond(16); + buffer.model.color = option_color(type); + buffer.model.instances.format = InstanceVBuffer::EFormat::BatchedModel; + } + break; +#else if (wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) { buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Model; buffer.shader = "gouraud_light_instanced"; @@ -819,6 +838,7 @@ void GCodeViewer::render() buffer.shader = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20) ? "options_120" : "options_110"; } break; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #else buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Point; buffer.vertices.format = VBuffer::EFormat::Position; @@ -1221,7 +1241,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) unsigned int ibuffer_id, IndexBuffer& indices, size_t move_id) { if (prev.type != curr.type || !buffer.paths.back().matches(curr)) { // add starting index - indices.push_back(static_cast(indices.size())); + indices.push_back(static_cast(indices.size())); buffer.add_path(curr, ibuffer_id, indices.size() - 1, move_id - 1); buffer.paths.back().sub_paths.front().first.position = prev.position; } @@ -1229,11 +1249,11 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) Path& last_path = buffer.paths.back(); if (last_path.sub_paths.front().first.i_id != last_path.sub_paths.back().last.i_id) { // add previous index - indices.push_back(static_cast(indices.size())); + indices.push_back(static_cast(indices.size())); } // add current index - indices.push_back(static_cast(indices.size())); + indices.push_back(static_cast(indices.size())); last_path.sub_paths.back().last = { ibuffer_id, indices.size() - 1, move_id, curr.position }; }; @@ -1436,7 +1456,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) }; #if ENABLE_SEAMS_USING_MODELS - // format data into the buffers to be rendered as model + // format data into the buffers to be rendered as instanced model auto add_model_instance = [](const GCodeProcessor::MoveVertex& curr, InstanceBuffer& instances, InstanceIdBuffer& instances_ids, size_t move_id) { // append position instances.push_back(curr.position.x()); @@ -1450,6 +1470,49 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // append id instances_ids.push_back(move_id); }; + +#if ENABLE_SEAMS_USING_BATCHED_MODELS + // format data into the buffers to be rendered as batched model + auto add_vertices_as_model_batch = [](const GCodeProcessor::MoveVertex& curr, const GLModel::InitializationData& data, VertexBuffer& vertices, InstanceBuffer& instances, InstanceIdBuffer& instances_ids, size_t move_id) { + const double width = static_cast(1.5f * curr.width); + const double height = static_cast(1.5f * curr.height); + + const Transform3d trafo = Geometry::assemble_transform(curr.position.cast(), Vec3d::Zero(), { width, width, height }); + const Eigen::Matrix normal_matrix = trafo.matrix().template block<3, 3>(0, 0).inverse().transpose(); + + for (const auto& entity : data.entities) { + // append vertices + for (size_t i = 0; i < entity.positions.size(); ++i) { + // append position + const Vec3d position = trafo * entity.positions[i].cast(); + vertices.push_back(static_cast(position.x())); + vertices.push_back(static_cast(position.y())); + vertices.push_back(static_cast(position.z())); + + // append normal + const Vec3d normal = normal_matrix * entity.normals[i].cast(); + vertices.push_back(static_cast(normal.x())); + vertices.push_back(static_cast(normal.y())); + vertices.push_back(static_cast(normal.z())); + } + } + + // append instance position + instances.push_back(curr.position.x()); + instances.push_back(curr.position.y()); + instances.push_back(curr.position.z()); + // append instance id + instances_ids.push_back(move_id); + }; + + auto add_indices_as_model_batch = [](const GLModel::InitializationData& data, IndexBuffer& indices, IBufferType base_index) { + for (const auto& entity : data.entities) { + for (size_t i = 0; i < entity.indices.size(); ++i) { + indices.push_back(static_cast(entity.indices[i] + base_index)); + } + } + }; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS #if ENABLE_GCODE_VIEWER_STATISTICS @@ -1533,7 +1596,12 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // if adding the vertices for the current segment exceeds the threshold size of the current vertex buffer // add another vertex buffer +#if ENABLE_SEAMS_USING_BATCHED_MODELS + size_t vertices_size_to_add = (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) ? t_buffer.model.data.vertices_size_bytes() : t_buffer.max_vertices_per_segment_size_bytes(); + if (v_multibuffer.back().size() * sizeof(float) > t_buffer.vertices.max_size_bytes() - vertices_size_to_add) { +#else if (v_multibuffer.back().size() * sizeof(float) > t_buffer.vertices.max_size_bytes() - t_buffer.max_vertices_per_segment_size_bytes()) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS v_multibuffer.push_back(VertexBuffer()); if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Triangle) { Path& last_path = t_buffer.paths.back(); @@ -1550,6 +1618,24 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) case TBuffer::ERenderPrimitiveType::Line: { add_vertices_as_line(prev, curr, v_buffer); break; } case TBuffer::ERenderPrimitiveType::Triangle: { add_vertices_as_solid(prev, curr, t_buffer, static_cast(v_multibuffer.size()) - 1, v_buffer, i); break; } #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + case TBuffer::ERenderPrimitiveType::InstancedModel: + { + add_model_instance(curr, inst_buffer, inst_id_buffer, i); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.instances_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + break; + } + case TBuffer::ERenderPrimitiveType::BatchedModel: + { + add_vertices_as_model_batch(curr, t_buffer.model.data, v_buffer, inst_buffer, inst_id_buffer, i); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.batched_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + break; + } +#else case TBuffer::ERenderPrimitiveType::Model: { add_model_instance(curr, inst_buffer, inst_id_buffer, i); @@ -1559,6 +1645,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) break; } #endif // ENABLE_SEAMS_USING_MODELS +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS } // collect options zs for later use @@ -1741,6 +1828,23 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::InstancedModel) { + const InstanceBuffer& inst_buffer = instances[i]; + if (!inst_buffer.empty()) { + t_buffer.model.instances.buffer = inst_buffer; + t_buffer.model.instances.s_ids = instances_ids[i]; + } + } + else { + if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) { + const InstanceBuffer& inst_buffer = instances[i]; + if (!inst_buffer.empty()) { + t_buffer.model.instances.buffer = inst_buffer; + t_buffer.model.instances.s_ids = instances_ids[i]; + } + } +#else if (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { const InstanceBuffer& inst_buffer = instances[i]; if (!inst_buffer.empty()) { @@ -1749,6 +1853,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) } } else { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS const MultiVertexBuffer& v_multibuffer = vertices[i]; for (const VertexBuffer& v_buffer : v_multibuffer) { @@ -1842,10 +1947,20 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // if adding the indices for the current segment exceeds the threshold size of the current index buffer // create another index buffer +#if ENABLE_SEAMS_USING_BATCHED_MODELS + size_t indiced_size_to_add = (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) ? t_buffer.model.data.indices_size_bytes() : t_buffer.max_indices_per_segment_size_bytes(); + if (i_multibuffer.back().size() * sizeof(IBufferType) >= IBUFFER_THRESHOLD_BYTES - indiced_size_to_add) { +#else if (i_multibuffer.back().size() * sizeof(IBufferType) >= IBUFFER_THRESHOLD_BYTES - t_buffer.max_indices_per_segment_size_bytes()) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS i_multibuffer.push_back(IndexBuffer()); vbo_index_list.push_back(t_buffer.vertices.vbos[curr_vertex_buffer.first]); +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Point && + t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::BatchedModel) { +#else if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Point) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS Path& last_path = t_buffer.paths.back(); last_path.add_sub_path(prev, static_cast(i_multibuffer.size()) - 1, 0, i - 1); } @@ -1853,14 +1968,24 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) // if adding the vertices for the current segment exceeds the threshold size of the current vertex buffer // create another index buffer +#if ENABLE_SEAMS_USING_BATCHED_MODELS + size_t vertices_size_to_add = (t_buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) ? t_buffer.model.data.vertices_size_bytes() : t_buffer.max_vertices_per_segment_size_bytes(); + if (curr_vertex_buffer.second * t_buffer.vertices.vertex_size_bytes() > t_buffer.vertices.max_size_bytes() - vertices_size_to_add) { +#else if (curr_vertex_buffer.second * t_buffer.vertices.vertex_size_bytes() > t_buffer.vertices.max_size_bytes() - t_buffer.max_vertices_per_segment_size_bytes()) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS i_multibuffer.push_back(IndexBuffer()); ++curr_vertex_buffer.first; curr_vertex_buffer.second = 0; vbo_index_list.push_back(t_buffer.vertices.vbos[curr_vertex_buffer.first]); +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Point && + t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::BatchedModel) { +#else if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Point) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS Path& last_path = t_buffer.paths.back(); last_path.add_sub_path(prev, static_cast(i_multibuffer.size()) - 1, 0, i - 1); } @@ -1884,6 +2009,13 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) add_indices_as_solid(prev, curr, next, t_buffer, curr_vertex_buffer.second, static_cast(i_multibuffer.size()) - 1, i_buffer, i); break; } +#if ENABLE_SEAMS_USING_BATCHED_MODELS + case TBuffer::ERenderPrimitiveType::BatchedModel: { + add_indices_as_model_batch(t_buffer.model.data, i_buffer, curr_vertex_buffer.second); + curr_vertex_buffer.second += t_buffer.model.data.vertices_count(); + break; + } +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS default: { break; } } } @@ -1898,7 +2030,11 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) for (size_t i = 0; i < m_buffers.size(); ++i) { TBuffer& t_buffer = m_buffers[i]; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::InstancedModel) { +#else if (t_buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS const MultiIndexBuffer& i_multibuffer = indices[i]; for (const IndexBuffer& i_buffer : i_multibuffer) { @@ -2174,7 +2310,12 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool continue; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::InstancedModel || + buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) { +#else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS for (size_t id : buffer.model.instances.s_ids) { if (id < m_layers.get_endpoints_at(m_layers_z_range[0]).first || m_layers.get_endpoints_at(m_layers_z_range[1]).last < id) continue; @@ -2239,7 +2380,12 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool bool found = false; for (const TBuffer& buffer : m_buffers) { #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::InstancedModel || + buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) { +#else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS for (size_t i = 0; i < buffer.model.instances.s_ids.size(); ++i) { if (buffer.model.instances.s_ids[i] == m_sequential_view.current.last) { size_t offset = i * buffer.model.instances.instance_size_floats(); @@ -2400,10 +2546,19 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool } #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + // second pass: for buffers using instanced and batched models, update the instances render ranges +#else // second pass: for buffers using instanced models, update the instances render ranges +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS for (size_t b = 0; b < m_buffers.size(); ++b) { TBuffer& buffer = const_cast(m_buffers[b]); +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::InstancedModel && + buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::BatchedModel) +#else if (buffer.render_primitive_type != TBuffer::ERenderPrimitiveType::Model) +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS continue; buffer.model.instances.render_ranges.reset(); @@ -2689,6 +2844,66 @@ void GCodeViewer::render_toolpaths() } } }; + +#if ENABLE_SEAMS_USING_BATCHED_MODELS +#if ENABLE_GCODE_VIEWER_STATISTICS + auto render_as_batched_model = [this](TBuffer& buffer, GLShaderProgram& shader) { +#else + auto render_as_batched_model = [](TBuffer& buffer, GLShaderProgram& shader) { +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + struct Range + { + unsigned int first; + unsigned int last; + bool intersects(const Range& other) const { return (other.last < first || other.first > last) ? false : true; } + }; + Range buffer_range = { 0, 0 }; + size_t indices_per_instance = buffer.model.data.indices_count(); + + for (size_t j = 0; j < buffer.indices.size(); ++j) { + const IBuffer& i_buffer = buffer.indices[j]; + buffer_range.last = buffer_range.first + i_buffer.count / indices_per_instance; + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, i_buffer.vbo)); + glsafe(::glVertexPointer(buffer.vertices.position_size_floats(), GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.position_offset_bytes())); + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + bool has_normals = buffer.vertices.normal_size_floats() > 0; + if (has_normals) { + glsafe(::glNormalPointer(GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.normal_offset_bytes())); + glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_buffer.ibo)); + + for (auto& range : buffer.model.instances.render_ranges.ranges) { + Range range_range = { range.offset, range.offset + range.count }; + if (range_range.intersects(buffer_range)) { + shader.set_uniform("uniform_color", range.color); + unsigned int offset = (range_range.first > buffer_range.first) ? range_range.first - buffer_range.first : 0; + size_t offset_bytes = static_cast(offset) * indices_per_instance * sizeof(IBufferType); + Range render_range = { std::max(range_range.first, buffer_range.first), std::min(range_range.last, buffer_range.last) }; + size_t count = static_cast(render_range.last - render_range.first) * indices_per_instance; + if (count > 0) { + glsafe(::glDrawElements(GL_TRIANGLES, (GLsizei)count, GL_UNSIGNED_SHORT, (const void*)offset_bytes)); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.gl_batched_models_calls_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + } + } + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + if (has_normals) + glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); + + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + buffer_range.first = buffer_range.last; + } + }; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS auto line_width = [](double zoom) { @@ -2712,11 +2927,22 @@ void GCodeViewer::render_toolpaths() shader->start_using(); #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::InstancedModel) { +#else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Model) { +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS shader->set_uniform("emission_factor", 0.25f); render_as_instanced_model(buffer, *shader); shader->set_uniform("emission_factor", 0.0f); } +#if ENABLE_SEAMS_USING_BATCHED_MODELS + else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::BatchedModel) { + shader->set_uniform("emission_factor", 0.25f); + render_as_batched_model(buffer, *shader); + shader->set_uniform("emission_factor", 0.0f); + } +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS else { #endif // ENABLE_SEAMS_USING_MODELS for (size_t j = 0; j < buffer.indices.size(); ++j) { @@ -2882,6 +3108,7 @@ void GCodeViewer::render_legend(float& legend_height) } case EItemType::Circle: { ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size)); +#if !ENABLE_SEAMS_USING_BATCHED_MODELS if (m_buffers[buffer_id(EMoveType::Retract)].shader == "options_120") { draw_list->AddCircleFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16); @@ -2891,6 +3118,7 @@ void GCodeViewer::render_legend(float& legend_height) draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16); } else +#endif // !ENABLE_SEAMS_USING_BATCHED_MODELS draw_list->AddCircleFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16); break; @@ -3756,7 +3984,11 @@ void GCodeViewer::render_statistics() add_counter(std::string("Multi GL_TRIANGLES:"), m_statistics.gl_multi_triangles_calls_count); add_counter(std::string("GL_TRIANGLES:"), m_statistics.gl_triangles_calls_count); #if ENABLE_SEAMS_USING_MODELS + ImGui::Separator(); add_counter(std::string("Instanced models:"), m_statistics.gl_instanced_models_calls_count); +#if ENABLE_SEAMS_USING_BATCHED_MODELS + add_counter(std::string("Batched models:"), m_statistics.gl_batched_models_calls_count); +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } @@ -3788,6 +4020,9 @@ void GCodeViewer::render_statistics() add_counter(std::string("Extrude segments count:"), m_statistics.extrude_segments_count); #if ENABLE_SEAMS_USING_MODELS add_counter(std::string("Instances count:"), m_statistics.instances_count); +#if ENABLE_SEAMS_USING_BATCHED_MODELS + add_counter(std::string("Batched count:"), m_statistics.batched_count); +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS ImGui::Separator(); add_counter(std::string("VBuffers count:"), m_statistics.vbuffers_count); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 429175fe68..86a7dcf856 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -105,9 +105,16 @@ class GCodeViewer }; #if ENABLE_SEAMS_USING_MODELS +#if ENABLE_SEAMS_USING_BATCHED_MODELS + // buffer containing instances data used to render a toolpaths using instanced or batched models + // instance record format: + // instanced models: 5 floats -> position.x|position.y|position.z|width|height (which are sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced()) + // batched models: 3 floats -> position.x|position.y|position.z +#else // buffer containing instances data used to render a toolpaths using instanced models // instance record format: 5 floats -> position.x|position.y|position.z|width|height // which is sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced() +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS struct InstanceVBuffer { // ranges used to render only subparts of the intances @@ -130,6 +137,16 @@ class GCodeViewer void reset(); }; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + enum class EFormat : unsigned char + { + InstancedModel, + BatchedModel + }; + + EFormat format; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS + // cpu-side buffer containing all instances data InstanceBuffer buffer; // indices of the moves for all instances @@ -138,7 +155,18 @@ class GCodeViewer size_t data_size_bytes() const { return s_ids.size() * instance_size_bytes(); } +#if ENABLE_SEAMS_USING_BATCHED_MODELS + size_t instance_size_floats() const { + switch (format) + { + case EFormat::InstancedModel: { return 5; } + case EFormat::BatchedModel: { return 3; } + default: { return 0; } + } + } +#else size_t instance_size_floats() const { return 5; } +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS size_t instance_size_bytes() const { return instance_size_floats() * sizeof(float); } void reset(); @@ -280,7 +308,12 @@ class GCodeViewer Line, #if ENABLE_SEAMS_USING_MODELS Triangle, +#if ENABLE_SEAMS_USING_BATCHED_MODELS + InstancedModel, + BatchedModel +#else Model +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #else Triangle #endif // ENABLE_SEAMS_USING_MODELS @@ -298,6 +331,9 @@ class GCodeViewer GLModel model; Color color; InstanceVBuffer instances; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + GLModel::InitializationData data; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS void reset(); }; @@ -362,7 +398,15 @@ class GCodeViewer case ERenderPrimitiveType::Triangle: { return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; } +#if ENABLE_SEAMS_USING_BATCHED_MODELS + case ERenderPrimitiveType::InstancedModel: { return model.model.is_initialized() && !model.instances.buffer.empty(); } + case ERenderPrimitiveType::BatchedModel: { + return model.data.vertices_count() > 0 && model.data.indices_count() && + !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; + } +#else case ERenderPrimitiveType::Model: { return model.model.is_initialized() && !model.instances.buffer.empty(); } +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS default: { return false; } } } @@ -520,6 +564,9 @@ class GCodeViewer int64_t gl_triangles_calls_count{ 0 }; #if ENABLE_SEAMS_USING_MODELS int64_t gl_instanced_models_calls_count{ 0 }; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + int64_t gl_batched_models_calls_count{ 0 }; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS // memory int64_t results_size{ 0 }; @@ -541,6 +588,9 @@ class GCodeViewer int64_t extrude_segments_count{ 0 }; #if ENABLE_SEAMS_USING_MODELS int64_t instances_count{ 0 }; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + int64_t batched_count{ 0 }; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS int64_t vbuffers_count{ 0 }; int64_t ibuffers_count{ 0 }; @@ -569,6 +619,9 @@ class GCodeViewer gl_triangles_calls_count = 0; #if ENABLE_SEAMS_USING_MODELS gl_instanced_models_calls_count = 0; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + gl_batched_models_calls_count = 0; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS } @@ -594,6 +647,9 @@ class GCodeViewer extrude_segments_count = 0; #if ENABLE_SEAMS_USING_MODELS instances_count = 0; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + batched_count = 0; +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS #endif // ENABLE_SEAMS_USING_MODELS vbuffers_count = 0; ibuffers_count = 0; diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 5cddd7fa3b..d22925673b 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -19,6 +19,26 @@ namespace Slic3r { namespace GUI { +#if ENABLE_SEAMS_USING_BATCHED_MODELS +size_t GLModel::InitializationData::vertices_count() const +{ + size_t ret = 0; + for (const Entity& entity : entities) { + ret += entity.positions.size(); + } + return ret; +} + +size_t GLModel::InitializationData::indices_count() const +{ + size_t ret = 0; + for (const Entity& entity : entities) { + ret += entity.indices.size(); + } + return ret; +} +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS + void GLModel::init_from(const InitializationData& data) { if (!m_render_data.empty()) // call reset() if you want to reuse this model diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index 8e0039b083..7422466f0d 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -48,6 +48,15 @@ namespace GUI { }; std::vector entities; + +#if ENABLE_SEAMS_USING_BATCHED_MODELS + size_t vertices_count() const; + size_t vertices_size_floats() const { return vertices_count() * 6; } + size_t vertices_size_bytes() const { return vertices_size_floats() * sizeof(float); } + + size_t indices_count() const; + size_t indices_size_bytes() const { return indices_count() * sizeof(unsigned int); } +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS }; private: diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index edbe2bb46d..5dd478b578 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -33,11 +33,19 @@ std::pair GLShadersManager::init() bool valid = true; +#if ENABLE_SEAMS_USING_BATCHED_MODELS + // used to render bed axes and model, selection hints, gcode sequential view marker model, preview shells, options in gcode preview +#else // used to render bed axes and model, selection hints, gcode sequential view marker model, preview shells +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS valid &= append_shader("gouraud_light", { "gouraud_light.vs", "gouraud_light.fs" }); // used to render printbed valid &= append_shader("printbed", { "printbed.vs", "printbed.fs" }); // used to render options in gcode preview +#if ENABLE_SEAMS_USING_BATCHED_MODELS + if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) + valid &= append_shader("gouraud_light_instanced", { "gouraud_light_instanced.vs", "gouraud_light_instanced.fs" }); +#else #if ENABLE_SEAMS_USING_MODELS if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) valid &= append_shader("gouraud_light_instanced", { "gouraud_light_instanced.vs", "gouraud_light_instanced.fs" }); @@ -49,6 +57,7 @@ std::pair GLShadersManager::init() #if ENABLE_SEAMS_USING_MODELS } #endif // ENABLE_SEAMS_USING_MODELS +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS // used to render extrusion and travel paths as lines in gcode preview valid &= append_shader("toolpaths_lines", { "toolpaths_lines.vs", "toolpaths_lines.fs" }); // used to render objects in 3d editor From 2e250c1463c5d8e8bc94d0769e3e82d648e99e3b Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Sep 2021 16:16:43 +0200 Subject: [PATCH 097/151] Speed-up in GCodeViewer vertex buffer generator --- src/slic3r/GUI/GCodeViewer.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 3fc6242648..656a674066 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -70,18 +70,9 @@ static std::vector> decode_colors(const std::vector Date: Fri, 3 Sep 2021 16:17:24 +0200 Subject: [PATCH 098/151] Commenting out untested and unused Polyline::simplify_by_visibility() --- src/libslic3r/Polyline.cpp | 4 ++- src/libslic3r/Polyline.hpp | 2 +- xs/t/09_polyline.t | 70 +++++++++++++++++++------------------- xs/xsp/Polyline.xsp | 2 -- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index a6be642994..68c40fc204 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -120,7 +120,8 @@ void Polyline::simplify(double tolerance) this->points = MultiPoint::_douglas_peucker(this->points, tolerance); } -/* This method simplifies all *lines* contained in the supplied area */ +#if 0 +// This method simplifies all *lines* contained in the supplied area template void Polyline::simplify_by_visibility(const T &area) { @@ -141,6 +142,7 @@ void Polyline::simplify_by_visibility(const T &area) } template void Polyline::simplify_by_visibility(const ExPolygon &area); template void Polyline::simplify_by_visibility(const ExPolygonCollection &area); +#endif void Polyline::split_at(const Point &point, Polyline* p1, Polyline* p2) const { diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 51dcf9d36d..758fc38cd5 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -72,7 +72,7 @@ public: void extend_start(double distance); Points equally_spaced_points(double distance) const; void simplify(double tolerance); - template void simplify_by_visibility(const T &area); +// template void simplify_by_visibility(const T &area); void split_at(const Point &point, Polyline* p1, Polyline* p2) const; bool is_straight() const; bool is_closed() const { return this->points.front() == this->points.back(); } diff --git a/xs/t/09_polyline.t b/xs/t/09_polyline.t index 0a7b8dcb05..5203ec5ef4 100644 --- a/xs/t/09_polyline.t +++ b/xs/t/09_polyline.t @@ -89,40 +89,40 @@ is_deeply $polyline->pp, [ @$points, @$points ], 'append_polyline'; } # disabled because we now use a more efficient but incomplete algorithm -if (0) { - my $polyline = Slic3r::Polyline->new( - map [$_,10], (0,10,20,30,40,50,60) - ); - { - my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( - [25,0], [55,0], [55,30], [25,30], - )); - my $p = $polyline->clone; - $p->simplify_by_visibility($expolygon); - is_deeply $p->pp, [ - map [$_,10], (0,10,20,30,50,60) - ], 'simplify_by_visibility()'; - } - { - my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( - [-15,0], [75,0], [75,30], [-15,30], - )); - my $p = $polyline->clone; - $p->simplify_by_visibility($expolygon); - is_deeply $p->pp, [ - map [$_,10], (0,60) - ], 'simplify_by_visibility()'; - } - { - my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( - [-15,0], [25,0], [25,30], [-15,30], - )); - my $p = $polyline->clone; - $p->simplify_by_visibility($expolygon); - is_deeply $p->pp, [ - map [$_,10], (0,20,30,40,50,60) - ], 'simplify_by_visibility()'; - } -} +#if (0) { +# my $polyline = Slic3r::Polyline->new( +# map [$_,10], (0,10,20,30,40,50,60) +# ); +# { +# my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( +# [25,0], [55,0], [55,30], [25,30], +# )); +# my $p = $polyline->clone; +# $p->simplify_by_visibility($expolygon); +# is_deeply $p->pp, [ +# map [$_,10], (0,10,20,30,50,60) +# ], 'simplify_by_visibility()'; +# } +# { +# my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( +# [-15,0], [75,0], [75,30], [-15,30], +# )); +# my $p = $polyline->clone; +# $p->simplify_by_visibility($expolygon); +# is_deeply $p->pp, [ +# map [$_,10], (0,60) +# ], 'simplify_by_visibility()'; +# } +# { +# my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( +# [-15,0], [25,0], [25,30], [-15,30], +# )); +# my $p = $polyline->clone; +# $p->simplify_by_visibility($expolygon); +# is_deeply $p->pp, [ +# map [$_,10], (0,20,30,40,50,60) +# ], 'simplify_by_visibility()'; +# } +#} __END__ diff --git a/xs/xsp/Polyline.xsp b/xs/xsp/Polyline.xsp index 10bbb263f8..7846ea5f4c 100644 --- a/xs/xsp/Polyline.xsp +++ b/xs/xsp/Polyline.xsp @@ -31,8 +31,6 @@ void extend_end(double distance); void extend_start(double distance); void simplify(double tolerance); - void simplify_by_visibility(ExPolygon* expolygon) - %code{% THIS->simplify_by_visibility(*expolygon); %}; void split_at(Point* point, Polyline* p1, Polyline* p2) %code{% THIS->split_at(*point, p1, p2); %}; bool is_straight(); From dc7272391191a76b4315d1b7849ff2e570fd69fe Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Sep 2021 16:19:16 +0200 Subject: [PATCH 099/151] Optimization of GCodeReader: 1) Use std::from_chars() instead of strtod() 2) Own implementation of buffered readline() --- src/libslic3r/GCodeReader.cpp | 40 +++++++++++++++++++++++++++++------ src/libslic3r/GCodeReader.hpp | 13 ++++++------ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index 7dc3c9a8ac..54e6802e62 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -32,7 +33,7 @@ void GCodeReader::apply_config(const DynamicPrintConfig &config) m_extrusion_axis = get_extrusion_axis_char(m_config); } -const char* GCodeReader::parse_line_internal(const char *ptr, GCodeLine &gline, std::pair &command) +const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, GCodeLine &gline, std::pair &command) { PROFILE_FUNC(); @@ -70,9 +71,9 @@ const char* GCodeReader::parse_line_internal(const char *ptr, GCodeLine &gline, } if (axis != NUM_AXES_WITH_UNKNOWN) { // Try to parse the numeric value. - char *pend = nullptr; - double v = strtod(++ c, &pend); - if (pend != nullptr && is_end_of_word(*pend)) { + double v; + auto [pend, ec] = std::from_chars(++ c, end, v); + if (pend != c && is_end_of_word(*pend)) { // The axis value has been parsed correctly. if (axis != UNKNOWN_AXIS) gline.m_axis[int(axis)] = float(v); @@ -128,10 +129,37 @@ void GCodeReader::update_coordinates(GCodeLine &gline, std::pair buffer(65536 * 10, 0); std::string line; m_parsing = true; - while (m_parsing && std::getline(f, line)) - this->parse_line(line, callback); + GCodeLine gline; + bool eof = false; + while (m_parsing && ! eof) { + f.read(buffer.data(), buffer.size()); + auto it = buffer.begin(); + auto it_bufend = buffer.begin() + f.gcount(); + eof = ! f.good(); + while (it != it_bufend) { + bool eol = false; + auto it_end = it; + for (; it_end != it_bufend && ! (eol = *it_end == '\r' || *it_end == '\n'); ++ it_end) ; + eol |= eof && it_end == it_bufend; + if (eol) { + gline.reset(); + if (line.empty()) + this->parse_line(&(*it), &(*it_end), gline, callback); + else { + line.insert(line.end(), it, it_end); + this->parse_line(line.c_str(), line.c_str() + line.size(), gline, callback); + line.clear(); + } + } else + line.insert(line.end(), it, it_end); + // Skip all the empty lines. + for (it = it_end; it != it_bufend && (*it == '\r' || *it == '\n'); ++ it) ; + } + } } bool GCodeReader::GCodeLine::has(char axis) const diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 6b58608e61..88c9df2841 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -83,11 +83,12 @@ public: void parse_buffer(const std::string &buffer, Callback callback) { const char *ptr = buffer.c_str(); + const char *end = ptr + buffer.size(); GCodeLine gline; m_parsing = true; while (m_parsing && *ptr != 0) { gline.reset(); - ptr = this->parse_line(ptr, gline, callback); + ptr = this->parse_line(ptr, end, gline, callback); } } @@ -95,18 +96,18 @@ public: { this->parse_buffer(buffer, [](GCodeReader&, const GCodeReader::GCodeLine&){}); } template - const char* parse_line(const char *ptr, GCodeLine &gline, Callback &callback) + const char* parse_line(const char *ptr, const char *end, GCodeLine &gline, Callback &callback) { std::pair cmd; - const char *end = parse_line_internal(ptr, gline, cmd); + const char *line_end = parse_line_internal(ptr, end, gline, cmd); callback(*this, gline); update_coordinates(gline, cmd); - return end; + return line_end; } template void parse_line(const std::string &line, Callback callback) - { GCodeLine gline; this->parse_line(line.c_str(), gline, callback); } + { GCodeLine gline; this->parse_line(line.c_str(), line.c_str() + line.size(), gline, callback); } void parse_file(const std::string &file, callback_t callback); void quit_parsing() { m_parsing = false; } @@ -127,7 +128,7 @@ public: // void set_extrusion_axis(char axis) { m_extrusion_axis = axis; } private: - const char* parse_line_internal(const char *ptr, GCodeLine &gline, std::pair &command); + const char* parse_line_internal(const char *ptr, const char *end, GCodeLine &gline, std::pair &command); void update_coordinates(GCodeLine &gline, std::pair &command); static bool is_whitespace(char c) { return c == ' ' || c == '\t'; } From 97d1fe35ad784d41f15dda896448d163e4d8e56a Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Sep 2021 17:22:28 +0200 Subject: [PATCH 100/151] G-code writer optimization: Don't use std::strstream, it is slow! --- src/libslic3r/GCodeWriter.cpp | 151 +++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 49 deletions(-) diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 24549fd895..3aecdfd1f3 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,17 +1,21 @@ #include "GCodeWriter.hpp" #include "CustomGCode.hpp" #include +#include #include #include #include #include +#define XYZF_EXPORT_DIGITS 3 +#define E_EXPORT_DIGITS 5 + #define FLAVOR_IS(val) this->config.gcode_flavor == val #define FLAVOR_IS_NOT(val) this->config.gcode_flavor != val #define COMMENT(comment) if (this->config.gcode_comments && !comment.empty()) gcode << " ; " << comment; #define PRECISION(val, precision) std::fixed << std::setprecision(precision) << (val) -#define XYZF_NUM(val) PRECISION(val, 3) -#define E_NUM(val) PRECISION(val, 5) +#define XYZF_NUM(val) PRECISION(val, XYZF_EXPORT_DIGITS) +#define E_NUM(val) PRECISION(val, E_EXPORT_DIGITS) namespace Slic3r { @@ -288,16 +292,80 @@ std::string GCodeWriter::toolchange(unsigned int extruder_id) return gcode.str(); } +class G1Writer { +private: + static constexpr const size_t buflen = 256; + char buf[buflen]; + char *buf_end; + std::to_chars_result ptr_err; + +public: + G1Writer() { + this->buf[0] = 'G'; + this->buf[1] = '1'; + this->buf_end = this->buf + buflen; + this->ptr_err.ptr = this->buf + 2; + } + + void emit_axis(const char axis, const double v, size_t digits) { + *ptr_err.ptr ++ = ' '; *ptr_err.ptr ++ = axis; + this->ptr_err = std::to_chars(this->ptr_err.ptr, this->buf_end, v, std::chars_format::fixed, digits); + } + + void emit_xy(const Vec2d &point) { + this->emit_axis('X', point.x(), XYZF_EXPORT_DIGITS); + this->emit_axis('Y', point.y(), XYZF_EXPORT_DIGITS); + } + + void emit_xyz(const Vec3d &point) { + this->emit_axis('X', point.x(), XYZF_EXPORT_DIGITS); + this->emit_axis('Y', point.y(), XYZF_EXPORT_DIGITS); + this->emit_z(point.z()); + } + + void emit_z(const double z) { + this->emit_axis('Z', z, XYZF_EXPORT_DIGITS); + } + + void emit_e(const std::string &axis, double v) { + if (! axis.empty()) { + // not gcfNoExtrusion + this->emit_axis(axis[0], v, E_EXPORT_DIGITS); + } + } + + void emit_f(double speed) { + this->emit_axis('F', speed, XYZF_EXPORT_DIGITS); + } + + void emit_string(const std::string &s) { + strncpy(ptr_err.ptr, s.c_str(), s.size()); + ptr_err.ptr += s.size(); + } + + void emit_comment(bool allow_comments, const std::string &comment) { + if (allow_comments && ! comment.empty()) { + *ptr_err.ptr ++ = ' '; *ptr_err.ptr ++ = ';'; *ptr_err.ptr ++ = ' '; + this->emit_string(comment); + } + } + + std::string string() { + *ptr_err.ptr ++ = '\n'; + return std::string(this->buf, ptr_err.ptr - buf); + } +}; + std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker) const { assert(F > 0.); assert(F < 100000.); - std::ostringstream gcode; - gcode << "G1 F" << XYZF_NUM(F); - COMMENT(comment); - gcode << cooling_marker; - gcode << "\n"; - return gcode.str(); + + G1Writer w; + w.emit_f(F); + w.emit_comment(this->config.gcode_comments, comment); + w.emit_string(cooling_marker); + return w.string(); } std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) @@ -305,13 +373,11 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com m_pos(0) = point(0); m_pos(1) = point(1); - std::ostringstream gcode; - gcode << "G1 X" << XYZF_NUM(point(0)) - << " Y" << XYZF_NUM(point(1)) - << " F" << XYZF_NUM(this->config.travel_speed.value * 60.0); - COMMENT(comment); - gcode << "\n"; - return gcode.str(); + G1Writer w; + w.emit_xy(point); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment) @@ -340,14 +406,11 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co m_lifted = 0; m_pos = point; - std::ostringstream gcode; - gcode << "G1 X" << XYZF_NUM(point(0)) - << " Y" << XYZF_NUM(point(1)) - << " Z" << XYZF_NUM(point(2)) - << " F" << XYZF_NUM(this->config.travel_speed.value * 60.0); - COMMENT(comment); - gcode << "\n"; - return gcode.str(); + G1Writer w; + w.emit_xyz(point); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } std::string GCodeWriter::travel_to_z(double z, const std::string &comment) @@ -377,12 +440,11 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) if (speed == 0.) speed = this->config.travel_speed.value; - std::ostringstream gcode; - gcode << "G1 Z" << XYZF_NUM(z) - << " F" << XYZF_NUM(speed * 60.0); - COMMENT(comment); - gcode << "\n"; - return gcode.str(); + G1Writer w; + w.emit_z(z); + w.emit_f(speed * 60.0); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } bool GCodeWriter::will_move_z(double z) const @@ -402,16 +464,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: m_pos(0) = point(0); m_pos(1) = point(1); m_extruder->extrude(dE); - - std::ostringstream gcode; - gcode << "G1 X" << XYZF_NUM(point(0)) - << " Y" << XYZF_NUM(point(1)); - if (! m_extrusion_axis.empty()) - // not gcfNoExtrusion - gcode << " " << m_extrusion_axis << E_NUM(m_extruder->E()); - COMMENT(comment); - gcode << "\n"; - return gcode.str(); + + G1Writer w; + w.emit_xy(point); + w.emit_e(m_extrusion_axis, m_extruder->E()); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment) @@ -420,16 +478,11 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std m_lifted = 0; m_extruder->extrude(dE); - std::ostringstream gcode; - gcode << "G1 X" << XYZF_NUM(point(0)) - << " Y" << XYZF_NUM(point(1)) - << " Z" << XYZF_NUM(point(2)); - if (! m_extrusion_axis.empty()) - // not gcfNoExtrusion - gcode << " " << m_extrusion_axis << E_NUM(m_extruder->E()); - COMMENT(comment); - gcode << "\n"; - return gcode.str(); + G1Writer w; + w.emit_xyz(point); + w.emit_e(m_extrusion_axis, m_extruder->E()); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } std::string GCodeWriter::retract(bool before_wipe) From 86d06b0be98d0dc53f0afbbcf714475aa21f6e45 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 3 Sep 2021 17:22:53 +0200 Subject: [PATCH 101/151] G-code processor tiny change: In place initialization --- src/libslic3r/GCode/GCodeProcessor.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 0d71a19f53..73ee0d164e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2916,7 +2916,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type) m_line_id + 1 : ((type == EMoveType::Seam) ? m_last_line_id : m_line_id); - MoveVertex vertex = { + m_result.moves.push_back({ m_last_line_id, type, m_extrusion_role, @@ -2931,8 +2931,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type) m_fan_speed, m_extruder_temps[m_extruder_id], static_cast(m_result.moves.size()) - }; - m_result.moves.emplace_back(vertex); + }); // stores stop time placeholders for later use if (type == EMoveType::Color_change || type == EMoveType::Pause_Print) { From d04ece92c379adc9eeb0bc4e42ee0d0610c30907 Mon Sep 17 00:00:00 2001 From: Jason Scurtu Date: Fri, 3 Sep 2021 22:20:12 +0200 Subject: [PATCH 102/151] folder not needed anymore. --- ...om.prusa3d.PrusaSlicer.GCodeViewer.desktop | 10 -- .../flatpak/com.prusa3d.PrusaSlicer.desktop | 20 ---- .../com.prusa3d.PrusaSlicer.metainfo.xml | 106 ------------------ 3 files changed, 136 deletions(-) delete mode 100644 src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop delete mode 100755 src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop delete mode 100755 src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml diff --git a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop deleted file mode 100644 index 3177854734..0000000000 --- a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.GCodeViewer.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Name=Prusa GCode viewer -Name[de]=Prusa GCode Vorschau -Exec=prusa-slicer --gcodeviewer %F -Icon=com.prusa3d.PrusaSlicer.GCodeViewer -Terminal=false -Type=Application -MimeType=text/x.gcode; -Categories=Graphics;3DGraphics; -Keywords=3D;Printing;Slicer; diff --git a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop deleted file mode 100755 index 1ea3e157e7..0000000000 --- a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.desktop +++ /dev/null @@ -1,20 +0,0 @@ -[Desktop Entry] -Name=PrusaSlicer -Name[de]=PrusaSlicer -GenericName=3D Printing Software -Icon=com.prusa3d.PrusaSlicer -Exec=prusa-slicer %F -Terminal=false -Type=Application -MimeType=model/stl;model/x-wavefront-obj;model/3mf;model/x-geomview-off;application/x-amf; -Categories=Graphics;3DGraphics;Engineering; -Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA -StartupNotify=false -StartupWMClass=prusa-slicer -Actions=GCodeViewer; - -[Desktop Action GCodeViewer] -Exec=prusa-slicer --gcodeviewer %F -Name=G-Code Viewer -Name[de]=G-Code Vorschau -Icon=com.prusa3d.PrusaSlicer.GCodeViewer diff --git a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml b/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml deleted file mode 100755 index c37f43d69f..0000000000 --- a/src/platform/unix/flatpak/com.prusa3d.PrusaSlicer.metainfo.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - com.prusa3d.PrusaSlicer - com.prusa3d.PrusaSlicer.desktop - - prusa-slicer.desktop - - PrusaSlicer -

Powerful 3D printing slicer optimized for Prusa printers - 0BSD - AGPL-3.0-only - -

- PrusaSlicer takes 3D models (STL, OBJ, AMF) and converts them into G-code - instructions for FFF printers or PNG layers for mSLA 3D printers. It's - compatible with any modern printer based on the RepRap toolchain, including all - those based on the Marlin, Prusa, Sprinter and Repetier firmware. It also works - with Mach3, LinuxCNC and Machinekit controllers. -

-

- PrusaSlicer is based on Slic3r by Alessandro Ranelucci and the RepRap community. -

-

- What are some of PrusaSlicer's main features? -

-
    -
  • multi-platform (Linux/Mac/Win) and packaged as standalone-app with no dependencies required
  • -
  • complete command-line interface to use it with no GUI
  • -
  • multi-material (multiple extruders) object printing
  • -
  • multiple G-code flavors supported (RepRap, Makerbot, Mach3, Machinekit etc.)
  • -
  • ability to plate multiple objects having distinct print settings
  • -
  • multithread processing
  • -
  • STL auto-repair (tolerance for broken models)
  • -
  • wide automated unit testing
  • -
-
- https://www.prusa3d.com/prusaslicer/ - https://help.prusa3d.com - https://github.com/prusa3d/PrusaSlicer/issues - - - https://user-images.githubusercontent.com/590307/78981854-24d07580-7b21-11ea-9441-77923534a659.png - - - https://user-images.githubusercontent.com/590307/78981860-2863fc80-7b21-11ea-8c2d-8ff79ced2578.png - - - https://user-images.githubusercontent.com/590307/78981862-28fc9300-7b21-11ea-9b0d-d03e16b709d3.png - - - - - - -

This is the first alpha release of PrusaSlicer 2.4.0, introducing Multi-material painting tool, improved FDM supports and raft, - Windows dark mode, Fuzzy Skin, per object Brim configuration, Negative volumes, automatic Color Print for signs, Shape Gallery, - "Tip of the day" notifications, Model simplification, support for Marlin 2 acceleration control, support of 3DLabPrint airplane models, - new 3rd party printer profiles and many more new features, improvements and bugfixes.

-
-
- - -

PrusaSlicer 2.3.3 is a patch release following PrusaSlicer 2.3.2 release, fixing an unfortunate bug in handling FDM multi-material - project and configuration files #6711.

-
-
- - -

This is a final release of PrusaSlicer 2.3.2, following 2.3.2-beta and 2.3.2-rc. - For the new features in the 2.3.2 series, please read the change logs of the beta and release candidate. - The final release is functionally equal to the release candidate, with a single additional improvement: - Before installing profile updates, a configuration snapshot is taken. In rare circumstances when the current configuration is not consistent, - taking a configuration snapshot fails. PrusaSlicer 2.3.2 newly informs about the issue and offers either to install the configuration updates - even if taking the configuration snapshot failed or to abort update of the profiles.

-
-
- - -

This a final release of PrusaSlicer 2.3.1, introducing native builds for the new Apple Silicon MacBooks, Chrome OS support, performance improvements - in G-code rendering, security fixes and new 3rd party printer profiles. For full changelog on PrusaSlicer 2.3.1 series, please check the PrusaSlicer - 2.3.1-rc change log.

-
-
- - -

This a final release of PrusaSlicer 2.3.0, introducing paint-on supports, ironing, monotonic infill, seam painting, adaptive and support cubic infill, - print time per feature analysis, standalone G-code viewer application, better auto-arrange with a customizable gap and rotation, - search function for the settings, reworked "Avoid crossing perimeters" function, physical printers (network settings), - many new 3rd party printer profiles and much, much more.

-
-
- - -

Development/Unstable Snapshot

-
-
- - -

This is final release of PrusaSlicer 2.2.0 introducing SLA hollowing and hole drilling, support for 3rd party printer vendors, 3Dconnexion support, - automatic variable layer height, macOS dark mode support, greatly improved ColorPrint feature and much, much more. - Several bugs found in the previous release candidate are fixed in this final release. See the respective change logs of the previous releases for all the - new features, improvements and bugfixes in the 2.2.0 series.

-
-
-
- From 9c8c44e4ddc111548e338e6d9ca78d38f97383a6 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 6 Sep 2021 11:37:59 +0200 Subject: [PATCH 103/151] ENABLE_SEAMS_USING_BATCHED_MODELS - Fixed preview legend update --- src/slic3r/GUI/GCodeViewer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1e0b9b1242..8bf5ba5d85 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -3764,7 +3764,11 @@ void GCodeViewer::render_legend(float& legend_height) auto add_option = [this, append_item](EMoveType move_type, EOptionsColors color, const std::string& text) { const TBuffer& buffer = m_buffers[buffer_id(move_type)]; if (buffer.visible && buffer.has_data()) +#if ENABLE_SEAMS_USING_BATCHED_MODELS + append_item(EItemType::Circle, Options_Colors[static_cast(color)], text); +#else append_item((buffer.shader == "options_110") ? EItemType::Rect : EItemType::Circle, Options_Colors[static_cast(color)], text); +#endif // ENABLE_SEAMS_USING_BATCHED_MODELS }; // options section From ad41c3f01c94f888e500462e4127574f1fc60766 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 6 Sep 2021 13:15:36 +0200 Subject: [PATCH 104/151] Tech ENABLE_SEAMS_USING_MODELS - Fixed crash when enabling visualization of tool change markers in preview --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 656a674066..8c2ef573fc 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2399,7 +2399,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool buffer.model.instances.render_ranges.reset(); - if (!buffer.visible) + if (!buffer.visible || buffer.model.instances.s_ids.empty()) continue; buffer.model.instances.render_ranges.ranges.push_back({ 0, 0, 0, buffer.model.color }); From 7b4c98d72704a86acc99eb57837fd3bcb941f981 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 6 Sep 2021 14:31:10 +0200 Subject: [PATCH 105/151] #6828 - Clamping of toolpaths width performed only for gcodes files produced by 3rd part softwares (Tech ENABLE_CLAMP_TOOLPATHS_WIDTH) --- src/libslic3r/GCode/GCodeProcessor.cpp | 6 ++++++ src/libslic3r/Technologies.hpp | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 73ee0d164e..c4f262675a 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2252,8 +2252,14 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) // cross section: rectangle + 2 semicircles m_width = delta_pos[E] * static_cast(M_PI * sqr(filament_radius)) / (delta_xyz * m_height) + static_cast(1.0 - 0.25 * M_PI) * m_height; +#if ENABLE_CLAMP_TOOLPATHS_WIDTH + if (m_producers_enabled && m_producer != EProducer::PrusaSlicer) + // clamp width to avoid artifacts which may arise from wrong values of m_height + m_width = std::min(m_width, std::max(1.0f, 4.0f * m_height)); +#else // clamp width to avoid artifacts which may arise from wrong values of m_height m_width = std::min(m_width, std::max(1.0f, 4.0f * m_height)); +#endif // ENABLE_CLAMP_TOOLPATHS_WIDTH #if ENABLE_GCODE_VIEWER_DATA_CHECKING m_width_compare.update(m_width, m_extrusion_role); diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 6132430f1a..405119fc0d 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -57,4 +57,13 @@ #define ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED (1 && ENABLE_2_4_0_ALPHA0) +//==================== +// 2.4.0.alpha2 techs +//==================== +#define ENABLE_2_4_0_ALPHA2 1 + +// Enable clamping toolpaths width only for gcodes files produced by 3rd part softwares +#define ENABLE_CLAMP_TOOLPATHS_WIDTH (1 && ENABLE_2_4_0_ALPHA2) + + #endif // _prusaslicer_technologies_h_ From ce0beabb434b80d8db9117a8db3f056e63bd8bd7 Mon Sep 17 00:00:00 2001 From: Jason Scurtu Date: Fri, 3 Sep 2021 11:56:04 +0200 Subject: [PATCH 106/151] add new mac icon and update MainFrame.cpp --- resources/icons/PrusaSlicer-mac_128px.png | Bin 0 -> 6890 bytes src/slic3r/GUI/MainFrame.cpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/icons/PrusaSlicer-mac_128px.png diff --git a/resources/icons/PrusaSlicer-mac_128px.png b/resources/icons/PrusaSlicer-mac_128px.png new file mode 100644 index 0000000000000000000000000000000000000000..ff0b093beb1d3c6b8cf7e215397a654e03549cde GIT binary patch literal 6890 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 - m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer_128px.png"), wxBITMAP_TYPE_PNG), "PrusaSlicer"); + m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer-mac_128px.png"), wxBITMAP_TYPE_PNG), "PrusaSlicer"); break; case GUI_App::EAppMode::GCodeViewer: m_taskbar_icon = std::make_unique(wxTBI_DOCK); From 07bdb5fbe3df35f18e7c800b136d7da52c2a1fcc Mon Sep 17 00:00:00 2001 From: Jason Scurtu Date: Fri, 3 Sep 2021 12:08:24 +0200 Subject: [PATCH 107/151] add gcodeviewer --- .../icons/PrusaSlicer-gcodeviewer-mac-128px.png | Bin 0 -> 12218 bytes src/slic3r/GUI/MainFrame.cpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/icons/PrusaSlicer-gcodeviewer-mac-128px.png diff --git a/resources/icons/PrusaSlicer-gcodeviewer-mac-128px.png b/resources/icons/PrusaSlicer-gcodeviewer-mac-128px.png new file mode 100644 index 0000000000000000000000000000000000000000..b62f3fddc84e662f1b011cc19aad7128781cb3ca GIT binary patch literal 12218 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 KAYLVE82BGiN2+}Q literal 0 HcmV?d00001 diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 687333f190..31acc1c576 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -146,7 +146,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S break; case GUI_App::EAppMode::GCodeViewer: m_taskbar_icon = std::make_unique(wxTBI_DOCK); - m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer-gcodeviewer_128px.png"), wxBITMAP_TYPE_PNG), "G-code Viewer"); + m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer-gcodeviewer-mac-128px.png"), wxBITMAP_TYPE_PNG), "G-code Viewer"); break; } #endif // __APPLE__ From b41e3214736ad4fdf8789f39db7a8e206a54807b Mon Sep 17 00:00:00 2001 From: Jason Scurtu Date: Fri, 3 Sep 2021 12:09:32 +0200 Subject: [PATCH 108/151] rename --- ...px.png => PrusaSlicer-gcodeviewer-mac_128px.png} | Bin src/slic3r/GUI/MainFrame.cpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename resources/icons/{PrusaSlicer-gcodeviewer-mac-128px.png => PrusaSlicer-gcodeviewer-mac_128px.png} (100%) diff --git a/resources/icons/PrusaSlicer-gcodeviewer-mac-128px.png b/resources/icons/PrusaSlicer-gcodeviewer-mac_128px.png similarity index 100% rename from resources/icons/PrusaSlicer-gcodeviewer-mac-128px.png rename to resources/icons/PrusaSlicer-gcodeviewer-mac_128px.png diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 31acc1c576..51dbc201d3 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -146,7 +146,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S break; case GUI_App::EAppMode::GCodeViewer: m_taskbar_icon = std::make_unique(wxTBI_DOCK); - m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer-gcodeviewer-mac-128px.png"), wxBITMAP_TYPE_PNG), "G-code Viewer"); + m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer-gcodeviewer-mac_128px.png"), wxBITMAP_TYPE_PNG), "G-code Viewer"); break; } #endif // __APPLE__ From 428509ac00885348cf5455ae1b6eadfd0e96f6ca Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 6 Sep 2021 18:08:13 +0200 Subject: [PATCH 109/151] Pimped up GCodeProcessor::TimeProcessor::post_process(): replaced implicit lambda capture with explicit listing of captured context for readability and code correctness. Captured this as const. --- src/libslic3r/GCode/GCodeProcessor.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index c4f262675a..e375179e23 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -476,11 +476,19 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st g1_times_cache_it.emplace_back(machine.g1_times_cache.begin()); // add lines M73 to exported gcode - auto process_line_G1 = [&]() { + auto process_line_G1 = [ + // Lambdas, mostly for string formatting, all with an empty capture block. + time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute, + &self = std::as_const(*this), + // Caches, to be modified + &g1_times_cache_it, &last_exported_main, &last_exported_stop, + // String output + &export_line] + (const size_t g1_lines_counter) { unsigned int exported_lines_count = 0; - if (export_remaining_time_enabled) { + if (self.export_remaining_time_enabled) { for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - const TimeMachine& machine = machines[i]; + const TimeMachine& machine = self.machines[i]; if (machine.enabled) { // export pair // Skip all machine.g1_times_cache below g1_lines_counter. @@ -581,8 +589,7 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st parser.parse_line(gcode_line, [&](GCodeReader& reader, const GCodeReader::GCodeLine& line) { if (line.cmd_is("G1")) { - unsigned int extra_lines_count = process_line_G1(); - ++g1_lines_counter; + unsigned int extra_lines_count = process_line_G1(g1_lines_counter ++); if (extra_lines_count > 0) offsets.push_back({ line_id, extra_lines_count }); } From 0bc77cef3ec30075b14a01702e3a3877964516a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Tue, 7 Sep 2021 07:33:57 +0200 Subject: [PATCH 110/151] Fixed build on Linux and macOS that was failing because of using std::to_chars and std::from_chars with floating-point values. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old version of GCC and Clang support only integers to be passed to std::to_chars and std::from_chars. macOS older version of Clang doesn't support std::from_chars at all. So for Linux and macOS, it was replaced std::from_chars with strtod and temporarily was replace std::to_chars with snprintf. --- src/libslic3r/GCodeReader.cpp | 7 +++++++ src/libslic3r/GCodeWriter.cpp | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index 54e6802e62..aa52685397 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -71,9 +71,16 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G } if (axis != NUM_AXES_WITH_UNKNOWN) { // Try to parse the numeric value. +#ifdef WIN32 double v; auto [pend, ec] = std::from_chars(++ c, end, v); if (pend != c && is_end_of_word(*pend)) { +#else + // The older version of GCC and Clang support std::from_chars just for integers, so strtod we used it instead. + char *pend = nullptr; + double v = strtod(++ c, &pend); + if (pend != nullptr && is_end_of_word(*pend)) { +#endif // The axis value has been parsed correctly. if (axis != UNKNOWN_AXIS) gline.m_axis[int(axis)] = float(v); diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 3aecdfd1f3..c97180982d 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -309,7 +309,16 @@ public: void emit_axis(const char axis, const double v, size_t digits) { *ptr_err.ptr ++ = ' '; *ptr_err.ptr ++ = axis; +#ifdef WIN32 this->ptr_err = std::to_chars(this->ptr_err.ptr, this->buf_end, v, std::chars_format::fixed, digits); +#else + int buf_capacity = int(this->buf_end - this->ptr_err.ptr); + int ret = snprintf(this->ptr_err.ptr, buf_capacity, "%.*lf", int(digits), v); + if (ret <= 0 || ret > buf_capacity) + ptr_err.ec = std::errc::value_too_large; + else + this->ptr_err.ptr = this->ptr_err.ptr + ret; +#endif } void emit_xy(const Vec2d &point) { From d35183921b0696effd194a0cb47cce83020f5efc Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 7 Sep 2021 09:41:14 +0200 Subject: [PATCH 111/151] Revert of 7b4c98d72704a86acc99eb57837fd3bcb941f981, clamp toolpaths widths increased to 2mm and added default values for toolpaths width and height --- src/libslic3r/GCode/GCodeProcessor.cpp | 26 +++++++++++++------------- src/libslic3r/Technologies.hpp | 9 --------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index e375179e23..2c58c2a25e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -21,6 +21,9 @@ #include +static const float DEFAULT_TOOLPATH_WIDTH = 0.4f; +static const float DEFAULT_TOOLPATH_HEIGHT = 0.2f; + static const float INCHES_TO_MM = 25.4f; static const float MMMIN_TO_MMSEC = 1.0f / 60.0f; static const float DEFAULT_ACCELERATION = 1500.0f; // Prusa Firmware 1_75mm_MK2 @@ -2217,9 +2220,6 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) return; EMoveType type = move_type(delta_pos); - if (type == EMoveType::Extrude && m_end_position[Z] == 0.0f) - type = EMoveType::Travel; - if (type == EMoveType::Extrude) { float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z])); float volume_extruded_filament = area_filament_cross_section * delta_pos[E]; @@ -2243,6 +2243,12 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) } } + if (m_height == 0.0f) + m_height = DEFAULT_TOOLPATH_HEIGHT; + + if (m_end_position[Z] == 0.0f) + m_end_position[Z] = m_height; + #if ENABLE_GCODE_VIEWER_DATA_CHECKING m_height_compare.update(m_height, m_extrusion_role); #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING @@ -2259,23 +2265,17 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) // cross section: rectangle + 2 semicircles m_width = delta_pos[E] * static_cast(M_PI * sqr(filament_radius)) / (delta_xyz * m_height) + static_cast(1.0 - 0.25 * M_PI) * m_height; -#if ENABLE_CLAMP_TOOLPATHS_WIDTH - if (m_producers_enabled && m_producer != EProducer::PrusaSlicer) - // clamp width to avoid artifacts which may arise from wrong values of m_height - m_width = std::min(m_width, std::max(1.0f, 4.0f * m_height)); -#else + if (m_width == 0.0f) + m_width = DEFAULT_TOOLPATH_WIDTH; + // clamp width to avoid artifacts which may arise from wrong values of m_height - m_width = std::min(m_width, std::max(1.0f, 4.0f * m_height)); -#endif // ENABLE_CLAMP_TOOLPATHS_WIDTH + m_width = std::min(m_width, std::max(2.0f, 4.0f * m_height)); #if ENABLE_GCODE_VIEWER_DATA_CHECKING m_width_compare.update(m_width, m_extrusion_role); #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING } - if (type == EMoveType::Extrude && (m_width == 0.0f || m_height == 0.0f)) - type = EMoveType::Travel; - // time estimate section auto move_length = [](const AxisCoords& delta_pos) { float sq_xyz_length = sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]); diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 405119fc0d..6132430f1a 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -57,13 +57,4 @@ #define ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED (1 && ENABLE_2_4_0_ALPHA0) -//==================== -// 2.4.0.alpha2 techs -//==================== -#define ENABLE_2_4_0_ALPHA2 1 - -// Enable clamping toolpaths width only for gcodes files produced by 3rd part softwares -#define ENABLE_CLAMP_TOOLPATHS_WIDTH (1 && ENABLE_2_4_0_ALPHA2) - - #endif // _prusaslicer_technologies_h_ From 5c9bc26ed5efee227dad831e89702f331a5891b5 Mon Sep 17 00:00:00 2001 From: Justin Schuh Date: Tue, 7 Sep 2021 00:45:10 -0700 Subject: [PATCH 112/151] Replace OpenProcess with GetCurrentProcess where appropriate (#6914) GetCurrentProcess is more correct and resolves spurious AV warnings. --- src/libslic3r/BlacklistedLibraryCheck.cpp | 11 ++++------- src/libslic3r/utils.cpp | 15 +++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/libslic3r/BlacklistedLibraryCheck.cpp b/src/libslic3r/BlacklistedLibraryCheck.cpp index d9dea188f5..76f675c707 100644 --- a/src/libslic3r/BlacklistedLibraryCheck.cpp +++ b/src/libslic3r/BlacklistedLibraryCheck.cpp @@ -33,22 +33,20 @@ std::wstring BlacklistedLibraryCheck::get_blacklisted_string() bool BlacklistedLibraryCheck::perform_check() { - // Get a handle to the process. - HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, GetCurrentProcessId()); - if (NULL == hProcess) - return false; + // Get the pseudo-handle for the current process. + HANDLE hCurrentProcess = GetCurrentProcess(); // Get a list of all the modules in this process. HMODULE hMods[1024]; DWORD cbNeeded; - if (EnumProcessModulesEx(hProcess, hMods, sizeof(hMods), &cbNeeded, LIST_MODULES_ALL)) + if (EnumProcessModulesEx(hCurrentProcess, hMods, sizeof(hMods), &cbNeeded, LIST_MODULES_ALL)) { //printf("Total Dlls: %d\n", cbNeeded / sizeof(HMODULE)); for (unsigned int i = 0; i < cbNeeded / sizeof(HMODULE); ++ i) { wchar_t szModName[MAX_PATH]; // Get the full path to the module's file. - if (GetModuleFileNameExW(hProcess, hMods[i], szModName, MAX_PATH)) + if (GetModuleFileNameExW(hCurrentProcess, hMods[i], szModName, MAX_PATH)) { // Add to list if blacklisted if (BlacklistedLibraryCheck::is_blacklisted(szModName)) { @@ -61,7 +59,6 @@ bool BlacklistedLibraryCheck::perform_check() } } - CloseHandle(hProcess); //printf("\n"); return !m_found.empty(); } diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index c330f34b2b..c5dbdac9cd 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -984,16 +984,11 @@ std::string log_memory_info(bool ignore_loglevel) } PROCESS_MEMORY_COUNTERS_EX, *PPROCESS_MEMORY_COUNTERS_EX; #endif /* PROCESS_MEMORY_COUNTERS_EX */ - - HANDLE hProcess = ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, ::GetCurrentProcessId()); - if (hProcess != nullptr) { - PROCESS_MEMORY_COUNTERS_EX pmc; - if (GetProcessMemoryInfo(hProcess, (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) - out = " WorkingSet: " + format_memsize_MB(pmc.WorkingSetSize) + "; PrivateBytes: " + format_memsize_MB(pmc.PrivateUsage) + "; Pagefile(peak): " + format_memsize_MB(pmc.PagefileUsage) + "(" + format_memsize_MB(pmc.PeakPagefileUsage) + ")"; - else - out += " Used memory: N/A"; - CloseHandle(hProcess); - } + PROCESS_MEMORY_COUNTERS_EX pmc; + if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) + out = " WorkingSet: " + format_memsize_MB(pmc.WorkingSetSize) + "; PrivateBytes: " + format_memsize_MB(pmc.PrivateUsage) + "; Pagefile(peak): " + format_memsize_MB(pmc.PagefileUsage) + "(" + format_memsize_MB(pmc.PeakPagefileUsage) + ")"; + else + out += " Used memory: N/A"; #elif defined(__linux__) or defined(__APPLE__) // Get current memory usage. #ifdef __APPLE__ From c1c8a60271083e3577a150b19042c052787a5891 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 7 Sep 2021 11:13:12 +0200 Subject: [PATCH 113/151] Changed color for toolpaths whose extrusion role is unknown --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 8c2ef573fc..49fd17ce84 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -503,7 +503,7 @@ void GCodeViewer::SequentialView::render(float legend_height) const } const std::vector GCodeViewer::Extrusion_Role_Colors {{ - { 0.75f, 0.75f, 0.75f, 1.0f }, // erNone + { 0.90f, 0.70f, 0.70f, 1.0f }, // erNone { 1.00f, 0.90f, 0.30f, 1.0f }, // erPerimeter { 1.00f, 0.49f, 0.22f, 1.0f }, // erExternalPerimeter { 0.12f, 0.12f, 1.00f, 1.0f }, // erOverhangPerimeter From 771a8927763f312401fa73b92cb950c31d2fd89b Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 7 Sep 2021 11:18:07 +0200 Subject: [PATCH 114/151] Optimization of GCodeProcessor for speed. --- src/libslic3r/GCode/GCodeProcessor.cpp | 306 +++++++++++++++++-------- src/libslic3r/GCodeReader.cpp | 12 +- src/libslic3r/GCodeReader.hpp | 15 +- 3 files changed, 232 insertions(+), 101 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 2c58c2a25e..142dd015df 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -360,7 +360,8 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for writing.\n")); auto time_in_minutes = [](float time_in_seconds) { - return int(::roundf(time_in_seconds / 60.0f)); + assert(time_in_seconds >= 0.f); + return int((time_in_seconds + 0.5f) / 60.0f); }; auto time_in_last_minute = [](float time_in_seconds) { @@ -392,7 +393,6 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st return std::string(line_M73); }; - GCodeReader parser; std::string gcode_line; size_t g1_lines_counter = 0; // keeps track of last exported pair @@ -411,11 +411,12 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st std::string export_line; // replace placeholder lines with the proper final value - auto process_placeholders = [&](const std::string& gcode_line) { + // gcode_line is in/out parameter, to reduce expensive memory allocation + auto process_placeholders = [&](std::string& gcode_line) { unsigned int extra_lines_count = 0; // remove trailing '\n' - std::string line = gcode_line.substr(0, gcode_line.length() - 1); + auto line = std::string_view(gcode_line).substr(0, gcode_line.length() - 1); std::string ret; if (line.length() > 1) { @@ -456,7 +457,10 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st } } - return std::tuple(!ret.empty(), ret.empty() ? gcode_line : ret, (extra_lines_count == 0) ? extra_lines_count : extra_lines_count - 1); + if (! ret.empty()) + // Not moving the move operator on purpose, so that the gcode_line allocation will grow and it will not be reallocated after handful of lines are processed. + gcode_line = ret; + return std::tuple(!ret.empty(), (extra_lines_count == 0) ? extra_lines_count : extra_lines_count - 1); }; // check for temporary lines @@ -569,39 +573,56 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st unsigned int line_id = 0; std::vector> offsets; - while (std::getline(in, gcode_line)) { - if (!in.good()) { - fclose(out); - throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nError while reading from file.\n")); - } + { + // Read the input stream 64kB at a time, extract lines and process them. + in.sync_with_stdio(false); + std::vector buffer(65536 * 10, 0); + // Line buffer. + assert(gcode_line.empty()); + while (! in.eof()) { + in.read(buffer.data(), buffer.size()); + if (! in.eof() && ! in.good()) { + fclose(out); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nError while reading from file.\n")); + } + auto it = buffer.begin(); + auto it_bufend = buffer.begin() + in.gcount(); + while (it != it_bufend) { + // Find end of line. + bool eol = false; + auto it_end = it; + for (; it_end != it_bufend && ! (eol = *it_end == '\r' || *it_end == '\n'); ++ it_end) ; + // End of line is indicated also if end of file was reached. + eol |= in.eof() && it_end == it_bufend; + gcode_line.insert(gcode_line.end(), it, it_end); + if (eol) { + ++line_id; - ++line_id; - - gcode_line += "\n"; - // replace placeholder lines - auto [processed, result, lines_added_count] = process_placeholders(gcode_line); - if (processed && lines_added_count > 0) - offsets.push_back({ line_id, lines_added_count }); - gcode_line = result; - if (!processed) { - // remove temporary lines - if (is_temporary_decoration(gcode_line)) - continue; - - // add lines M73 where needed - parser.parse_line(gcode_line, - [&](GCodeReader& reader, const GCodeReader::GCodeLine& line) { - if (line.cmd_is("G1")) { + gcode_line += "\n"; + // replace placeholder lines + auto [processed, lines_added_count] = process_placeholders(gcode_line); + if (processed && lines_added_count > 0) + offsets.push_back({ line_id, lines_added_count }); + if (! processed && ! is_temporary_decoration(gcode_line) && GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) { + // remove temporary lines, add lines M73 where needed unsigned int extra_lines_count = process_line_G1(g1_lines_counter ++); if (extra_lines_count > 0) offsets.push_back({ line_id, extra_lines_count }); } - }); - } - export_line += gcode_line; - if (export_line.length() > 65535) - write_string(export_line); + export_line += gcode_line; + if (export_line.length() > 65535) + write_string(export_line); + gcode_line.clear(); + } + // Skip EOL. + it = it_end; + if (it != it_bufend && *it == '\r') + ++ it; + if (it != it_bufend && *it == '\n') + ++ it; + } + } } if (!export_line.empty()) @@ -1171,8 +1192,6 @@ void GCodeProcessor::reset() void GCodeProcessor::process_file(const std::string& filename, bool apply_postprocess, std::function cancel_callback) { - auto last_cancel_callback_time = std::chrono::high_resolution_clock::now(); - CNumericLocalesSetter locales_setter; #if ENABLE_GCODE_VIEWER_STATISTICS @@ -1184,7 +1203,7 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr if (m_producers_enabled) { m_parser.parse_file(filename, [this](GCodeReader& reader, const GCodeReader::GCodeLine& line) { const std::string_view cmd = line.cmd(); - if (cmd.length() == 0) { + if (cmd.empty()) { const std::string_view comment = line.comment(); if (comment.length() > 1 && detect_producer(comment)) m_parser.quit_parsing(); @@ -1211,17 +1230,15 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr m_result.id = ++s_result_id; // 1st move must be a dummy move m_result.moves.emplace_back(MoveVertex()); - m_parser.parse_file(filename, [this, cancel_callback, &last_cancel_callback_time](GCodeReader& reader, const GCodeReader::GCodeLine& line) { - if (cancel_callback != nullptr) { - // call the cancel callback every 100 ms - auto curr_time = std::chrono::high_resolution_clock::now(); - if (std::chrono::duration_cast(curr_time - last_cancel_callback_time).count() > 100) { - cancel_callback(); - last_cancel_callback_time = curr_time; - } + size_t parse_line_callback_cntr = 10000; + m_parser.parse_file(filename, [this, cancel_callback, &parse_line_callback_cntr](GCodeReader& reader, const GCodeReader::GCodeLine& line) { + if (-- parse_line_callback_cntr == 0) { + // Don't call the cancel_callback() too often, do it every at every 10000'th line. + parse_line_callback_cntr = 10000; + cancel_callback(); } - process_gcode_line(line); - }); + this->process_gcode_line(line); + }); // update width/height of wipe moves for (MoveVertex& move : m_result.moves) { @@ -1401,61 +1418,170 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line) const std::string_view cmd = line.cmd(); if (cmd.length() > 1) { // process command lines - switch (::toupper(cmd[0])) + switch (cmd[0]) { + case 'g': case 'G': - { - switch (::atoi(&cmd[1])) - { - case 0: { process_G0(line); break; } // Move - case 1: { process_G1(line); break; } // Move - case 10: { process_G10(line); break; } // Retract - case 11: { process_G11(line); break; } // Unretract - case 20: { process_G20(line); break; } // Set Units to Inches - case 21: { process_G21(line); break; } // Set Units to Millimeters - case 22: { process_G22(line); break; } // Firmware controlled retract - case 23: { process_G23(line); break; } // Firmware controlled unretract - case 28: { process_G28(line); break; } // Move to origin - case 90: { process_G90(line); break; } // Set to Absolute Positioning - case 91: { process_G91(line); break; } // Set to Relative Positioning - case 92: { process_G92(line); break; } // Set Position - default: { break; } + switch (cmd.size()) { + case 2: + switch (cmd[1]) { + case '0': { process_G0(line); break; } // Move + case '1': { process_G1(line); break; } // Move + default: break; } break; + case 3: + switch (cmd[1]) { + case '1': + switch (cmd[2]) { + case '0': { process_G10(line); break; } // Retract + case '1': { process_G11(line); break; } // Unretract + default: break; + } + break; + case '2': + switch (cmd[2]) { + case '0': { process_G20(line); break; } // Set Units to Inches + case '1': { process_G21(line); break; } // Set Units to Millimeters + case '2': { process_G22(line); break; } // Firmware controlled retract + case '3': { process_G23(line); break; } // Firmware controlled unretract + case '8': { process_G28(line); break; } // Move to origin + default: break; + } + break; + case '9': + switch (cmd[2]) { + case '0': { process_G90(line); break; } // Set to Absolute Positioning + case '1': { process_G91(line); break; } // Set to Relative Positioning + case '2': { process_G92(line); break; } // Set Position + default: break; + } + break; + } + break; + default: + break; } + break; + case 'm': case 'M': - { - switch (::atoi(&cmd[1])) - { - case 1: { process_M1(line); break; } // Sleep or Conditional stop - case 82: { process_M82(line); break; } // Set extruder to absolute mode - case 83: { process_M83(line); break; } // Set extruder to relative mode - case 104: { process_M104(line); break; } // Set extruder temperature - case 106: { process_M106(line); break; } // Set fan speed - case 107: { process_M107(line); break; } // Disable fan - case 108: { process_M108(line); break; } // Set tool (Sailfish) - case 109: { process_M109(line); break; } // Set extruder temperature and wait - case 132: { process_M132(line); break; } // Recall stored home offsets - case 135: { process_M135(line); break; } // Set tool (MakerWare) - case 201: { process_M201(line); break; } // Set max printing acceleration - case 203: { process_M203(line); break; } // Set maximum feedrate - case 204: { process_M204(line); break; } // Set default acceleration - case 205: { process_M205(line); break; } // Advanced settings - case 221: { process_M221(line); break; } // Set extrude factor override percentage - case 401: { process_M401(line); break; } // Repetier: Store x, y and z position - case 402: { process_M402(line); break; } // Repetier: Go to stored position - case 566: { process_M566(line); break; } // Set allowable instantaneous speed change - case 702: { process_M702(line); break; } // Unload the current filament into the MK3 MMU2 unit at the end of print. - default: { break; } + switch (cmd.size()) { + case 2: + switch (cmd[1]) { + case '1': { process_M1(line); break; } // Sleep or Conditional stop + default: break; } break; - } - case 'T': - { - process_T(line); // Select Tool + case 3: + switch (cmd[1]) { + case '8': + switch (cmd[2]) { + case '2': { process_M82(line); break; } // Set extruder to absolute mode + case '3': { process_M83(line); break; } // Set extruder to relative mode + default: break; + } + break; + default: + break; + } + break; + case 4: + switch (cmd[1]) { + case '1': + switch (cmd[2]) { + case '0': + switch (cmd[3]) { + case '4': { process_M104(line); break; } // Set extruder temperature + case '6': { process_M106(line); break; } // Set fan speed + case '7': { process_M107(line); break; } // Disable fan + case '8': { process_M108(line); break; } // Set tool (Sailfish) + case '9': { process_M109(line); break; } // Set extruder temperature and wait + default: break; + } + break; + case '3': + switch (cmd[3]) { + case '2': { process_M132(line); break; } // Recall stored home offsets + case '5': { process_M135(line); break; } // Set tool (MakerWare) + default: break; + } + break; + default: + break; + } + break; + case '2': + switch (cmd[2]) { + case '0': + switch (cmd[3]) { + case '1': { process_M201(line); break; } // Set max printing acceleration + case '3': { process_M203(line); break; } // Set maximum feedrate + case '4': { process_M204(line); break; } // Set default acceleration + case '5': { process_M205(line); break; } // Advanced settings + default: break; + } + break; + case '2': + switch (cmd[3]) { + case '1': { process_M221(line); break; } // Set extrude factor override percentage + default: break; + } + break; + default: + break; + } + break; + case '4': + switch (cmd[2]) { + case '0': + switch (cmd[3]) { + case '1': { process_M401(line); break; } // Repetier: Store x, y and z position + case '2': { process_M402(line); break; } // Repetier: Go to stored position + default: break; + } + break; + default: + break; + } + break; + case '5': + switch (cmd[2]) { + case '6': + switch (cmd[3]) { + case '6': { process_M566(line); break; } // Set allowable instantaneous speed change + default: break; + } + break; + default: + break; + } + break; + case '7': + switch (cmd[2]) { + case '0': + switch (cmd[3]) { + case '2': { process_M702(line); break; } // Unload the current filament into the MK3 MMU2 unit at the end of print. + default: break; + } + break; + default: + break; + } + break; + default: + break; + } + break; + default: break; } - default: { break; } + break; + case 't': + case 'T': + process_T(line); // Select Tool + break; + default: + break; } } else { diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index aa52685397..4c4bebee44 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -133,7 +133,7 @@ void GCodeReader::update_coordinates(GCodeLine &gline, std::pairhas(Y) ? (this->y() - reader.y()) : 0; return sqrt(x*x + y*y); } - bool cmd_is(const char *cmd_test) const { - const char *cmd = GCodeReader::skip_whitespaces(m_raw.c_str()); - size_t len = strlen(cmd_test); - return strncmp(cmd, cmd_test, len) == 0 && GCodeReader::is_end_of_word(cmd[len]); - } + bool cmd_is(const char *cmd_test) const { return cmd_is(m_raw, cmd_test); } bool extruding(const GCodeReader &reader) const { return this->cmd_is("G1") && this->dist_E(reader) > 0; } bool retracting(const GCodeReader &reader) const { return this->cmd_is("G1") && this->dist_E(reader) < 0; } bool travel() const { return this->cmd_is("G1") && ! this->has(E); } @@ -66,6 +62,12 @@ public: float e() const { return m_axis[E]; } float f() const { return m_axis[F]; } + static bool cmd_is(const std::string &gcode_line, const char *cmd_test) { + const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str()); + size_t len = strlen(cmd_test); + return strncmp(cmd, cmd_test, len) == 0 && GCodeReader::is_end_of_word(cmd[len]); + } + private: std::string m_raw; float m_axis[NUM_AXES]; @@ -109,7 +111,8 @@ public: void parse_line(const std::string &line, Callback callback) { GCodeLine gline; this->parse_line(line.c_str(), line.c_str() + line.size(), gline, callback); } - void parse_file(const std::string &file, callback_t callback); + // Returns false if reading the file failed. + bool parse_file(const std::string &file, callback_t callback); void quit_parsing() { m_parsing = false; } float& x() { return m_position[X]; } From af20419ed4997bc08f419b13dead78eb905a10b3 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Tue, 7 Sep 2021 11:18:58 +0200 Subject: [PATCH 115/151] Fix for #6903 - Compare presets window titles inaccurate... --- src/slic3r/GUI/UnsavedChangesDialog.cpp | 16 +++++++++++----- src/slic3r/GUI/UnsavedChangesDialog.hpp | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 2b63bc79b6..ef480f60f8 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -696,7 +696,12 @@ void DiffViewCtrl::context_menu(wxDataViewEvent& event) auto it = m_items_map.find(item); if (it == m_items_map.end() || !it->second.is_long) return; - FullCompareDialog(it->second.opt_name, it->second.old_val, it->second.new_val).ShowModal(); + + size_t column_cnt = this->GetColumnCount(); + const wxString old_value_header = this->GetColumn(column_cnt - 2)->GetTitle(); + const wxString new_value_header = this->GetColumn(column_cnt - 1)->GetTitle(); + FullCompareDialog(it->second.opt_name, it->second.old_val, it->second.new_val, + old_value_header, new_value_header).ShowModal(); #ifdef __WXOSX__ wxWindow* parent = this->GetParent(); @@ -1281,7 +1286,8 @@ void UnsavedChangesDialog::on_sys_color_changed() // FullCompareDialog //------------------------------------------ -FullCompareDialog::FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& new_value) +FullCompareDialog::FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& new_value, + const wxString& old_value_header, const wxString& new_value_header) : wxDialog(nullptr, wxID_ANY, option_name, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { wxGetApp().UpdateDarkUI(this); @@ -1302,8 +1308,8 @@ FullCompareDialog::FullCompareDialog(const wxString& option_name, const wxString grid_sizer->Add(text, 0, wxALL, border); }; - add_header(_L("Old value")); - add_header(_L("New value")); + add_header(old_value_header); + add_header(new_value_header); auto get_set_from_val = [](wxString str) { if (str.Find("\n") == wxNOT_FOUND) @@ -1327,7 +1333,7 @@ FullCompareDialog::FullCompareDialog(const wxString& option_name, const wxString std::set_difference(new_set.begin(), new_set.end(), old_set.begin(), old_set.end(), std::inserter(new_old_diff_set, new_old_diff_set.begin())); auto add_value = [grid_sizer, border, this](wxString label, const std::set& diff_set, bool is_colored = false) { - wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, label, wxDefaultPosition, wxSize(400, 400), wxTE_MULTILINE | wxTE_READONLY | wxBORDER_SIMPLE | wxTE_RICH); + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, label, wxDefaultPosition, wxSize(400, 400), wxTE_MULTILINE | wxTE_READONLY | wxBORDER_DEFAULT | wxTE_RICH); wxGetApp().UpdateDarkUI(text); text->SetStyle(0, label.Len(), wxTextAttr(is_colored ? wxColour(orange) : wxNullColour, wxNullColour, this->GetFont())); diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index 9667430334..9dbaf6e99c 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -299,7 +299,8 @@ protected: class FullCompareDialog : public wxDialog { public: - FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& new_value); + FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& new_value, + const wxString& old_value_header, const wxString& new_value_header); ~FullCompareDialog() {} }; From 761c06ed9282c76475ab9986cd0cd0a5c340022d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 7 Sep 2021 12:25:30 +0200 Subject: [PATCH 116/151] GCodeViewer - Fixed crash when importing gcode generated with Simplify3D and switching to tool view --- src/libslic3r/GCode/GCodeProcessor.cpp | 13 +++++++++---- src/slic3r/GUI/GCodeViewer.cpp | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 2c58c2a25e..4b38020c11 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -961,10 +961,9 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) } // replace missing values with default - std::string default_color = "#FF8000"; for (size_t i = 0; i < m_result.extruder_colors.size(); ++i) { if (m_result.extruder_colors[i].empty()) - m_result.extruder_colors[i] = default_color; + m_result.extruder_colors[i] = "#FF8000"; } m_extruder_colors.resize(m_result.extruder_colors.size()); @@ -1350,7 +1349,7 @@ void GCodeProcessor::apply_config_simplify3d(const std::string& filename) if (pos != cmt.npos) { std::string data_str = cmt.substr(pos + 1); std::vector values_str; - boost::split(values_str, data_str, boost::is_any_of("|"), boost::token_compress_on); + boost::split(values_str, data_str, boost::is_any_of("|,"), boost::token_compress_on); for (const std::string& s : values_str) { out.emplace_back(static_cast(string_to_double_decimal_point(s))); } @@ -1374,10 +1373,16 @@ void GCodeProcessor::apply_config_simplify3d(const std::string& filename) m_result.filament_densities.clear(); extract_floats(comment, "filamentDensities", m_result.filament_densities); } + else if (comment.find("extruderDiameter") != comment.npos) { + std::vector extruder_diameters; + extract_floats(comment, "extruderDiameter", extruder_diameters); + m_result.extruders_count = extruder_diameters.size(); + } } }); - m_result.extruders_count = std::max(1, std::min(m_result.filament_diameters.size(), m_result.filament_densities.size())); + if (m_result.extruders_count == 0) + m_result.extruders_count = std::max(1, std::min(m_result.filament_diameters.size(), m_result.filament_densities.size())); if (bed_size.is_defined()) { m_result.bed_shape = { diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 49fd17ce84..979214fb5a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -695,8 +695,8 @@ void GCodeViewer::refresh(const GCodeProcessor::Result& gcode_result, const std: // update tool colors m_tool_colors = decode_colors(str_tool_colors); - // ensure at least one (default) color is defined - if (m_tool_colors.empty()) + // ensure there are enough colors defined + while (m_tool_colors.size() < std::max(size_t(1), gcode_result.extruders_count)) m_tool_colors.push_back(decode_color("#FF8000")); // update ranges for coloring / legend From e30ff22b8a2d586abb3b023519b40f715c44e1ad Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 7 Sep 2021 14:20:16 +0200 Subject: [PATCH 117/151] GCodePostProcessor - use C files instead of C++ streams, C files are faster. Also fixed a regression - crashes on undefined cancellation callback. --- src/libslic3r/GCode/GCodeProcessor.cpp | 48 +++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index f7709e9d68..aa6912fc48 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -347,17 +347,25 @@ void GCodeProcessor::TimeProcessor::reset() machines[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].enabled = true; } +struct FilePtr { + FilePtr(FILE *f) : f(f) {} + ~FilePtr() { this->close(); } + void close() { if (f) ::fclose(f); } + FILE* f = nullptr; +}; + void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, std::vector& moves) { - boost::nowide::ifstream in(filename); - if (!in.good()) + FilePtr in{ boost::nowide::fopen(filename.c_str(), "rb") }; + if (in.f == nullptr) throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for reading.\n")); // temporary file to contain modified gcode std::string out_path = filename + ".postprocess"; - FILE* out = boost::nowide::fopen(out_path.c_str(), "wb"); - if (out == nullptr) + FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") }; + if (out.f == nullptr) { throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for writing.\n")); + } auto time_in_minutes = [](float time_in_seconds) { assert(time_in_seconds >= 0.f); @@ -559,11 +567,10 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st }; // helper function to write to disk - auto write_string = [&](const std::string& str) { - fwrite((const void*)export_line.c_str(), 1, export_line.length(), out); - if (ferror(out)) { - in.close(); - fclose(out); + auto write_string = [&export_line, &out, &out_path](const std::string& str) { + fwrite((const void*)export_line.c_str(), 1, export_line.length(), out.f); + if (ferror(out.f)) { + out.close(); boost::nowide::remove(out_path.c_str()); throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nIs the disk full?\n")); } @@ -575,25 +582,23 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st { // Read the input stream 64kB at a time, extract lines and process them. - in.sync_with_stdio(false); std::vector buffer(65536 * 10, 0); // Line buffer. assert(gcode_line.empty()); - while (! in.eof()) { - in.read(buffer.data(), buffer.size()); - if (! in.eof() && ! in.good()) { - fclose(out); + for (;;) { + size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f); + if (::ferror(in.f)) throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nError while reading from file.\n")); - } + bool eof = cnt_read == 0; auto it = buffer.begin(); - auto it_bufend = buffer.begin() + in.gcount(); - while (it != it_bufend) { + auto it_bufend = buffer.begin() + cnt_read; + while (it != it_bufend || (eof && ! gcode_line.empty())) { // Find end of line. bool eol = false; auto it_end = it; for (; it_end != it_bufend && ! (eol = *it_end == '\r' || *it_end == '\n'); ++ it_end) ; // End of line is indicated also if end of file was reached. - eol |= in.eof() && it_end == it_bufend; + eol |= eof && it_end == it_bufend; gcode_line.insert(gcode_line.end(), it, it_end); if (eol) { ++line_id; @@ -622,13 +627,15 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st if (it != it_bufend && *it == '\n') ++ it; } + if (eof) + break; } } if (!export_line.empty()) write_string(export_line); - fclose(out); + out.close(); in.close(); // updates moves' gcode ids which have been modified by the insertion of the M73 lines @@ -1234,7 +1241,8 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr if (-- parse_line_callback_cntr == 0) { // Don't call the cancel_callback() too often, do it every at every 10000'th line. parse_line_callback_cntr = 10000; - cancel_callback(); + if (cancel_callback) + cancel_callback(); } this->process_gcode_line(line); }); From 719c91514bd71cf04d4abec70a958fd3710f5f01 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Tue, 7 Sep 2021 15:19:59 +0200 Subject: [PATCH 118/151] ObjectList:: Allow use BACKSPACE to delete selected items --- src/slic3r/GUI/GUI_ObjectList.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index c99d9090cb..bf174cbf12 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1044,13 +1044,8 @@ void ObjectList::key_event(wxKeyEvent& event) { if (event.GetKeyCode() == WXK_TAB) Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward); - else if (event.GetKeyCode() == WXK_DELETE -#ifdef __WXOSX__ - || event.GetKeyCode() == WXK_BACK -#endif //__WXOSX__ - ) { + else if (event.GetKeyCode() == WXK_DELETE || event.GetKeyCode() == WXK_BACK ) remove(); - } else if (event.GetKeyCode() == WXK_F5) wxGetApp().plater()->reload_all_from_disk(); else if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL/*WXK_SHIFT*/)) From 32733b7db943ccf823a27f9fca83a17c8aaae350 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 7 Sep 2021 15:42:50 +0200 Subject: [PATCH 119/151] GCodeProcessor collects positions of line ends for GCodeViewer, GCodeViewer no more parses G-code just to extract line end positions. Removed start_mapping_gcode_window(), void stop_mapping_gcode_window(), they are no more needed. --- src/libslic3r/GCode/GCodeProcessor.cpp | 16 ++++-- src/libslic3r/GCode/GCodeProcessor.hpp | 4 +- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 5 +- src/slic3r/GUI/GCodeViewer.cpp | 62 ++++----------------- src/slic3r/GUI/GCodeViewer.hpp | 9 +-- src/slic3r/GUI/GLCanvas3D.cpp | 10 ---- src/slic3r/GUI/GLCanvas3D.hpp | 3 - src/slic3r/GUI/Plater.cpp | 10 ---- src/slic3r/GUI/Plater.hpp | 3 - 9 files changed, 30 insertions(+), 92 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index aa6912fc48..ec586dbef7 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -354,7 +354,7 @@ struct FilePtr { FILE* f = nullptr; }; -void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, std::vector& moves) +void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, std::vector& moves, std::vector& lines_ends) { FilePtr in{ boost::nowide::fopen(filename.c_str(), "rb") }; if (in.f == nullptr) @@ -567,13 +567,19 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st }; // helper function to write to disk - auto write_string = [&export_line, &out, &out_path](const std::string& str) { + size_t out_file_pos = 0; + lines_ends.clear(); + auto write_string = [&export_line, &out, &out_path, &out_file_pos, &lines_ends](const std::string& str) { fwrite((const void*)export_line.c_str(), 1, export_line.length(), out.f); if (ferror(out.f)) { out.close(); boost::nowide::remove(out_path.c_str()); throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nIs the disk full?\n")); } + for (size_t i = 0; i < export_line.size(); ++ i) + if (export_line[i] == '\n') + lines_ends.emplace_back(out_file_pos + i + 1); + out_file_pos += export_line.size(); export_line.clear(); }; @@ -736,7 +742,9 @@ void GCodeProcessor::Result::reset() { } #else void GCodeProcessor::Result::reset() { - moves = std::vector(); + + moves.clear(); + lines_ends.clear(); bed_shape = Pointfs(); settings_ids.reset(); extruders_count = 0; @@ -1270,7 +1278,7 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr // post-process to add M73 lines into the gcode if (apply_postprocess) - m_time_processor.post_process(filename, m_result.moves); + m_time_processor.post_process(filename, m_result.moves, m_result.lines_ends); #if ENABLE_GCODE_VIEWER_DATA_CHECKING std::cout << "\n"; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 4fcdd8df39..bb93108881 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -306,7 +306,7 @@ namespace Slic3r { // post process the file with the given filename to add remaining time lines M73 // and updates moves' gcode ids accordingly - void post_process(const std::string& filename, std::vector& moves); + void post_process(const std::string& filename, std::vector& moves, std::vector& lines_ends); }; struct UsedFilaments // filaments per ColorChange @@ -350,6 +350,8 @@ namespace Slic3r { std::string filename; unsigned int id; std::vector moves; + // Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code. + std::vector lines_ends; Pointfs bed_shape; SettingsIds settings_ids; size_t extruders_count; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 8a58e5ec93..b4de4a509f 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -574,11 +574,8 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const Dyn // Some FFF status was invalidated, and the G-code was not exported yet. // Let the G-code preview UI know that the final G-code preview is not valid. // In addition, this early memory deallocation reduces memory footprint. - if (m_gcode_result != nullptr) { - //FIXME calling platter from here is not a staple of a good architecture. - GUI::wxGetApp().plater()->stop_mapping_gcode_window(); + if (m_gcode_result != nullptr) m_gcode_result->reset(); - } } return invalidated; } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 979214fb5a..946cbb2759 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -284,45 +284,14 @@ void GCodeViewer::SequentialView::Marker::render() const ImGui::PopStyleVar(); } -void GCodeViewer::SequentialView::GCodeWindow::load_gcode() +void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& filename, const std::vector &lines_ends) { - if (m_filename.empty()) - return; - + assert(! m_file.is_open()); if (m_file.is_open()) return; - try - { - // generate mapping for accessing data in file by line number - boost::nowide::ifstream f(m_filename); - - f.seekg(0, f.end); - uint64_t file_length = static_cast(f.tellg()); - f.seekg(0, f.beg); - - std::string line; - uint64_t offset = 0; - while (std::getline(f, line)) { - uint64_t line_length = static_cast(line.length()); - m_lines_map.push_back({ offset, line_length }); - offset += static_cast(line_length) + 1; - } - - if (offset != file_length) { - // if the final offset does not match with file length, lines are terminated with CR+LF - // so update all offsets accordingly - for (size_t i = 0; i < m_lines_map.size(); ++i) { - m_lines_map[i].first += static_cast(i); - } - } - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Unable to load data from " << m_filename << ". Cannot show G-code window."; - reset(); - return; - } + m_filename = filename; + m_lines_ends = std::move(lines_ends); m_selected_line_id = 0; m_last_lines_size = 0; @@ -345,7 +314,9 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u ret.reserve(end_id - start_id + 1); for (uint64_t id = start_id; id <= end_id; ++id) { // read line from file - std::string gline(m_file.data() + m_lines_map[id - 1].first, m_lines_map[id - 1].second); + const size_t start = id == 1 ? 0 : m_lines_ends[id - 2]; + const size_t len = m_lines_ends[id - 1] - start; + std::string gline(m_file.data() + start, len); std::string command; std::string parameters; @@ -379,7 +350,7 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u static const ImVec4 PARAMETERS_COLOR = { 1.0f, 1.0f, 1.0f, 1.0f }; static const ImVec4 COMMENT_COLOR = { 0.7f, 0.7f, 0.7f, 1.0f }; - if (!m_visible || m_filename.empty() || m_lines_map.empty() || curr_line_id == 0) + if (!m_visible || m_filename.empty() || m_lines_ends.empty() || curr_line_id == 0) return; // window height @@ -397,8 +368,8 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u const uint64_t half_lines_count = lines_count / 2; uint64_t start_id = (curr_line_id >= half_lines_count) ? curr_line_id - half_lines_count : 0; uint64_t end_id = start_id + lines_count - 1; - if (end_id >= static_cast(m_lines_map.size())) { - end_id = static_cast(m_lines_map.size()) - 1; + if (end_id >= static_cast(m_lines_ends.size())) { + end_id = static_cast(m_lines_ends.size()) - 1; start_id = end_id - lines_count + 1; } @@ -606,8 +577,7 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& // release gpu memory, if used reset(); - m_sequential_view.gcode_window.set_filename(gcode_result.filename); - m_sequential_view.gcode_window.load_gcode(); + m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends); #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER if (wxGetApp().is_gcode_viewer()) @@ -1146,16 +1116,6 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const fclose(fp); } -void GCodeViewer::start_mapping_gcode_window() -{ - m_sequential_view.gcode_window.load_gcode(); -} - -void GCodeViewer::stop_mapping_gcode_window() -{ - m_sequential_view.gcode_window.stop_mapping_file(); -} - void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) { // max index buffer size, in bytes diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 429175fe68..b918dee238 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -639,18 +639,17 @@ public: std::string m_filename; boost::iostreams::mapped_file_source m_file; // map for accessing data in file by line number - std::vector> m_lines_map; + std::vector m_lines_ends; // current visible lines std::vector m_lines; public: GCodeWindow() = default; ~GCodeWindow() { stop_mapping_file(); } - void set_filename(const std::string& filename) { m_filename = filename; } - void load_gcode(); + void load_gcode(const std::string& filename, const std::vector &lines_ends); void reset() { stop_mapping_file(); - m_lines_map.clear(); + m_lines_ends.clear(); m_lines.clear(); m_filename.clear(); } @@ -777,8 +776,6 @@ public: void export_toolpaths_to_obj(const char* filename) const; - void start_mapping_gcode_window(); - void stop_mapping_gcode_window(); void toggle_gcode_window_visibility() { m_sequential_view.gcode_window.toggle_visibility(); } #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index e281892204..f50c7fe25b 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1114,16 +1114,6 @@ int GLCanvas3D::check_volumes_outside_state() const return (int)state; } -void GLCanvas3D::start_mapping_gcode_window() -{ - m_gcode_viewer.start_mapping_gcode_window(); -} - -void GLCanvas3D::stop_mapping_gcode_window() -{ - m_gcode_viewer.stop_mapping_gcode_window(); -} - void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx) { m_render_sla_auxiliaries = visible; diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 3750e3a3e2..799fa11c79 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -621,9 +621,6 @@ public: const GCodeViewer::SequentialView& get_gcode_sequential_view() const { return m_gcode_viewer.get_sequential_view(); } void update_gcode_sequential_view_current(unsigned int first, unsigned int last) { m_gcode_viewer.update_sequential_view_current(first, last); } - void start_mapping_gcode_window(); - void stop_mapping_gcode_window(); - void toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo = nullptr, int instance_idx = -1); void toggle_model_objects_visibility(bool visible, const ModelObject* mo = nullptr, int instance_idx = -1); void update_instance_printable_state_for_object(size_t obj_idx); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index af8734184f..2c77dcb7ac 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6143,16 +6143,6 @@ BoundingBoxf Plater::bed_shape_bb() const return p->bed_shape_bb(); } -void Plater::start_mapping_gcode_window() -{ - p->preview->get_canvas3d()->start_mapping_gcode_window(); -} - -void Plater::stop_mapping_gcode_window() -{ - p->preview->get_canvas3d()->stop_mapping_gcode_window(); -} - void Plater::arrange() { p->m_ui_jobs.arrange(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 99f7e3cdaa..f1a493b0f0 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -282,9 +282,6 @@ public: GLCanvas3D* get_current_canvas3D(); BoundingBoxf bed_shape_bb() const; - void start_mapping_gcode_window(); - void stop_mapping_gcode_window(); - void arrange(); void find_new_position(const ModelInstancePtrs &instances); From b5a007a683124ec3b01b61e892ed8eb39c034248 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 7 Sep 2021 16:23:43 +0200 Subject: [PATCH 120/151] WIP to G-code export parallelization through pipelining: New class GCodeOutputStream as a G-code consumer. In the following steps the GCodeOutputStream will be pipelined with GCodeProcessor. --- src/libslic3r/GCode.cpp | 152 ++++++++++++++++++++-------------------- src/libslic3r/GCode.hpp | 48 ++++++++----- 2 files changed, 107 insertions(+), 93 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 19909b2bcf..54815188eb 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -744,27 +744,27 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessor::Result* re std::string path_tmp(path); path_tmp += ".tmp"; - FILE *file = boost::nowide::fopen(path_tmp.c_str(), "wb"); - if (file == nullptr) + GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb")); + if (! file.is_open()) throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); try { m_placeholder_parser_failed_templates.clear(); this->_do_export(*print, file, thumbnail_cb); - fflush(file); - if (ferror(file)) { - fclose(file); + file.flush(); + if (file.is_error()) { + file.close(); boost::nowide::remove(path_tmp.c_str()); throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); } } catch (std::exception & /* ex */) { // Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown. // Close and remove the file. - fclose(file); + file.close(); boost::nowide::remove(path_tmp.c_str()); throw; } - fclose(file); + file.close(); if (! m_placeholder_parser_failed_templates.empty()) { // G-code export proceeded, but some of the PlaceholderParser substitutions failed. @@ -1046,7 +1046,7 @@ std::vector sort_object_instances_by_model_order(const Pri return instances; } -void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thumbnail_cb) +void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb) { PROFILE_FUNC(); @@ -1111,10 +1111,10 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu #endif /* HAS_PRESSURE_EQUALIZER */ // Write information on the generator. - _write_format(file, "; %s\n\n", Slic3r::header_slic3r_generated().c_str()); + file.write_format("; %s\n\n", Slic3r::header_slic3r_generated().c_str()); DoExport::export_thumbnails_to_file(thumbnail_cb, print.full_print_config().option("thumbnails")->values, - [this, file](const char* sz) { this->_write(file, sz); }, + [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); }); // Write notes (content of the Print Settings tab -> Notes) @@ -1125,10 +1125,10 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // Remove the trailing '\r' from the '\r\n' sequence. if (! line.empty() && line.back() == '\r') line.pop_back(); - _write_format(file, "; %s\n", line.c_str()); + file.write_format("; %s\n", line.c_str()); } if (! lines.empty()) - _write(file, "\n"); + file.write("\n"); } print.throw_if_canceled(); @@ -1139,22 +1139,22 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu const double first_layer_height = print.config().first_layer_height.value; for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) { const PrintRegion ®ion = print.get_print_region(region_id); - _write_format(file, "; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); - _write_format(file, "; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width()); - _write_format(file, "; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width()); - _write_format(file, "; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width()); - _write_format(file, "; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width()); + file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); + file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width()); + file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width()); + file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width()); + file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width()); if (print.has_support_material()) - _write_format(file, "; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width()); + file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width()); if (print.config().first_layer_extrusion_width.value > 0) - _write_format(file, "; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, first_layer_height, true).width()); - _write_format(file, "\n"); + file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, first_layer_height, true).width()); + file.write_format("\n"); } print.throw_if_canceled(); // adds tags for time estimators if (print.config().remaining_times.value) - _write_format(file, ";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str()); + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str()); // Prepare the helper object for replacing placeholders in custom G-code and output filename. m_placeholder_parser = print.placeholder_parser(); @@ -1218,7 +1218,7 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // Disable fan. if (! print.config().cooling.get_at(initial_extruder_id) || print.config().disable_fan_first_layers.get_at(initial_extruder_id)) - _write(file, m_writer.set_fan(0, true)); + file.write(m_writer.set_fan(0, true)); // Let the start-up script prime the 1st printing tool. m_placeholder_parser.set("initial_tool", initial_extruder_id); @@ -1261,10 +1261,10 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu this->_print_first_layer_extruder_temperatures(file, print, start_gcode, initial_extruder_id, false); // adds tag for processor - _write_format(file, ";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); + file.write_format(";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); // Write the custom start G-code - _writeln(file, start_gcode); + file.writeln(start_gcode); // Process filament-specific gcode. /* if (has_wipe_tower) { @@ -1272,14 +1272,14 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu } else { DynamicConfig config; config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(initial_extruder_id))); - _writeln(file, this->placeholder_parser_process("start_filament_gcode", print.config().start_filament_gcode.values[initial_extruder_id], initial_extruder_id, &config)); + file.writeln(this->placeholder_parser_process("start_filament_gcode", print.config().start_filament_gcode.values[initial_extruder_id], initial_extruder_id, &config)); } */ this->_print_first_layer_extruder_temperatures(file, print, start_gcode, initial_extruder_id, true); print.throw_if_canceled(); // Set other general things. - _write(file, this->preamble()); + file.write(this->preamble()); // Calculate wiping points if needed DoExport::init_ooze_prevention(print, m_ooze_prevention); @@ -1291,7 +1291,7 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu if (! (has_wipe_tower && print.config().single_extruder_multi_material_priming)) { // Set initial extruder only after custom start G-code. // Ugly hack: Do not set the initial extruder if the extruder is primed using the MMU priming towers at the edge of the print bed. - _write(file, this->set_extruder(initial_extruder_id, 0.)); + file.write(this->set_extruder(initial_extruder_id, 0.)); } // Do all objects for each layer. @@ -1317,8 +1317,8 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // This happens before Z goes down to layer 0 again, so that no collision happens hopefully. m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); - _write(file, this->retract()); - _write(file, this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); + file.write(this->retract()); + file.write(this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. m_avoid_crossing_perimeters.disable_once(); @@ -1330,7 +1330,7 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // Set first layer bed and extruder temperatures, don't wait for it to reach the temperature. this->_print_first_layer_bed_temperature(file, print, between_objects_gcode, initial_extruder_id, false); this->_print_first_layer_extruder_temperatures(file, print, between_objects_gcode, initial_extruder_id, false); - _writeln(file, between_objects_gcode); + file.writeln(between_objects_gcode); } // Reset the cooling buffer internal state (the current position, feed rate, accelerations). m_cooling_buffer->reset(); @@ -1346,7 +1346,7 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu } #ifdef HAS_PRESSURE_EQUALIZER if (m_pressure_equalizer) - _write(file, m_pressure_equalizer->process("", true)); + file.write(m_pressure_equalizer->process("", true)); #endif /* HAS_PRESSURE_EQUALIZER */ ++ finished_objects; // Flag indicating whether the nozzle temperature changes from 1st to 2nd layer were performed. @@ -1361,9 +1361,9 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // Prusa Multi-Material wipe tower. if (has_wipe_tower && ! layers_to_print.empty()) { m_wipe_tower.reset(new WipeTowerIntegration(print.config(), *print.wipe_tower_data().priming.get(), print.wipe_tower_data().tool_changes, *print.wipe_tower_data().final_purge.get())); - _write(file, m_writer.travel_to_z(first_layer_height + m_config.z_offset.value, "Move to the first layer height")); + file.write(m_writer.travel_to_z(first_layer_height + m_config.z_offset.value, "Move to the first layer height")); if (print.config().single_extruder_multi_material_priming) { - _write(file, m_wipe_tower->prime(*this)); + file.write(m_wipe_tower->prime(*this)); // Verify, whether the print overaps the priming extrusions. BoundingBoxf bbox_print(get_print_extrusions_extents(print)); coordf_t twolayers_printz = ((layers_to_print.size() == 1) ? layers_to_print.front() : layers_to_print[1]).first + EPSILON; @@ -1375,15 +1375,15 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu bool overlap = bbox_prime.overlap(bbox_print); if (print.config().gcode_flavor == gcfMarlinLegacy || print.config().gcode_flavor == gcfMarlinFirmware) { - _write(file, this->retract()); - _write(file, "M300 S800 P500\n"); // Beep for 500ms, tone 800Hz. + file.write(this->retract()); + file.write("M300 S800 P500\n"); // Beep for 500ms, tone 800Hz. if (overlap) { // Wait for the user to remove the priming extrusions. - _write(file, "M1 Remove priming towers and click button.\n"); + file.write("M1 Remove priming towers and click button.\n"); } else { // Just wait for a bit to let the user check, that the priming succeeded. //TODO Add a message explaining what the printer is waiting for. This needs a firmware fix. - _write(file, "M1 S10\n"); + file.write("M1 S10\n"); } } else { // This is not Marlin, M1 command is probably not supported. @@ -1410,19 +1410,19 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu } #ifdef HAS_PRESSURE_EQUALIZER if (m_pressure_equalizer) - _write(file, m_pressure_equalizer->process("", true)); + file.write(m_pressure_equalizer->process("", true)); #endif /* HAS_PRESSURE_EQUALIZER */ if (m_wipe_tower) // Purge the extruder, pull out the active filament. - _write(file, m_wipe_tower->finalize(*this)); + file.write(m_wipe_tower->finalize(*this)); } // Write end commands to file. - _write(file, this->retract()); - _write(file, m_writer.set_fan(false)); + file.write(this->retract()); + file.write(m_writer.set_fan(false)); // adds tag for processor - _write_format(file, ";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); + file.write_format(";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); // Process filament-specific gcode in extruder order. { @@ -1434,48 +1434,48 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // Process the end_filament_gcode for the active filament only. int extruder_id = m_writer.extruder()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); - _writeln(file, this->placeholder_parser_process("end_filament_gcode", print.config().end_filament_gcode.get_at(extruder_id), extruder_id, &config)); + file.writeln(this->placeholder_parser_process("end_filament_gcode", print.config().end_filament_gcode.get_at(extruder_id), extruder_id, &config)); } else { for (const std::string &end_gcode : print.config().end_filament_gcode.values) { int extruder_id = (unsigned int)(&end_gcode - &print.config().end_filament_gcode.values.front()); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); - _writeln(file, this->placeholder_parser_process("end_filament_gcode", end_gcode, extruder_id, &config)); + file.writeln(this->placeholder_parser_process("end_filament_gcode", end_gcode, extruder_id, &config)); } } - _writeln(file, this->placeholder_parser_process("end_gcode", print.config().end_gcode, m_writer.extruder()->id(), &config)); + file.writeln(this->placeholder_parser_process("end_gcode", print.config().end_gcode, m_writer.extruder()->id(), &config)); } - _write(file, m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% - _write(file, m_writer.postamble()); + file.write(m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% + file.write(m_writer.postamble()); // adds tags for time estimators if (print.config().remaining_times.value) - _write_format(file, ";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Last_Line_M73_Placeholder).c_str()); + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Last_Line_M73_Placeholder).c_str()); print.throw_if_canceled(); // Get filament stats. - _write(file, DoExport::update_print_stats_and_format_filament_stats( + file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), m_writer.extruders(), // Modifies print.m_print_statistics)); - _write(file, "\n"); - _write_format(file, "; total filament used [g] = %.2lf\n", print.m_print_statistics.total_weight); - _write_format(file, "; total filament cost = %.2lf\n", print.m_print_statistics.total_cost); + file.write("\n"); + file.write_format("; total filament used [g] = %.2lf\n", print.m_print_statistics.total_weight); + file.write_format("; total filament cost = %.2lf\n", print.m_print_statistics.total_cost); if (print.m_print_statistics.total_toolchanges > 0) - _write_format(file, "; total toolchanges = %i\n", print.m_print_statistics.total_toolchanges); - _write_format(file, ";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); + file.write_format("; total toolchanges = %i\n", print.m_print_statistics.total_toolchanges); + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); // Append full config, delimited by two 'phony' configuration keys prusaslicer_config = begin and prusaslicer_config = end. // The delimiters are structured as configuration key / value pairs to be parsable by older versions of PrusaSlicer G-code viewer. { - _write(file, "\n; prusaslicer_config = begin\n"); + file.write("\n; prusaslicer_config = begin\n"); std::string full_config; append_full_config(print, full_config); if (!full_config.empty()) - _write(file, full_config); - _write(file, "; prusaslicer_config = end\n"); + file.write(full_config); + file.write("; prusaslicer_config = end\n"); } print.throw_if_canceled(); } @@ -1565,16 +1565,16 @@ static bool custom_gcode_sets_temperature(const std::string &gcode, const int mc // Print the machine envelope G-code for the Marlin firmware based on the "machine_max_xxx" parameters. // Do not process this piece of G-code by the time estimator, it already knows the values through another sources. -void GCode::print_machine_envelope(FILE *file, Print &print) +void GCode::print_machine_envelope(GCodeOutputStream &file, Print &print) { if ((print.config().gcode_flavor.value == gcfMarlinLegacy || print.config().gcode_flavor.value == gcfMarlinFirmware) && print.config().machine_limits_usage.value == MachineLimitsUsage::EmitToGCode) { - fprintf(file, "M201 X%d Y%d Z%d E%d ; sets maximum accelerations, mm/sec^2\n", + file.write_format("M201 X%d Y%d Z%d E%d ; sets maximum accelerations, mm/sec^2\n", int(print.config().machine_max_acceleration_x.values.front() + 0.5), int(print.config().machine_max_acceleration_y.values.front() + 0.5), int(print.config().machine_max_acceleration_z.values.front() + 0.5), int(print.config().machine_max_acceleration_e.values.front() + 0.5)); - fprintf(file, "M203 X%d Y%d Z%d E%d ; sets maximum feedrates, mm/sec\n", + file.write_format("M203 X%d Y%d Z%d E%d ; sets maximum feedrates, mm/sec\n", int(print.config().machine_max_feedrate_x.values.front() + 0.5), int(print.config().machine_max_feedrate_y.values.front() + 0.5), int(print.config().machine_max_feedrate_z.values.front() + 0.5), @@ -1587,18 +1587,18 @@ void GCode::print_machine_envelope(FILE *file, Print &print) int travel_acc = print.config().gcode_flavor == gcfMarlinLegacy ? int(print.config().machine_max_acceleration_extruding.values.front() + 0.5) : int(print.config().machine_max_acceleration_travel.values.front() + 0.5); - fprintf(file, "M204 P%d R%d T%d ; sets acceleration (P, T) and retract acceleration (R), mm/sec^2\n", + file.write_format("M204 P%d R%d T%d ; sets acceleration (P, T) and retract acceleration (R), mm/sec^2\n", int(print.config().machine_max_acceleration_extruding.values.front() + 0.5), int(print.config().machine_max_acceleration_retracting.values.front() + 0.5), travel_acc); assert(is_decimal_separator_point()); - fprintf(file, "M205 X%.2lf Y%.2lf Z%.2lf E%.2lf ; sets the jerk limits, mm/sec\n", + file.write_format("M205 X%.2lf Y%.2lf Z%.2lf E%.2lf ; sets the jerk limits, mm/sec\n", print.config().machine_max_jerk_x.values.front(), print.config().machine_max_jerk_y.values.front(), print.config().machine_max_jerk_z.values.front(), print.config().machine_max_jerk_e.values.front()); - fprintf(file, "M205 S%d T%d ; sets the minimum extruding and travel feed rate, mm/sec\n", + file.write_format("M205 S%d T%d ; sets the minimum extruding and travel feed rate, mm/sec\n", int(print.config().machine_min_extruding_rate.values.front() + 0.5), int(print.config().machine_min_travel_rate.values.front() + 0.5)); } @@ -1608,7 +1608,7 @@ void GCode::print_machine_envelope(FILE *file, Print &print) // Only do that if the start G-code does not already contain any M-code controlling an extruder temperature. // M140 - Set Extruder Temperature // M190 - Set Extruder Temperature and Wait -void GCode::_print_first_layer_bed_temperature(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) +void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) { // Initial bed temperature based on the first extruder. int temp = print.config().first_layer_bed_temperature.get_at(first_printing_extruder_id); @@ -1621,7 +1621,7 @@ void GCode::_print_first_layer_bed_temperature(FILE *file, Print &print, const s // the custom start G-code emited these. std::string set_temp_gcode = m_writer.set_bed_temperature(temp, wait); if (! temp_set_by_gcode) - _write(file, set_temp_gcode); + file.write(set_temp_gcode); } // Write 1st layer extruder temperatures into the G-code. @@ -1629,7 +1629,7 @@ void GCode::_print_first_layer_bed_temperature(FILE *file, Print &print, const s // M104 - Set Extruder Temperature // M109 - Set Extruder Temperature and Wait // RepRapFirmware: G10 Sxx -void GCode::_print_first_layer_extruder_temperatures(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) +void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) { // Is the bed temperature set by the provided custom G-code? int temp_by_gcode = -1; @@ -1646,7 +1646,7 @@ void GCode::_print_first_layer_extruder_temperatures(FILE *file, Print &print, c // Set temperature of the first printing extruder only. int temp = print.config().first_layer_temperature.get_at(first_printing_extruder_id); if (temp > 0) - _write(file, m_writer.set_temperature(temp, wait, first_printing_extruder_id)); + file.write(m_writer.set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -1654,7 +1654,7 @@ void GCode::_print_first_layer_extruder_temperatures(FILE *file, Print &print, c if (print.config().ooze_prevention.value) temp += print.config().standby_temperature_delta.value; if (temp > 0) - _write(file, m_writer.set_temperature(temp, wait, tool_id)); + file.write(m_writer.set_temperature(temp, wait, tool_id)); } } } @@ -1891,7 +1891,7 @@ namespace Skirt { // and performing the extruder specific extrusions together. void GCode::process_layer( // Write into the output file. - FILE *file, + GCodeOutputStream &file, const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. const std::vector &layers, @@ -2306,7 +2306,7 @@ void GCode::process_layer( // printf("G-code after filter:\n%s\n", out.c_str()); #endif /* HAS_PRESSURE_EQUALIZER */ - _write(file, gcode); + file.write(gcode); BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z << log_memory_info(); } @@ -2642,22 +2642,22 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill return gcode; } -void GCode::_write(FILE* file, const char *what) +void GCode::GCodeOutputStream::write(const char *what) { if (what != nullptr) { const char* gcode = what; // writes string to file - fwrite(gcode, 1, ::strlen(gcode), file); + fwrite(gcode, 1, ::strlen(gcode), this->f); } } -void GCode::_writeln(FILE* file, const std::string &what) +void GCode::GCodeOutputStream::writeln(const std::string &what) { if (! what.empty()) - _write(file, (what.back() == '\n') ? what : (what + '\n')); + this->write(what.back() == '\n' ? what : what + '\n'); } -void GCode::_write_format(FILE* file, const char* format, ...) +void GCode::GCodeOutputStream::write_format(const char* format, ...) { va_list args; va_start(args, format); @@ -2681,7 +2681,7 @@ void GCode::_write_format(FILE* file, const char* format, ...) char *bufptr = buffer_dynamic ? (char*)malloc(buflen) : buffer; int res = ::vsnprintf(bufptr, buflen, format, args); if (res > 0) - _write(file, bufptr); + this->write(bufptr); if (buffer_dynamic) free(bufptr); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d2d241054f..06900410ae 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -184,13 +184,39 @@ public: }; private: - void _do_export(Print &print, FILE *file, ThumbnailsGeneratorCallback thumbnail_cb); + class GCodeOutputStream { + public: + GCodeOutputStream(FILE *f) : f(f) {} + ~GCodeOutputStream() { this->close(); } + + bool is_open() const { return f; } + bool is_error() const { return ::ferror(f); } + + void flush() { ::fflush(f); } + void close() { if (f) ::fclose(f); } + + // Write a string into a file. + void write(const std::string& what) { this->write(what.c_str()); } + void write(const char* what); + + // Write a string into a file. + // Add a newline, if the string does not end with a newline already. + // Used to export a custom G-code section processed by the PlaceholderParser. + void writeln(const std::string& what); + + // Formats and write into a file the given data. + void write_format(const char* format, ...); + + private: + FILE *f = nullptr; + }; + void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); static std::vector collect_layers_to_print(const PrintObject &object); static std::vector>> collect_layers_to_print(const Print &print); void process_layer( // Write into the output file. - FILE *file, + GCodeOutputStream &file, const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. const std::vector &layers, @@ -358,22 +384,10 @@ private: // Processor GCodeProcessor m_processor; - // Write a string into a file. - void _write(FILE* file, const std::string& what) { this->_write(file, what.c_str()); } - void _write(FILE* file, const char *what); - - // Write a string into a file. - // Add a newline, if the string does not end with a newline already. - // Used to export a custom G-code section processed by the PlaceholderParser. - void _writeln(FILE* file, const std::string& what); - - // Formats and write into a file the given data. - void _write_format(FILE* file, const char* format, ...); - std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1); - void print_machine_envelope(FILE *file, Print &print); - void _print_first_layer_bed_temperature(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); - void _print_first_layer_extruder_temperatures(FILE *file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); + void print_machine_envelope(GCodeOutputStream &file, Print &print); + void _print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); + void _print_first_layer_extruder_temperatures(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); // On the first printing layer. This flag triggers first layer speeds. bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0; } // To control print speed of 1st object layer over raft interface. From eb2debc2c6f212dfe3bed71f0a072bd674a20e5b Mon Sep 17 00:00:00 2001 From: YuSanka Date: Tue, 7 Sep 2021 16:48:13 +0200 Subject: [PATCH 121/151] ObjectList: Fixed DnD of Volumes inside the Object --- src/slic3r/GUI/ObjectDataViewModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 9417364ef1..88527e9fd3 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -1361,7 +1361,8 @@ wxDataViewItem ObjectDataViewModel::ReorganizeChildren( const int current_volume if (!node_parent) // happens if item.IsOk()==false return ret_item; - const size_t shift = node_parent->GetChildren().Item(0)->m_type == itSettings ? 1 : 0; + size_t shift; + for (shift = 0; shift < node_parent->GetChildCount() && node_parent->GetNthChild(shift)->GetType() != itVolume; shift ++); ObjectDataViewModelNode *deleted_node = node_parent->GetNthChild(current_volume_id+shift); node_parent->GetChildren().Remove(deleted_node); From 152e236ddad9170ac22afb7a82db455c357f66b8 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 7 Sep 2021 17:25:53 +0200 Subject: [PATCH 122/151] Trying to fix the windows build --- src/libslic3r/GCode.cpp | 4 ++++ src/libslic3r/GCode.hpp | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 54815188eb..ff00768807 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2642,6 +2642,10 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill return gcode; } +bool GCode::GCodeOutputStream::is_error() const { return ::ferror(f); } +void GCode::GCodeOutputStream::flush() { ::fflush(f); } +void GCode::GCodeOutputStream::close() { if (f) ::fclose(f); } + void GCode::GCodeOutputStream::write(const char *what) { if (what != nullptr) { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 06900410ae..89e4570a95 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -190,10 +190,10 @@ private: ~GCodeOutputStream() { this->close(); } bool is_open() const { return f; } - bool is_error() const { return ::ferror(f); } + bool is_error() const; - void flush() { ::fflush(f); } - void close() { if (f) ::fclose(f); } + void flush(); + void close(); // Write a string into a file. void write(const std::string& what) { this->write(what.c_str()); } From 0da0a7b2a0e2a45f55ce51434761f37d34937df6 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 7 Sep 2021 17:58:06 +0200 Subject: [PATCH 123/151] Fix of recent GCode / GCodeProcessor refactoring: Don't close a FILE twice. --- src/libslic3r/GCode.cpp | 20 +++++++++++++++++--- src/libslic3r/GCode/GCodeProcessor.cpp | 7 ++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ff00768807..f2deba0467 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2642,9 +2642,23 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill return gcode; } -bool GCode::GCodeOutputStream::is_error() const { return ::ferror(f); } -void GCode::GCodeOutputStream::flush() { ::fflush(f); } -void GCode::GCodeOutputStream::close() { if (f) ::fclose(f); } +bool GCode::GCodeOutputStream::is_error() const +{ + return ::ferror(this->f); +} + +void GCode::GCodeOutputStream::flush() +{ + ::fflush(this->f); +} + +void GCode::GCodeOutputStream::close() +{ + if (this->f) { + ::fclose(this->f); + this->f = nullptr; + } +} void GCode::GCodeOutputStream::write(const char *what) { diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ec586dbef7..60af399c06 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -350,7 +350,12 @@ void GCodeProcessor::TimeProcessor::reset() struct FilePtr { FilePtr(FILE *f) : f(f) {} ~FilePtr() { this->close(); } - void close() { if (f) ::fclose(f); } + void close() { + if (this->f) { + ::fclose(this->f); + this->f = nullptr; + } + } FILE* f = nullptr; }; From 9abebe5fbe6950d809b51926af8772317fe2657f Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 8 Sep 2021 13:04:43 +0200 Subject: [PATCH 124/151] Techs ENABLE_SEAMS_USING_MODELS and ENABLE_SEAMS_USING_BATCHED_MODELS - Fixed seams (and other options) zs to match toolpaths zs in preview --- resources/shaders/gouraud_light_instanced.vs | 4 +++- src/slic3r/GUI/GCodeViewer.cpp | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/shaders/gouraud_light_instanced.vs b/resources/shaders/gouraud_light_instanced.vs index a8931361d0..997b6a2bf9 100644 --- a/resources/shaders/gouraud_light_instanced.vs +++ b/resources/shaders/gouraud_light_instanced.vs @@ -34,7 +34,9 @@ void main() float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; - vec4 world_position = vec4(v_position * vec3(vec2(i_scales.x), i_scales.y) + i_offset, 1.0); + float width = 1.5 * i_scales.x; + float height = 1.5 * i_scales.y; + vec4 world_position = vec4(v_position * vec3(vec2(width), height) + i_offset - vec3(0.0, 0.0, 0.5 * i_scales.y), 1.0); vec3 eye_position = (gl_ModelViewMatrix * world_position).xyz; intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(eye_position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 7e8e587a6e..ac97b8dbc5 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1414,9 +1414,9 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) instances.push_back(curr.position.y()); instances.push_back(curr.position.z()); // append width - instances.push_back(1.5f * curr.width); + instances.push_back(curr.width); // append height - instances.push_back(1.5f * curr.height); + instances.push_back(curr.height); // append id instances_ids.push_back(move_id); @@ -1428,7 +1428,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) const double width = static_cast(1.5f * curr.width); const double height = static_cast(1.5f * curr.height); - const Transform3d trafo = Geometry::assemble_transform(curr.position.cast(), Vec3d::Zero(), { width, width, height }); + const Transform3d trafo = Geometry::assemble_transform((curr.position - 0.5f * curr.height * Vec3f::UnitZ()).cast(), Vec3d::Zero(), { width, width, height }); const Eigen::Matrix normal_matrix = trafo.matrix().template block<3, 3>(0, 0).inverse().transpose(); for (const auto& entity : data.entities) { From 6f678c09d1358fdbfbe50452604797d8f03c3056 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 8 Sep 2021 14:30:40 +0200 Subject: [PATCH 125/151] Tech ENABLE_FIX_PREVIEW_OPTIONS_Z - Enable fixing the z position of seams (and other options) in preview --- src/libslic3r/GCode/GCodeProcessor.cpp | 23 ++++++++++++++ src/libslic3r/GCode/GCodeProcessor.hpp | 42 ++++++++++++++++++++++++++ src/libslic3r/Technologies.hpp | 9 ++++++ src/slic3r/GUI/GCodeViewer.cpp | 4 +++ 4 files changed, 78 insertions(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 60af399c06..a217fa1020 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -828,6 +828,9 @@ bool GCodeProcessor::contains_reserved_tags(const std::string& gcode, unsigned i } GCodeProcessor::GCodeProcessor() +#if ENABLE_FIX_PREVIEW_OPTIONS_Z +: m_options_z_corrector(m_result) +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z { reset(); m_time_processor.machines[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].line_m73_main_mask = "M73 P%s R%s\n"; @@ -1202,6 +1205,10 @@ void GCodeProcessor::reset() m_last_default_color_id = 0; #endif // ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + m_options_z_corrector.reset(); +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z + #if ENABLE_GCODE_VIEWER_DATA_CHECKING m_mm3_per_mm_compare.reset(); m_height_compare.reset(); @@ -1781,6 +1788,9 @@ void GCodeProcessor::process_tags(const std::string_view comment) #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER CustomGCode::Item item = { static_cast(m_end_position[2]), CustomGCode::ColorChange, extruder_id + 1, color, "" }; m_result.custom_gcode_per_print_z.emplace_back(item); +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + m_options_z_corrector.set(); +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z process_custom_gcode_time(CustomGCode::ColorChange); process_filaments(CustomGCode::ColorChange); #endif // ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER @@ -1800,6 +1810,9 @@ void GCodeProcessor::process_tags(const std::string_view comment) #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER CustomGCode::Item item = { static_cast(m_end_position[2]), CustomGCode::PausePrint, m_extruder_id + 1, "", "" }; m_result.custom_gcode_per_print_z.emplace_back(item); +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + m_options_z_corrector.set(); +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z #endif // ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER process_custom_gcode_time(CustomGCode::PausePrint); return; @@ -1811,6 +1824,9 @@ void GCodeProcessor::process_tags(const std::string_view comment) #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER CustomGCode::Item item = { static_cast(m_end_position[2]), CustomGCode::Custom, m_extruder_id + 1, "", "" }; m_result.custom_gcode_per_print_z.emplace_back(item); +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + m_options_z_corrector.set(); +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z #endif // ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER return; } @@ -2391,7 +2407,9 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) else { if (m_end_position[Z] > m_extruded_last_z + EPSILON) { m_height = m_end_position[Z] - m_extruded_last_z; +#if !ENABLE_FIX_PREVIEW_OPTIONS_Z m_extruded_last_z = m_end_position[Z]; +#endif // !ENABLE_FIX_PREVIEW_OPTIONS_Z } } @@ -2401,6 +2419,11 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) if (m_end_position[Z] == 0.0f) m_end_position[Z] = m_height; +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + m_extruded_last_z = m_end_position[Z]; + m_options_z_corrector.update(m_height); +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z + #if ENABLE_GCODE_VIEWER_DATA_CHECKING m_height_compare.update(m_height, m_extrusion_role); #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index bb93108881..a1730daf24 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -390,6 +390,45 @@ namespace Slic3r { bool has_first_vertex() const { return m_first_vertex.has_value(); } }; +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + // Helper class used to fix the z for color change, pause print and + // custom gcode markes + class OptionsZCorrector + { + Result& m_result; + std::optional m_move_id; + std::optional m_custom_gcode_per_print_z_id; + + public: + explicit OptionsZCorrector(Result& result) : m_result(result) { + } + + void set() { + m_move_id = m_result.moves.size() - 1; + m_custom_gcode_per_print_z_id = m_result.custom_gcode_per_print_z.size() - 1; + } + + void update(float height) { + if (!m_move_id.has_value() || !m_custom_gcode_per_print_z_id.has_value()) + return; + + const Vec3f position = m_result.moves.back().position; + + MoveVertex& move = m_result.moves.emplace_back(m_result.moves[m_move_id.value()]); + move.position = position; + move.height = height; + m_result.moves.erase(m_result.moves.begin() + m_move_id.value()); + m_result.custom_gcode_per_print_z[m_custom_gcode_per_print_z_id.value()].print_z = position.z(); + reset(); + } + + void reset() { + m_move_id.reset(); + m_custom_gcode_per_print_z_id.reset(); + } + }; +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z + #if ENABLE_GCODE_VIEWER_DATA_CHECKING struct DataChecker { @@ -494,6 +533,9 @@ namespace Slic3r { CpColor m_cp_color; bool m_use_volumetric_e; SeamsDetector m_seams_detector; +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + OptionsZCorrector m_options_z_corrector; +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER size_t m_last_default_color_id; #endif // ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 6132430f1a..adcde55723 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -57,4 +57,13 @@ #define ENABLE_SAVE_COMMANDS_ALWAYS_ENABLED (1 && ENABLE_2_4_0_ALPHA0) +//==================== +// 2.4.0.alpha2 techs +//==================== +#define ENABLE_2_4_0_ALPHA2 1 + +// Enable fixing the z position of color change, pause print and custom gcode markers in preview +#define ENABLE_FIX_PREVIEW_OPTIONS_Z (1 && ENABLE_SEAMS_USING_MODELS && ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER && ENABLE_2_4_0_ALPHA2) + + #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 946cbb2759..9b4e2ef8db 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2370,7 +2370,11 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool if (m_sequential_view.current.first <= buffer.model.instances.s_ids.back() && buffer.model.instances.s_ids.front() <= m_sequential_view.current.last) { for (size_t id : buffer.model.instances.s_ids) { if (has_second_range) { +#if ENABLE_FIX_PREVIEW_OPTIONS_Z + if (id < m_sequential_view.endpoints.first) { +#else if (id <= m_sequential_view.endpoints.first) { +#endif // ENABLE_FIX_PREVIEW_OPTIONS_Z ++buffer.model.instances.render_ranges.ranges.front().offset; if (id <= m_sequential_view.current.first) ++buffer.model.instances.render_ranges.ranges.back().offset; From 88d608351d0b5f3d4c678ffe673cb818a1ad6dd5 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 8 Sep 2021 14:34:31 +0200 Subject: [PATCH 126/151] Fix after merge with master --- src/libslic3r/Technologies.hpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 1183b51df4..ac92ecd108 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -64,13 +64,6 @@ // Enable rendering seams (and other options) in preview using batched models on systems not supporting OpenGL 3.3 #define ENABLE_SEAMS_USING_BATCHED_MODELS (1 && ENABLE_SEAMS_USING_MODELS && ENABLE_2_4_0_ALPHA2) - - -//==================== -// 2.4.0.alpha2 techs -//==================== -#define ENABLE_2_4_0_ALPHA2 1 - // Enable fixing the z position of color change, pause print and custom gcode markers in preview #define ENABLE_FIX_PREVIEW_OPTIONS_Z (1 && ENABLE_SEAMS_USING_MODELS && ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER && ENABLE_2_4_0_ALPHA2) From beee18f22991e369b1722a43bbcb692fa0d68af0 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 8 Sep 2021 15:06:05 +0200 Subject: [PATCH 127/151] Follow-up to b5a007a683124ec3b01b61e892ed8eb39c034248 WIP to G-code export parallelization through pipelining: GCodeProcessor is called during the G-code export, the G-code is no more reopened and re-read, but it is pipelined from the G-code generator. --- src/libslic3r/GCode.cpp | 7 +++- src/libslic3r/GCode.hpp | 3 +- src/libslic3r/GCode/GCodeProcessor.cpp | 54 ++++++++++++++++++-------- src/libslic3r/GCode/GCodeProcessor.hpp | 13 ++++--- src/libslic3r/GCodeReader.hpp | 3 +- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 3 +- 7 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f2deba0467..efea240e55 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -744,7 +744,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessor::Result* re std::string path_tmp(path); path_tmp += ".tmp"; - GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb")); + m_processor.initialize(path_tmp); + GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); @@ -782,7 +783,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessor::Result* re } BOOST_LOG_TRIVIAL(debug) << "Start processing gcode, " << log_memory_info(); - m_processor.process_file(path_tmp, true, [print]() { print->throw_if_canceled(); }); + m_processor.finalize(); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics); if (result != nullptr) { @@ -2666,6 +2667,8 @@ void GCode::GCodeOutputStream::write(const char *what) const char* gcode = what; // writes string to file fwrite(gcode, 1, ::strlen(gcode), this->f); + //FIXME don't allocate a string, maybe process a batch of lines? + m_processor.process_buffer(std::string(gcode)); } } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 89e4570a95..1dbc5b70a0 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -186,7 +186,7 @@ public: private: class GCodeOutputStream { public: - GCodeOutputStream(FILE *f) : f(f) {} + GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {} ~GCodeOutputStream() { this->close(); } bool is_open() const { return f; } @@ -209,6 +209,7 @@ private: private: FILE *f = nullptr; + GCodeProcessor &m_processor; }; void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 60af399c06..1891c90b4a 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1189,7 +1189,6 @@ void GCodeProcessor::reset() m_cp_color.reset(); m_producer = EProducer::Unknown; - m_producers_enabled = false; m_time_processor.reset(); m_used_filaments.reset(); @@ -1209,7 +1208,7 @@ void GCodeProcessor::reset() #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING } -void GCodeProcessor::process_file(const std::string& filename, bool apply_postprocess, std::function cancel_callback) +void GCodeProcessor::process_file(const std::string& filename, std::function cancel_callback) { CNumericLocalesSetter locales_setter; @@ -1219,7 +1218,7 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr // pre-processing // parse the gcode file to detect its producer - if (m_producers_enabled) { + { m_parser.parse_file(filename, [this](GCodeReader& reader, const GCodeReader::GCodeLine& line) { const std::string_view cmd = line.cmd(); if (cmd.empty()) { @@ -1228,6 +1227,7 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr m_parser.quit_parsing(); } }); + m_parser.reset(); // if the gcode was produced by PrusaSlicer, // extract the config from it @@ -1257,9 +1257,37 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr if (cancel_callback) cancel_callback(); } - this->process_gcode_line(line); + this->process_gcode_line(line, true); }); + this->finalize(); + + // post-process to add M73 lines into the gcode +#if ENABLE_GCODE_VIEWER_STATISTICS + m_result.time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS +} + +void GCodeProcessor::initialize(const std::string& filename) +{ + assert(is_decimal_separator_point()); + // process gcode + m_result.filename = filename; + m_result.id = ++s_result_id; + // 1st move must be a dummy move + m_result.moves.emplace_back(MoveVertex()); +} + +void GCodeProcessor::process_buffer(const std::string &buffer) +{ + //FIXME maybe cache GCodeLine gline to be over multiple parse_buffer() invocations. + m_parser.parse_buffer(buffer, [this](GCodeReader&, const GCodeReader::GCodeLine& line) { + this->process_gcode_line(line, false); + }); +} + +void GCodeProcessor::finalize() +{ // update width/height of wipe moves for (MoveVertex& move : m_result.moves) { if (move.type == EMoveType::Wipe) { @@ -1281,10 +1309,6 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr update_estimated_times_stats(); - // post-process to add M73 lines into the gcode - if (apply_postprocess) - m_time_processor.post_process(filename, m_result.moves, m_result.lines_ends); - #if ENABLE_GCODE_VIEWER_DATA_CHECKING std::cout << "\n"; m_mm3_per_mm_compare.output(); @@ -1292,9 +1316,7 @@ void GCodeProcessor::process_file(const std::string& filename, bool apply_postpr m_width_compare.output(); #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING -#if ENABLE_GCODE_VIEWER_STATISTICS - m_result.time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); -#endif // ENABLE_GCODE_VIEWER_STATISTICS + m_time_processor.post_process(m_result.filename, m_result.moves, m_result.lines_ends); } float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const @@ -1432,7 +1454,7 @@ void GCodeProcessor::apply_config_simplify3d(const std::string& filename) } } -void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line) +void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line, bool producers_enabled) { /* std::cout << line.raw() << std::endl; */ @@ -1615,7 +1637,7 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line) if (comment.length() > 2 && comment.front() == ';') // Process tags embedded into comments. Tag comments always start at the start of a line // with a comment and continue with a tag without any whitespace separator. - process_tags(comment.substr(1)); + process_tags(comment.substr(1), producers_enabled); } } @@ -1664,10 +1686,10 @@ template } } -void GCodeProcessor::process_tags(const std::string_view comment) +void GCodeProcessor::process_tags(const std::string_view comment, bool producers_enabled) { // producers tags - if (m_producers_enabled && process_producers_tags(comment)) + if (producers_enabled && process_producers_tags(comment)) return; // extrusion role tag @@ -1691,7 +1713,7 @@ void GCodeProcessor::process_tags(const std::string_view comment) return; } - if (!m_producers_enabled || m_producer == EProducer::PrusaSlicer) { + if (!producers_enabled || m_producer == EProducer::PrusaSlicer) { // height tag if (boost::starts_with(comment, reserved_tag(ETags::Height))) { if (!parse_number(comment.substr(reserved_tag(ETags::Height).size()), m_forced_height)) diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index bb93108881..1665fb9f2d 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -513,7 +513,6 @@ namespace Slic3r { static const std::vector> Producers; EProducer m_producer; - bool m_producers_enabled; TimeProcessor m_time_processor; UsedFilaments m_used_filaments; @@ -536,7 +535,6 @@ namespace Slic3r { return m_time_processor.machines[static_cast(PrintEstimatedStatistics::ETimeMode::Stealth)].enabled; } void enable_machine_envelope_processing(bool enabled) { m_time_processor.machine_envelope_processing_enabled = enabled; } - void enable_producers(bool enabled) { m_producers_enabled = enabled; } void reset(); const Result& get_result() const { return m_result; } @@ -544,7 +542,12 @@ namespace Slic3r { // Process the gcode contained in the file with the given filename // throws CanceledException through print->throw_if_canceled() (sent by the caller as callback). - void process_file(const std::string& filename, bool apply_postprocess, std::function cancel_callback = nullptr); + void process_file(const std::string& filename, std::function cancel_callback = nullptr); + + // Streaming interface, for processing G-codes just generated by PrusaSlicer in a pipelined fashion. + void initialize(const std::string& filename); + void process_buffer(const std::string& buffer); + void finalize(); float get_time(PrintEstimatedStatistics::ETimeMode mode) const; std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const; @@ -557,10 +560,10 @@ namespace Slic3r { private: void apply_config(const DynamicPrintConfig& config); void apply_config_simplify3d(const std::string& filename); - void process_gcode_line(const GCodeReader::GCodeLine& line); + void process_gcode_line(const GCodeReader::GCodeLine& line, bool producers_enabled); // Process tags embedded into comments - void process_tags(const std::string_view comment); + void process_tags(const std::string_view comment, bool producers_enabled); bool process_producers_tags(const std::string_view comment); bool process_prusaslicer_tags(const std::string_view comment); bool process_cura_tags(const std::string_view comment); diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 8c820b95df..15376c0fc6 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -77,7 +77,8 @@ public: typedef std::function callback_t; - GCodeReader() : m_verbose(false), m_extrusion_axis('E') { memset(m_position, 0, sizeof(m_position)); } + GCodeReader() : m_verbose(false), m_extrusion_axis('E') { this->reset(); } + void reset() { memset(m_position, 0, sizeof(m_position)); } void apply_config(const GCodeConfig &config); void apply_config(const DynamicPrintConfig &config); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index ac97b8dbc5..e61d1356a1 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1190,7 +1190,7 @@ void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) }; auto add_indices_as_line = [](const GCodeProcessor::MoveVertex& prev, const GCodeProcessor::MoveVertex& curr, TBuffer& buffer, unsigned int ibuffer_id, IndexBuffer& indices, size_t move_id) { - if (prev.type != curr.type || !buffer.paths.back().matches(curr)) { + if (buffer.paths.empty() || prev.type != curr.type || !buffer.paths.back().matches(curr)) { // add starting index indices.push_back(static_cast(indices.size())); buffer.add_path(curr, ibuffer_id, indices.size() - 1, move_id - 1); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2c77dcb7ac..76612a718c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4871,10 +4871,9 @@ void Plater::load_gcode(const wxString& filename) // process gcode GCodeProcessor processor; - processor.enable_producers(true); try { - processor.process_file(filename.ToUTF8().data(), false); + processor.process_file(filename.ToUTF8().data()); } catch (const std::exception& ex) { From a115da63347d52704fdea659e7b3f1f2438dca38 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 8 Sep 2021 15:30:51 +0200 Subject: [PATCH 128/151] Fix for #6921 - Error while disabling filament overrides in Dutch language --- src/slic3r/GUI/Field.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 1aaac54690..06cfd0a1c1 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -230,18 +230,21 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true } double val; + bool is_na_value = m_opt.nullable && str == na_value(); + const char dec_sep = is_decimal_separator_point() ? '.' : ','; const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - if (str.Replace(dec_sep_alt, dec_sep, false) != 0) + // Replace the first incorrect separator in decimal number, + // if this value doesn't "N/A" value in some language + // see https://github.com/prusa3d/PrusaSlicer/issues/6921 + if (!is_na_value && str.Replace(dec_sep_alt, dec_sep, false) != 0) set_value(str, false); - if (str == dec_sep) val = 0.0; else { - if (m_opt.nullable && str == na_value()) + if (is_na_value) val = ConfigOptionFloatsNullable::nil_value(); else if (!str.ToDouble(&val)) { From d3382b666fed0fee2067a8c30314e664fce8a8e7 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 8 Sep 2021 15:47:27 +0200 Subject: [PATCH 129/151] Fixed build on MAC --- src/libslic3r/GCode/GCodeProcessor.hpp | 6 +++--- src/slic3r/GUI/GCodeViewer.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index a1730daf24..ac0fb0d870 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -414,11 +414,11 @@ namespace Slic3r { const Vec3f position = m_result.moves.back().position; - MoveVertex& move = m_result.moves.emplace_back(m_result.moves[m_move_id.value()]); + MoveVertex& move = m_result.moves.emplace_back(m_result.moves[*m_move_id]); move.position = position; move.height = height; - m_result.moves.erase(m_result.moves.begin() + m_move_id.value()); - m_result.custom_gcode_per_print_z[m_custom_gcode_per_print_z_id.value()].print_z = position.z(); + m_result.moves.erase(m_result.moves.begin() + *m_move_id); + m_result.custom_gcode_per_print_z[*m_custom_gcode_per_print_z_id].print_z = position.z(); reset(); } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 7e83831205..0f019b2bf8 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -3045,7 +3045,7 @@ void GCodeViewer::render_legend(float& legend_height) bool imperial_units = wxGetApp().app_config->get("use_inches") == "1"; - auto append_item = [this, icon_size, percent_bar_size, &imgui, imperial_units](EItemType type, const Color& color, const std::string& label, + auto append_item = [icon_size, percent_bar_size, &imgui, imperial_units](EItemType type, const Color& color, const std::string& label, bool visible = true, const std::string& time = "", float percent = 0.0f, float max_percent = 0.0f, const std::array& offsets = { 0.0f, 0.0f, 0.0f, 0.0f }, double used_filament_m = 0.0, double used_filament_g = 0.0, std::function callback = nullptr) { From fe8e2ae0d1889127e5df1b504cafc1c259fbf042 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 9 Sep 2021 09:35:58 +0200 Subject: [PATCH 130/151] ENABLE_GCODE_VIEWER_STATISTICS - Fixed calculation of time required by GCodeProcessor --- src/libslic3r/GCode/GCodeProcessor.cpp | 15 +++++++++------ src/libslic3r/GCode/GCodeProcessor.hpp | 3 +++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 1891c90b4a..8ebd82d663 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1213,7 +1213,7 @@ void GCodeProcessor::process_file(const std::string& filename, std::functionfinalize(); - - // post-process to add M73 lines into the gcode -#if ENABLE_GCODE_VIEWER_STATISTICS - m_result.time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); -#endif // ENABLE_GCODE_VIEWER_STATISTICS } void GCodeProcessor::initialize(const std::string& filename) { assert(is_decimal_separator_point()); + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_start_time = std::chrono::high_resolution_clock::now(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + // process gcode m_result.filename = filename; m_result.id = ++s_result_id; @@ -1317,6 +1317,9 @@ void GCodeProcessor::finalize() #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING m_time_processor.post_process(m_result.filename, m_result.moves, m_result.lines_ends); +#if ENABLE_GCODE_VIEWER_STATISTICS + m_result.time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - m_start_time).count(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS } float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 1665fb9f2d..ea594b3fce 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -497,6 +497,9 @@ namespace Slic3r { #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER size_t m_last_default_color_id; #endif // ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER +#if ENABLE_GCODE_VIEWER_STATISTICS + std::chrono::time_point m_start_time; +#endif // ENABLE_GCODE_VIEWER_STATISTICS enum class EProducer { From 9c5c9a0e78660f8f7715aeb8c9b2896403a3c0f8 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 9 Sep 2021 17:15:55 +0200 Subject: [PATCH 131/151] ObjectList: Fixed a bug related to an update of selection in 3DScene. Steps to repro: 1. Create some object with several parts. 2. Increase instances count. 3. Select some volume in ObjectList => all related volumes for each instance are selected in 3DScene (CORRECT) 4. Select last instance in ObjectList => all volumes (except one) of selected instance are selected in 3DScene (UNCORRECT). ALL volumes of selected instance have to be selected in 3DScene Fix: To avoid lost of some volumes in selection check non-selected volumes only if 3DScene-selection mode wasn't changed or there is no single selection in ObjectList --- src/slic3r/GUI/GUI_ObjectList.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index bf174cbf12..378f91cbc6 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -3465,7 +3465,11 @@ void ObjectList::update_selections_on_canvas() else { // add - volume_idxs = selection.get_unselected_volume_idxs_from(volume_idxs); + // to avoid lost of some volumes in selection + // check non-selected volumes only if selection mode wasn't changed + // OR there is no single selection + if (selection.get_mode() == mode || !single_selection) + volume_idxs = selection.get_unselected_volume_idxs_from(volume_idxs); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Selection-Add from list"))); selection.add_volumes(mode, volume_idxs, single_selection); } From 0abab45efa6f67179ec86c6eceb3ea597ffb3579 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 9 Sep 2021 17:50:14 +0200 Subject: [PATCH 132/151] ObjectDataViewModel: Fixed AddInfoChild() and Delete() functions Delete () function did not account for InfoItems that were added before VolumeItems As a result, There was possibility when deletion of penult VolumeItem wasn't invoke deletion of the last VolumeItem AddInfoChild() was not respect to existed SettingsItem SettingsItem have to be on a first place always. --- src/slic3r/GUI/ObjectDataViewModel.cpp | 68 +++++++++++++------------- src/slic3r/GUI/ObjectDataViewModel.hpp | 1 + 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 88527e9fd3..928249e5b0 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -406,8 +406,10 @@ wxDataViewItem ObjectDataViewModel::AddInfoChild(const wxDataViewItem &parent_it // The new item should be added according to its order in InfoItemType. // Find last info item with lower index and append after it. const auto& children = root->GetChildren(); - int idx = -1; - for (int i=0; i 0 && children[0]->GetType() == itSettings; + int idx = is_settings_item ? 0 : -1; + for (size_t i = is_settings_item ? 1 : 0; i < children.size(); ++i) { if (children[i]->GetType() == itInfo && int(children[i]->GetInfoItemType()) < int(info_type) ) idx = i; } @@ -619,6 +621,15 @@ wxDataViewItem ObjectDataViewModel::AddLayersChild(const wxDataViewItem &parent_ return layer_item; } +size_t ObjectDataViewModel::GetItemIndexForFirstVolume(ObjectDataViewModelNode* node_parent) +{ + assert(node_parent->m_volumes_cnt > 0); + for (size_t vol_idx = 0; vol_idx < node_parent->GetChildCount(); vol_idx++) + if (node_parent->GetNthChild(vol_idx)->GetType() == itVolume) + return vol_idx; + return -1; +} + wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) { auto ret_item = wxDataViewItem(0); @@ -714,44 +725,34 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) } // if there is last volume item after deleting, delete this last volume too - if (node_parent->GetChildCount() <= 3) // 3??? #ys_FIXME + if (node_parent->m_volumes_cnt == 1) { - int vol_cnt = 0; - int vol_idx = 0; - for (size_t i = 0; i < node_parent->GetChildCount(); ++i) { - if (node_parent->GetNthChild(i)->GetType() == itVolume) { - vol_idx = i; - vol_cnt++; - } - if (vol_cnt > 1) - break; - } + // delete selected (penult) volume + delete node; + ItemDeleted(parent, item); - if (vol_cnt == 1) { - delete node; - ItemDeleted(parent, item); + // get index of the last VolumeItem in CildrenList + size_t vol_idx = GetItemIndexForFirstVolume(node_parent); - ObjectDataViewModelNode *last_child_node = node_parent->GetNthChild(vol_idx); - DeleteSettings(wxDataViewItem(last_child_node)); - node_parent->GetChildren().Remove(last_child_node); - node_parent->m_volumes_cnt = 0; - delete last_child_node; + // delete this last volume + ObjectDataViewModelNode *last_child_node = node_parent->GetNthChild(vol_idx); + DeleteSettings(wxDataViewItem(last_child_node)); + node_parent->GetChildren().Remove(last_child_node); + node_parent->m_volumes_cnt = 0; + delete last_child_node; #ifndef __WXGTK__ - if (node_parent->GetChildCount() == 0) - node_parent->m_container = false; + if (node_parent->GetChildCount() == 0) + node_parent->m_container = false; #endif //__WXGTK__ - ItemDeleted(parent, wxDataViewItem(last_child_node)); + ItemDeleted(parent, wxDataViewItem(last_child_node)); - wxCommandEvent event(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED); - auto it = find(m_objects.begin(), m_objects.end(), node_parent); - event.SetInt(it == m_objects.end() ? -1 : it - m_objects.begin()); - wxPostEvent(m_ctrl, event); + wxCommandEvent event(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED); + auto it = find(m_objects.begin(), m_objects.end(), node_parent); + event.SetInt(it == m_objects.end() ? -1 : it - m_objects.begin()); + wxPostEvent(m_ctrl, event); - ret_item = parent; - - return ret_item; - } + return parent; } } else @@ -1361,8 +1362,7 @@ wxDataViewItem ObjectDataViewModel::ReorganizeChildren( const int current_volume if (!node_parent) // happens if item.IsOk()==false return ret_item; - size_t shift; - for (shift = 0; shift < node_parent->GetChildCount() && node_parent->GetNthChild(shift)->GetType() != itVolume; shift ++); + size_t shift = GetItemIndexForFirstVolume(node_parent); ObjectDataViewModelNode *deleted_node = node_parent->GetNthChild(current_volume_id+shift); node_parent->GetChildren().Remove(deleted_node); diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 86e64a8540..f65f829f4b 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -279,6 +279,7 @@ public: const t_layer_height_range& layer_range, const int extruder = 0, const int index = -1); + size_t GetItemIndexForFirstVolume(ObjectDataViewModelNode* node_parent); wxDataViewItem Delete(const wxDataViewItem &item); wxDataViewItem DeleteLastInstance(const wxDataViewItem &parent_item, size_t num); void DeleteAll(); From ff82c82f5224fb68e94a8903468162e0ff8160c4 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 10 Sep 2021 08:01:45 +0200 Subject: [PATCH 133/151] MSW-specific: Application Mode Buttons: Added the orange-ish background over the bold font to indicate selection (see comments for #6860) --- src/slic3r/GUI/Notebook.cpp | 24 ++++++++++++++++++++++-- src/slic3r/GUI/wxExtensions.cpp | 3 +++ src/slic3r/GUI/wxExtensions.hpp | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index b328fc583f..9c5ccb834e 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -45,11 +45,12 @@ void ButtonsListCtrl::OnPaint(wxPaintEvent&) if (m_selection < 0 || m_selection >= (int)m_pageButtons.size()) return; - // highlight selected button - const wxColour& selected_btn_bg = Slic3r::GUI::wxGetApp().get_color_selected_btn_bg(); const wxColour& default_btn_bg = Slic3r::GUI::wxGetApp().get_highlight_default_clr(); const wxColour& btn_marker_color = Slic3r::GUI::wxGetApp().get_color_hovered_btn_label(); + + // highlight selected notebook button + for (int idx = 0; idx < int(m_pageButtons.size()); idx++) { wxButton* btn = m_pageButtons[idx]; @@ -63,6 +64,25 @@ void ButtonsListCtrl::OnPaint(wxPaintEvent&) dc.DrawRectangle(pos.x, pos.y + size.y, size.x, sz.y - size.y); } + // highlight selected mode button + + if (m_mode_sizer) { + const std::vector& mode_btns = m_mode_sizer->get_btns(); + for (int idx = 0; idx < int(mode_btns.size()); idx++) { + ModeButton* btn = mode_btns[idx]; + btn->SetBackgroundColour(btn->is_selected() ? selected_btn_bg : default_btn_bg); + + //wxPoint pos = btn->GetPosition(); + //wxSize size = btn->GetSize(); + //const wxColour& clr = btn->is_selected() ? btn_marker_color : default_btn_bg; + //dc.SetPen(clr); + //dc.SetBrush(clr); + //dc.DrawRectangle(pos.x, pos.y + size.y, size.x, sz.y - size.y); + } + } + + // Draw orange bottom line + dc.SetPen(btn_marker_color); dc.SetBrush(btn_marker_color); dc.DrawRectangle(1, sz.y - m_line_margin, sz.x, m_line_margin); diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 8903f3b30b..a3d6384e05 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -672,6 +672,9 @@ void ModeButton::focus_button(const bool focus) Slic3r::GUI::wxGetApp().normal_font(); SetFont(new_font); +//#ifdef _WIN32 +// GetParent()->Refresh(); +//#else #ifndef _WIN32 SetForegroundColour(wxSystemSettings::GetColour(focus ? wxSYS_COLOUR_BTNTEXT : #if defined (__linux__) && defined (__WXGTK3__) diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 1462e8aa01..25876bd235 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -283,6 +283,7 @@ public: void OnLeaveBtn(wxMouseEvent& event) { focus_button(m_is_selected); event.Skip(); } void SetState(const bool state); + bool is_selected() { return m_is_selected; } protected: void focus_button(const bool focus); @@ -312,6 +313,7 @@ public: void set_items_border(int border); void msw_rescale(); + const std::vector& get_btns() { return m_mode_btns; } private: std::vector m_mode_btns; From 8bd0f8ada9e571ece441857a9f1a37ff90030688 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 10 Sep 2021 08:27:03 +0200 Subject: [PATCH 134/151] Removed duplicated method GLVolumeCollection::check_outside_state() --- src/slic3r/GUI/3DScene.cpp | 68 ++++++---------------------------- src/slic3r/GUI/3DScene.hpp | 1 - src/slic3r/GUI/GLCanvas3D.cpp | 13 ++++--- src/slic3r/GUI/GLCanvas3D.hpp | 2 +- src/slic3r/GUI/GUI_Preview.cpp | 5 --- src/slic3r/GUI/GUI_Preview.hpp | 2 - src/slic3r/GUI/Plater.cpp | 4 +- 7 files changed, 22 insertions(+), 73 deletions(-) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 7a9fdc3886..2d49a8bf38 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -827,6 +827,12 @@ GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCo void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disable_cullface, const Transform3d& view_matrix, std::function filter_func) const { +#if ENABLE_SINKING_CONTOURS + GLVolumeWithIdAndZList to_render = volumes_to_render(volumes, type, view_matrix, filter_func); + if (to_render.empty()) + return; +#endif // ENABLE_SINKING_CONTOURS + GLShaderProgram* shader = GUI::wxGetApp().get_current_shader(); if (shader == nullptr) return; @@ -841,7 +847,6 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab glsafe(::glDisable(GL_CULL_FACE)); #if ENABLE_SINKING_CONTOURS - GLVolumeWithIdAndZList to_render = volumes_to_render(volumes, type, view_matrix, filter_func); for (GLVolumeWithIdAndZ& volume : to_render) { volume.first->set_render_color(); @@ -961,8 +966,10 @@ bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config, M if (opt == nullptr) return false; - BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values)); - BoundingBoxf3 print_volume({ unscale(bed_box_2D.min(0)), unscale(bed_box_2D.min(1)), 0.0 }, { unscale(bed_box_2D.max(0)), unscale(bed_box_2D.max(1)), config->opt_float("max_print_height") }); + const BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values)); + BoundingBoxf3 print_volume({ unscale(bed_box_2D.min.x()), unscale(bed_box_2D.min.y()), 0.0 }, + { unscale(bed_box_2D.max.x()), unscale(bed_box_2D.max.y()), + config->opt_float("max_print_height") }); // Allow the objects to protrude below the print bed print_volume.min(2) = -1e10; print_volume.min(0) -= BedEpsilon; @@ -975,7 +982,7 @@ bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config, M bool contained_min_one = false; for (GLVolume* volume : this->volumes) { - if (volume == nullptr || volume->is_modifier || (volume->is_wipe_tower && !volume->shader_outside_printer_detection_enabled) || (volume->composite_id.volume_id < 0 && !volume->shader_outside_printer_detection_enabled)) + if (volume->is_modifier || (!volume->shader_outside_printer_detection_enabled && (volume->is_wipe_tower || volume->composite_id.volume_id < 0))) continue; const BoundingBoxf3& bb = volume->transformed_convex_hull_bounding_box(); @@ -985,8 +992,7 @@ bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config, M if (!volume->printable) continue; - if (contained) - contained_min_one = true; + contained_min_one |= contained; if (state == ModelInstancePVS_Inside && volume->is_outside) state = ModelInstancePVS_Fully_Outside; @@ -1001,56 +1007,6 @@ bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config, M return contained_min_one; } -bool GLVolumeCollection::check_outside_state(const DynamicPrintConfig* config, bool& partlyOut, bool& fullyOut) const -{ - if (config == nullptr) - return false; - - const ConfigOptionPoints* opt = dynamic_cast(config->option("bed_shape")); - if (opt == nullptr) - return false; - - const BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values)); - BoundingBoxf3 print_volume(Vec3d(unscale(bed_box_2D.min.x()), unscale(bed_box_2D.min.y()), 0.0), Vec3d(unscale(bed_box_2D.max.x()), unscale(bed_box_2D.max.y()), config->opt_float("max_print_height"))); - // Allow the objects to protrude below the print bed - print_volume.min(2) = -1e10; - print_volume.min(0) -= BedEpsilon; - print_volume.min(1) -= BedEpsilon; - print_volume.max(0) += BedEpsilon; - print_volume.max(1) += BedEpsilon; - - bool contained_min_one = false; - - partlyOut = false; - fullyOut = false; - for (GLVolume* volume : this->volumes) { - if (volume == nullptr || volume->is_modifier || (volume->is_wipe_tower && !volume->shader_outside_printer_detection_enabled) || (volume->composite_id.volume_id < 0 && !volume->shader_outside_printer_detection_enabled)) - continue; - - const BoundingBoxf3& bb = volume->transformed_convex_hull_bounding_box(); - bool contained = print_volume.contains(bb); - - volume->is_outside = !contained; - if (!volume->printable) - continue; - - if (contained) - contained_min_one = true; - - if (volume->is_outside) { - if (print_volume.intersects(bb)) - partlyOut = true; - else - fullyOut = true; - } - } - /* - if (out_state != nullptr) - *out_state = state; - */ - return contained_min_one; -} - void GLVolumeCollection::reset_outside_state() { for (GLVolume* volume : this->volumes) diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index e5298ca931..35abe8bd4d 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -614,7 +614,6 @@ public: // returns true if all the volumes are completely contained in the print volume // returns the containment state in the given out_state, if non-null bool check_outside_state(const DynamicPrintConfig* config, ModelInstanceEPrintVolumeState* out_state) const; - bool check_outside_state(const DynamicPrintConfig* config, bool& partlyOut, bool& fullyOut) const; void reset_outside_state(); void update_colors_by_extruder(const DynamicPrintConfig* config); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index f50c7fe25b..37493e754c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1107,11 +1107,11 @@ void GLCanvas3D::reset_volumes() _set_warning_notification(EWarning::ObjectOutside, false); } -int GLCanvas3D::check_volumes_outside_state() const +ModelInstanceEPrintVolumeState GLCanvas3D::check_volumes_outside_state() const { ModelInstanceEPrintVolumeState state; m_volumes.check_outside_state(m_config, &state); - return (int)state; + return state; } void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx) @@ -2039,9 +2039,10 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re // checks for geometry outside the print volume to render it accordingly if (!m_volumes.empty()) { - bool partlyOut = false; - bool fullyOut = false; - const bool contained_min_one = m_volumes.check_outside_state(m_config, partlyOut, fullyOut); + ModelInstanceEPrintVolumeState state; + const bool contained_min_one = m_volumes.check_outside_state(m_config, &state); + const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside); + const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside); _set_warning_notification(EWarning::ObjectClashed, partlyOut); _set_warning_notification(EWarning::ObjectOutside, fullyOut); @@ -2121,7 +2122,7 @@ void GLCanvas3D::load_sla_preview() if (m_canvas != nullptr && print != nullptr) { _set_current(); // Release OpenGL data before generating new data. - this->reset_volumes(); + reset_volumes(); _load_sla_shells(); _update_sla_shells_outside_state(); _set_warning_notification_if_needed(EWarning::SlaSupportsOutside); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 799fa11c79..eb0220cd47 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -615,7 +615,7 @@ public: unsigned int get_volumes_count() const; const GLVolumeCollection& get_volumes() const { return m_volumes; } void reset_volumes(); - int check_volumes_outside_state() const; + ModelInstanceEPrintVolumeState check_volumes_outside_state() const; void reset_gcode_toolpaths() { m_gcode_viewer.reset(); } const GCodeViewer::SequentialView& get_gcode_sequential_view() const { return m_gcode_viewer.get_sequential_view(); } diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 8c10fb1576..45da928f57 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -128,11 +128,6 @@ void View3D::mirror_selection(Axis axis) m_canvas->mirror_selection(axis); } -int View3D::check_volumes_outside_state() const -{ - return (m_canvas != nullptr) ? m_canvas->check_volumes_outside_state() : false; -} - bool View3D::is_layers_editing_enabled() const { return (m_canvas != nullptr) ? m_canvas->is_layers_editing_enabled() : false; diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index f8a41fd926..97ced0a1e0 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -59,8 +59,6 @@ public: void delete_selected(); void mirror_selection(Axis axis); - int check_volumes_outside_state() const; - bool is_layers_editing_enabled() const; bool is_layers_editing_allowed() const; void enable_layers_editing(bool enable); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 76612a718c..656b879722 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2724,7 +2724,7 @@ void Plater::priv::object_list_changed() { const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty()); // XXX: is this right? - const bool model_fits = view3D->check_volumes_outside_state() == ModelInstancePVS_Inside; + const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits); } @@ -3664,7 +3664,7 @@ void Plater::priv::set_current_panel(wxPanel* panel) // see: Plater::priv::object_list_changed() // FIXME: it may be better to have a single function making this check and let it be called wherever needed bool export_in_progress = this->background_process.is_export_scheduled(); - bool model_fits = view3D->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; if (!model.objects.empty() && !export_in_progress && model_fits) this->q->reslice(); // keeps current gcode preview, if any From 8f4a6b3b7c90f618d99e294e3567156263cf9936 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 10 Sep 2021 09:06:09 +0200 Subject: [PATCH 135/151] Fixed color of objects in preview for SLA printers --- src/slic3r/GUI/GLCanvas3D.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 37493e754c..d790e937c7 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2118,12 +2118,14 @@ void GLCanvas3D::refresh_gcode_preview_render_paths() void GLCanvas3D::load_sla_preview() { - const SLAPrint* print = this->sla_print(); + const SLAPrint* print = sla_print(); if (m_canvas != nullptr && print != nullptr) { _set_current(); // Release OpenGL data before generating new data. reset_volumes(); _load_sla_shells(); + const BoundingBoxf3& bed_bb = wxGetApp().plater()->get_bed().get_bounding_box(false); + m_volumes.set_print_box(float(bed_bb.min.x()) - BedEpsilon, float(bed_bb.min.y()) - BedEpsilon, 0.0f, float(bed_bb.max.x()) + BedEpsilon, float(bed_bb.max.y()) + BedEpsilon, (float)m_config->opt_float("max_print_height")); _update_sla_shells_outside_state(); _set_warning_notification_if_needed(EWarning::SlaSupportsOutside); } From 0d0c09288058ee0b3e5565f3087321cd530038d6 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 10 Sep 2021 10:46:58 +0200 Subject: [PATCH 136/151] Object warning notification Opens when loading object, closes at deletion. Replaces SimplifySuggesion. --- src/slic3r/GUI/NotificationManager.cpp | 17 +++++++++++++++++ src/slic3r/GUI/NotificationManager.hpp | 13 ++++++++++--- src/slic3r/GUI/Plater.cpp | 17 +++++++++++++---- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 4e94d82608..172e38d53a 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1250,6 +1250,14 @@ void NotificationManager::close_slicing_error_notification(const std::string& te } } } +void NotificationManager::push_object_warning_notification(const std::string& text, ObjectID object_id, const std::string& hypertext/* = ""*/, std::function callback/* = std::function()*/) +{ + NotificationData data{ NotificationType::ObjectWarning, NotificationLevel::WarningNotification, 0, text, hypertext, callback }; + auto notification = std::make_unique(data, m_id_provider, m_evt_handler); + notification->object_id = object_id; + notification->warning_step = 0; + push_notification_data(std::move(notification), 0); +} void NotificationManager::push_slicing_complete_notification(int timestamp, bool large) { std::string hypertext; @@ -1304,6 +1312,15 @@ void NotificationManager::remove_slicing_warnings_of_released_objects(const std: notification->close(); } } +void NotificationManager::remove_object_warnings_of_released_objects(const std::vector& living_oids) +{ + for (std::unique_ptr& notification : m_pop_notifications) + if (notification->get_type() == NotificationType::ObjectWarning) { + if (!std::binary_search(living_oids.begin(), living_oids.end(), + static_cast(notification.get())->object_id)) + notification->close(); + } +} void NotificationManager::push_exporting_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable) { close_notification_of_type(NotificationType::ExportFinished); diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 1e667e4525..eb95aec580 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -71,6 +71,9 @@ enum class NotificationType PlaterError, // Object fully outside the print volume, or extrusion outside the print volume. Slicing is not disabled. PlaterWarning, + // Warning connected to single object id, appears at loading object, disapears at deletition. + // Example: advice to simplify object with big amount of triangles. + ObjectWarning, // Progress bar instead of text. ProgressBar, // Progress bar with info from Print Host Upload Queue dialog. @@ -97,8 +100,6 @@ enum class NotificationType // Shows when ObjectList::update_info_items finds information that should be stressed to the user // Might contain logo taken from gizmos UpdatedItemsInfo, - // Give user advice to simplify object with big amount of triangles - SimplifySuggestion }; class NotificationManager @@ -155,6 +156,12 @@ public: // Closes error or warning of the same text void close_plater_error_notification(const std::string& text); void close_plater_warning_notification(const std::string& text); + // Object warning with ObjectID, closes when object is deleted. ID used is of object not print like in slicing warning. + void push_object_warning_notification(const std::string& text, ObjectID object_id, const std::string& hypertext = "", + std::function callback = std::function()); + // Close object warnings, whose ObjectID is not in the list. + // living_oids is expected to be sorted. + void remove_object_warnings_of_released_objects(const std::vector& living_oids); // Creates special notification slicing complete. // If large = true (Plater side bar is closed), then printing time and export button is shown // at the notification and fade-out is disabled. Otherwise the fade out time is set to 10s. @@ -576,7 +583,7 @@ private: NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, - NotificationType::SimplifySuggestion + NotificationType::ObjectWarning }; //prepared (basic) notifications static const NotificationData basic_notifications[]; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 656b879722..ac76fe6559 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1736,6 +1736,7 @@ struct Plater::priv void add_warning(const Slic3r::PrintStateBase::Warning &warning, size_t oid); // Update notification manager with the current state of warnings produced by the background process (slicing). void actualize_slicing_warnings(const PrintBase &print); + void actualize_object_warnings(const PrintBase& print); // Displays dialog window with list of warnings. // Returns true if user clicks OK. // Returns true if current_warnings vector is empty without showning the dialog @@ -3048,6 +3049,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool //actualizate warnings if (invalidated != Print::APPLY_STATUS_UNCHANGED) { actualize_slicing_warnings(*this->background_process.current_print()); + actualize_object_warnings(*this->background_process.current_print()); show_warning_dialog = false; process_completed_with_error = false; } @@ -3587,10 +3589,7 @@ void Plater::priv::create_simplify_notification(const std::vector& obj_i manager.open_gizmo(GLGizmosManager::EType::Simplify); return true; }; - notification_manager->push_notification( - NotificationType::SimplifySuggestion, - NotificationManager::NotificationLevel::WarningNotification, - text.str(), hypertext, open_simplify); + notification_manager->push_object_warning_notification(text.str(), model.objects[object_id]->id(), hypertext, open_simplify); } } @@ -3855,6 +3854,16 @@ void Plater::priv::actualize_slicing_warnings(const PrintBase &print) notification_manager->remove_slicing_warnings_of_released_objects(ids); notification_manager->set_all_slicing_warnings_gray(true); } +void Plater::priv::actualize_object_warnings(const PrintBase& print) +{ + std::vector ids; + for (const ModelObject* object : print.model().objects ) + { + ids.push_back(object->id()); + } + std::sort(ids.begin(), ids.end()); + notification_manager->remove_object_warnings_of_released_objects(ids); +} void Plater::priv::clear_warnings() { notification_manager->close_slicing_errors_and_warnings(); From 03b60486840f32e32dc54103dc3051f94e79b35a Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 10 Sep 2021 11:43:53 +0200 Subject: [PATCH 137/151] Follow-up to beee18f22991e369b1722a43bbcb692fa0d68af0 WIP to G-code export parallelization through pipelining: Decoupled CoolingBuffer from GCode / GCodeWriter, ready to be pipelined on a different thread. --- src/libslic3r/GCode.cpp | 6 +-- src/libslic3r/GCode/CoolingBuffer.cpp | 65 +++++++++++------------ src/libslic3r/GCode/CoolingBuffer.hpp | 23 ++++++--- src/libslic3r/GCodeWriter.cpp | 74 ++++++++++++++------------- src/libslic3r/GCodeWriter.hpp | 10 ++-- t/cooling.t | 2 +- xs/xsp/GCode.xsp | 1 - 7 files changed, 96 insertions(+), 85 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index efea240e55..7e1afb5a8a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1219,7 +1219,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Disable fan. if (! print.config().cooling.get_at(initial_extruder_id) || print.config().disable_fan_first_layers.get_at(initial_extruder_id)) - file.write(m_writer.set_fan(0, true)); + file.write(m_writer.set_fan(0)); // Let the start-up script prime the 1st printing tool. m_placeholder_parser.set("initial_tool", initial_extruder_id); @@ -1334,7 +1334,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.writeln(between_objects_gcode); } // Reset the cooling buffer internal state (the current position, feed rate, accelerations). - m_cooling_buffer->reset(); + m_cooling_buffer->reset(this->writer().get_position()); m_cooling_buffer->set_current_extruder(initial_extruder_id); // Pair the object layers with the support layers by z, extrude them. std::vector layers_to_print = collect_layers_to_print(object); @@ -1420,7 +1420,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Write end commands to file. file.write(this->retract()); - file.write(m_writer.set_fan(false)); + file.write(m_writer.set_fan(0)); // adds tag for processor file.write_format(";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 17f3e0b737..9ca85c7281 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -16,19 +16,25 @@ namespace Slic3r { -CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_gcodegen(gcodegen), m_current_extruder(0) +CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0) { - this->reset(); + this->reset(gcodegen.writer().get_position()); + + const std::vector &extruders = gcodegen.writer().extruders(); + m_extruder_ids.reserve(extruders.size()); + for (const Extruder &ex : extruders) { + m_num_extruders = std::max(ex.id() + 1, m_num_extruders); + m_extruder_ids.emplace_back(ex.id()); + } } -void CoolingBuffer::reset() +void CoolingBuffer::reset(const Vec3d &position) { m_current_pos.assign(5, 0.f); - Vec3d pos = m_gcodegen.writer().get_position(); - m_current_pos[0] = float(pos(0)); - m_current_pos[1] = float(pos(1)); - m_current_pos[2] = float(pos(2)); - m_current_pos[4] = float(m_gcodegen.config().travel_speed.value); + m_current_pos[0] = float(position.x()); + m_current_pos[1] = float(position.y()); + m_current_pos[2] = float(position.z()); + m_current_pos[4] = float(m_config.travel_speed.value); } struct CoolingLine @@ -303,30 +309,23 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b // Return the list of parsed lines, bucketed by an extruder. std::vector CoolingBuffer::parse_layer_gcode(const std::string &gcode, std::vector ¤t_pos) const { - const FullPrintConfig &config = m_gcodegen.config(); - const std::vector &extruders = m_gcodegen.writer().extruders(); - unsigned int num_extruders = 0; - for (const Extruder &ex : extruders) - num_extruders = std::max(ex.id() + 1, num_extruders); - - std::vector per_extruder_adjustments(extruders.size()); - std::vector map_extruder_to_per_extruder_adjustment(num_extruders, 0); - for (size_t i = 0; i < extruders.size(); ++ i) { + std::vector per_extruder_adjustments(m_extruder_ids.size()); + std::vector map_extruder_to_per_extruder_adjustment(m_num_extruders, 0); + for (size_t i = 0; i < m_extruder_ids.size(); ++ i) { PerExtruderAdjustments &adj = per_extruder_adjustments[i]; - unsigned int extruder_id = extruders[i].id(); + unsigned int extruder_id = m_extruder_ids[i]; adj.extruder_id = extruder_id; - adj.cooling_slow_down_enabled = config.cooling.get_at(extruder_id); - adj.slowdown_below_layer_time = float(config.slowdown_below_layer_time.get_at(extruder_id)); - adj.min_print_speed = float(config.min_print_speed.get_at(extruder_id)); + adj.cooling_slow_down_enabled = m_config.cooling.get_at(extruder_id); + adj.slowdown_below_layer_time = float(m_config.slowdown_below_layer_time.get_at(extruder_id)); + adj.min_print_speed = float(m_config.min_print_speed.get_at(extruder_id)); map_extruder_to_per_extruder_adjustment[extruder_id] = i; } - const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); unsigned int current_extruder = m_current_extruder; PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[current_extruder]]; const char *line_start = gcode.c_str(); const char *line_end = line_start; - const char extrusion_axis = get_extrusion_axis(config)[0]; + const char extrusion_axis = get_extrusion_axis(m_config)[0]; // Index of an existing CoolingLine of the current adjustment, which holds the feedrate setting command // for a sequence of extrusion moves. size_t active_speed_modifier = size_t(-1); @@ -387,7 +386,7 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: } if ((line.type & CoolingLine::TYPE_G92) == 0) { // G0 or G1. Calculate the duration. - if (config.use_relative_e_distances.value) + if (m_config.use_relative_e_distances.value) // Reset extruder accumulator. current_pos[3] = 0.f; float dif[4]; @@ -430,8 +429,8 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { line.type = CoolingLine::TYPE_EXTRUDE_END; active_speed_modifier = size_t(-1); - } else if (boost::starts_with(sline, toolchange_prefix)) { - unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size()); + } else if (boost::starts_with(sline, m_toolchange_prefix)) { + unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + m_toolchange_prefix.size()); // Only change extruder in case the number is meaningful. User could provide an out-of-range index through custom gcodes - those shall be ignored. if (new_extruder < map_extruder_to_per_extruder_adjustment.size()) { if (new_extruder != current_extruder) { @@ -641,7 +640,7 @@ float CoolingBuffer::calculate_layer_slowdown(std::vector slowdown_below_layer_time) { // The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything. } else { - // Adjust this and all the following (higher config.slowdown_below_layer_time) extruders. + // Adjust this and all the following (higher m_config.slowdown_below_layer_time) extruders. // Sum maximum slow down time as if everything was slowed down including the external perimeters. float max_time = elapsed_time_total0; for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) @@ -694,8 +693,7 @@ std::string CoolingBuffer::apply_layer_cooldown( bool bridge_fan_control = false; int bridge_fan_speed = 0; auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() { - const FullPrintConfig &config = m_gcodegen.config(); -#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder) +#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_current_extruder) int min_fan_speed = EXTRUDER_CONFIG(min_fan_speed); int fan_speed_new = EXTRUDER_CONFIG(fan_always_on) ? min_fan_speed : 0; int disable_fan_first_layers = EXTRUDER_CONFIG(disable_fan_first_layers); @@ -737,13 +735,12 @@ std::string CoolingBuffer::apply_layer_cooldown( } if (fan_speed_new != fan_speed) { fan_speed = fan_speed_new; - new_gcode += m_gcodegen.writer().set_fan(fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_config.gcode_comments, fan_speed); } }; const char *pos = gcode.c_str(); int current_feedrate = 0; - const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); change_extruder_set_fan(); for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; @@ -751,7 +748,7 @@ std::string CoolingBuffer::apply_layer_cooldown( if (line_start > pos) new_gcode.append(pos, line_start - pos); if (line->type & CoolingLine::TYPE_SET_TOOL) { - unsigned int new_extruder = (unsigned int)atoi(line_start + toolchange_prefix.size()); + unsigned int new_extruder = (unsigned int)atoi(line_start + m_toolchange_prefix.size()); if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; change_extruder_set_fan(); @@ -759,10 +756,10 @@ std::string CoolingBuffer::apply_layer_cooldown( new_gcode.append(line_start, line_end - line_start); } else if (line->type & CoolingLine::TYPE_BRIDGE_FAN_START) { if (bridge_fan_control) - new_gcode += m_gcodegen.writer().set_fan(bridge_fan_speed, true); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_config.gcode_comments, bridge_fan_speed); } else if (line->type & CoolingLine::TYPE_BRIDGE_FAN_END) { if (bridge_fan_control) - new_gcode += m_gcodegen.writer().set_fan(fan_speed, true); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_config.gcode_comments, fan_speed); } else if (line->type & CoolingLine::TYPE_EXTRUDE_END) { // Just remove this comment. } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 0932d62d37..5f49ef4557 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -23,10 +23,9 @@ struct PerExtruderAdjustments; class CoolingBuffer { public: CoolingBuffer(GCode &gcodegen); - void reset(); + void reset(const Vec3d &position); void set_current_extruder(unsigned int extruder_id) { m_current_extruder = extruder_id; } std::string process_layer(std::string &&gcode, size_t layer_id, bool flush); - GCode* gcodegen() { return &m_gcodegen; } private: CoolingBuffer& operator=(const CoolingBuffer&) = delete; @@ -36,17 +35,25 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - GCode& m_gcodegen; // G-code snippet cached for the support layers preceding an object layer. - std::string m_gcode; + std::string m_gcode; // Internal data. // X,Y,Z,E,F - std::vector m_axis; - std::vector m_current_pos; - unsigned int m_current_extruder; + std::vector m_axis; + std::vector m_current_pos; + // Cached from GCodeWriter. + // Printing extruder IDs, zero based. + std::vector m_extruder_ids; + // Highest of m_extruder_ids plus 1. + unsigned int m_num_extruders { 0 }; + const std::string m_toolchange_prefix; + // Referencs GCode::m_config, which is FullPrintConfig. While the PrintObjectConfig slice of FullPrintConfig is being modified, + // the PrintConfig slice of FullPrintConfig is constant, thus no thread synchronization is required. + const PrintConfig &m_config; + unsigned int m_current_extruder; // Old logic: proportional. - bool m_cooling_logic_proportional = false; + bool m_cooling_logic_proportional = false; }; } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index c97180982d..793a666752 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -156,41 +156,6 @@ std::string GCodeWriter::set_bed_temperature(unsigned int temperature, bool wait return gcode.str(); } -std::string GCodeWriter::set_fan(unsigned int speed, bool dont_save) -{ - std::ostringstream gcode; - if (m_last_fan_speed != speed || dont_save) { - if (!dont_save) m_last_fan_speed = speed; - - if (speed == 0) { - if (FLAVOR_IS(gcfTeacup)) { - gcode << "M106 S0"; - } else if (FLAVOR_IS(gcfMakerWare) || FLAVOR_IS(gcfSailfish)) { - gcode << "M127"; - } else { - gcode << "M107"; - } - if (this->config.gcode_comments) gcode << " ; disable fan"; - gcode << "\n"; - } else { - if (FLAVOR_IS(gcfMakerWare) || FLAVOR_IS(gcfSailfish)) { - gcode << "M126"; - } else { - gcode << "M106 "; - if (FLAVOR_IS(gcfMach3) || FLAVOR_IS(gcfMachinekit)) { - gcode << "P"; - } else { - gcode << "S"; - } - gcode << (255.0 * speed / 100.0); - } - if (this->config.gcode_comments) gcode << " ; enable fan"; - gcode << "\n"; - } - } - return gcode.str(); -} - std::string GCodeWriter::set_acceleration(unsigned int acceleration) { // Clamp the acceleration to the allowed maximum. @@ -611,4 +576,43 @@ std::string GCodeWriter::unlift() return gcode; } +std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, bool gcode_comments, unsigned int speed) +{ + std::ostringstream gcode; + if (speed == 0) { + switch (gcode_flavor) { + case gcfTeacup: + gcode << "M106 S0"; break; + case gcfMakerWare: + case gcfSailfish: + gcode << "M127"; break; + default: + gcode << "M107"; break; + } + if (gcode_comments) + gcode << " ; disable fan"; + gcode << "\n"; + } else { + switch (gcode_flavor) { + case gcfMakerWare: + case gcfSailfish: + gcode << "M126"; break; + case gcfMach3: + case gcfMachinekit: + gcode << "M106 P" << 255.0 * speed / 100.0; break; + default: + gcode << "M106 S" << 255.0 * speed / 100.0; break; + } + if (gcode_comments) + gcode << " ; enable fan"; + gcode << "\n"; + } + return gcode.str(); +} + +std::string GCodeWriter::set_fan(unsigned int speed) const +{ + return GCodeWriter::set_fan(this->config.gcode_flavor, this->config.gcode_comments, speed); +} + } diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 2de95ecc5b..dd602ca804 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -18,7 +18,7 @@ public: GCodeWriter() : multiple_extruders(false), m_extrusion_axis("E"), m_extruder(nullptr), m_single_extruder_multi_material(false), - m_last_acceleration(0), m_max_acceleration(0), m_last_fan_speed(0), + m_last_acceleration(0), m_max_acceleration(0), m_last_bed_temperature(0), m_last_bed_temperature_reached(true), m_lifted(0) {} @@ -42,7 +42,6 @@ public: std::string postamble() const; std::string set_temperature(unsigned int temperature, bool wait = false, int tool = -1) const; std::string set_bed_temperature(unsigned int temperature, bool wait = false); - std::string set_fan(unsigned int speed, bool dont_save = false); std::string set_acceleration(unsigned int acceleration); std::string reset_e(bool force = false); std::string update_progress(unsigned int num, unsigned int tot, bool allow_100 = false) const; @@ -69,6 +68,12 @@ public: std::string unlift(); Vec3d get_position() const { return m_pos; } + // To be called by the CoolingBuffer from another thread. + static std::string set_fan(const GCodeFlavor gcode_flavor, bool gcode_comments, unsigned int speed); + // To be called by the main thread. It always emits the G-code, it does not remember the previous state. + // Keeping the state is left to the CoolingBuffer, which runs asynchronously on another thread. + std::string set_fan(unsigned int speed) const; + private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_extruders; @@ -79,7 +84,6 @@ private: // Limit for setting the acceleration, to respect the machine limits set for the Marlin firmware. // If set to zero, the limit is not in action. unsigned int m_max_acceleration; - unsigned int m_last_fan_speed; unsigned int m_last_bed_temperature; bool m_last_bed_temperature_reached; double m_lifted; diff --git a/t/cooling.t b/t/cooling.t index 2f444cf9d1..a7720fd07c 100644 --- a/t/cooling.t +++ b/t/cooling.t @@ -132,7 +132,7 @@ $config->set('disable_fan_first_layers', [ 0 ]); 'fan_below_layer_time' => [ $print_time2 + 1, $print_time2 + 1 ], 'slowdown_below_layer_time' => [ $print_time2 + 2, $print_time2 + 2 ] }); - $buffer->gcodegen->set_extruders([ 0, 1 ]); + $gcodegen->set_extruders([ 0, 1 ]); my $gcode = $buffer->process_layer($gcode1 . "T1\nG1 X0 E1 F3000\n", 0); like $gcode, qr/^M106/, 'fan is activated for the 1st tool'; like $gcode, qr/.*M107/, 'fan is disabled for the 2nd tool'; diff --git a/xs/xsp/GCode.xsp b/xs/xsp/GCode.xsp index 78a5dde376..4c25838946 100644 --- a/xs/xsp/GCode.xsp +++ b/xs/xsp/GCode.xsp @@ -10,7 +10,6 @@ CoolingBuffer(GCode* gcode) %code{% RETVAL = new CoolingBuffer(*gcode); %}; ~CoolingBuffer(); - Ref gcodegen(); std::string process_layer(std::string gcode, size_t layer_id) %code{% RETVAL = THIS->process_layer(std::move(gcode), layer_id, true); %}; From dc4b783e9ec38756432789e97b6089d62b6b1d14 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 10 Sep 2021 12:10:00 +0200 Subject: [PATCH 138/151] Pimping up SpiralVase code, fix of 03b60486840f32e32dc54103dc3051f94e79b35a --- src/libslic3r/GCode.cpp | 2 +- src/libslic3r/GCode/SpiralVase.cpp | 2 +- src/libslic3r/GCode/SpiralVase.hpp | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7e1afb5a8a..2347225ab7 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1099,7 +1099,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_volumetric_speed = DoExport::autospeed_volumetric_limit(print); print.throw_if_canceled(); - m_cooling_buffer = make_unique(*this); if (print.config().spiral_vase.value) m_spiral_vase = make_unique(print.config()); #ifdef HAS_PRESSURE_EQUALIZER @@ -1212,6 +1211,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); + m_cooling_buffer = make_unique(*this); m_cooling_buffer->set_current_extruder(initial_extruder_id); // Emit machine envelope limits for the Marlin firmware. diff --git a/src/libslic3r/GCode/SpiralVase.cpp b/src/libslic3r/GCode/SpiralVase.cpp index acb6ad0348..c3caee2dc4 100644 --- a/src/libslic3r/GCode/SpiralVase.cpp +++ b/src/libslic3r/GCode/SpiralVase.cpp @@ -54,7 +54,7 @@ std::string SpiralVase::process_layer(const std::string &gcode) // For absolute extruder distances it will be switched off. // Tapering the absolute extruder distances requires to process every extrusion value after the first transition // layer. - bool transition = m_transition_layer && m_config->use_relative_e_distances.value; + bool transition = m_transition_layer && m_config.use_relative_e_distances.value; float layer_height_factor = layer_height / total_layer_length; float len = 0.f; m_reader.parse_buffer(gcode, [&new_gcode, &z, total_layer_length, layer_height_factor, transition, &len] diff --git a/src/libslic3r/GCode/SpiralVase.hpp b/src/libslic3r/GCode/SpiralVase.hpp index 5353901fe6..fb461c2015 100644 --- a/src/libslic3r/GCode/SpiralVase.hpp +++ b/src/libslic3r/GCode/SpiralVase.hpp @@ -8,10 +8,10 @@ namespace Slic3r { class SpiralVase { public: - SpiralVase(const PrintConfig &config) : m_config(&config) + SpiralVase(const PrintConfig &config) : m_config(config) { - m_reader.z() = (float)m_config->z_offset; - m_reader.apply_config(*m_config); + m_reader.z() = (float)m_config.z_offset; + m_reader.apply_config(m_config); }; void enable(bool en) { @@ -22,7 +22,7 @@ public: std::string process_layer(const std::string &gcode); private: - const PrintConfig *m_config; + const PrintConfig &m_config; GCodeReader m_reader; bool m_enabled = false; @@ -32,4 +32,4 @@ private: } -#endif +#endif // slic3r_SpiralVase_hpp_ From 34c4b74af49c12e89fc6b5cc2bcfa36c69797578 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 10 Sep 2021 12:28:52 +0200 Subject: [PATCH 139/151] Fixed Perl unit tests --- t/cooling.t | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/t/cooling.t b/t/cooling.t index a7720fd07c..9ac5a24f4d 100644 --- a/t/cooling.t +++ b/t/cooling.t @@ -33,7 +33,10 @@ sub buffer { $gcodegen = Slic3r::GCode->new; $gcodegen->apply_print_config($print_config); $gcodegen->set_layer_count(10); - $gcodegen->set_extruders([ 0 ]); + + my @extruders = shift; + @extruders = [ 0 ] if int(@extruders) == 0; + $gcodegen->set_extruders(\@extruders); return Slic3r::GCode::CoolingBuffer->new($gcodegen); } @@ -131,8 +134,8 @@ $config->set('disable_fan_first_layers', [ 0 ]); 'cooling' => [ 1 , 0 ], 'fan_below_layer_time' => [ $print_time2 + 1, $print_time2 + 1 ], 'slowdown_below_layer_time' => [ $print_time2 + 2, $print_time2 + 2 ] - }); - $gcodegen->set_extruders([ 0, 1 ]); + }, + [ 0, 1]); my $gcode = $buffer->process_layer($gcode1 . "T1\nG1 X0 E1 F3000\n", 0); like $gcode, qr/^M106/, 'fan is activated for the 1st tool'; like $gcode, qr/.*M107/, 'fan is disabled for the 2nd tool'; From 3db4804e476aa4b6cb320f696bc6ddd094c9087a Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 10 Sep 2021 14:07:29 +0200 Subject: [PATCH 140/151] MSW specific: ObjectList: Fixed eternal Editor Control Steps to reproduce of a bug: 1. Add object 2. Click the Cog icon with the left mouse button 3. DoubleClick on name of object > An Editor Control appears that cannot be deleted any way --- src/slic3r/GUI/ExtraRenderers.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp index e9fb7339f3..5fe86db275 100644 --- a/src/slic3r/GUI/ExtraRenderers.cpp +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -197,6 +197,17 @@ wxWindow* BitmapTextRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelRec labelRect.SetWidth(labelRect.GetWidth() - bmp_width); } +#ifdef __WXMSW__ + // Case when from some reason we try to create next EditorCtrl till old one was not deleted + if (auto children = parent->GetChildren(); children.GetCount() > 0) + for (auto child : children) + if (dynamic_cast(child)) { + parent->RemoveChild(child); + child->Destroy(); + break; + } +#endif // __WXMSW__ + wxTextCtrl* text_editor = new wxTextCtrl(parent, wxID_ANY, data.GetText(), position, labelRect.GetSize(), wxTE_PROCESS_ENTER); text_editor->SetInsertionPointEnd(); From b9dab7540e5de0d81a74cb53c2b00fb4b5857a38 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 10 Sep 2021 14:31:31 +0200 Subject: [PATCH 141/151] Removes bottom status bar. StatusBar class calls are commented out and replaced with notifications. SlicicingProgress notification shows progress of slicing, ProgressIndicator notification handles other progress information, like arrange objects etc. --- src/slic3r/GUI/GUI_App.cpp | 4 +- src/slic3r/GUI/GUI_App.hpp | 2 +- src/slic3r/GUI/GUI_Preview.cpp | 4 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 4 +- src/slic3r/GUI/HintNotification.cpp | 2 +- src/slic3r/GUI/Jobs/ArrangeJob.hpp | 5 +- src/slic3r/GUI/Jobs/FillBedJob.hpp | 5 +- src/slic3r/GUI/Jobs/Job.cpp | 24 +- src/slic3r/GUI/Jobs/Job.hpp | 7 +- src/slic3r/GUI/Jobs/PlaterJob.hpp | 5 +- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 6 +- src/slic3r/GUI/Jobs/SLAImportJob.cpp | 5 +- src/slic3r/GUI/Jobs/SLAImportJob.hpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 16 +- src/slic3r/GUI/MainFrame.hpp | 2 +- src/slic3r/GUI/NotificationManager.cpp | 739 ++++++++++++++---- src/slic3r/GUI/NotificationManager.hpp | 228 ++++-- src/slic3r/GUI/Plater.cpp | 126 +-- src/slic3r/GUI/Plater.hpp | 6 +- src/slic3r/GUI/ProgressStatusBar.cpp | 1 + src/slic3r/GUI/ProgressStatusBar.hpp | 1 + 23 files changed, 908 insertions(+), 292 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 3d5e56b462..79186a8ea2 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -958,6 +958,8 @@ bool GUI_App::on_init_inner() // update_mode(); // !!! do that later SetTopWindow(mainframe); + plater_->init_notification_manager(); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); if (is_gcode_viewer()) { @@ -2278,7 +2280,7 @@ wxBookCtrlBase* GUI_App::tab_panel() const return mainframe->m_tabpanel; } -NotificationManager* GUI_App::notification_manager() +std::shared_ptr GUI_App::notification_manager() { return plater_->get_notification_manager(); } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index a088e10d4c..5acab0c948 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -276,7 +276,7 @@ public: ObjectLayers* obj_layers(); Plater* plater(); Model& model(); - NotificationManager* notification_manager(); + std::shared_ptr notification_manager(); // Parameters extracted from the command line to be passed to GUI after initialization. GUI_InitParams* init_params { nullptr }; diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 45da928f57..eb511177df 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -734,9 +734,9 @@ void Preview::update_layers_slider(const std::vector& layers_z, bool kee double top_area = area(object->get_layer(int(object->layers().size()) - 1)->lslices); if( bottom_area - top_area > delta_area) { - NotificationManager* notif_mngr = wxGetApp().plater()->get_notification_manager(); + std::shared_ptr notif_mngr = wxGetApp().plater()->get_notification_manager(); notif_mngr->push_notification( - NotificationType::SignDetected, NotificationManager::NotificationLevel::RegularNotification, + NotificationType::SignDetected, NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("NOTE:") + "\n" + _u8L("Sliced object looks like the sign") + "\n", _u8L("Apply auto color change to print"), [this](wxEvtHandler*) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 608e7bd1fa..4bd98f8c87 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -22,7 +22,7 @@ static inline void show_notification_extruders_limit_exceeded() wxGetApp() .plater() ->get_notification_manager() - ->push_notification(NotificationType::MmSegmentationExceededExtrudersLimit, NotificationManager::NotificationLevel::RegularNotification, + ->push_notification(NotificationType::MmSegmentationExceededExtrudersLimit, NotificationManager::NotificationLevel::RegularNotificationLevel, GUI::format(_L("Your printer has more extruders than the multi-material painting gizmo supports. For this reason, only the " "first %1% extruders will be able to be used for painting."), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index 9f49f9734a..a2c4910e34 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -337,7 +337,7 @@ void GLGizmoSimplify::on_set_state() auto notification_manager = wxGetApp().plater()->get_notification_manager(); notification_manager->push_notification( NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("ERROR: Wait until Simplification ends or Cancel process.")); return; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 93eba3042c..7a9373a5f4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -194,7 +194,7 @@ bool GLGizmosManager::check_gizmos_closed_except(EType type) const if (get_current_type() != type && get_current_type() != Undefined) { wxGetApp().plater()->get_notification_manager()->push_notification( NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::RegularNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("ERROR: Please close all manipulators available from " "the left toolbar first")); return false; @@ -1256,7 +1256,7 @@ bool GLGizmosManager::is_in_editing_mode(bool error_notification) const if (error_notification) wxGetApp().plater()->get_notification_manager()->push_notification( NotificationType::QuitSLAManualMode, - NotificationManager::NotificationLevel::ErrorNotification, + NotificationManager::NotificationLevel::ErrorNotificationLevel, _u8L("You are currently editing SLA support points. Please, " "apply or discard your changes first.")); diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index fe5eeb67c2..53435e318c 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -1012,7 +1012,7 @@ void NotificationManager::HintNotification::retrieve_data(bool new_hint/* = true if(hint_data != nullptr) { NotificationData nd { NotificationType::DidYouKnowHint, - NotificationLevel::RegularNotification, + NotificationLevel::RegularNotificationLevel, 0, hint_data->text, hint_data->hypertext, nullptr, diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 2744920da8..2ccb7a04f1 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -9,6 +9,7 @@ namespace Slic3r { class ModelInstance; namespace GUI { +class NotificationManager; class ArrangeJob : public PlaterJob { @@ -39,8 +40,8 @@ protected: void process() override; public: - ArrangeJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} + ArrangeJob(std::shared_ptr nm, Plater *plater) + : PlaterJob{nm, plater} {} int status_range() const override diff --git a/src/slic3r/GUI/Jobs/FillBedJob.hpp b/src/slic3r/GUI/Jobs/FillBedJob.hpp index bf407656d1..548974fa6b 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.hpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.hpp @@ -6,6 +6,7 @@ namespace Slic3r { namespace GUI { class Plater; +class NotificationManager; class FillBedJob : public PlaterJob { @@ -27,8 +28,8 @@ protected: void process() override; public: - FillBedJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} + FillBedJob(std::shared_ptr nm, Plater *plater) + : PlaterJob{nm, plater} {} int status_range() const override diff --git a/src/slic3r/GUI/Jobs/Job.cpp b/src/slic3r/GUI/Jobs/Job.cpp index 6346227aab..c198de40ca 100644 --- a/src/slic3r/GUI/Jobs/Job.cpp +++ b/src/slic3r/GUI/Jobs/Job.cpp @@ -2,9 +2,11 @@ #include #include "Job.hpp" +#include "../NotificationManager.hpp" #include #include + namespace Slic3r { void GUI::Job::run(std::exception_ptr &eptr) @@ -30,8 +32,8 @@ void GUI::Job::update_status(int st, const wxString &msg) wxQueueEvent(this, evt); } -GUI::Job::Job(std::shared_ptr pri) - : m_progress(std::move(pri)) +GUI::Job::Job(std::shared_ptr nm) + : m_notifications(nm) { m_thread_evt_id = wxNewId(); @@ -40,21 +42,21 @@ GUI::Job::Job(std::shared_ptr pri) auto msg = evt.GetString(); if (!msg.empty() && !m_worker_error) - m_progress->set_status_text(msg.ToUTF8().data()); + m_notifications->progress_indicator_set_status_text(msg.ToUTF8().data()); if (m_finalized) return; - m_progress->set_progress(evt.GetInt()); + m_notifications->progress_indicator_set_progress(evt.GetInt()); if (evt.GetInt() == status_range() || m_worker_error) { // set back the original range and cancel callback - m_progress->set_range(m_range); - m_progress->set_cancel_callback(); + m_notifications->progress_indicator_set_range(m_range); + m_notifications->progress_indicator_set_cancel_callback(); wxEndBusyCursor(); if (m_worker_error) { m_finalized = true; - m_progress->set_status_text(""); - m_progress->set_progress(m_range); + m_notifications->progress_indicator_set_status_text(""); + m_notifications->progress_indicator_set_progress(m_range); on_exception(m_worker_error); } else { @@ -86,12 +88,12 @@ void GUI::Job::start() prepare(); // Save the current status indicatior range and push the new one - m_range = m_progress->get_range(); - m_progress->set_range(status_range()); + m_range = m_notifications->progress_indicator_get_range(); + m_notifications->progress_indicator_set_range(status_range()); // init cancellation flag and set the cancel callback m_canceled.store(false); - m_progress->set_cancel_callback( + m_notifications->progress_indicator_set_cancel_callback( [this]() { m_canceled.store(true); }); m_finalized = false; diff --git a/src/slic3r/GUI/Jobs/Job.hpp b/src/slic3r/GUI/Jobs/Job.hpp index 8243ce9430..ed80d8b5fc 100644 --- a/src/slic3r/GUI/Jobs/Job.hpp +++ b/src/slic3r/GUI/Jobs/Job.hpp @@ -8,14 +8,13 @@ #include -#include "ProgressIndicator.hpp" - #include #include namespace Slic3r { namespace GUI { +class NotificationManager; // A class to handle UI jobs like arranging and optimizing rotation. // These are not instant jobs, the user has to be informed about their // state in the status progress indicator. On the other hand they are @@ -33,7 +32,7 @@ class Job : public wxEvtHandler boost::thread m_thread; std::atomic m_running{false}, m_canceled{false}; bool m_finalized = false, m_finalizing = false; - std::shared_ptr m_progress; + std::shared_ptr m_notifications; std::exception_ptr m_worker_error = nullptr; void run(std::exception_ptr &); @@ -65,7 +64,7 @@ protected: } public: - Job(std::shared_ptr pri); + Job(std::shared_ptr nm); bool is_finalized() const { return m_finalized; } diff --git a/src/slic3r/GUI/Jobs/PlaterJob.hpp b/src/slic3r/GUI/Jobs/PlaterJob.hpp index fcf0a54b80..52e5741aa4 100644 --- a/src/slic3r/GUI/Jobs/PlaterJob.hpp +++ b/src/slic3r/GUI/Jobs/PlaterJob.hpp @@ -6,6 +6,7 @@ namespace Slic3r { namespace GUI { class Plater; +class NotificationManager; class PlaterJob : public Job { protected: @@ -15,8 +16,8 @@ protected: public: - PlaterJob(std::shared_ptr pri, Plater *plater): - Job{std::move(pri)}, m_plater{plater} {} + PlaterJob(std::shared_ptr nm, Plater *plater): + Job{nm}, m_plater{plater} {} }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index cdb367f23a..f967bb7a37 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -10,6 +10,8 @@ namespace Slic3r { namespace GUI { +class NotificationManager; + class RotoptimizeJob : public PlaterJob { using FindFn = std::function pri, Plater *plater) - : PlaterJob{std::move(pri), plater} + RotoptimizeJob(std::shared_ptr nm, Plater *plater) + : PlaterJob{nm, plater} {} void finalize() override; diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 027bf68492..9b9151f773 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/NotificationManager.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -124,8 +125,8 @@ public: priv(Plater *plt) : plater{plt} {} }; -SLAImportJob::SLAImportJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater}, p{std::make_unique(plater)} +SLAImportJob::SLAImportJob(std::shared_ptr nm, Plater *plater) + : PlaterJob{nm, plater}, p{std::make_unique(plater)} {} SLAImportJob::~SLAImportJob() = default; diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.hpp b/src/slic3r/GUI/Jobs/SLAImportJob.hpp index 583c986559..7bc4d5e7e3 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.hpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.hpp @@ -5,6 +5,8 @@ namespace Slic3r { namespace GUI { +class NotificationManager; + class SLAImportJob : public PlaterJob { class priv; @@ -16,7 +18,7 @@ protected: void finalize() override; public: - SLAImportJob(std::shared_ptr pri, Plater *plater); + SLAImportJob(std::shared_ptr nm, Plater *plater); ~SLAImportJob(); void reset(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 51dbc201d3..6d5fbf11c7 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -155,13 +155,13 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S SetIcon(main_frame_icon(wxGetApp().get_app_mode())); // initialize status bar - m_statusbar = std::make_shared(this); - m_statusbar->set_font(GUI::wxGetApp().normal_font()); - if (wxGetApp().is_editor()) - m_statusbar->embed(this); - m_statusbar->set_status_text(_L("Version") + " " + - SLIC3R_VERSION + " - " + - _L("Remember to check for updates at https://github.com/prusa3d/PrusaSlicer/releases")); +// m_statusbar = std::make_shared(this); +// m_statusbar->set_font(GUI::wxGetApp().normal_font()); +// if (wxGetApp().is_editor()) +// m_statusbar->embed(this); +// m_statusbar->set_status_text(_L("Version") + " " + +// SLIC3R_VERSION + " - " + +// _L("Remember to check for updates at https://github.com/prusa3d/PrusaSlicer/releases")); // initialize tabpanel and menubar init_tabpanel(); @@ -1033,7 +1033,7 @@ void MainFrame::on_sys_color_changed() wxGetApp().init_label_colours(); #ifdef __WXMSW__ wxGetApp().UpdateDarkUI(m_tabpanel); - m_statusbar->update_dark_ui(); + // m_statusbar->update_dark_ui(); #ifdef _MSW_DARK_MODE // update common mode sizer if (!wxGetApp().tabs_as_menu()) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index e87f94f650..487d002af2 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -204,7 +204,7 @@ public: wxWindow* m_plater_page{ nullptr }; wxProgressDialog* m_progress_dialog { nullptr }; PrintHostQueueDialog* m_printhost_queue_dlg; - std::shared_ptr m_statusbar; +// std::shared_ptr m_statusbar; #ifdef __APPLE__ std::unique_ptr m_taskbar_icon; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 172e38d53a..0841efcb06 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -33,32 +33,32 @@ wxDEFINE_EVENT(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, ExportGcodeNotificationClic wxDEFINE_EVENT(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, PresetUpdateAvailableClickedEvent); const NotificationManager::NotificationData NotificationManager::basic_notifications[] = { - {NotificationType::Mouse3dDisconnected, NotificationLevel::RegularNotification, 10, _u8L("3D Mouse disconnected.") }, - {NotificationType::PresetUpdateAvailable, NotificationLevel::ImportantNotification, 20, _u8L("Configuration update is available."), _u8L("See more."), + {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::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New version is available."), _u8L("See Releases page."), [](wxEvtHandler* evnthndlr) { + {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::EmptyColorChangeCode, NotificationLevel::RegularNotification, 10, + {NotificationType::EmptyColorChangeCode, NotificationLevel::RegularNotificationLevel, 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::RegularNotification, 10, + {NotificationType::EmptyAutoColorChange, NotificationLevel::RegularNotificationLevel, 10, _u8L("No color change event was added to the print. The print does not look like a sign.") }, - {NotificationType::DesktopIntegrationSuccess, NotificationLevel::RegularNotification, 10, + {NotificationType::DesktopIntegrationSuccess, NotificationLevel::RegularNotificationLevel, 10, _u8L("Desktop integration was successful.") }, - {NotificationType::DesktopIntegrationFail, NotificationLevel::WarningNotification, 10, + {NotificationType::DesktopIntegrationFail, NotificationLevel::WarningNotificationLevel, 10, _u8L("Desktop integration failed.") }, - {NotificationType::UndoDesktopIntegrationSuccess, NotificationLevel::RegularNotification, 10, + {NotificationType::UndoDesktopIntegrationSuccess, NotificationLevel::RegularNotificationLevel, 10, _u8L("Undo desktop integration was successful.") }, - {NotificationType::UndoDesktopIntegrationFail, NotificationLevel::WarningNotification, 10, + {NotificationType::UndoDesktopIntegrationFail, NotificationLevel::WarningNotificationLevel, 10, _u8L("Undo desktop integration failed.") }, - //{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") }, - //{NotificationType::LoadingFailed, NotificationLevel::RegularNotification, 20, _u8L("Loading of model has Failed") }, - //{NotificationType::DeviceEjected, NotificationLevel::RegularNotification, 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 + //{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 { @@ -255,13 +255,13 @@ bool NotificationManager::PopNotification::push_background_color() push_style_color(ImGuiCol_WindowBg, backcolor, m_state == EState::FadingOut, m_current_fade_opacity); return true; } - if (m_data.level == NotificationLevel::ErrorNotification) { + if (m_data.level == NotificationLevel::ErrorNotificationLevel) { ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); backcolor.x += 0.3f; push_style_color(ImGuiCol_WindowBg, backcolor, m_state == EState::FadingOut, m_current_fade_opacity); return true; } - if (m_data.level == NotificationLevel::WarningNotification) { + if (m_data.level == NotificationLevel::WarningNotificationLevel) { ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); backcolor.x += 0.3f; backcolor.y += 0.15f; @@ -276,9 +276,9 @@ void NotificationManager::PopNotification::count_spaces() m_line_height = ImGui::CalcTextSize("A").y; m_left_indentation = m_line_height; - if (m_data.level == NotificationLevel::ErrorNotification || m_data.level == NotificationLevel::WarningNotification) { + if (m_data.level == NotificationLevel::ErrorNotificationLevel || m_data.level == NotificationLevel::WarningNotificationLevel) { std::string text; - text = (m_data.level == NotificationLevel::ErrorNotification ? ImGui::ErrorMarker : ImGui::WarningMarker); + text = (m_data.level == NotificationLevel::ErrorNotificationLevel ? ImGui::ErrorMarker : ImGui::WarningMarker); float picture_width = ImGui::CalcTextSize(text.c_str()).x; m_left_indentation = picture_width + m_line_height / 2; } @@ -505,9 +505,9 @@ void NotificationManager::PopNotification::render_close_button(ImGuiWrapper& img void NotificationManager::PopNotification::render_left_sign(ImGuiWrapper& imgui) { - if (m_data.level == NotificationLevel::ErrorNotification || m_data.level == NotificationLevel::WarningNotification) { + if (m_data.level == NotificationLevel::ErrorNotificationLevel || m_data.level == NotificationLevel::WarningNotificationLevel) { std::string text; - text = (m_data.level == NotificationLevel::ErrorNotification ? ImGui::ErrorMarker : ImGui::WarningMarker); + text = (m_data.level == NotificationLevel::ErrorNotificationLevel ? ImGui::ErrorMarker : ImGui::WarningMarker); ImGui::SetCursorPosX(m_line_height / 3); ImGui::SetCursorPosY(m_window_height / 2 - m_line_height); imgui.text(text.c_str()); @@ -583,19 +583,23 @@ bool NotificationManager::PopNotification::update_state(bool paused, const int64 int64_t now = GLCanvas3D::timestamp_now(); + // reset fade opacity for non-closing notifications or hover during fading + if (m_state != EState::FadingOut && m_state != EState::ClosePending && m_state != EState::Finished) { + m_current_fade_opacity = 1.0f; + } + // reset timers - hovered state is set in render if (m_state == EState::Hovered) { - m_current_fade_opacity = 1.0f; m_state = EState::Unknown; init(); // Timers when not fading - } else if (m_state != EState::NotFading && m_state != EState::FadingOut && m_data.duration != 0 && !paused) { + } else if (m_state != EState::NotFading && m_state != EState::FadingOut && get_duration() != 0 && !paused) { int64_t up_time = now - m_notification_start; - if (up_time >= m_data.duration * 1000) { + if (up_time >= get_duration() * 1000) { m_state = EState::FadingOut; m_fading_start = now; } else { - m_next_render = m_data.duration * 1000 - up_time; + m_next_render = get_duration() * 1000 - up_time; } } // Timers when fading @@ -626,53 +630,6 @@ bool NotificationManager::PopNotification::update_state(bool paused, const int64 return false; } -NotificationManager::SlicingCompleteLargeNotification::SlicingCompleteLargeNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, bool large) : - NotificationManager::PopNotification(n, id_provider, evt_handler) -{ - set_large(large); -} -void NotificationManager::SlicingCompleteLargeNotification::render_text(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) -{ - if (!m_is_large) - PopNotification::render_text(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); - else { - ImVec2 win_size(win_size_x, win_size_y); - ImVec2 text1_size = ImGui::CalcTextSize(m_text1.c_str()); - float x_offset = m_left_indentation; - std::string fulltext = m_text1 + m_hypertext + m_text2; - ImVec2 text_size = ImGui::CalcTextSize(fulltext.c_str()); - float cursor_y = win_size.y / 2 - text_size.y / 2; - if (m_has_print_info) { - x_offset = 20; - cursor_y = win_size.y / 2 + win_size.y / 6 - text_size.y / 2; - ImGui::SetCursorPosX(x_offset); - ImGui::SetCursorPosY(cursor_y); - imgui.text(m_print_info.c_str()); - cursor_y = win_size.y / 2 - win_size.y / 6 - text_size.y / 2; - } - ImGui::SetCursorPosX(x_offset); - ImGui::SetCursorPosY(cursor_y); - imgui.text(m_text1.c_str()); - - render_hypertext(imgui, x_offset + text1_size.x + 4, cursor_y, m_hypertext); - } -} -void NotificationManager::SlicingCompleteLargeNotification::set_print_info(const std::string &info) -{ - m_print_info = info; - m_has_print_info = true; - if (m_is_large) - m_lines_count = 2; -} -void NotificationManager::SlicingCompleteLargeNotification::set_large(bool l) -{ - m_is_large = l; - //FIXME this information should not be lost (change m_data?) -// m_counting_down = !l; - m_hypertext = l ? _u8L("Export G-Code.") : std::string(); - m_state = l ? EState::Shown : EState::Hidden; - init(); -} //---------------ExportFinishedNotification----------- void NotificationManager::ExportFinishedNotification::count_spaces() { @@ -680,9 +637,9 @@ void NotificationManager::ExportFinishedNotification::count_spaces() m_line_height = ImGui::CalcTextSize("A").y; m_left_indentation = m_line_height; - if (m_data.level == NotificationLevel::ErrorNotification || m_data.level == NotificationLevel::WarningNotification) { + if (m_data.level == NotificationLevel::ErrorNotificationLevel || m_data.level == NotificationLevel::WarningNotificationLevel) { std::string text; - text = (m_data.level == NotificationLevel::ErrorNotification ? ImGui::ErrorMarker : ImGui::WarningMarker); + text = (m_data.level == NotificationLevel::ErrorNotificationLevel ? ImGui::ErrorMarker : ImGui::WarningMarker); float picture_width = ImGui::CalcTextSize(text.c_str()).x; m_left_indentation = picture_width + m_line_height / 2; } @@ -793,6 +750,9 @@ void NotificationManager::ProgressBarNotification::init() { PopNotification::init(); //m_lines_count++; + if (m_endlines.empty()) { + m_endlines.push_back(0); + } if(m_lines_count >= 2) { m_lines_count = 3; m_multiline = true; @@ -897,8 +857,17 @@ void NotificationManager::ProgressBarNotification::render_bar(ImGuiWrapper& imgu ImVec2 lineEnd = ImVec2(win_pos_x - m_window_width_offset, win_pos_y + win_size_y / 2 + (m_multiline ? m_line_height / 2 : 0)); ImVec2 lineStart = ImVec2(win_pos_x - win_size_x + m_left_indentation, win_pos_y + win_size_y / 2 + (m_multiline ? m_line_height / 2 : 0)); ImVec2 midPoint = ImVec2(lineStart.x + (lineEnd.x - lineStart.x) * m_percentage, lineStart.y); - ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, IM_COL32((int)(gray_color.x * 255), (int)(gray_color.y * 255), (int)(gray_color.z * 255), (1.0f * 255.f)), m_line_height * 0.2f); - ImGui::GetWindowDrawList()->AddLine(lineStart, midPoint, IM_COL32((int)(orange_color.x * 255), (int)(orange_color.y * 255), (int)(orange_color.z * 255), (1.0f * 255.f)), m_line_height * 0.2f); + ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, IM_COL32((int)(gray_color.x * 255), (int)(gray_color.y * 255), (int)(gray_color.z * 255), (m_current_fade_opacity * 255.f)), m_line_height * 0.2f); + ImGui::GetWindowDrawList()->AddLine(lineStart, midPoint, IM_COL32((int)(orange_color.x * 255), (int)(orange_color.y * 255), (int)(orange_color.z * 255), (m_current_fade_opacity * 255.f)), m_line_height * 0.2f); + if (m_render_percentage) { + std::string text; + std::stringstream stream; + stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "%"; + text = stream.str(); + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4)); + imgui.text(text.c_str()); + } } //------PrintHostUploadNotification---------------- void NotificationManager::PrintHostUploadNotification::init() @@ -915,7 +884,7 @@ void NotificationManager::PrintHostUploadNotification::count_spaces() m_left_indentation = m_line_height; if (m_uj_state == UploadJobState::PB_ERROR) { std::string text; - text = (m_data.level == NotificationLevel::ErrorNotification ? ImGui::ErrorMarker : ImGui::WarningMarker); + text = (m_data.level == NotificationLevel::ErrorNotificationLevel ? ImGui::ErrorMarker : ImGui::WarningMarker); float picture_width = ImGui::CalcTextSize(text.c_str()).x; m_left_indentation = picture_width + m_line_height / 2; } @@ -1102,15 +1071,382 @@ void NotificationManager::UpdatedItemsInfoNotification::render_left_sign(ImGuiWr imgui.text(text.c_str()); } +//------SlicingProgressNotificastion +void NotificationManager::SlicingProgressNotification::init() +{ + if (m_sp_state == SlicingProgressState::SP_PROGRESS) { + ProgressBarNotification::init(); + //if (m_state == EState::NotFading && m_percentage >= 1.0f) + // m_state = EState::Shown; + } + else { + PopNotification::init(); + } + +} +void NotificationManager::SlicingProgressNotification::set_progress_state(float percent) +{ + if (percent < 0.f) + set_progress_state(SlicingProgressState::SP_CANCELLED); + else if (percent >= 1.f) + set_progress_state(SlicingProgressState::SP_COMPLETED); + else + set_progress_state(SlicingProgressState::SP_PROGRESS, percent); +} +void NotificationManager::SlicingProgressNotification::set_progress_state(NotificationManager::SlicingProgressNotification::SlicingProgressState state, float percent/* = 0.f*/) +{ + switch (state) + { + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_NO_SLICING: + m_state = EState::Hidden; + set_percentage(-1); + m_has_print_info = false; + set_export_possible(false); + break; + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_PROGRESS: + set_percentage(percent); + m_has_cancel_button = true; + break; + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_CANCELLED: + set_percentage(-1); + m_has_cancel_button = false; + m_has_print_info = false; + set_export_possible(false); + break; + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_COMPLETED: + set_percentage(1); + m_has_cancel_button = false; + m_has_print_info = false; + // m_export_possible is important only for PROGRESS state, thus we can reset it here + set_export_possible(false); + break; + default: + break; + } + m_sp_state = state; +} +void NotificationManager::SlicingProgressNotification::set_status_text(const std::string& text) +{ + switch (m_sp_state) + { + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_NO_SLICING: + m_state = EState::Hidden; + break; + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_PROGRESS: + { + NotificationData data{ NotificationType::SlicingProgress, NotificationLevel::ProgressBarNotificationLevel, 0, text + ".", m_is_fff ? _u8L("Export G-Code.") : _u8L("Export.") }; + update(data); + m_state = EState::NotFading; + } + break; + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_CANCELLED: + { + NotificationData data{ NotificationType::SlicingProgress, NotificationLevel::ProgressBarNotificationLevel, 0, text }; + update(data); + m_state = EState::Shown; + } + break; + case Slic3r::GUI::NotificationManager::SlicingProgressNotification::SlicingProgressState::SP_COMPLETED: + { + NotificationData data{ NotificationType::SlicingProgress, NotificationLevel::ProgressBarNotificationLevel, 0, _u8L("Slicing finished."), m_is_fff ? _u8L("Export G-Code.") : _u8L("Export.") }; + update(data); + m_state = EState::Shown; + } + break; + default: + break; + } +} +void NotificationManager::SlicingProgressNotification::set_print_info(const std::string& info) +{ + if (m_sp_state != SlicingProgressState::SP_COMPLETED) { + set_progress_state (SlicingProgressState::SP_COMPLETED); + } else { + m_has_print_info = true; + m_print_info = info; + } +} +void NotificationManager::SlicingProgressNotification::set_sidebar_collapsed(bool collapsed) +{ + m_sidebar_collapsed = collapsed; + if (m_sp_state == SlicingProgressState::SP_COMPLETED) + m_state = EState::Shown; +} + +void NotificationManager::SlicingProgressNotification::on_cancel_button() +{ + if (m_cancel_callback){ + m_cancel_callback(); + } +} +int NotificationManager::SlicingProgressNotification::get_duration() +{ + if (m_sp_state == SlicingProgressState::SP_CANCELLED) + return 10; + else if (m_sp_state == SlicingProgressState::SP_COMPLETED && !m_sidebar_collapsed) + return 5; + else + return 0; +} +bool NotificationManager::SlicingProgressNotification::update_state(bool paused, const int64_t delta) +{ + bool ret = ProgressBarNotification::update_state(paused, delta); + // sets Estate to hidden + if (get_state() == PopNotification::EState::ClosePending || get_state() == PopNotification::EState::Finished) + set_progress_state(SlicingProgressState::SP_NO_SLICING); + return ret; +} +void NotificationManager::SlicingProgressNotification::render_text(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + if (m_sp_state == SlicingProgressState::SP_PROGRESS || (m_sp_state == SlicingProgressState::SP_COMPLETED && !m_sidebar_collapsed)) { + ProgressBarNotification::render_text(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + /* // enable for hypertext during slicing (correct call of export_enabled needed) + if (m_multiline) { + // two lines text, one line bar + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(m_line_height / 4); + imgui.text(m_text1.substr(0, m_endlines[0]).c_str()); + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(m_line_height + m_line_height / 4); + std::string line = m_text1.substr(m_endlines[0] + (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0), m_endlines[1] - m_endlines[0] - (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0)); + imgui.text(line.c_str()); + if (m_sidebar_collapsed && m_sp_state == SlicingProgressState::SP_PROGRESS && m_export_possible) { + ImVec2 text_size = ImGui::CalcTextSize(line.c_str()); + render_hypertext(imgui, m_left_indentation + text_size.x + 4, m_line_height + m_line_height / 4, m_hypertext); + } + if (m_has_cancel_button) + 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 { + //one line text, one line bar + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(m_line_height / 4); + std::string line = m_text1.substr(0, m_endlines[0]); + imgui.text(line.c_str()); + if (m_sidebar_collapsed && m_sp_state == SlicingProgressState::SP_PROGRESS && m_export_possible) { + ImVec2 text_size = ImGui::CalcTextSize(line.c_str()); + render_hypertext(imgui, m_left_indentation + text_size.x + 4, m_line_height / 4, m_hypertext); + } + if (m_has_cancel_button) + 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 if (m_sp_state == SlicingProgressState::SP_COMPLETED) { + // "Slicing Finished" on line 1 + hypertext, print info on line + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 text1_size = ImGui::CalcTextSize(m_text1.c_str()); + float x_offset = m_left_indentation; + std::string fulltext = m_text1 + m_hypertext + m_text2; + ImVec2 text_size = ImGui::CalcTextSize(fulltext.c_str()); + float cursor_y = win_size.y / 2 - text_size.y / 2; + if (m_sidebar_collapsed && m_has_print_info) { + x_offset = 20; + cursor_y = win_size.y / 2 + win_size.y / 6 - text_size.y / 2; + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(m_print_info.c_str()); + cursor_y = win_size.y / 2 - win_size.y / 6 - text_size.y / 2; + } + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(m_text1.c_str()); + if (m_sidebar_collapsed) + render_hypertext(imgui, x_offset + text1_size.x + 4, cursor_y, m_hypertext); + } else { + PopNotification::render_text(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + } +} +void NotificationManager::SlicingProgressNotification::render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + if (!(m_sp_state == SlicingProgressState::SP_PROGRESS || (m_sp_state == SlicingProgressState::SP_COMPLETED && !m_sidebar_collapsed))) { + return; + } + //std::string text; + ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + /* + std::stringstream stream; + stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "%"; + text = stream.str(); + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4)); + imgui.text(text.c_str()); + */ +} +void NotificationManager::SlicingProgressNotification::render_cancel_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CancelButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CancelHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + on_cancel_button(); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + on_cancel_button(); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); +} + +void NotificationManager::SlicingProgressNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + // Do not render close button while showing progress - cancel button is rendered instead + if (m_sp_state != SlicingProgressState::SP_PROGRESS) { + ProgressBarNotification::render_close_button(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + } +} +//------ProgressIndicatorNotification------- +void NotificationManager::ProgressIndicatorNotification::set_status_text(const char* text) +{ + NotificationData data{ NotificationType::ProgressIndicator, NotificationLevel::ProgressBarNotificationLevel, 0, text }; + update(data); +} + +void NotificationManager::ProgressIndicatorNotification::init() +{ + // skip ProgressBarNotification::init (same code here) + PopNotification::init(); + if (m_endlines.empty()) { + m_endlines.push_back(0); + } + if (m_lines_count >= 2) { + m_lines_count = 3; + m_multiline = true; + while (m_endlines.size() < 3) + m_endlines.push_back(m_endlines.back()); + } + else { + m_lines_count = 2; + m_endlines.push_back(m_endlines.back()); + } + switch (m_progress_state) + { + case Slic3r::GUI::NotificationManager::ProgressIndicatorNotification::ProgressIndicatorState::PIS_HIDDEN: + m_state = EState::Hidden; + break; + case Slic3r::GUI::NotificationManager::ProgressIndicatorNotification::ProgressIndicatorState::PIS_PROGRESS_REQUEST: + case Slic3r::GUI::NotificationManager::ProgressIndicatorNotification::ProgressIndicatorState::PIS_PROGRESS_UPDATED: + m_state = EState::NotFading; + break; + case Slic3r::GUI::NotificationManager::ProgressIndicatorNotification::ProgressIndicatorState::PIS_COMPLETED: + m_state = EState::Shown; + break; + default: + break; + } +} +void NotificationManager::ProgressIndicatorNotification::set_percentage(float percent) +{ + ProgressBarNotification::set_percentage(percent); + if (percent >= 0.0f && percent < 1.0f) { + m_state = EState::NotFading; + m_has_cancel_button = true; + m_progress_state = ProgressIndicatorState::PIS_PROGRESS_REQUEST; + } else if (percent >= 1.0f) { + m_state = EState::Shown; + m_progress_state = ProgressIndicatorState::PIS_COMPLETED; + m_has_cancel_button = false; + } else { + m_progress_state = ProgressIndicatorState::PIS_HIDDEN; + m_state = EState::Hidden; + } +} +bool NotificationManager::ProgressIndicatorNotification::update_state(bool paused, const int64_t delta) +{ + if (m_progress_state == ProgressIndicatorState::PIS_PROGRESS_REQUEST) { + // percentage was changed (and it called schedule_extra_frame), now update must know this needs render + m_next_render = 0; + m_progress_state = ProgressIndicatorState::PIS_PROGRESS_UPDATED; + return true; + } + bool ret = ProgressBarNotification::update_state(paused, delta); + if (get_state() == PopNotification::EState::ClosePending || get_state() == PopNotification::EState::Finished) + // go to PIS_HIDDEN state + set_percentage(-1.0f); + return ret; +} + +void NotificationManager::ProgressIndicatorNotification::render_cancel_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CancelButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CancelHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + on_cancel_button(); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + on_cancel_button(); + } + ImGui::PopStyleColor(5); +} +void NotificationManager::ProgressIndicatorNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + // Do not render close button while showing progress - cancel button is rendered instead + if (m_percentage >= 1.0f) + { + ProgressBarNotification::render_close_button(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + } +} //------NotificationManager-------- NotificationManager::NotificationManager(wxEvtHandler* evt_handler) : m_evt_handler(evt_handler) { } -NotificationManager::~NotificationManager() -{ - HintDatabase::get_instance().uninit(); -} + void NotificationManager::push_notification(const NotificationType type, int timestamp) { auto it = std::find_if(std::begin(basic_notifications), std::end(basic_notifications), @@ -1121,7 +1457,7 @@ void NotificationManager::push_notification(const NotificationType type, int tim } void NotificationManager::push_notification(const std::string& text, int timestamp) { - push_notification_data({ NotificationType::CustomNotification, NotificationLevel::RegularNotification, 10, text }, timestamp); + push_notification_data({ NotificationType::CustomNotification, NotificationLevel::RegularNotificationLevel, 10, text }, timestamp); } void NotificationManager::push_notification(NotificationType type, @@ -1133,11 +1469,11 @@ void NotificationManager::push_notification(NotificationType type, { int duration = 0; switch (level) { - case NotificationLevel::RegularNotification: duration = 10; break; - case NotificationLevel::ErrorNotification: break; - case NotificationLevel::WarningNotification: break; - case NotificationLevel::ImportantNotification: break; - case NotificationLevel::ProgressBarNotification: break; + case NotificationLevel::RegularNotificationLevel: duration = 10; break; + case NotificationLevel::ErrorNotificationLevel: break; + case NotificationLevel::WarningNotificationLevel: break; + case NotificationLevel::ImportantNotificationLevel: break; + case NotificationLevel::ProgressBarNotificationLevel: break; default: assert(false); return; @@ -1146,18 +1482,19 @@ void NotificationManager::push_notification(NotificationType type, } void NotificationManager::push_validate_error_notification(const std::string& text) { - push_notification_data({ NotificationType::ValidateError, NotificationLevel::ErrorNotification, 0, _u8L("ERROR:") + "\n" + text }, 0); + push_notification_data({ NotificationType::ValidateError, NotificationLevel::ErrorNotificationLevel, 0, _u8L("ERROR:") + "\n" + text }, 0); + set_slicing_progress_hidden(); } void NotificationManager::push_slicing_error_notification(const std::string& text) { set_all_slicing_errors_gray(false); - push_notification_data({ NotificationType::SlicingError, NotificationLevel::ErrorNotification, 0, _u8L("ERROR:") + "\n" + text }, 0); - close_notification_of_type(NotificationType::SlicingComplete); + push_notification_data({ NotificationType::SlicingError, NotificationLevel::ErrorNotificationLevel, 0, _u8L("ERROR:") + "\n" + text }, 0); + set_slicing_progress_hidden(); } void NotificationManager::push_slicing_warning_notification(const std::string& text, bool gray, ObjectID oid, int warning_step) { - NotificationData data { NotificationType::SlicingWarning, NotificationLevel::WarningNotification, 0, _u8L("WARNING:") + "\n" + text }; + NotificationData data { NotificationType::SlicingWarning, NotificationLevel::WarningNotificationLevel, 0, _u8L("WARNING:") + "\n" + text }; auto notification = std::make_unique(data, m_id_provider, m_evt_handler); notification->object_id = oid; @@ -1168,7 +1505,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t } void NotificationManager::push_plater_error_notification(const std::string& text) { - push_notification_data({ NotificationType::PlaterError, NotificationLevel::ErrorNotification, 0, _u8L("ERROR:") + "\n" + text }, 0); + push_notification_data({ NotificationType::PlaterError, NotificationLevel::ErrorNotificationLevel, 0, _u8L("ERROR:") + "\n" + text }, 0); } void NotificationManager::close_plater_error_notification(const std::string& text) @@ -1192,7 +1529,7 @@ void NotificationManager::push_plater_warning_notification(const std::string& te } } - NotificationData data{ NotificationType::PlaterWarning, NotificationLevel::WarningNotification, 0, _u8L("WARNING:") + "\n" + text }; + NotificationData data{ NotificationType::PlaterWarning, NotificationLevel::WarningNotificationLevel, 0, _u8L("WARNING:") + "\n" + text }; auto notification = std::make_unique(data, m_id_provider, m_evt_handler); push_notification_data(std::move(notification), 0); @@ -1252,49 +1589,12 @@ void NotificationManager::close_slicing_error_notification(const std::string& te } void NotificationManager::push_object_warning_notification(const std::string& text, ObjectID object_id, const std::string& hypertext/* = ""*/, std::function callback/* = std::function()*/) { - NotificationData data{ NotificationType::ObjectWarning, NotificationLevel::WarningNotification, 0, text, hypertext, callback }; + NotificationData data{ NotificationType::ObjectWarning, NotificationLevel::WarningNotificationLevel, 0, text, hypertext, callback }; auto notification = std::make_unique(data, m_id_provider, m_evt_handler); notification->object_id = object_id; notification->warning_step = 0; push_notification_data(std::move(notification), 0); } -void NotificationManager::push_slicing_complete_notification(int timestamp, bool large) -{ - std::string hypertext; - int time = 10; - if (has_slicing_error_notification()) - return; - if (large) { - hypertext = _u8L("Export G-Code."); - time = 0; - } - NotificationData data{ NotificationType::SlicingComplete, NotificationLevel::RegularNotification, time, _u8L("Slicing finished."), hypertext, - [](wxEvtHandler* evnthndlr){ - if (evnthndlr != nullptr) - wxPostEvent(evnthndlr, ExportGcodeNotificationClickedEvent(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED)); - return true; - } - }; - push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, large), timestamp); -} -void NotificationManager::set_slicing_complete_print_time(const std::string &info) -{ - for (std::unique_ptr ¬ification : m_pop_notifications) { - if (notification->get_type() == NotificationType::SlicingComplete) { - dynamic_cast(notification.get())->set_print_info(info); - break; - } - } -} -void NotificationManager::set_slicing_complete_large(bool large) -{ - for (std::unique_ptr ¬ification : m_pop_notifications) { - if (notification->get_type() == NotificationType::SlicingComplete) { - dynamic_cast(notification.get())->set_large(large); - break; - } - } -} void NotificationManager::close_notification_of_type(const NotificationType type) { for (std::unique_ptr ¬ification : m_pop_notifications) { @@ -1324,7 +1624,7 @@ void NotificationManager::remove_object_warnings_of_released_objects(const std:: void NotificationManager::push_exporting_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable) { close_notification_of_type(NotificationType::ExportFinished); - NotificationData data{ NotificationType::ExportFinished, NotificationLevel::RegularNotification, on_removable ? 0 : 20, _u8L("Exporting finished.") + "\n" + path }; + NotificationData data{ NotificationType::ExportFinished, NotificationLevel::RegularNotificationLevel, on_removable ? 0 : 20, _u8L("Exporting finished.") + "\n" + path }; push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, on_removable, path, dir_path), 0); } @@ -1338,7 +1638,7 @@ void NotificationManager::push_upload_job_notification(int id, float filesize, } } std::string text = PrintHostUploadNotification::get_upload_job_text(id, filename, host); - NotificationData data{ NotificationType::PrintHostUpload, NotificationLevel::ProgressBarNotification, 10, text }; + NotificationData data{ NotificationType::PrintHostUpload, NotificationLevel::ProgressBarNotificationLevel, 10, text }; push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, 0, id, filesize), 0); } void NotificationManager::set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage) @@ -1380,6 +1680,153 @@ void NotificationManager::upload_job_notification_show_error(int id, const std:: } } } + +void NotificationManager::init_slicing_progress_notification(std::function cancel_callback) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + dynamic_cast(notification.get())->set_cancel_callback(cancel_callback); + return; + } + } + NotificationData data{ NotificationType::SlicingProgress, NotificationLevel::ProgressBarNotificationLevel, 0, std::string(),std::string(), + [](wxEvtHandler* evnthndlr) { + if (evnthndlr != nullptr) + wxPostEvent(evnthndlr, ExportGcodeNotificationClickedEvent(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED)); + return true; + } + }; + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, cancel_callback), 0); +} +void NotificationManager::set_slicing_progress_percentage(const std::string& text, float percentage) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + SlicingProgressNotification* spn = dynamic_cast(notification.get()); + spn->set_progress_state(percentage); + spn->set_status_text(text); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } + // Slicing progress notification was not found - init it thru plater so correct cancel callback function is appended + wxGetApp().plater()->init_notification_manager(); +} + +void NotificationManager::set_slicing_progress_hidden() +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + SlicingProgressNotification* notif = dynamic_cast(notification.get()); + notif->set_progress_state(SlicingProgressNotification::SlicingProgressState::SP_NO_SLICING); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } + // Slicing progress notification was not found - init it thru plater so correct cancel callback function is appended + wxGetApp().plater()->init_notification_manager(); +} +void NotificationManager::set_slicing_complete_print_time(const std::string& info, bool sidebar_colapsed) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + dynamic_cast(notification.get())->set_sidebar_collapsed(sidebar_colapsed); + dynamic_cast(notification.get())->set_print_info(info); + break; + } + } +} +void NotificationManager::set_sidebar_collapsed(bool collapsed) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + dynamic_cast(notification.get())->set_sidebar_collapsed(collapsed); + break; + } + } +} +void NotificationManager::set_fff(bool fff) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + dynamic_cast(notification.get())->set_fff(fff); + break; + } + } +} +void NotificationManager::set_slicing_progress_export_possible() +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingProgress) { + dynamic_cast(notification.get())->set_export_possible(true); + break; + } + } +} +void NotificationManager::init_progress_indicator() +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::ProgressIndicator) { + return; + } + } + NotificationData data{ NotificationType::ProgressIndicator, NotificationLevel::ProgressBarNotificationLevel, 2}; + auto notification = std::make_unique(data, m_id_provider, m_evt_handler); + push_notification_data(std::move(notification), 0); +} + +void NotificationManager::progress_indicator_set_range(int range) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::ProgressIndicator) { + dynamic_cast(notification.get())->set_range(range); + return; + } + } + init_progress_indicator(); +} +void NotificationManager::progress_indicator_set_cancel_callback(CancelFn callback/* = CancelFn()*/) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::ProgressIndicator) { + dynamic_cast(notification.get())->set_cancel_callback(callback); + return; + } + } + init_progress_indicator(); +} +void NotificationManager::progress_indicator_set_progress(int pr) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::ProgressIndicator) { + dynamic_cast(notification.get())->set_progress(pr); + // Ask for rendering - needs to be done on every progress. Calls to here doesnt trigger IDLE event or rendering. + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(100); + return; + } + } + init_progress_indicator(); +} +void NotificationManager::progress_indicator_set_status_text(const char* text) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::ProgressIndicator) { + dynamic_cast(notification.get())->set_status_text(text); + return; + } + } + init_progress_indicator(); +} +int NotificationManager::progress_indicator_get_range() const +{ + for (const std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::ProgressIndicator) { + return dynamic_cast(notification.get())->get_range(); + } + } + return 0; +} + void NotificationManager::push_hint_notification(bool open_next) { for (std::unique_ptr& notification : m_pop_notifications) { @@ -1389,7 +1836,7 @@ void NotificationManager::push_hint_notification(bool open_next) } } - NotificationData data{ NotificationType::DidYouKnowHint, NotificationLevel::RegularNotification, 300, "" }; + NotificationData data{ NotificationType::DidYouKnowHint, NotificationLevel::HintNotificationLevel, 300, "" }; // from user - open now if (!open_next) { push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, open_next), 0); @@ -1397,8 +1844,8 @@ void NotificationManager::push_hint_notification(bool open_next) // at startup - delay for half a second to let other notification pop up, than try every 30 seconds // show only if no notifications are shown } else { - auto condition = [this]() { - return this->get_notification_count() == 0; + auto condition = [&self = std::as_const(*this)]() { + return self.get_notification_count() == 0; }; push_delayed_notification(std::make_unique(data, m_id_provider, m_evt_handler, open_next), condition, 500, 30000); } @@ -1412,7 +1859,10 @@ bool NotificationManager::is_hint_notification_open() } return false; } - +void NotificationManager::deactivate_loaded_hints() +{ + HintDatabase::get_instance().uninit(); +} void NotificationManager::push_updated_item_info_notification(InfoItemType type) { for (std::unique_ptr& notification : m_pop_notifications) { @@ -1422,7 +1872,7 @@ void NotificationManager::push_updated_item_info_notification(InfoItemType type) } } - NotificationData data{ NotificationType::UpdatedItemsInfo, NotificationLevel::RegularNotification, 5, "" }; + NotificationData data{ NotificationType::UpdatedItemsInfo, NotificationLevel::RegularNotificationLevel, 5, "" }; auto notification = std::make_unique(data, m_id_provider, m_evt_handler, type); if (push_notification_data(std::move(notification), 0)) { (dynamic_cast(m_pop_notifications.back().get()))->add_type(type); @@ -1444,17 +1894,20 @@ bool NotificationManager::push_notification_data(std::unique_ptrget_current_canvas3D(); - + bool retval = false; if (this->activate_existing(notification.get())) { - m_pop_notifications.back()->update(notification->get_data()); - canvas.schedule_extra_frame(0); - return false; + if (m_initialized) { // ignore update action - it cant be initialized if canvas and imgui context is not ready + m_pop_notifications.back()->update(notification->get_data()); + } } else { m_pop_notifications.emplace_back(std::move(notification)); - canvas.schedule_extra_frame(0); - return true; + retval = true; } + if (!m_initialized) + return retval; + GLCanvas3D& canvas = *wxGetApp().plater()->get_current_canvas3D(); + canvas.schedule_extra_frame(0); + return retval; } void NotificationManager::push_delayed_notification(std::unique_ptr notification, std::function condition_callback, int64_t initial_delay, int64_t delay_interval) @@ -1608,6 +2061,8 @@ void NotificationManager::set_in_preview(bool preview) notification->hide(preview); if (notification->get_type() == NotificationType::SignDetected) notification->hide(!preview); + if (m_in_preview && notification->get_type() == NotificationType::DidYouKnowHint) + notification->close(); } } diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index eb95aec580..76dc9a6887 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -6,6 +6,7 @@ #include "GLCanvas3D.hpp" #include "Event.hpp" #include "I18N.hpp" +#include "Jobs/ProgressIndicator.hpp" #include #include @@ -27,6 +28,8 @@ wxDECLARE_EVENT(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, ExportGcodeNotificationCli using PresetUpdateAvailableClickedEvent = SimpleEvent; wxDECLARE_EVENT(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, PresetUpdateAvailableClickedEvent); +using CancelFn = std::function; + class GLCanvas3D; class ImGuiWrapper; enum class InfoItemType; @@ -34,9 +37,6 @@ enum class InfoItemType; enum class NotificationType { CustomNotification, - // Notification on end of slicing and G-code processing (the full G-code preview is available). - // Contains a hyperlink to export the G-code to a removable media. - SlicingComplete, // SlicingNotPossible, // Notification on end of export to a removable media, with hyperling to eject the external media. // Obsolete by ExportFinished @@ -78,6 +78,10 @@ enum class NotificationType ProgressBar, // Progress bar with info from Print Host Upload Queue dialog. PrintHostUpload, + // Progress bar with cancel button, cannot be closed + // On end of slicing and G-code processing (the full G-code preview is available), + // contains a hyperlink to export the G-code to a removable media or hdd. + SlicingProgress, // Notification, when Color Change G-code is empty and user try to add color change on DoubleSlider. EmptyColorChangeCode, // Notification that custom supports/seams were deleted after mesh repair. @@ -100,6 +104,8 @@ enum class NotificationType // Shows when ObjectList::update_info_items finds information that should be stressed to the user // Might contain logo taken from gizmos UpdatedItemsInfo, + // Progress bar notification with methods to replace ProgressIndicator class. + ProgressIndicator }; class NotificationManager @@ -109,27 +115,31 @@ public: { // The notifications will be presented in the order of importance, thus these enum values // are sorted by the importance. - // "Good to know" notification, usually but not always with a quick fade-out. - RegularNotification = 1, + // Important notification with progress bar, no fade-out, might appear again after closing. Position at the bottom. + ProgressBarNotificationLevel = 1, + // "Did you know" notification with special icon and buttons, Position close to bottom. + HintNotificationLevel, + // "Good to know" notification, usually but not always with a quick fade-out. + RegularNotificationLevel, // Information notification without a fade-out or with a longer fade-out. - ImportantNotification, - // Important notification with progress bar, no fade-out, might appear again after closing. - ProgressBarNotification, + ImportantNotificationLevel, // Warning, no fade-out. - WarningNotification, - // Error, no fade-out. - ErrorNotification, + WarningNotificationLevel, + // Error, no fade-out. Top most position. + ErrorNotificationLevel, }; NotificationManager(wxEvtHandler* evt_handler); - ~NotificationManager(); + ~NotificationManager(){} + // init is called after canvas3d is created. Notifications added before init are not showed or updated + void init() { m_initialized = true; } // Push a prefabricated notification from basic_notifications (see the table at the end of this file). void push_notification(const NotificationType type, int timestamp = 0); - // Push a NotificationType::CustomNotification with NotificationLevel::RegularNotification and 10s fade out interval. + // Push a NotificationType::CustomNotification with NotificationLevel::RegularNotificationLevel and 10s fade out interval. void push_notification(const std::string& text, int timestamp = 0); - // Push a NotificationType::CustomNotification with provided notification level and 10s for RegularNotification. - // ErrorNotification and ImportantNotification are never faded out. + // Push a NotificationType::CustomNotification with provided notification level and 10s for RegularNotificationLevel. + // ErrorNotificationLevel and ImportantNotificationLevel are never faded out. void push_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext = "", std::function callback = std::function(), int timestamp = 0); // Creates Validate Error notification with a custom text and no fade out. @@ -162,25 +172,44 @@ public: // Close object warnings, whose ObjectID is not in the list. // living_oids is expected to be sorted. void remove_object_warnings_of_released_objects(const std::vector& living_oids); - // Creates special notification slicing complete. - // If large = true (Plater side bar is closed), then printing time and export button is shown - // at the notification and fade-out is disabled. Otherwise the fade out time is set to 10s. - void push_slicing_complete_notification(int timestamp, bool large); - // Add a print time estimate to an existing SlicingComplete notification. - void set_slicing_complete_print_time(const std::string &info); // Called when the side bar changes its visibility, as the "slicing complete" notification supplements // the "slicing info" normally shown at the side bar. - void set_slicing_complete_large(bool large); + void set_sidebar_collapsed(bool collapsed); + // Set technology for correct text in SlicingProgress. + void set_fff(bool b); + void set_fdm(bool b) { set_fff(b); } + void set_sla(bool b) { set_fff(!b); } // Exporting finished, show this information with path, button to open containing folder and if ejectable - eject button void push_exporting_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable); - // notification with progress bar + // notifications with progress bar + // print host upload void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0); void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage); void upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host); void upload_job_notification_show_error(int id, const std::string& filename, const std::string& host); + // slicing progress + void init_slicing_progress_notification(std::function cancel_callback); + // percentage negative = canceled, <0-1) = progress, 1 = completed + void set_slicing_progress_percentage(const std::string& text, float percentage); + // hides slicing progress notification imidietly + void set_slicing_progress_hidden(); + // Add a print time estimate to an existing SlicingProgress notification. Set said notification to SP_COMPLETED state. + void set_slicing_complete_print_time(const std::string& info, bool sidebar_colapsed); + void set_slicing_progress_export_possible(); + // ProgressIndicator notification + // init adds hidden instance of progress indi notif that should always live (goes to hidden instead of erasing) + void init_progress_indicator(); + // functions equal to ProgressIndicator class + void progress_indicator_set_range(int range); + void progress_indicator_set_cancel_callback(CancelFn callback = CancelFn()); + void progress_indicator_set_progress(int pr); + void progress_indicator_set_status_text(const char*); // utf8 char array + int progress_indicator_get_range() const; // Hint (did you know) notification void push_hint_notification(bool open_next); bool is_hint_notification_open(); + // Forces Hints to reload its content when next hint should be showed + void deactivate_loaded_hints(); void push_updated_item_info_notification(InfoItemType type); // Close old notification ExportFinished. void new_export_began(bool on_removable); @@ -267,7 +296,7 @@ private: virtual bool compare_text(const std::string& text) const; void hide(bool h) { if (is_finished()) return; m_state = h ? EState::Hidden : EState::Unknown; } // sets m_next_render with time of next mandatory rendering. Delta is time since last render. - bool update_state(bool paused, const int64_t delta); + virtual bool update_state(bool paused, const int64_t delta); int64_t next_render() const { return is_finished() ? 0 : m_next_render; } EState get_state() const { return m_state; } bool is_hovered() const { return m_state == EState::Hovered; } @@ -303,6 +332,8 @@ private: virtual void count_lines(); // returns true if PopStyleColor should be called later to pop this push virtual bool push_background_color(); + // used this function instead of reading directly m_data.duration. Some notifications might need to return changing value. + virtual int get_duration() { return m_data.duration; } const NotificationData m_data; // For reusing ImGUI windows. @@ -359,29 +390,7 @@ private: wxEvtHandler* m_evt_handler; }; - class SlicingCompleteLargeNotification : public PopNotification - { - public: - SlicingCompleteLargeNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, bool largeds); - void set_large(bool l); - bool get_large() { return m_is_large; } - void set_print_info(const std::string &info); - void render(GLCanvas3D& canvas, float initial_y, bool move_from_overlay, float overlay_width) override - { - // This notification is always hidden if !large (means side bar is collapsed) - if (!get_large() && !is_finished()) - m_state = EState::Hidden; - PopNotification::render(canvas, initial_y, move_from_overlay, overlay_width); - } - protected: - void render_text(ImGuiWrapper& imgui, - const float win_size_x, const float win_size_y, - const float win_pos_x, const float win_pos_y) - override; - bool m_is_large; - bool m_has_print_info { false }; - std::string m_print_info; - }; + class SlicingWarningNotification : public PopNotification { @@ -405,7 +414,7 @@ private: { public: - ProgressBarNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, float percentage) : PopNotification(n, id_provider, evt_handler) { } + ProgressBarNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler) : PopNotification(n, id_provider, evt_handler) { } virtual void set_percentage(float percent) { m_percentage = percent; } protected: virtual void init() override; @@ -423,9 +432,10 @@ private: {} void render_minimize_button(ImGuiWrapper& imgui, const float win_pos_x, const float win_pos_y) override {} - float m_percentage; + float m_percentage {0.0f}; bool m_has_cancel_button {false}; + bool m_render_percentage {false}; // local time of last hover for showing tooltip }; @@ -443,7 +453,7 @@ private: PB_COMPLETED }; PrintHostUploadNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, float percentage, int job_id, float filesize) - :ProgressBarNotification(n, id_provider, evt_handler, percentage) + :ProgressBarNotification(n, id_provider, evt_handler) , m_job_id(job_id) , m_file_size(filesize) { @@ -472,7 +482,117 @@ private: // Size of uploaded size to be displayed in MB float m_file_size; long m_hover_time{ 0 }; - UploadJobState m_uj_state{ UploadJobState::PB_PROGRESS }; + UploadJobState m_uj_state{ UploadJobState::PB_PROGRESS }; + }; + + class SlicingProgressNotification : public ProgressBarNotification + { + public: + // Inner state of notification, Each state changes bahaviour of the notification + enum class SlicingProgressState + { + SP_NO_SLICING, // hidden + SP_PROGRESS, // never fades outs, no close button, has cancel button + SP_CANCELLED, // fades after 10 seconds, simple message + SP_COMPLETED // Has export hyperlink and print info, fades after 20 sec if sidebar is shown, otherwise no fade out + }; + SlicingProgressNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, std::function callback) + : ProgressBarNotification(n, id_provider, evt_handler) + , m_cancel_callback(callback) + { + set_progress_state(SlicingProgressState::SP_NO_SLICING); + m_has_cancel_button = false; + m_render_percentage = true; + } + // sets text of notification - call after setting progress state + void set_status_text(const std::string& text); + // sets cancel button callback + void set_cancel_callback(std::function callback) { m_cancel_callback = callback; } + bool has_cancel_callback() const { return m_cancel_callback != nullptr; } + // sets SlicingProgressState, negative percent means canceled + void set_progress_state(float percent); + // sets SlicingProgressState, percent is used only at progress state. + void set_progress_state(SlicingProgressState state,float percent = 0.f); + // sets additional string of print info and puts notification into Completed state. + void set_print_info(const std::string& info); + // sets fading if in Completed state. + void set_sidebar_collapsed(bool collapsed); + // Calls inherited update_state and ensures Estate goes to hidden not closing. + bool update_state(bool paused, const int64_t delta) override; + // Switch between technology to provide correct text. + void set_fff(bool b) { m_is_fff = b; } + void set_fdm(bool b) { m_is_fff = b; } + void set_sla(bool b) { m_is_fff = !b; } + void set_export_possible(bool b) { m_export_possible = b; } + protected: + void init() override; + void count_lines() override + { + if (m_sp_state == SlicingProgressState::SP_PROGRESS) + ProgressBarNotification::count_lines(); + else + PopNotification::count_lines(); + } + void render_text(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) override; + void render_bar(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void render_cancel_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void render_close_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void on_cancel_button(); + int get_duration() override; + std::function m_cancel_callback; + SlicingProgressState m_sp_state { SlicingProgressState::SP_PROGRESS }; + bool m_has_print_info { false }; + std::string m_print_info; + bool m_sidebar_collapsed { false }; + bool m_is_fff { true }; + // if true, it is possible show export hyperlink in state SP_PROGRESS + bool m_export_possible { false }; + }; + + class ProgressIndicatorNotification : public ProgressBarNotification + { + public: + enum class ProgressIndicatorState + { + PIS_HIDDEN, // hidden + PIS_PROGRESS_REQUEST, // progress was updated, request render on next update_state() call + PIS_PROGRESS_UPDATED, // render was requested + PIS_COMPLETED // both completed and canceled state. fades out into PIS_NO_SLICING + }; + ProgressIndicatorNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler) + : ProgressBarNotification(n, id_provider, evt_handler) + { + m_render_percentage = true; + } + // ProgressIndicator + void set_range(int range) { m_range = range; } + void set_cancel_callback(CancelFn callback) { m_cancel_callback = callback; } + void set_progress(int pr) { set_percentage((float)pr / (float)m_range); } + void set_status_text(const char*); // utf8 char array + int get_range() const { return m_range; } + // ProgressBarNotification + void init() override; + void set_percentage(float percent) override; + bool update_state(bool paused, const int64_t delta) override; + // Own + protected: + int m_range { 100 }; + CancelFn m_cancel_callback { nullptr }; + ProgressIndicatorState m_progress_state { ProgressIndicatorState::PIS_HIDDEN }; + + void render_close_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void render_cancel_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void on_cancel_button() { if (m_cancel_callback) m_cancel_callback(); } }; class ExportFinishedNotification : public PopNotification @@ -499,7 +619,7 @@ private: void render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) override; - void render_eject_button(ImGuiWrapper& imgui, + void render_eject_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y); void render_minimize_button(ImGuiWrapper& imgui, const float win_pos_x, const float win_pos_y) override @@ -562,13 +682,15 @@ private: // If there is some error notification active, then the "Export G-code" notification after the slicing is finished is suppressed. bool has_slicing_error_notification(); + // set by init(), until false notifications are only added not updated and frame is not requested after push + bool m_initialized{ false }; // Target for wxWidgets events sent by clicking on the hyperlink available at some notifications. wxEvtHandler* m_evt_handler; // Cache of IDs to identify and reuse ImGUI windows. NotificationIDProvider m_id_provider; std::deque> m_pop_notifications; // delayed waiting notifications, first is remaining time - std::deque m_waiting_notifications; + std::vector m_waiting_notifications; //timestamps used for slicing finished - notification could be gone so it needs to be stored here std::unordered_set m_used_timestamps; // True if G-code preview is active. False if the Plater is active. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ac76fe6559..7d344c389b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1208,6 +1208,8 @@ void Sidebar::update_sliced_info_sizer() wxString t_est = std::isnan(ps.estimated_print_time) ? "N/A" : get_time_dhms(float(ps.estimated_print_time)); p->sliced_info->SetTextAndShow(siEstimatedTime, t_est, _L("Estimated printing time") + ":"); + p->plater->get_notification_manager()->set_slicing_complete_print_time(_utf8("Estimated printing time: ") + boost::nowide::narrow(t_est), p->plater->is_sidebar_collapsed()); + // Hide non-SLA sliced info parameters p->sliced_info->SetTextAndShow(siFilament_m, "N/A"); p->sliced_info->SetTextAndShow(siFilament_mm3, "N/A"); @@ -1296,10 +1298,7 @@ void Sidebar::update_sliced_info_sizer() new_label += format_wxstr("\n - %1%", _L("normal mode")); info_text += format_wxstr("\n%1%", short_time(ps.estimated_normal_print_time)); - // uncomment next line to not disappear slicing finished notif when colapsing sidebar before time estimate - //if (p->plater->is_sidebar_collapsed()) - p->plater->get_notification_manager()->set_slicing_complete_large(p->plater->is_sidebar_collapsed()); - p->plater->get_notification_manager()->set_slicing_complete_print_time("Estimated printing time: " + ps.estimated_normal_print_time); + p->plater->get_notification_manager()->set_slicing_complete_print_time(_utf8("Estimated printing time: ") + ps.estimated_normal_print_time, p->plater->is_sidebar_collapsed()); } if (ps.estimated_silent_print_time != "N/A") { @@ -1502,7 +1501,7 @@ struct Plater::priv GLToolbar view_toolbar; GLToolbar collapse_toolbar; Preview *preview; - NotificationManager* notification_manager { nullptr }; + std::shared_ptr notification_manager; ProjectDirtyStateManager dirty_state; @@ -1523,10 +1522,10 @@ struct Plater::priv public: Jobs(priv *_m) : m(_m) { - m_arrange_id = add_job(std::make_unique(m->statusbar(), m->q)); - m_fill_bed_id = add_job(std::make_unique(m->statusbar(), m->q)); - m_rotoptimize_id = add_job(std::make_unique(m->statusbar(), m->q)); - m_sla_import_id = add_job(std::make_unique(m->statusbar(), m->q)); + m_arrange_id = add_job(std::make_unique(m->notification_manager, m->q)); + m_fill_bed_id = add_job(std::make_unique(m->notification_manager, m->q)); + m_rotoptimize_id = add_job(std::make_unique(m->notification_manager, m->q)); + m_sla_import_id = add_job(std::make_unique(m->notification_manager, m->q)); } void arrange() @@ -1637,7 +1636,7 @@ struct Plater::priv void apply_free_camera_correction(bool apply = true); void update_ui_from_settings(); void update_main_toolbar_tooltips(); - std::shared_ptr statusbar(); +// std::shared_ptr statusbar(); std::string get_config(const std::string &key) const; BoundingBoxf bed_shape_bb() const; BoundingBox scaled_bed_shape_bb() const; @@ -1788,6 +1787,8 @@ struct Plater::priv // extension should contain the leading dot, i.e.: ".3mf" wxString get_project_filename(const wxString& extension = wxEmptyString) const; void set_project_filename(const wxString& filename); + // Call after plater and Canvas#D is initialized + void init_notification_manager(); // Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will not override it. mutable bool ready_to_slice = { false }; @@ -1797,6 +1798,7 @@ struct Plater::priv std::string last_output_dir_path; bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } bool process_completed_with_error { false }; + private: bool layers_height_allowed() const; @@ -1845,6 +1847,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "support_material_contact_distance", "support_material_bottom_contact_distance", "raft_layers" })) , sidebar(new Sidebar(q)) + , notification_manager(std::make_shared(q)) , m_ui_jobs(this) , delayed_scene_refresh(false) , view_toolbar(GLToolbar::Radio, "View") @@ -2012,7 +2015,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); #endif /* _WIN32 */ - notification_manager = new NotificationManager(this->q); + //notification_manager = new NotificationManager(this->q); + if (wxGetApp().is_editor()) { this->q->Bind(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, [this](EjectDriveNotificationClickedEvent&) { this->q->eject_drive(); }); this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); }); @@ -2022,12 +2026,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->show_action_buttons(this->ready_to_slice); notification_manager->close_notification_of_type(NotificationType::ExportFinished); notification_manager->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, format(_L("Successfully unmounted. The device %s(%s) can now be safely removed from the computer."), evt.data.first.name, evt.data.first.path) ); } else { notification_manager->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::ErrorNotification, + NotificationManager::NotificationLevel::ErrorNotificationLevel, format(_L("Ejecting of device %s(%s) has failed."), evt.data.first.name, evt.data.first.path) ); } @@ -2074,8 +2078,7 @@ Plater::priv::~priv() { if (config != nullptr) delete config; - if (notification_manager != nullptr) - delete notification_manager; + notification_manager->deactivate_loaded_hints(); } void Plater::priv::update(unsigned int flags) @@ -2150,6 +2153,8 @@ void Plater::priv::collapse_sidebar(bool collapse) new_tooltip += " [Shift+Tab]"; int id = collapse_toolbar.get_item_id("collapse_sidebar"); collapse_toolbar.set_tooltip(id, new_tooltip); + + notification_manager->set_sidebar_collapsed(collapse); } @@ -2177,10 +2182,11 @@ void Plater::priv::update_main_toolbar_tooltips() view3D->get_canvas3d()->update_tooltip_for_settings_item_in_main_toolbar(); } -std::shared_ptr Plater::priv::statusbar() -{ - return main_frame->m_statusbar; -} +//std::shared_ptr Plater::priv::statusbar() +//{ +// return nullptr; +// return main_frame->m_statusbar; +//} std::string Plater::priv::get_config(const std::string &key) const { @@ -2325,7 +2331,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (std::string& name : names) notif_text += "\n - " + name; notification_manager->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotification, notif_text); + NotificationManager::NotificationLevel::RegularNotificationLevel, notif_text); } } @@ -2467,7 +2473,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_model) { wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); // XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames... - statusbar()->set_status_text(_L("Loaded")); +// statusbar()->set_status_text(_L("Loaded")); } // automatic selection of added objects @@ -2880,7 +2886,7 @@ void Plater::priv::split_object() // If we splited object which is contain some parts/modifiers then all non-solid parts (modifiers) were deleted if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size()) notification_manager->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("All non-solid parts (modifiers) were deleted")); Plater::TakeSnapshot snapshot(q, _L("Split to Objects")); @@ -2956,7 +2962,7 @@ void Plater::priv::process_validation_warning(const std::string& warning) const notification_manager->push_notification( NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotification, + NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("WARNING:") + "\n" + text, hypertext, action_fn ); } @@ -3002,6 +3008,8 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // In SLA mode, we need to reload the 3D scene every time to show the support structures. if (printer_technology == ptSLA || (printer_technology == ptFFF && config->opt_bool("wipe_tower"))) return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; + + notification_manager->set_slicing_progress_hidden(); } if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && ! background_process.empty()) { @@ -3074,9 +3082,9 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool else { // Background data is valid. - if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || - (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0 ) - this->statusbar()->set_status_text(_L("Ready to slice")); +// if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || +// (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0 ) +// this->statusbar()->set_status_text(_L("Ready to slice")); sidebar->set_btn_label(ActionButtonType::abExport, _(label_btn_export)); sidebar->set_btn_label(ActionButtonType::abSendGCode, _(label_btn_send)); @@ -3113,10 +3121,10 @@ bool Plater::priv::restart_background_process(unsigned int state) (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) { // The print is valid and it can be started. if (this->background_process.start()) { - this->statusbar()->set_cancel_callback([this]() { - this->statusbar()->set_status_text(_L("Cancelling")); - this->background_process.stop(); - }); +// this->statusbar()->set_cancel_callback([this]() { +// this->statusbar()->set_status_text(_L("Cancelling")); +// this->background_process.stop(); +// }); if (!show_warning_dialog) on_slicing_began(); return true; @@ -3753,9 +3761,9 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) return; } - this->statusbar()->set_progress(evt.status.percent); - this->statusbar()->set_status_text(_(evt.status.text) + wxString::FromUTF8("…")); - //notification_manager->set_progress_bar_percentage("Slicing progress", (float)evt.status.percent / 100.0f); +// this->statusbar()->set_progress(evt.status.percent); +// this->statusbar()->set_status_text(_(evt.status.text) + wxString::FromUTF8("…")); + notification_manager->set_slicing_progress_percentage(evt.status.text, (float)evt.status.percent / 100.0f); } if (evt.status.flags & (PrintBase::SlicingStatus::RELOAD_SCENE | PrintBase::SlicingStatus::RELOAD_SLA_SUPPORT_POINTS)) { switch (this->printer_technology) { @@ -3806,7 +3814,6 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) void Plater::priv::on_slicing_completed(wxCommandEvent & evt) { - notification_manager->push_slicing_complete_notification(evt.GetInt(), is_sidebar_collapsed()); switch (this->printer_technology) { case ptFFF: this->update_fff_scene(); @@ -3829,8 +3836,8 @@ void Plater::priv::on_export_began(wxCommandEvent& evt) void Plater::priv::on_slicing_began() { clear_warnings(); - notification_manager->close_notification_of_type(NotificationType::SlicingComplete); notification_manager->close_notification_of_type(NotificationType::SignDetected); + notification_manager->close_notification_of_type(NotificationType::ExportFinished); } void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid) { @@ -3895,8 +3902,9 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) // At this point of time the thread should be either finished or canceled, // so the following call just confirms, that the produced data were consumed. this->background_process.stop(); - this->statusbar()->reset_cancel_callback(); - this->statusbar()->stop_busy(); +// this->statusbar()->reset_cancel_callback(); +// this->statusbar()->stop_busy(); + notification_manager->set_slicing_progress_export_possible(); // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. this->background_process.reset_export(); @@ -3913,7 +3921,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) show_error(q, message.first, message.second); } else notification_manager->push_slicing_error_notification(message.first); - this->statusbar()->set_status_text(from_u8(message.first)); +// this->statusbar()->set_status_text(from_u8(message.first)); if (evt.invalidate_plater()) { const wxString invalid_str = _L("Invalid data"); @@ -3923,8 +3931,10 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) } has_error = true; } - if (evt.cancelled()) - this->statusbar()->set_status_text(_L("Cancelled")); + if (evt.cancelled()) { +// this->statusbar()->set_status_text(_L("Cancelled")); + this->notification_manager->set_slicing_progress_percentage(_utf8("Slicing Cancelled."), -1); + } this->sidebar->show_sliced_info_sizer(evt.success()); @@ -4128,6 +4138,20 @@ void Plater::priv::set_project_filename(const wxString& filename) wxGetApp().mainframe->add_to_recent_projects(filename); } +void Plater::priv::init_notification_manager() +{ + if (!notification_manager) + return; + notification_manager->init(); + + auto cancel_callback = [this]() { + this->background_process.stop(); + }; + notification_manager->init_slicing_progress_notification(cancel_callback); + notification_manager->set_fff(printer_technology == ptFFF); + notification_manager->init_progress_indicator(); +} + void Plater::priv::set_current_canvas_as_dirty() { if (current_panel == view3D) @@ -5566,7 +5590,7 @@ void Plater::export_stl(bool extended, bool selection_only) } Slic3r::store_stl(path_u8.c_str(), &mesh, true); - p->statusbar()->set_status_text(format_wxstr(_L("STL file exported to %s"), path)); +// p->statusbar()->set_status_text(format_wxstr(_L("STL file exported to %s"), path)); } void Plater::export_amf() @@ -5583,10 +5607,10 @@ void Plater::export_amf() bool full_pathnames = wxGetApp().app_config->get("export_sources_full_pathnames") == "1"; if (Slic3r::store_amf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames)) { // Success - p->statusbar()->set_status_text(format_wxstr(_L("AMF file exported to %s"), path)); +// p->statusbar()->set_status_text(format_wxstr(_L("AMF file exported to %s"), path)); } else { // Failure - p->statusbar()->set_status_text(format_wxstr(_L("Error exporting AMF file %s"), path)); +// p->statusbar()->set_status_text(format_wxstr(_L("Error exporting AMF file %s"), path)); } } @@ -5625,12 +5649,12 @@ bool Plater::export_3mf(const boost::filesystem::path& output_path) bool ret = Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, full_pathnames, &thumbnail_data); if (ret) { // Success - p->statusbar()->set_status_text(format_wxstr(_L("3MF file exported to %s"), path)); +// p->statusbar()->set_status_text(format_wxstr(_L("3MF file exported to %s"), path)); p->set_project_filename(path); } else { // Failure - p->statusbar()->set_status_text(format_wxstr(_L("Error exporting 3MF file %s"), path)); +// p->statusbar()->set_status_text(format_wxstr(_L("Error exporting 3MF file %s"), path)); } return ret; } @@ -6204,6 +6228,8 @@ bool Plater::set_printer_technology(PrinterTechnology printer_technology) p->sidebar->get_searcher().set_printer_technology(printer_technology); + p->notification_manager->set_fff(printer_technology == ptFFF); + return ret; } @@ -6224,7 +6250,7 @@ void Plater::clear_before_change_mesh(int obj_idx) // snapshot_time is captured by copy so the lambda knows where to undo/redo to. get_notification_manager()->push_notification( NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::RegularNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("Custom supports, seams and multimaterial painting were " "removed after repairing the mesh.")); // _u8L("Undo the repair"), @@ -6237,7 +6263,7 @@ void Plater::clear_before_change_mesh(int obj_idx) // else // notification_manager->push_notification( // NotificationType::CustomSupportsAndSeamRemovedAfterRepair, -// NotificationManager::NotificationLevel::RegularNotification, +// NotificationManager::NotificationLevel::RegularNotificationLevel, // _u8L("Cannot undo to before the mesh repair!")); // return true; // }); @@ -6503,14 +6529,14 @@ Mouse3DController& Plater::get_mouse3d_controller() return p->mouse3d_controller; } -const NotificationManager* Plater::get_notification_manager() const +std::shared_ptr Plater::get_notification_manager() { return p->notification_manager; } -NotificationManager* Plater::get_notification_manager() +void Plater::init_notification_manager() { - return p->notification_manager; + p->init_notification_manager(); } bool Plater::can_delete() const { return p->can_delete(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f1a493b0f0..4a98797e5d 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -359,11 +359,11 @@ public: void set_bed_shape() const; void set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false) const; - const NotificationManager* get_notification_manager() const; - NotificationManager* get_notification_manager(); + std::shared_ptr get_notification_manager(); + void init_notification_manager(); void bring_instance_forward(); - + // ROII wrapper for suppressing the Undo / Redo snapshot to be taken. class SuppressSnapshots { diff --git a/src/slic3r/GUI/ProgressStatusBar.cpp b/src/slic3r/GUI/ProgressStatusBar.cpp index 10c3911613..18485659c5 100644 --- a/src/slic3r/GUI/ProgressStatusBar.cpp +++ b/src/slic3r/GUI/ProgressStatusBar.cpp @@ -194,3 +194,4 @@ void ProgressStatusBar::hide_cancel_button() } } + diff --git a/src/slic3r/GUI/ProgressStatusBar.hpp b/src/slic3r/GUI/ProgressStatusBar.hpp index 73c046be6d..3ce3c85792 100644 --- a/src/slic3r/GUI/ProgressStatusBar.hpp +++ b/src/slic3r/GUI/ProgressStatusBar.hpp @@ -25,6 +25,7 @@ namespace Slic3r { * of the Slicer main window. It consists of a message area to the left and a * progress indication area to the right with an optional cancel button. */ + class ProgressStatusBar : public ProgressIndicator { wxStatusBar *self; // we cheat! It should be the base class but: perl! From 670ec06b971a96f2e2fb4953ca4f1eb2e9866db2 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 10 Sep 2021 15:31:03 +0200 Subject: [PATCH 142/151] "CANCEL" button is deleted from "Support Generator" MessageDialog to avoid confusions when "Supports mode" is switched from right panel --- src/slic3r/GUI/ConfigManipulation.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index d4920d8364..070773c116 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -171,23 +171,14 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con "- Detect bridging perimeters")); if (is_global_config) msg_text += "\n\n" + _(L("Shall I adjust those settings for supports?")); - //wxMessageDialog dialog(nullptr, msg_text, _(L("Support Generator")), - MessageDialog dialog(nullptr, msg_text, _(L("Support Generator")), - wxICON_WARNING | (is_global_config ? wxYES | wxNO | wxCANCEL : wxOK)); + MessageDialog dialog(nullptr, msg_text, _L("Support Generator"), wxICON_WARNING | (is_global_config ? wxYES | wxNO : wxOK)); DynamicPrintConfig new_conf = *config; auto answer = dialog.ShowModal(); if (!is_global_config || answer == wxID_YES) { // Enable "detect bridging perimeters". new_conf.set_key_value("overhangs", new ConfigOptionBool(true)); } - else if (answer == wxID_NO) { - // Do nothing, leave supports on and "detect bridging perimeters" off. - } - else if (answer == wxID_CANCEL) { - // Disable supports. - new_conf.set_key_value("support_material", new ConfigOptionBool(false)); - support_material_overhangs_queried = false; - } + //else Do nothing, leave supports on and "detect bridging perimeters" off. apply(config, &new_conf); } } From e3ac2a9e4556c766a636d876f00c03190ee5b41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Sat, 11 Sep 2021 00:53:24 +0200 Subject: [PATCH 143/151] Fixed Perl unit tests after 34c4b74af49c12e89fc6b5cc2bcfa36c69797578. --- t/cooling.t | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/t/cooling.t b/t/cooling.t index 9ac5a24f4d..e46cfa2f77 100644 --- a/t/cooling.t +++ b/t/cooling.t @@ -34,9 +34,9 @@ sub buffer { $gcodegen->apply_print_config($print_config); $gcodegen->set_layer_count(10); - my @extruders = shift; - @extruders = [ 0 ] if int(@extruders) == 0; - $gcodegen->set_extruders(\@extruders); + my $extruders_ref = shift; + $extruders_ref = [ 0 ] if !defined $extruders_ref; + $gcodegen->set_extruders($extruders_ref); return Slic3r::GCode::CoolingBuffer->new($gcodegen); } From ad65366ac7c4a8fa527bf87369d781029c858ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Thu, 9 Sep 2021 15:25:14 +0200 Subject: [PATCH 144/151] Added fast_float library as a replacement for std::from_chars and strtod. --- src/fast_float/fast_float.h | 2449 ++++++++++++++++++++++++++++++++ src/libslic3r/GCodeReader.cpp | 11 +- src/slic3r/GUI/AboutDialog.cpp | 4 +- 3 files changed, 2454 insertions(+), 10 deletions(-) create mode 100644 src/fast_float/fast_float.h diff --git a/src/fast_float/fast_float.h b/src/fast_float/fast_float.h new file mode 100644 index 0000000000..8974005319 --- /dev/null +++ b/src/fast_float/fast_float.h @@ -0,0 +1,2449 @@ +// fast_float by Daniel Lemire +// fast_float by João Paulo Magalhaes + + +// with contributions from Eugene Golushkov +// with contributions from Maksim Kita +// with contributions from Marcin Wojdyr +// with contributions from Neal Richardson +// with contributions from Tim Paine +// with contributions from Fabio Pellacini + + +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + + +#ifndef FASTFLOAT_FAST_FLOAT_H +#define FASTFLOAT_FAST_FLOAT_H + +#include + +namespace fast_float { +enum chars_format { + scientific = 1<<0, + fixed = 1<<2, + hex = 1<<3, + general = fixed | scientific +}; + + +struct from_chars_result { + const char *ptr; + std::errc ec; +}; + +struct parse_options { + explicit parse_options(chars_format fmt = chars_format::general, + char dot = '.') + : format(fmt), decimal_point(dot) {} + + /** Which number formats are accepted */ + chars_format format; + /** The character used as decimal point */ + char decimal_point; +}; + +/** + * This function parses the character sequence [first,last) for a number. It parses floating-point numbers expecting + * a locale-indepent format equivalent to what is used by std::strtod in the default ("C") locale. + * The resulting floating-point value is the closest floating-point values (using either float or double), + * using the "round to even" convention for values that would otherwise fall right in-between two values. + * That is, we provide exact parsing according to the IEEE standard. + * + * Given a successful parse, the pointer (`ptr`) in the returned value is set to point right after the + * parsed number, and the `value` referenced is set to the parsed value. In case of error, the returned + * `ec` contains a representative error, otherwise the default (`std::errc()`) value is stored. + * + * The implementation does not throw and does not allocate memory (e.g., with `new` or `malloc`). + * + * Like the C++17 standard, the `fast_float::from_chars` functions take an optional last argument of + * the type `fast_float::chars_format`. It is a bitset value: we check whether + * `fmt & fast_float::chars_format::fixed` and `fmt & fast_float::chars_format::scientific` are set + * to determine whether we allowe the fixed point and scientific notation respectively. + * The default is `fast_float::chars_format::general` which allows both `fixed` and `scientific`. + */ +template +from_chars_result from_chars(const char *first, const char *last, + T &value, chars_format fmt = chars_format::general) noexcept; + +/** + * Like from_chars, but accepts an `options` argument to govern number parsing. + */ +template +from_chars_result from_chars_advanced(const char *first, const char *last, + T &value, parse_options options) noexcept; + +} +#endif // FASTFLOAT_FAST_FLOAT_H + + +#ifndef FASTFLOAT_FLOAT_COMMON_H +#define FASTFLOAT_FLOAT_COMMON_H + +#include +#include +#include + +#if (defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) \ + || defined(__amd64) || defined(__aarch64__) || defined(_M_ARM64) \ + || defined(__MINGW64__) \ + || defined(__s390x__) \ + || (defined(__ppc64__) || defined(__PPC64__) || defined(__ppc64le__) || defined(__PPC64LE__)) \ + || defined(__EMSCRIPTEN__)) +#define FASTFLOAT_64BIT +#elif (defined(__i386) || defined(__i386__) || defined(_M_IX86) \ + || defined(__arm__) || defined(_M_ARM) \ + || defined(__MINGW32__)) +#define FASTFLOAT_32BIT +#else + // Need to check incrementally, since SIZE_MAX is a size_t, avoid overflow. + // We can never tell the register width, but the SIZE_MAX is a good approximation. + // UINTPTR_MAX and INTPTR_MAX are optional, so avoid them for max portability. + #if SIZE_MAX == 0xffff + #error Unknown platform (16-bit, unsupported) + #elif SIZE_MAX == 0xffffffff + #define FASTFLOAT_32BIT + #elif SIZE_MAX == 0xffffffffffffffff + #define FASTFLOAT_64BIT + #else + #error Unknown platform (not 32-bit, not 64-bit?) + #endif +#endif + +#if ((defined(_WIN32) || defined(_WIN64)) && !defined(__clang__)) +#include +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#define FASTFLOAT_VISUAL_STUDIO 1 +#endif + +#ifdef _WIN32 +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#else +#if defined(__APPLE__) || defined(__FreeBSD__) +#include +#elif defined(sun) || defined(__sun) +#include +#else +#include +#endif +# +#ifndef __BYTE_ORDER__ +// safe choice +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#endif +# +#ifndef __ORDER_LITTLE_ENDIAN__ +// safe choice +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#endif +# +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#else +#define FASTFLOAT_IS_BIG_ENDIAN 1 +#endif +#endif + +#ifdef FASTFLOAT_VISUAL_STUDIO +#define fastfloat_really_inline __forceinline +#else +#define fastfloat_really_inline inline __attribute__((always_inline)) +#endif + +namespace fast_float { + +// Compares two ASCII strings in a case insensitive manner. +inline bool fastfloat_strncasecmp(const char *input1, const char *input2, + size_t length) { + char running_diff{0}; + for (size_t i = 0; i < length; i++) { + running_diff |= (input1[i] ^ input2[i]); + } + return (running_diff == 0) || (running_diff == 32); +} + +#ifndef FLT_EVAL_METHOD +#error "FLT_EVAL_METHOD should be defined, please include cfloat." +#endif + +namespace { +constexpr uint32_t max_digits = 768; +constexpr uint32_t max_digit_without_overflow = 19; +constexpr int32_t decimal_point_range = 2047; +} // namespace + +struct value128 { + uint64_t low; + uint64_t high; + value128(uint64_t _low, uint64_t _high) : low(_low), high(_high) {} + value128() : low(0), high(0) {} +}; + +/* result might be undefined when input_num is zero */ +fastfloat_really_inline int leading_zeroes(uint64_t input_num) { + assert(input_num > 0); +#ifdef FASTFLOAT_VISUAL_STUDIO + #if defined(_M_X64) || defined(_M_ARM64) + unsigned long leading_zero = 0; + // Search the mask data from most significant bit (MSB) + // to least significant bit (LSB) for a set bit (1). + _BitScanReverse64(&leading_zero, input_num); + return (int)(63 - leading_zero); + #else + int last_bit = 0; + if(input_num & uint64_t(0xffffffff00000000)) input_num >>= 32, last_bit |= 32; + if(input_num & uint64_t( 0xffff0000)) input_num >>= 16, last_bit |= 16; + if(input_num & uint64_t( 0xff00)) input_num >>= 8, last_bit |= 8; + if(input_num & uint64_t( 0xf0)) input_num >>= 4, last_bit |= 4; + if(input_num & uint64_t( 0xc)) input_num >>= 2, last_bit |= 2; + if(input_num & uint64_t( 0x2)) input_num >>= 1, last_bit |= 1; + return 63 - last_bit; + #endif +#else + return __builtin_clzll(input_num); +#endif +} + +#ifdef FASTFLOAT_32BIT + +// slow emulation routine for 32-bit +fastfloat_really_inline uint64_t emulu(uint32_t x, uint32_t y) { + return x * (uint64_t)y; +} + +// slow emulation routine for 32-bit +#if !defined(__MINGW64__) +fastfloat_really_inline uint64_t _umul128(uint64_t ab, uint64_t cd, + uint64_t *hi) { + uint64_t ad = emulu((uint32_t)(ab >> 32), (uint32_t)cd); + uint64_t bd = emulu((uint32_t)ab, (uint32_t)cd); + uint64_t adbc = ad + emulu((uint32_t)ab, (uint32_t)(cd >> 32)); + uint64_t adbc_carry = !!(adbc < ad); + uint64_t lo = bd + (adbc << 32); + *hi = emulu((uint32_t)(ab >> 32), (uint32_t)(cd >> 32)) + (adbc >> 32) + + (adbc_carry << 32) + !!(lo < bd); + return lo; +} +#endif // !__MINGW64__ + +#endif // FASTFLOAT_32BIT + + +// compute 64-bit a*b +fastfloat_really_inline value128 full_multiplication(uint64_t a, + uint64_t b) { + value128 answer; +#ifdef _M_ARM64 + // ARM64 has native support for 64-bit multiplications, no need to emulate + answer.high = __umulh(a, b); + answer.low = a * b; +#elif defined(FASTFLOAT_32BIT) || (defined(_WIN64) && !defined(__clang__)) + answer.low = _umul128(a, b, &answer.high); // _umul128 not available on ARM64 +#elif defined(FASTFLOAT_64BIT) + __uint128_t r = ((__uint128_t)a) * b; + answer.low = uint64_t(r); + answer.high = uint64_t(r >> 64); +#else + #error Not implemented +#endif + return answer; +} + + +struct adjusted_mantissa { + uint64_t mantissa{0}; + int power2{0}; // a negative value indicates an invalid result + adjusted_mantissa() = default; + bool operator==(const adjusted_mantissa &o) const { + return mantissa == o.mantissa && power2 == o.power2; + } + bool operator!=(const adjusted_mantissa &o) const { + return mantissa != o.mantissa || power2 != o.power2; + } +}; + +struct decimal { + uint32_t num_digits{0}; + int32_t decimal_point{0}; + bool negative{false}; + bool truncated{false}; + uint8_t digits[max_digits]; + decimal() = default; + // Copies are not allowed since this is a fat object. + decimal(const decimal &) = delete; + // Copies are not allowed since this is a fat object. + decimal &operator=(const decimal &) = delete; + // Moves are allowed: + decimal(decimal &&) = default; + decimal &operator=(decimal &&other) = default; +}; + +constexpr static double powers_of_ten_double[] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22}; +constexpr static float powers_of_ten_float[] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, + 1e6, 1e7, 1e8, 1e9, 1e10}; + +template struct binary_format { + static inline constexpr int mantissa_explicit_bits(); + static inline constexpr int minimum_exponent(); + static inline constexpr int infinite_power(); + static inline constexpr int sign_index(); + static inline constexpr int min_exponent_fast_path(); + static inline constexpr int max_exponent_fast_path(); + static inline constexpr int max_exponent_round_to_even(); + static inline constexpr int min_exponent_round_to_even(); + static inline constexpr uint64_t max_mantissa_fast_path(); + static inline constexpr int largest_power_of_ten(); + static inline constexpr int smallest_power_of_ten(); + static inline constexpr T exact_power_of_ten(int64_t power); +}; + +template <> inline constexpr int binary_format::mantissa_explicit_bits() { + return 52; +} +template <> inline constexpr int binary_format::mantissa_explicit_bits() { + return 23; +} + +template <> inline constexpr int binary_format::max_exponent_round_to_even() { + return 23; +} + +template <> inline constexpr int binary_format::max_exponent_round_to_even() { + return 10; +} + +template <> inline constexpr int binary_format::min_exponent_round_to_even() { + return -4; +} + +template <> inline constexpr int binary_format::min_exponent_round_to_even() { + return -17; +} + +template <> inline constexpr int binary_format::minimum_exponent() { + return -1023; +} +template <> inline constexpr int binary_format::minimum_exponent() { + return -127; +} + +template <> inline constexpr int binary_format::infinite_power() { + return 0x7FF; +} +template <> inline constexpr int binary_format::infinite_power() { + return 0xFF; +} + +template <> inline constexpr int binary_format::sign_index() { return 63; } +template <> inline constexpr int binary_format::sign_index() { return 31; } + +template <> inline constexpr int binary_format::min_exponent_fast_path() { +#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) + return 0; +#else + return -22; +#endif +} +template <> inline constexpr int binary_format::min_exponent_fast_path() { +#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) + return 0; +#else + return -10; +#endif +} + +template <> inline constexpr int binary_format::max_exponent_fast_path() { + return 22; +} +template <> inline constexpr int binary_format::max_exponent_fast_path() { + return 10; +} + +template <> inline constexpr uint64_t binary_format::max_mantissa_fast_path() { + return uint64_t(2) << mantissa_explicit_bits(); +} +template <> inline constexpr uint64_t binary_format::max_mantissa_fast_path() { + return uint64_t(2) << mantissa_explicit_bits(); +} + +template <> +inline constexpr double binary_format::exact_power_of_ten(int64_t power) { + return powers_of_ten_double[power]; +} +template <> +inline constexpr float binary_format::exact_power_of_ten(int64_t power) { + + return powers_of_ten_float[power]; +} + + +template <> +inline constexpr int binary_format::largest_power_of_ten() { + return 308; +} +template <> +inline constexpr int binary_format::largest_power_of_ten() { + return 38; +} + +template <> +inline constexpr int binary_format::smallest_power_of_ten() { + return -342; +} +template <> +inline constexpr int binary_format::smallest_power_of_ten() { + return -65; +} + +} // namespace fast_float + +// for convenience: +template +inline OStream& operator<<(OStream &out, const fast_float::decimal &d) { + out << "0."; + for (size_t i = 0; i < d.num_digits; i++) { + out << int32_t(d.digits[i]); + } + out << " * 10 ** " << d.decimal_point; + return out; +} + +#endif + + +#ifndef FASTFLOAT_ASCII_NUMBER_H +#define FASTFLOAT_ASCII_NUMBER_H + +#include +#include +#include +#include + + +namespace fast_float { + +// Next function can be micro-optimized, but compilers are entirely +// able to optimize it well. +fastfloat_really_inline bool is_integer(char c) noexcept { return c >= '0' && c <= '9'; } + +fastfloat_really_inline uint64_t byteswap(uint64_t val) { + return (val & 0xFF00000000000000) >> 56 + | (val & 0x00FF000000000000) >> 40 + | (val & 0x0000FF0000000000) >> 24 + | (val & 0x000000FF00000000) >> 8 + | (val & 0x00000000FF000000) << 8 + | (val & 0x0000000000FF0000) << 24 + | (val & 0x000000000000FF00) << 40 + | (val & 0x00000000000000FF) << 56; +} + +fastfloat_really_inline uint64_t read_u64(const char *chars) { + uint64_t val; + ::memcpy(&val, chars, sizeof(uint64_t)); +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + // Need to read as-if the number was in little-endian order. + val = byteswap(val); +#endif + return val; +} + +fastfloat_really_inline void write_u64(uint8_t *chars, uint64_t val) { +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + // Need to read as-if the number was in little-endian order. + val = byteswap(val); +#endif + ::memcpy(chars, &val, sizeof(uint64_t)); +} + +// credit @aqrit +fastfloat_really_inline uint32_t parse_eight_digits_unrolled(uint64_t val) { + const uint64_t mask = 0x000000FF000000FF; + const uint64_t mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32) + const uint64_t mul2 = 0x0000271000000001; // 1 + (10000ULL << 32) + val -= 0x3030303030303030; + val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8; + val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32; + return uint32_t(val); +} + +fastfloat_really_inline uint32_t parse_eight_digits_unrolled(const char *chars) noexcept { + return parse_eight_digits_unrolled(read_u64(chars)); +} + +// credit @aqrit +fastfloat_really_inline bool is_made_of_eight_digits_fast(uint64_t val) noexcept { + return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) & + 0x8080808080808080)); +} + +fastfloat_really_inline bool is_made_of_eight_digits_fast(const char *chars) noexcept { + return is_made_of_eight_digits_fast(read_u64(chars)); +} + +struct parsed_number_string { + int64_t exponent; + uint64_t mantissa; + const char *lastmatch; + bool negative; + bool valid; + bool too_many_digits; +}; + + +// Assuming that you use no more than 19 digits, this will +// parse an ASCII string. +fastfloat_really_inline +parsed_number_string parse_number_string(const char *p, const char *pend, parse_options options) noexcept { + const chars_format fmt = options.format; + const char decimal_point = options.decimal_point; + + parsed_number_string answer; + answer.valid = false; + answer.too_many_digits = false; + answer.negative = (*p == '-'); + if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here + ++p; + if (p == pend) { + return answer; + } + if (!is_integer(*p) && (*p != decimal_point)) { // a sign must be followed by an integer or the dot + return answer; + } + } + const char *const start_digits = p; + + uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad) + + while ((p != pend) && is_integer(*p)) { + // a multiplication by 10 is cheaper than an arbitrary integer + // multiplication + i = 10 * i + + uint64_t(*p - '0'); // might overflow, we will handle the overflow later + ++p; + } + const char *const end_of_integer_part = p; + int64_t digit_count = int64_t(end_of_integer_part - start_digits); + int64_t exponent = 0; + if ((p != pend) && (*p == decimal_point)) { + ++p; + // Fast approach only tested under little endian systems + if ((p + 8 <= pend) && is_made_of_eight_digits_fast(p)) { + i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok + p += 8; + if ((p + 8 <= pend) && is_made_of_eight_digits_fast(p)) { + i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok + p += 8; + } + } + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - '0'); + ++p; + i = i * 10 + digit; // in rare cases, this will overflow, but that's ok + } + exponent = end_of_integer_part + 1 - p; + digit_count -= exponent; + } + // we must have encountered at least one integer! + if (digit_count == 0) { + return answer; + } + int64_t exp_number = 0; // explicit exponential part + if ((fmt & chars_format::scientific) && (p != pend) && (('e' == *p) || ('E' == *p))) { + const char * location_of_e = p; + ++p; + bool neg_exp = false; + if ((p != pend) && ('-' == *p)) { + neg_exp = true; + ++p; + } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1) + ++p; + } + if ((p == pend) || !is_integer(*p)) { + if(!(fmt & chars_format::fixed)) { + // We are in error. + return answer; + } + // Otherwise, we will be ignoring the 'e'. + p = location_of_e; + } else { + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - '0'); + if (exp_number < 0x10000) { + exp_number = 10 * exp_number + digit; + } + ++p; + } + if(neg_exp) { exp_number = - exp_number; } + exponent += exp_number; + } + } else { + // If it scientific and not fixed, we have to bail out. + if((fmt & chars_format::scientific) && !(fmt & chars_format::fixed)) { return answer; } + } + answer.lastmatch = p; + answer.valid = true; + + // If we frequently had to deal with long strings of digits, + // we could extend our code by using a 128-bit integer instead + // of a 64-bit integer. However, this is uncommon. + // + // We can deal with up to 19 digits. + if (digit_count > 19) { // this is uncommon + // It is possible that the integer had an overflow. + // We have to handle the case where we have 0.0000somenumber. + // We need to be mindful of the case where we only have zeroes... + // E.g., 0.000000000...000. + const char *start = start_digits; + while ((start != pend) && (*start == '0' || *start == decimal_point)) { + if(*start == '0') { digit_count --; } + start++; + } + if (digit_count > 19) { + answer.too_many_digits = true; + // Let us start again, this time, avoiding overflows. + i = 0; + p = start_digits; + const uint64_t minimal_nineteen_digit_integer{1000000000000000000}; + while((i < minimal_nineteen_digit_integer) && (p != pend) && is_integer(*p)) { + i = i * 10 + uint64_t(*p - '0'); + ++p; + } + if (i >= minimal_nineteen_digit_integer) { // We have a big integers + exponent = end_of_integer_part - p + exp_number; + } else { // We have a value with a fractional component. + p++; // skip the dot + const char *first_after_period = p; + while((i < minimal_nineteen_digit_integer) && (p != pend) && is_integer(*p)) { + i = i * 10 + uint64_t(*p - '0'); + ++p; + } + exponent = first_after_period - p + exp_number; + } + // We have now corrected both exponent and i, to a truncated value + } + } + answer.exponent = exponent; + answer.mantissa = i; + return answer; +} + + +// This should always succeed since it follows a call to parse_number_string +// This function could be optimized. In particular, we could stop after 19 digits +// and try to bail out. Furthermore, we should be able to recover the computed +// exponent from the pass in parse_number_string. +fastfloat_really_inline decimal parse_decimal(const char *p, const char *pend, parse_options options) noexcept { + const char decimal_point = options.decimal_point; + + decimal answer; + answer.num_digits = 0; + answer.decimal_point = 0; + answer.truncated = false; + answer.negative = (*p == '-'); + if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here + ++p; + } + // skip leading zeroes + while ((p != pend) && (*p == '0')) { + ++p; + } + while ((p != pend) && is_integer(*p)) { + if (answer.num_digits < max_digits) { + answer.digits[answer.num_digits] = uint8_t(*p - '0'); + } + answer.num_digits++; + ++p; + } + if ((p != pend) && (*p == decimal_point)) { + ++p; + const char *first_after_period = p; + // if we have not yet encountered a zero, we have to skip it as well + if(answer.num_digits == 0) { + // skip zeros + while ((p != pend) && (*p == '0')) { + ++p; + } + } + // We expect that this loop will often take the bulk of the running time + // because when a value has lots of digits, these digits often + while ((p + 8 <= pend) && (answer.num_digits + 8 < max_digits)) { + uint64_t val = read_u64(p); + if(! is_made_of_eight_digits_fast(val)) { break; } + // We have eight digits, process them in one go! + val -= 0x3030303030303030; + write_u64(answer.digits + answer.num_digits, val); + answer.num_digits += 8; + p += 8; + } + while ((p != pend) && is_integer(*p)) { + if (answer.num_digits < max_digits) { + answer.digits[answer.num_digits] = uint8_t(*p - '0'); + } + answer.num_digits++; + ++p; + } + answer.decimal_point = int32_t(first_after_period - p); + } + // We want num_digits to be the number of significant digits, excluding + // leading *and* trailing zeros! Otherwise the truncated flag later is + // going to be misleading. + if(answer.num_digits > 0) { + // We potentially need the answer.num_digits > 0 guard because we + // prune leading zeros. So with answer.num_digits > 0, we know that + // we have at least one non-zero digit. + const char *preverse = p - 1; + int32_t trailing_zeros = 0; + while ((*preverse == '0') || (*preverse == decimal_point)) { + if(*preverse == '0') { trailing_zeros++; }; + --preverse; + } + answer.decimal_point += int32_t(answer.num_digits); + answer.num_digits -= uint32_t(trailing_zeros); + } + if(answer.num_digits > max_digits) { + answer.truncated = true; + answer.num_digits = max_digits; + } + if ((p != pend) && (('e' == *p) || ('E' == *p))) { + ++p; + bool neg_exp = false; + if ((p != pend) && ('-' == *p)) { + neg_exp = true; + ++p; + } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1) + ++p; + } + int32_t exp_number = 0; // exponential part + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - '0'); + if (exp_number < 0x10000) { + exp_number = 10 * exp_number + digit; + } + ++p; + } + answer.decimal_point += (neg_exp ? -exp_number : exp_number); + } + // In very rare cases, we may have fewer than 19 digits, we want to be able to reliably + // assume that all digits up to max_digit_without_overflow have been initialized. + for(uint32_t i = answer.num_digits; i < max_digit_without_overflow; i++) { answer.digits[i] = 0; } + + return answer; +} +} // namespace fast_float + +#endif + + +#ifndef FASTFLOAT_FAST_TABLE_H +#define FASTFLOAT_FAST_TABLE_H +#include + +namespace fast_float { + +/** + * When mapping numbers from decimal to binary, + * we go from w * 10^q to m * 2^p but we have + * 10^q = 5^q * 2^q, so effectively + * we are trying to match + * w * 2^q * 5^q to m * 2^p. Thus the powers of two + * are not a concern since they can be represented + * exactly using the binary notation, only the powers of five + * affect the binary significand. + */ + +/** + * The smallest non-zero float (binary64) is 2^−1074. + * We take as input numbers of the form w x 10^q where w < 2^64. + * We have that w * 10^-343 < 2^(64-344) 5^-343 < 2^-1076. + * However, we have that + * (2^64-1) * 10^-342 = (2^64-1) * 2^-342 * 5^-342 > 2^−1074. + * Thus it is possible for a number of the form w * 10^-342 where + * w is a 64-bit value to be a non-zero floating-point number. + ********* + * Any number of form w * 10^309 where w>= 1 is going to be + * infinite in binary64 so we never need to worry about powers + * of 5 greater than 308. + */ +template +struct powers_template { + +constexpr static int smallest_power_of_five = binary_format::smallest_power_of_ten(); +constexpr static int largest_power_of_five = binary_format::largest_power_of_ten(); +constexpr static int number_of_entries = 2 * (largest_power_of_five - smallest_power_of_five + 1); +// Powers of five from 5^-342 all the way to 5^308 rounded toward one. +static const uint64_t power_of_five_128[number_of_entries]; +}; + +template +const uint64_t powers_template::power_of_five_128[number_of_entries] = { + 0xeef453d6923bd65a,0x113faa2906a13b3f, + 0x9558b4661b6565f8,0x4ac7ca59a424c507, + 0xbaaee17fa23ebf76,0x5d79bcf00d2df649, + 0xe95a99df8ace6f53,0xf4d82c2c107973dc, + 0x91d8a02bb6c10594,0x79071b9b8a4be869, + 0xb64ec836a47146f9,0x9748e2826cdee284, + 0xe3e27a444d8d98b7,0xfd1b1b2308169b25, + 0x8e6d8c6ab0787f72,0xfe30f0f5e50e20f7, + 0xb208ef855c969f4f,0xbdbd2d335e51a935, + 0xde8b2b66b3bc4723,0xad2c788035e61382, + 0x8b16fb203055ac76,0x4c3bcb5021afcc31, + 0xaddcb9e83c6b1793,0xdf4abe242a1bbf3d, + 0xd953e8624b85dd78,0xd71d6dad34a2af0d, + 0x87d4713d6f33aa6b,0x8672648c40e5ad68, + 0xa9c98d8ccb009506,0x680efdaf511f18c2, + 0xd43bf0effdc0ba48,0x212bd1b2566def2, + 0x84a57695fe98746d,0x14bb630f7604b57, + 0xa5ced43b7e3e9188,0x419ea3bd35385e2d, + 0xcf42894a5dce35ea,0x52064cac828675b9, + 0x818995ce7aa0e1b2,0x7343efebd1940993, + 0xa1ebfb4219491a1f,0x1014ebe6c5f90bf8, + 0xca66fa129f9b60a6,0xd41a26e077774ef6, + 0xfd00b897478238d0,0x8920b098955522b4, + 0x9e20735e8cb16382,0x55b46e5f5d5535b0, + 0xc5a890362fddbc62,0xeb2189f734aa831d, + 0xf712b443bbd52b7b,0xa5e9ec7501d523e4, + 0x9a6bb0aa55653b2d,0x47b233c92125366e, + 0xc1069cd4eabe89f8,0x999ec0bb696e840a, + 0xf148440a256e2c76,0xc00670ea43ca250d, + 0x96cd2a865764dbca,0x380406926a5e5728, + 0xbc807527ed3e12bc,0xc605083704f5ecf2, + 0xeba09271e88d976b,0xf7864a44c633682e, + 0x93445b8731587ea3,0x7ab3ee6afbe0211d, + 0xb8157268fdae9e4c,0x5960ea05bad82964, + 0xe61acf033d1a45df,0x6fb92487298e33bd, + 0x8fd0c16206306bab,0xa5d3b6d479f8e056, + 0xb3c4f1ba87bc8696,0x8f48a4899877186c, + 0xe0b62e2929aba83c,0x331acdabfe94de87, + 0x8c71dcd9ba0b4925,0x9ff0c08b7f1d0b14, + 0xaf8e5410288e1b6f,0x7ecf0ae5ee44dd9, + 0xdb71e91432b1a24a,0xc9e82cd9f69d6150, + 0x892731ac9faf056e,0xbe311c083a225cd2, + 0xab70fe17c79ac6ca,0x6dbd630a48aaf406, + 0xd64d3d9db981787d,0x92cbbccdad5b108, + 0x85f0468293f0eb4e,0x25bbf56008c58ea5, + 0xa76c582338ed2621,0xaf2af2b80af6f24e, + 0xd1476e2c07286faa,0x1af5af660db4aee1, + 0x82cca4db847945ca,0x50d98d9fc890ed4d, + 0xa37fce126597973c,0xe50ff107bab528a0, + 0xcc5fc196fefd7d0c,0x1e53ed49a96272c8, + 0xff77b1fcbebcdc4f,0x25e8e89c13bb0f7a, + 0x9faacf3df73609b1,0x77b191618c54e9ac, + 0xc795830d75038c1d,0xd59df5b9ef6a2417, + 0xf97ae3d0d2446f25,0x4b0573286b44ad1d, + 0x9becce62836ac577,0x4ee367f9430aec32, + 0xc2e801fb244576d5,0x229c41f793cda73f, + 0xf3a20279ed56d48a,0x6b43527578c1110f, + 0x9845418c345644d6,0x830a13896b78aaa9, + 0xbe5691ef416bd60c,0x23cc986bc656d553, + 0xedec366b11c6cb8f,0x2cbfbe86b7ec8aa8, + 0x94b3a202eb1c3f39,0x7bf7d71432f3d6a9, + 0xb9e08a83a5e34f07,0xdaf5ccd93fb0cc53, + 0xe858ad248f5c22c9,0xd1b3400f8f9cff68, + 0x91376c36d99995be,0x23100809b9c21fa1, + 0xb58547448ffffb2d,0xabd40a0c2832a78a, + 0xe2e69915b3fff9f9,0x16c90c8f323f516c, + 0x8dd01fad907ffc3b,0xae3da7d97f6792e3, + 0xb1442798f49ffb4a,0x99cd11cfdf41779c, + 0xdd95317f31c7fa1d,0x40405643d711d583, + 0x8a7d3eef7f1cfc52,0x482835ea666b2572, + 0xad1c8eab5ee43b66,0xda3243650005eecf, + 0xd863b256369d4a40,0x90bed43e40076a82, + 0x873e4f75e2224e68,0x5a7744a6e804a291, + 0xa90de3535aaae202,0x711515d0a205cb36, + 0xd3515c2831559a83,0xd5a5b44ca873e03, + 0x8412d9991ed58091,0xe858790afe9486c2, + 0xa5178fff668ae0b6,0x626e974dbe39a872, + 0xce5d73ff402d98e3,0xfb0a3d212dc8128f, + 0x80fa687f881c7f8e,0x7ce66634bc9d0b99, + 0xa139029f6a239f72,0x1c1fffc1ebc44e80, + 0xc987434744ac874e,0xa327ffb266b56220, + 0xfbe9141915d7a922,0x4bf1ff9f0062baa8, + 0x9d71ac8fada6c9b5,0x6f773fc3603db4a9, + 0xc4ce17b399107c22,0xcb550fb4384d21d3, + 0xf6019da07f549b2b,0x7e2a53a146606a48, + 0x99c102844f94e0fb,0x2eda7444cbfc426d, + 0xc0314325637a1939,0xfa911155fefb5308, + 0xf03d93eebc589f88,0x793555ab7eba27ca, + 0x96267c7535b763b5,0x4bc1558b2f3458de, + 0xbbb01b9283253ca2,0x9eb1aaedfb016f16, + 0xea9c227723ee8bcb,0x465e15a979c1cadc, + 0x92a1958a7675175f,0xbfacd89ec191ec9, + 0xb749faed14125d36,0xcef980ec671f667b, + 0xe51c79a85916f484,0x82b7e12780e7401a, + 0x8f31cc0937ae58d2,0xd1b2ecb8b0908810, + 0xb2fe3f0b8599ef07,0x861fa7e6dcb4aa15, + 0xdfbdcece67006ac9,0x67a791e093e1d49a, + 0x8bd6a141006042bd,0xe0c8bb2c5c6d24e0, + 0xaecc49914078536d,0x58fae9f773886e18, + 0xda7f5bf590966848,0xaf39a475506a899e, + 0x888f99797a5e012d,0x6d8406c952429603, + 0xaab37fd7d8f58178,0xc8e5087ba6d33b83, + 0xd5605fcdcf32e1d6,0xfb1e4a9a90880a64, + 0x855c3be0a17fcd26,0x5cf2eea09a55067f, + 0xa6b34ad8c9dfc06f,0xf42faa48c0ea481e, + 0xd0601d8efc57b08b,0xf13b94daf124da26, + 0x823c12795db6ce57,0x76c53d08d6b70858, + 0xa2cb1717b52481ed,0x54768c4b0c64ca6e, + 0xcb7ddcdda26da268,0xa9942f5dcf7dfd09, + 0xfe5d54150b090b02,0xd3f93b35435d7c4c, + 0x9efa548d26e5a6e1,0xc47bc5014a1a6daf, + 0xc6b8e9b0709f109a,0x359ab6419ca1091b, + 0xf867241c8cc6d4c0,0xc30163d203c94b62, + 0x9b407691d7fc44f8,0x79e0de63425dcf1d, + 0xc21094364dfb5636,0x985915fc12f542e4, + 0xf294b943e17a2bc4,0x3e6f5b7b17b2939d, + 0x979cf3ca6cec5b5a,0xa705992ceecf9c42, + 0xbd8430bd08277231,0x50c6ff782a838353, + 0xece53cec4a314ebd,0xa4f8bf5635246428, + 0x940f4613ae5ed136,0x871b7795e136be99, + 0xb913179899f68584,0x28e2557b59846e3f, + 0xe757dd7ec07426e5,0x331aeada2fe589cf, + 0x9096ea6f3848984f,0x3ff0d2c85def7621, + 0xb4bca50b065abe63,0xfed077a756b53a9, + 0xe1ebce4dc7f16dfb,0xd3e8495912c62894, + 0x8d3360f09cf6e4bd,0x64712dd7abbbd95c, + 0xb080392cc4349dec,0xbd8d794d96aacfb3, + 0xdca04777f541c567,0xecf0d7a0fc5583a0, + 0x89e42caaf9491b60,0xf41686c49db57244, + 0xac5d37d5b79b6239,0x311c2875c522ced5, + 0xd77485cb25823ac7,0x7d633293366b828b, + 0x86a8d39ef77164bc,0xae5dff9c02033197, + 0xa8530886b54dbdeb,0xd9f57f830283fdfc, + 0xd267caa862a12d66,0xd072df63c324fd7b, + 0x8380dea93da4bc60,0x4247cb9e59f71e6d, + 0xa46116538d0deb78,0x52d9be85f074e608, + 0xcd795be870516656,0x67902e276c921f8b, + 0x806bd9714632dff6,0xba1cd8a3db53b6, + 0xa086cfcd97bf97f3,0x80e8a40eccd228a4, + 0xc8a883c0fdaf7df0,0x6122cd128006b2cd, + 0xfad2a4b13d1b5d6c,0x796b805720085f81, + 0x9cc3a6eec6311a63,0xcbe3303674053bb0, + 0xc3f490aa77bd60fc,0xbedbfc4411068a9c, + 0xf4f1b4d515acb93b,0xee92fb5515482d44, + 0x991711052d8bf3c5,0x751bdd152d4d1c4a, + 0xbf5cd54678eef0b6,0xd262d45a78a0635d, + 0xef340a98172aace4,0x86fb897116c87c34, + 0x9580869f0e7aac0e,0xd45d35e6ae3d4da0, + 0xbae0a846d2195712,0x8974836059cca109, + 0xe998d258869facd7,0x2bd1a438703fc94b, + 0x91ff83775423cc06,0x7b6306a34627ddcf, + 0xb67f6455292cbf08,0x1a3bc84c17b1d542, + 0xe41f3d6a7377eeca,0x20caba5f1d9e4a93, + 0x8e938662882af53e,0x547eb47b7282ee9c, + 0xb23867fb2a35b28d,0xe99e619a4f23aa43, + 0xdec681f9f4c31f31,0x6405fa00e2ec94d4, + 0x8b3c113c38f9f37e,0xde83bc408dd3dd04, + 0xae0b158b4738705e,0x9624ab50b148d445, + 0xd98ddaee19068c76,0x3badd624dd9b0957, + 0x87f8a8d4cfa417c9,0xe54ca5d70a80e5d6, + 0xa9f6d30a038d1dbc,0x5e9fcf4ccd211f4c, + 0xd47487cc8470652b,0x7647c3200069671f, + 0x84c8d4dfd2c63f3b,0x29ecd9f40041e073, + 0xa5fb0a17c777cf09,0xf468107100525890, + 0xcf79cc9db955c2cc,0x7182148d4066eeb4, + 0x81ac1fe293d599bf,0xc6f14cd848405530, + 0xa21727db38cb002f,0xb8ada00e5a506a7c, + 0xca9cf1d206fdc03b,0xa6d90811f0e4851c, + 0xfd442e4688bd304a,0x908f4a166d1da663, + 0x9e4a9cec15763e2e,0x9a598e4e043287fe, + 0xc5dd44271ad3cdba,0x40eff1e1853f29fd, + 0xf7549530e188c128,0xd12bee59e68ef47c, + 0x9a94dd3e8cf578b9,0x82bb74f8301958ce, + 0xc13a148e3032d6e7,0xe36a52363c1faf01, + 0xf18899b1bc3f8ca1,0xdc44e6c3cb279ac1, + 0x96f5600f15a7b7e5,0x29ab103a5ef8c0b9, + 0xbcb2b812db11a5de,0x7415d448f6b6f0e7, + 0xebdf661791d60f56,0x111b495b3464ad21, + 0x936b9fcebb25c995,0xcab10dd900beec34, + 0xb84687c269ef3bfb,0x3d5d514f40eea742, + 0xe65829b3046b0afa,0xcb4a5a3112a5112, + 0x8ff71a0fe2c2e6dc,0x47f0e785eaba72ab, + 0xb3f4e093db73a093,0x59ed216765690f56, + 0xe0f218b8d25088b8,0x306869c13ec3532c, + 0x8c974f7383725573,0x1e414218c73a13fb, + 0xafbd2350644eeacf,0xe5d1929ef90898fa, + 0xdbac6c247d62a583,0xdf45f746b74abf39, + 0x894bc396ce5da772,0x6b8bba8c328eb783, + 0xab9eb47c81f5114f,0x66ea92f3f326564, + 0xd686619ba27255a2,0xc80a537b0efefebd, + 0x8613fd0145877585,0xbd06742ce95f5f36, + 0xa798fc4196e952e7,0x2c48113823b73704, + 0xd17f3b51fca3a7a0,0xf75a15862ca504c5, + 0x82ef85133de648c4,0x9a984d73dbe722fb, + 0xa3ab66580d5fdaf5,0xc13e60d0d2e0ebba, + 0xcc963fee10b7d1b3,0x318df905079926a8, + 0xffbbcfe994e5c61f,0xfdf17746497f7052, + 0x9fd561f1fd0f9bd3,0xfeb6ea8bedefa633, + 0xc7caba6e7c5382c8,0xfe64a52ee96b8fc0, + 0xf9bd690a1b68637b,0x3dfdce7aa3c673b0, + 0x9c1661a651213e2d,0x6bea10ca65c084e, + 0xc31bfa0fe5698db8,0x486e494fcff30a62, + 0xf3e2f893dec3f126,0x5a89dba3c3efccfa, + 0x986ddb5c6b3a76b7,0xf89629465a75e01c, + 0xbe89523386091465,0xf6bbb397f1135823, + 0xee2ba6c0678b597f,0x746aa07ded582e2c, + 0x94db483840b717ef,0xa8c2a44eb4571cdc, + 0xba121a4650e4ddeb,0x92f34d62616ce413, + 0xe896a0d7e51e1566,0x77b020baf9c81d17, + 0x915e2486ef32cd60,0xace1474dc1d122e, + 0xb5b5ada8aaff80b8,0xd819992132456ba, + 0xe3231912d5bf60e6,0x10e1fff697ed6c69, + 0x8df5efabc5979c8f,0xca8d3ffa1ef463c1, + 0xb1736b96b6fd83b3,0xbd308ff8a6b17cb2, + 0xddd0467c64bce4a0,0xac7cb3f6d05ddbde, + 0x8aa22c0dbef60ee4,0x6bcdf07a423aa96b, + 0xad4ab7112eb3929d,0x86c16c98d2c953c6, + 0xd89d64d57a607744,0xe871c7bf077ba8b7, + 0x87625f056c7c4a8b,0x11471cd764ad4972, + 0xa93af6c6c79b5d2d,0xd598e40d3dd89bcf, + 0xd389b47879823479,0x4aff1d108d4ec2c3, + 0x843610cb4bf160cb,0xcedf722a585139ba, + 0xa54394fe1eedb8fe,0xc2974eb4ee658828, + 0xce947a3da6a9273e,0x733d226229feea32, + 0x811ccc668829b887,0x806357d5a3f525f, + 0xa163ff802a3426a8,0xca07c2dcb0cf26f7, + 0xc9bcff6034c13052,0xfc89b393dd02f0b5, + 0xfc2c3f3841f17c67,0xbbac2078d443ace2, + 0x9d9ba7832936edc0,0xd54b944b84aa4c0d, + 0xc5029163f384a931,0xa9e795e65d4df11, + 0xf64335bcf065d37d,0x4d4617b5ff4a16d5, + 0x99ea0196163fa42e,0x504bced1bf8e4e45, + 0xc06481fb9bcf8d39,0xe45ec2862f71e1d6, + 0xf07da27a82c37088,0x5d767327bb4e5a4c, + 0x964e858c91ba2655,0x3a6a07f8d510f86f, + 0xbbe226efb628afea,0x890489f70a55368b, + 0xeadab0aba3b2dbe5,0x2b45ac74ccea842e, + 0x92c8ae6b464fc96f,0x3b0b8bc90012929d, + 0xb77ada0617e3bbcb,0x9ce6ebb40173744, + 0xe55990879ddcaabd,0xcc420a6a101d0515, + 0x8f57fa54c2a9eab6,0x9fa946824a12232d, + 0xb32df8e9f3546564,0x47939822dc96abf9, + 0xdff9772470297ebd,0x59787e2b93bc56f7, + 0x8bfbea76c619ef36,0x57eb4edb3c55b65a, + 0xaefae51477a06b03,0xede622920b6b23f1, + 0xdab99e59958885c4,0xe95fab368e45eced, + 0x88b402f7fd75539b,0x11dbcb0218ebb414, + 0xaae103b5fcd2a881,0xd652bdc29f26a119, + 0xd59944a37c0752a2,0x4be76d3346f0495f, + 0x857fcae62d8493a5,0x6f70a4400c562ddb, + 0xa6dfbd9fb8e5b88e,0xcb4ccd500f6bb952, + 0xd097ad07a71f26b2,0x7e2000a41346a7a7, + 0x825ecc24c873782f,0x8ed400668c0c28c8, + 0xa2f67f2dfa90563b,0x728900802f0f32fa, + 0xcbb41ef979346bca,0x4f2b40a03ad2ffb9, + 0xfea126b7d78186bc,0xe2f610c84987bfa8, + 0x9f24b832e6b0f436,0xdd9ca7d2df4d7c9, + 0xc6ede63fa05d3143,0x91503d1c79720dbb, + 0xf8a95fcf88747d94,0x75a44c6397ce912a, + 0x9b69dbe1b548ce7c,0xc986afbe3ee11aba, + 0xc24452da229b021b,0xfbe85badce996168, + 0xf2d56790ab41c2a2,0xfae27299423fb9c3, + 0x97c560ba6b0919a5,0xdccd879fc967d41a, + 0xbdb6b8e905cb600f,0x5400e987bbc1c920, + 0xed246723473e3813,0x290123e9aab23b68, + 0x9436c0760c86e30b,0xf9a0b6720aaf6521, + 0xb94470938fa89bce,0xf808e40e8d5b3e69, + 0xe7958cb87392c2c2,0xb60b1d1230b20e04, + 0x90bd77f3483bb9b9,0xb1c6f22b5e6f48c2, + 0xb4ecd5f01a4aa828,0x1e38aeb6360b1af3, + 0xe2280b6c20dd5232,0x25c6da63c38de1b0, + 0x8d590723948a535f,0x579c487e5a38ad0e, + 0xb0af48ec79ace837,0x2d835a9df0c6d851, + 0xdcdb1b2798182244,0xf8e431456cf88e65, + 0x8a08f0f8bf0f156b,0x1b8e9ecb641b58ff, + 0xac8b2d36eed2dac5,0xe272467e3d222f3f, + 0xd7adf884aa879177,0x5b0ed81dcc6abb0f, + 0x86ccbb52ea94baea,0x98e947129fc2b4e9, + 0xa87fea27a539e9a5,0x3f2398d747b36224, + 0xd29fe4b18e88640e,0x8eec7f0d19a03aad, + 0x83a3eeeef9153e89,0x1953cf68300424ac, + 0xa48ceaaab75a8e2b,0x5fa8c3423c052dd7, + 0xcdb02555653131b6,0x3792f412cb06794d, + 0x808e17555f3ebf11,0xe2bbd88bbee40bd0, + 0xa0b19d2ab70e6ed6,0x5b6aceaeae9d0ec4, + 0xc8de047564d20a8b,0xf245825a5a445275, + 0xfb158592be068d2e,0xeed6e2f0f0d56712, + 0x9ced737bb6c4183d,0x55464dd69685606b, + 0xc428d05aa4751e4c,0xaa97e14c3c26b886, + 0xf53304714d9265df,0xd53dd99f4b3066a8, + 0x993fe2c6d07b7fab,0xe546a8038efe4029, + 0xbf8fdb78849a5f96,0xde98520472bdd033, + 0xef73d256a5c0f77c,0x963e66858f6d4440, + 0x95a8637627989aad,0xdde7001379a44aa8, + 0xbb127c53b17ec159,0x5560c018580d5d52, + 0xe9d71b689dde71af,0xaab8f01e6e10b4a6, + 0x9226712162ab070d,0xcab3961304ca70e8, + 0xb6b00d69bb55c8d1,0x3d607b97c5fd0d22, + 0xe45c10c42a2b3b05,0x8cb89a7db77c506a, + 0x8eb98a7a9a5b04e3,0x77f3608e92adb242, + 0xb267ed1940f1c61c,0x55f038b237591ed3, + 0xdf01e85f912e37a3,0x6b6c46dec52f6688, + 0x8b61313bbabce2c6,0x2323ac4b3b3da015, + 0xae397d8aa96c1b77,0xabec975e0a0d081a, + 0xd9c7dced53c72255,0x96e7bd358c904a21, + 0x881cea14545c7575,0x7e50d64177da2e54, + 0xaa242499697392d2,0xdde50bd1d5d0b9e9, + 0xd4ad2dbfc3d07787,0x955e4ec64b44e864, + 0x84ec3c97da624ab4,0xbd5af13bef0b113e, + 0xa6274bbdd0fadd61,0xecb1ad8aeacdd58e, + 0xcfb11ead453994ba,0x67de18eda5814af2, + 0x81ceb32c4b43fcf4,0x80eacf948770ced7, + 0xa2425ff75e14fc31,0xa1258379a94d028d, + 0xcad2f7f5359a3b3e,0x96ee45813a04330, + 0xfd87b5f28300ca0d,0x8bca9d6e188853fc, + 0x9e74d1b791e07e48,0x775ea264cf55347e, + 0xc612062576589dda,0x95364afe032a819e, + 0xf79687aed3eec551,0x3a83ddbd83f52205, + 0x9abe14cd44753b52,0xc4926a9672793543, + 0xc16d9a0095928a27,0x75b7053c0f178294, + 0xf1c90080baf72cb1,0x5324c68b12dd6339, + 0x971da05074da7bee,0xd3f6fc16ebca5e04, + 0xbce5086492111aea,0x88f4bb1ca6bcf585, + 0xec1e4a7db69561a5,0x2b31e9e3d06c32e6, + 0x9392ee8e921d5d07,0x3aff322e62439fd0, + 0xb877aa3236a4b449,0x9befeb9fad487c3, + 0xe69594bec44de15b,0x4c2ebe687989a9b4, + 0x901d7cf73ab0acd9,0xf9d37014bf60a11, + 0xb424dc35095cd80f,0x538484c19ef38c95, + 0xe12e13424bb40e13,0x2865a5f206b06fba, + 0x8cbccc096f5088cb,0xf93f87b7442e45d4, + 0xafebff0bcb24aafe,0xf78f69a51539d749, + 0xdbe6fecebdedd5be,0xb573440e5a884d1c, + 0x89705f4136b4a597,0x31680a88f8953031, + 0xabcc77118461cefc,0xfdc20d2b36ba7c3e, + 0xd6bf94d5e57a42bc,0x3d32907604691b4d, + 0x8637bd05af6c69b5,0xa63f9a49c2c1b110, + 0xa7c5ac471b478423,0xfcf80dc33721d54, + 0xd1b71758e219652b,0xd3c36113404ea4a9, + 0x83126e978d4fdf3b,0x645a1cac083126ea, + 0xa3d70a3d70a3d70a,0x3d70a3d70a3d70a4, + 0xcccccccccccccccc,0xcccccccccccccccd, + 0x8000000000000000,0x0, + 0xa000000000000000,0x0, + 0xc800000000000000,0x0, + 0xfa00000000000000,0x0, + 0x9c40000000000000,0x0, + 0xc350000000000000,0x0, + 0xf424000000000000,0x0, + 0x9896800000000000,0x0, + 0xbebc200000000000,0x0, + 0xee6b280000000000,0x0, + 0x9502f90000000000,0x0, + 0xba43b74000000000,0x0, + 0xe8d4a51000000000,0x0, + 0x9184e72a00000000,0x0, + 0xb5e620f480000000,0x0, + 0xe35fa931a0000000,0x0, + 0x8e1bc9bf04000000,0x0, + 0xb1a2bc2ec5000000,0x0, + 0xde0b6b3a76400000,0x0, + 0x8ac7230489e80000,0x0, + 0xad78ebc5ac620000,0x0, + 0xd8d726b7177a8000,0x0, + 0x878678326eac9000,0x0, + 0xa968163f0a57b400,0x0, + 0xd3c21bcecceda100,0x0, + 0x84595161401484a0,0x0, + 0xa56fa5b99019a5c8,0x0, + 0xcecb8f27f4200f3a,0x0, + 0x813f3978f8940984,0x4000000000000000, + 0xa18f07d736b90be5,0x5000000000000000, + 0xc9f2c9cd04674ede,0xa400000000000000, + 0xfc6f7c4045812296,0x4d00000000000000, + 0x9dc5ada82b70b59d,0xf020000000000000, + 0xc5371912364ce305,0x6c28000000000000, + 0xf684df56c3e01bc6,0xc732000000000000, + 0x9a130b963a6c115c,0x3c7f400000000000, + 0xc097ce7bc90715b3,0x4b9f100000000000, + 0xf0bdc21abb48db20,0x1e86d40000000000, + 0x96769950b50d88f4,0x1314448000000000, + 0xbc143fa4e250eb31,0x17d955a000000000, + 0xeb194f8e1ae525fd,0x5dcfab0800000000, + 0x92efd1b8d0cf37be,0x5aa1cae500000000, + 0xb7abc627050305ad,0xf14a3d9e40000000, + 0xe596b7b0c643c719,0x6d9ccd05d0000000, + 0x8f7e32ce7bea5c6f,0xe4820023a2000000, + 0xb35dbf821ae4f38b,0xdda2802c8a800000, + 0xe0352f62a19e306e,0xd50b2037ad200000, + 0x8c213d9da502de45,0x4526f422cc340000, + 0xaf298d050e4395d6,0x9670b12b7f410000, + 0xdaf3f04651d47b4c,0x3c0cdd765f114000, + 0x88d8762bf324cd0f,0xa5880a69fb6ac800, + 0xab0e93b6efee0053,0x8eea0d047a457a00, + 0xd5d238a4abe98068,0x72a4904598d6d880, + 0x85a36366eb71f041,0x47a6da2b7f864750, + 0xa70c3c40a64e6c51,0x999090b65f67d924, + 0xd0cf4b50cfe20765,0xfff4b4e3f741cf6d, + 0x82818f1281ed449f,0xbff8f10e7a8921a4, + 0xa321f2d7226895c7,0xaff72d52192b6a0d, + 0xcbea6f8ceb02bb39,0x9bf4f8a69f764490, + 0xfee50b7025c36a08,0x2f236d04753d5b4, + 0x9f4f2726179a2245,0x1d762422c946590, + 0xc722f0ef9d80aad6,0x424d3ad2b7b97ef5, + 0xf8ebad2b84e0d58b,0xd2e0898765a7deb2, + 0x9b934c3b330c8577,0x63cc55f49f88eb2f, + 0xc2781f49ffcfa6d5,0x3cbf6b71c76b25fb, + 0xf316271c7fc3908a,0x8bef464e3945ef7a, + 0x97edd871cfda3a56,0x97758bf0e3cbb5ac, + 0xbde94e8e43d0c8ec,0x3d52eeed1cbea317, + 0xed63a231d4c4fb27,0x4ca7aaa863ee4bdd, + 0x945e455f24fb1cf8,0x8fe8caa93e74ef6a, + 0xb975d6b6ee39e436,0xb3e2fd538e122b44, + 0xe7d34c64a9c85d44,0x60dbbca87196b616, + 0x90e40fbeea1d3a4a,0xbc8955e946fe31cd, + 0xb51d13aea4a488dd,0x6babab6398bdbe41, + 0xe264589a4dcdab14,0xc696963c7eed2dd1, + 0x8d7eb76070a08aec,0xfc1e1de5cf543ca2, + 0xb0de65388cc8ada8,0x3b25a55f43294bcb, + 0xdd15fe86affad912,0x49ef0eb713f39ebe, + 0x8a2dbf142dfcc7ab,0x6e3569326c784337, + 0xacb92ed9397bf996,0x49c2c37f07965404, + 0xd7e77a8f87daf7fb,0xdc33745ec97be906, + 0x86f0ac99b4e8dafd,0x69a028bb3ded71a3, + 0xa8acd7c0222311bc,0xc40832ea0d68ce0c, + 0xd2d80db02aabd62b,0xf50a3fa490c30190, + 0x83c7088e1aab65db,0x792667c6da79e0fa, + 0xa4b8cab1a1563f52,0x577001b891185938, + 0xcde6fd5e09abcf26,0xed4c0226b55e6f86, + 0x80b05e5ac60b6178,0x544f8158315b05b4, + 0xa0dc75f1778e39d6,0x696361ae3db1c721, + 0xc913936dd571c84c,0x3bc3a19cd1e38e9, + 0xfb5878494ace3a5f,0x4ab48a04065c723, + 0x9d174b2dcec0e47b,0x62eb0d64283f9c76, + 0xc45d1df942711d9a,0x3ba5d0bd324f8394, + 0xf5746577930d6500,0xca8f44ec7ee36479, + 0x9968bf6abbe85f20,0x7e998b13cf4e1ecb, + 0xbfc2ef456ae276e8,0x9e3fedd8c321a67e, + 0xefb3ab16c59b14a2,0xc5cfe94ef3ea101e, + 0x95d04aee3b80ece5,0xbba1f1d158724a12, + 0xbb445da9ca61281f,0x2a8a6e45ae8edc97, + 0xea1575143cf97226,0xf52d09d71a3293bd, + 0x924d692ca61be758,0x593c2626705f9c56, + 0xb6e0c377cfa2e12e,0x6f8b2fb00c77836c, + 0xe498f455c38b997a,0xb6dfb9c0f956447, + 0x8edf98b59a373fec,0x4724bd4189bd5eac, + 0xb2977ee300c50fe7,0x58edec91ec2cb657, + 0xdf3d5e9bc0f653e1,0x2f2967b66737e3ed, + 0x8b865b215899f46c,0xbd79e0d20082ee74, + 0xae67f1e9aec07187,0xecd8590680a3aa11, + 0xda01ee641a708de9,0xe80e6f4820cc9495, + 0x884134fe908658b2,0x3109058d147fdcdd, + 0xaa51823e34a7eede,0xbd4b46f0599fd415, + 0xd4e5e2cdc1d1ea96,0x6c9e18ac7007c91a, + 0x850fadc09923329e,0x3e2cf6bc604ddb0, + 0xa6539930bf6bff45,0x84db8346b786151c, + 0xcfe87f7cef46ff16,0xe612641865679a63, + 0x81f14fae158c5f6e,0x4fcb7e8f3f60c07e, + 0xa26da3999aef7749,0xe3be5e330f38f09d, + 0xcb090c8001ab551c,0x5cadf5bfd3072cc5, + 0xfdcb4fa002162a63,0x73d9732fc7c8f7f6, + 0x9e9f11c4014dda7e,0x2867e7fddcdd9afa, + 0xc646d63501a1511d,0xb281e1fd541501b8, + 0xf7d88bc24209a565,0x1f225a7ca91a4226, + 0x9ae757596946075f,0x3375788de9b06958, + 0xc1a12d2fc3978937,0x52d6b1641c83ae, + 0xf209787bb47d6b84,0xc0678c5dbd23a49a, + 0x9745eb4d50ce6332,0xf840b7ba963646e0, + 0xbd176620a501fbff,0xb650e5a93bc3d898, + 0xec5d3fa8ce427aff,0xa3e51f138ab4cebe, + 0x93ba47c980e98cdf,0xc66f336c36b10137, + 0xb8a8d9bbe123f017,0xb80b0047445d4184, + 0xe6d3102ad96cec1d,0xa60dc059157491e5, + 0x9043ea1ac7e41392,0x87c89837ad68db2f, + 0xb454e4a179dd1877,0x29babe4598c311fb, + 0xe16a1dc9d8545e94,0xf4296dd6fef3d67a, + 0x8ce2529e2734bb1d,0x1899e4a65f58660c, + 0xb01ae745b101e9e4,0x5ec05dcff72e7f8f, + 0xdc21a1171d42645d,0x76707543f4fa1f73, + 0x899504ae72497eba,0x6a06494a791c53a8, + 0xabfa45da0edbde69,0x487db9d17636892, + 0xd6f8d7509292d603,0x45a9d2845d3c42b6, + 0x865b86925b9bc5c2,0xb8a2392ba45a9b2, + 0xa7f26836f282b732,0x8e6cac7768d7141e, + 0xd1ef0244af2364ff,0x3207d795430cd926, + 0x8335616aed761f1f,0x7f44e6bd49e807b8, + 0xa402b9c5a8d3a6e7,0x5f16206c9c6209a6, + 0xcd036837130890a1,0x36dba887c37a8c0f, + 0x802221226be55a64,0xc2494954da2c9789, + 0xa02aa96b06deb0fd,0xf2db9baa10b7bd6c, + 0xc83553c5c8965d3d,0x6f92829494e5acc7, + 0xfa42a8b73abbf48c,0xcb772339ba1f17f9, + 0x9c69a97284b578d7,0xff2a760414536efb, + 0xc38413cf25e2d70d,0xfef5138519684aba, + 0xf46518c2ef5b8cd1,0x7eb258665fc25d69, + 0x98bf2f79d5993802,0xef2f773ffbd97a61, + 0xbeeefb584aff8603,0xaafb550ffacfd8fa, + 0xeeaaba2e5dbf6784,0x95ba2a53f983cf38, + 0x952ab45cfa97a0b2,0xdd945a747bf26183, + 0xba756174393d88df,0x94f971119aeef9e4, + 0xe912b9d1478ceb17,0x7a37cd5601aab85d, + 0x91abb422ccb812ee,0xac62e055c10ab33a, + 0xb616a12b7fe617aa,0x577b986b314d6009, + 0xe39c49765fdf9d94,0xed5a7e85fda0b80b, + 0x8e41ade9fbebc27d,0x14588f13be847307, + 0xb1d219647ae6b31c,0x596eb2d8ae258fc8, + 0xde469fbd99a05fe3,0x6fca5f8ed9aef3bb, + 0x8aec23d680043bee,0x25de7bb9480d5854, + 0xada72ccc20054ae9,0xaf561aa79a10ae6a, + 0xd910f7ff28069da4,0x1b2ba1518094da04, + 0x87aa9aff79042286,0x90fb44d2f05d0842, + 0xa99541bf57452b28,0x353a1607ac744a53, + 0xd3fa922f2d1675f2,0x42889b8997915ce8, + 0x847c9b5d7c2e09b7,0x69956135febada11, + 0xa59bc234db398c25,0x43fab9837e699095, + 0xcf02b2c21207ef2e,0x94f967e45e03f4bb, + 0x8161afb94b44f57d,0x1d1be0eebac278f5, + 0xa1ba1ba79e1632dc,0x6462d92a69731732, + 0xca28a291859bbf93,0x7d7b8f7503cfdcfe, + 0xfcb2cb35e702af78,0x5cda735244c3d43e, + 0x9defbf01b061adab,0x3a0888136afa64a7, + 0xc56baec21c7a1916,0x88aaa1845b8fdd0, + 0xf6c69a72a3989f5b,0x8aad549e57273d45, + 0x9a3c2087a63f6399,0x36ac54e2f678864b, + 0xc0cb28a98fcf3c7f,0x84576a1bb416a7dd, + 0xf0fdf2d3f3c30b9f,0x656d44a2a11c51d5, + 0x969eb7c47859e743,0x9f644ae5a4b1b325, + 0xbc4665b596706114,0x873d5d9f0dde1fee, + 0xeb57ff22fc0c7959,0xa90cb506d155a7ea, + 0x9316ff75dd87cbd8,0x9a7f12442d588f2, + 0xb7dcbf5354e9bece,0xc11ed6d538aeb2f, + 0xe5d3ef282a242e81,0x8f1668c8a86da5fa, + 0x8fa475791a569d10,0xf96e017d694487bc, + 0xb38d92d760ec4455,0x37c981dcc395a9ac, + 0xe070f78d3927556a,0x85bbe253f47b1417, + 0x8c469ab843b89562,0x93956d7478ccec8e, + 0xaf58416654a6babb,0x387ac8d1970027b2, + 0xdb2e51bfe9d0696a,0x6997b05fcc0319e, + 0x88fcf317f22241e2,0x441fece3bdf81f03, + 0xab3c2fddeeaad25a,0xd527e81cad7626c3, + 0xd60b3bd56a5586f1,0x8a71e223d8d3b074, + 0x85c7056562757456,0xf6872d5667844e49, + 0xa738c6bebb12d16c,0xb428f8ac016561db, + 0xd106f86e69d785c7,0xe13336d701beba52, + 0x82a45b450226b39c,0xecc0024661173473, + 0xa34d721642b06084,0x27f002d7f95d0190, + 0xcc20ce9bd35c78a5,0x31ec038df7b441f4, + 0xff290242c83396ce,0x7e67047175a15271, + 0x9f79a169bd203e41,0xf0062c6e984d386, + 0xc75809c42c684dd1,0x52c07b78a3e60868, + 0xf92e0c3537826145,0xa7709a56ccdf8a82, + 0x9bbcc7a142b17ccb,0x88a66076400bb691, + 0xc2abf989935ddbfe,0x6acff893d00ea435, + 0xf356f7ebf83552fe,0x583f6b8c4124d43, + 0x98165af37b2153de,0xc3727a337a8b704a, + 0xbe1bf1b059e9a8d6,0x744f18c0592e4c5c, + 0xeda2ee1c7064130c,0x1162def06f79df73, + 0x9485d4d1c63e8be7,0x8addcb5645ac2ba8, + 0xb9a74a0637ce2ee1,0x6d953e2bd7173692, + 0xe8111c87c5c1ba99,0xc8fa8db6ccdd0437, + 0x910ab1d4db9914a0,0x1d9c9892400a22a2, + 0xb54d5e4a127f59c8,0x2503beb6d00cab4b, + 0xe2a0b5dc971f303a,0x2e44ae64840fd61d, + 0x8da471a9de737e24,0x5ceaecfed289e5d2, + 0xb10d8e1456105dad,0x7425a83e872c5f47, + 0xdd50f1996b947518,0xd12f124e28f77719, + 0x8a5296ffe33cc92f,0x82bd6b70d99aaa6f, + 0xace73cbfdc0bfb7b,0x636cc64d1001550b, + 0xd8210befd30efa5a,0x3c47f7e05401aa4e, + 0x8714a775e3e95c78,0x65acfaec34810a71, + 0xa8d9d1535ce3b396,0x7f1839a741a14d0d, + 0xd31045a8341ca07c,0x1ede48111209a050, + 0x83ea2b892091e44d,0x934aed0aab460432, + 0xa4e4b66b68b65d60,0xf81da84d5617853f, + 0xce1de40642e3f4b9,0x36251260ab9d668e, + 0x80d2ae83e9ce78f3,0xc1d72b7c6b426019, + 0xa1075a24e4421730,0xb24cf65b8612f81f, + 0xc94930ae1d529cfc,0xdee033f26797b627, + 0xfb9b7cd9a4a7443c,0x169840ef017da3b1, + 0x9d412e0806e88aa5,0x8e1f289560ee864e, + 0xc491798a08a2ad4e,0xf1a6f2bab92a27e2, + 0xf5b5d7ec8acb58a2,0xae10af696774b1db, + 0x9991a6f3d6bf1765,0xacca6da1e0a8ef29, + 0xbff610b0cc6edd3f,0x17fd090a58d32af3, + 0xeff394dcff8a948e,0xddfc4b4cef07f5b0, + 0x95f83d0a1fb69cd9,0x4abdaf101564f98e, + 0xbb764c4ca7a4440f,0x9d6d1ad41abe37f1, + 0xea53df5fd18d5513,0x84c86189216dc5ed, + 0x92746b9be2f8552c,0x32fd3cf5b4e49bb4, + 0xb7118682dbb66a77,0x3fbc8c33221dc2a1, + 0xe4d5e82392a40515,0xfabaf3feaa5334a, + 0x8f05b1163ba6832d,0x29cb4d87f2a7400e, + 0xb2c71d5bca9023f8,0x743e20e9ef511012, + 0xdf78e4b2bd342cf6,0x914da9246b255416, + 0x8bab8eefb6409c1a,0x1ad089b6c2f7548e, + 0xae9672aba3d0c320,0xa184ac2473b529b1, + 0xda3c0f568cc4f3e8,0xc9e5d72d90a2741e, + 0x8865899617fb1871,0x7e2fa67c7a658892, + 0xaa7eebfb9df9de8d,0xddbb901b98feeab7, + 0xd51ea6fa85785631,0x552a74227f3ea565, + 0x8533285c936b35de,0xd53a88958f87275f, + 0xa67ff273b8460356,0x8a892abaf368f137, + 0xd01fef10a657842c,0x2d2b7569b0432d85, + 0x8213f56a67f6b29b,0x9c3b29620e29fc73, + 0xa298f2c501f45f42,0x8349f3ba91b47b8f, + 0xcb3f2f7642717713,0x241c70a936219a73, + 0xfe0efb53d30dd4d7,0xed238cd383aa0110, + 0x9ec95d1463e8a506,0xf4363804324a40aa, + 0xc67bb4597ce2ce48,0xb143c6053edcd0d5, + 0xf81aa16fdc1b81da,0xdd94b7868e94050a, + 0x9b10a4e5e9913128,0xca7cf2b4191c8326, + 0xc1d4ce1f63f57d72,0xfd1c2f611f63a3f0, + 0xf24a01a73cf2dccf,0xbc633b39673c8cec, + 0x976e41088617ca01,0xd5be0503e085d813, + 0xbd49d14aa79dbc82,0x4b2d8644d8a74e18, + 0xec9c459d51852ba2,0xddf8e7d60ed1219e, + 0x93e1ab8252f33b45,0xcabb90e5c942b503, + 0xb8da1662e7b00a17,0x3d6a751f3b936243, + 0xe7109bfba19c0c9d,0xcc512670a783ad4, + 0x906a617d450187e2,0x27fb2b80668b24c5, + 0xb484f9dc9641e9da,0xb1f9f660802dedf6, + 0xe1a63853bbd26451,0x5e7873f8a0396973, + 0x8d07e33455637eb2,0xdb0b487b6423e1e8, + 0xb049dc016abc5e5f,0x91ce1a9a3d2cda62, + 0xdc5c5301c56b75f7,0x7641a140cc7810fb, + 0x89b9b3e11b6329ba,0xa9e904c87fcb0a9d, + 0xac2820d9623bf429,0x546345fa9fbdcd44, + 0xd732290fbacaf133,0xa97c177947ad4095, + 0x867f59a9d4bed6c0,0x49ed8eabcccc485d, + 0xa81f301449ee8c70,0x5c68f256bfff5a74, + 0xd226fc195c6a2f8c,0x73832eec6fff3111, + 0x83585d8fd9c25db7,0xc831fd53c5ff7eab, + 0xa42e74f3d032f525,0xba3e7ca8b77f5e55, + 0xcd3a1230c43fb26f,0x28ce1bd2e55f35eb, + 0x80444b5e7aa7cf85,0x7980d163cf5b81b3, + 0xa0555e361951c366,0xd7e105bcc332621f, + 0xc86ab5c39fa63440,0x8dd9472bf3fefaa7, + 0xfa856334878fc150,0xb14f98f6f0feb951, + 0x9c935e00d4b9d8d2,0x6ed1bf9a569f33d3, + 0xc3b8358109e84f07,0xa862f80ec4700c8, + 0xf4a642e14c6262c8,0xcd27bb612758c0fa, + 0x98e7e9cccfbd7dbd,0x8038d51cb897789c, + 0xbf21e44003acdd2c,0xe0470a63e6bd56c3, + 0xeeea5d5004981478,0x1858ccfce06cac74, + 0x95527a5202df0ccb,0xf37801e0c43ebc8, + 0xbaa718e68396cffd,0xd30560258f54e6ba, + 0xe950df20247c83fd,0x47c6b82ef32a2069, + 0x91d28b7416cdd27e,0x4cdc331d57fa5441, + 0xb6472e511c81471d,0xe0133fe4adf8e952, + 0xe3d8f9e563a198e5,0x58180fddd97723a6, + 0x8e679c2f5e44ff8f,0x570f09eaa7ea7648,}; +using powers = powers_template<>; + +} + +#endif + +#ifndef FASTFLOAT_DECIMAL_TO_BINARY_H +#define FASTFLOAT_DECIMAL_TO_BINARY_H + +#include +#include +#include +#include +#include +#include +#include + +namespace fast_float { + +// This will compute or rather approximate w * 5**q and return a pair of 64-bit words approximating +// the result, with the "high" part corresponding to the most significant bits and the +// low part corresponding to the least significant bits. +// +template +fastfloat_really_inline +value128 compute_product_approximation(int64_t q, uint64_t w) { + const int index = 2 * int(q - powers::smallest_power_of_five); + // For small values of q, e.g., q in [0,27], the answer is always exact because + // The line value128 firstproduct = full_multiplication(w, power_of_five_128[index]); + // gives the exact answer. + value128 firstproduct = full_multiplication(w, powers::power_of_five_128[index]); + static_assert((bit_precision >= 0) && (bit_precision <= 64), " precision should be in (0,64]"); + constexpr uint64_t precision_mask = (bit_precision < 64) ? + (uint64_t(0xFFFFFFFFFFFFFFFF) >> bit_precision) + : uint64_t(0xFFFFFFFFFFFFFFFF); + if((firstproduct.high & precision_mask) == precision_mask) { // could further guard with (lower + w < lower) + // regarding the second product, we only need secondproduct.high, but our expectation is that the compiler will optimize this extra work away if needed. + value128 secondproduct = full_multiplication(w, powers::power_of_five_128[index + 1]); + firstproduct.low += secondproduct.high; + if(secondproduct.high > firstproduct.low) { + firstproduct.high++; + } + } + return firstproduct; +} + +namespace detail { +/** + * For q in (0,350), we have that + * f = (((152170 + 65536) * q ) >> 16); + * is equal to + * floor(p) + q + * where + * p = log(5**q)/log(2) = q * log(5)/log(2) + * + * For negative values of q in (-400,0), we have that + * f = (((152170 + 65536) * q ) >> 16); + * is equal to + * -ceil(p) + q + * where + * p = log(5**-q)/log(2) = -q * log(5)/log(2) + */ + fastfloat_really_inline int power(int q) noexcept { + return (((152170 + 65536) * q) >> 16) + 63; + } +} // namespace detail + + +// w * 10 ** q +// The returned value should be a valid ieee64 number that simply need to be packed. +// However, in some very rare cases, the computation will fail. In such cases, we +// return an adjusted_mantissa with a negative power of 2: the caller should recompute +// in such cases. +template +fastfloat_really_inline +adjusted_mantissa compute_float(int64_t q, uint64_t w) noexcept { + adjusted_mantissa answer; + if ((w == 0) || (q < binary::smallest_power_of_ten())) { + answer.power2 = 0; + answer.mantissa = 0; + // result should be zero + return answer; + } + if (q > binary::largest_power_of_ten()) { + // we want to get infinity: + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + return answer; + } + // At this point in time q is in [powers::smallest_power_of_five, powers::largest_power_of_five]. + + // We want the most significant bit of i to be 1. Shift if needed. + int lz = leading_zeroes(w); + w <<= lz; + + // The required precision is binary::mantissa_explicit_bits() + 3 because + // 1. We need the implicit bit + // 2. We need an extra bit for rounding purposes + // 3. We might lose a bit due to the "upperbit" routine (result too small, requiring a shift) + + value128 product = compute_product_approximation(q, w); + if(product.low == 0xFFFFFFFFFFFFFFFF) { // could guard it further + // In some very rare cases, this could happen, in which case we might need a more accurate + // computation that what we can provide cheaply. This is very, very unlikely. + // + const bool inside_safe_exponent = (q >= -27) && (q <= 55); // always good because 5**q <2**128 when q>=0, + // and otherwise, for q<0, we have 5**-q<2**64 and the 128-bit reciprocal allows for exact computation. + if(!inside_safe_exponent) { + answer.power2 = -1; // This (a negative value) indicates an error condition. + return answer; + } + } + // The "compute_product_approximation" function can be slightly slower than a branchless approach: + // value128 product = compute_product(q, w); + // but in practice, we can win big with the compute_product_approximation if its additional branch + // is easily predicted. Which is best is data specific. + int upperbit = int(product.high >> 63); + + answer.mantissa = product.high >> (upperbit + 64 - binary::mantissa_explicit_bits() - 3); + + answer.power2 = int(detail::power(int(q)) + upperbit - lz - binary::minimum_exponent()); + if (answer.power2 <= 0) { // we have a subnormal? + // Here have that answer.power2 <= 0 so -answer.power2 >= 0 + if(-answer.power2 + 1 >= 64) { // if we have more than 64 bits below the minimum exponent, you have a zero for sure. + answer.power2 = 0; + answer.mantissa = 0; + // result should be zero + return answer; + } + // next line is safe because -answer.power2 + 1 < 64 + answer.mantissa >>= -answer.power2 + 1; + // Thankfully, we can't have both "round-to-even" and subnormals because + // "round-to-even" only occurs for powers close to 0. + answer.mantissa += (answer.mantissa & 1); // round up + answer.mantissa >>= 1; + // There is a weird scenario where we don't have a subnormal but just. + // Suppose we start with 2.2250738585072013e-308, we end up + // with 0x3fffffffffffff x 2^-1023-53 which is technically subnormal + // whereas 0x40000000000000 x 2^-1023-53 is normal. Now, we need to round + // up 0x3fffffffffffff x 2^-1023-53 and once we do, we are no longer + // subnormal, but we can only know this after rounding. + // So we only declare a subnormal if we are smaller than the threshold. + answer.power2 = (answer.mantissa < (uint64_t(1) << binary::mantissa_explicit_bits())) ? 0 : 1; + return answer; + } + + // usually, we round *up*, but if we fall right in between and and we have an + // even basis, we need to round down + // We are only concerned with the cases where 5**q fits in single 64-bit word. + if ((product.low <= 1) && (q >= binary::min_exponent_round_to_even()) && (q <= binary::max_exponent_round_to_even()) && + ((answer.mantissa & 3) == 1) ) { // we may fall between two floats! + // To be in-between two floats we need that in doing + // answer.mantissa = product.high >> (upperbit + 64 - binary::mantissa_explicit_bits() - 3); + // ... we dropped out only zeroes. But if this happened, then we can go back!!! + if((answer.mantissa << (upperbit + 64 - binary::mantissa_explicit_bits() - 3)) == product.high) { + answer.mantissa &= ~uint64_t(1); // flip it so that we do not round up + } + } + + answer.mantissa += (answer.mantissa & 1); // round up + answer.mantissa >>= 1; + if (answer.mantissa >= (uint64_t(2) << binary::mantissa_explicit_bits())) { + answer.mantissa = (uint64_t(1) << binary::mantissa_explicit_bits()); + answer.power2++; // undo previous addition + } + + answer.mantissa &= ~(uint64_t(1) << binary::mantissa_explicit_bits()); + if (answer.power2 >= binary::infinite_power()) { // infinity + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + } + return answer; +} + + +} // namespace fast_float + +#endif + + +#ifndef FASTFLOAT_ASCII_NUMBER_H +#define FASTFLOAT_ASCII_NUMBER_H + +#include +#include +#include +#include + + +namespace fast_float { + +// Next function can be micro-optimized, but compilers are entirely +// able to optimize it well. +fastfloat_really_inline bool is_integer(char c) noexcept { return c >= '0' && c <= '9'; } + +fastfloat_really_inline uint64_t byteswap(uint64_t val) { + return (val & 0xFF00000000000000) >> 56 + | (val & 0x00FF000000000000) >> 40 + | (val & 0x0000FF0000000000) >> 24 + | (val & 0x000000FF00000000) >> 8 + | (val & 0x00000000FF000000) << 8 + | (val & 0x0000000000FF0000) << 24 + | (val & 0x000000000000FF00) << 40 + | (val & 0x00000000000000FF) << 56; +} + +fastfloat_really_inline uint64_t read_u64(const char *chars) { + uint64_t val; + ::memcpy(&val, chars, sizeof(uint64_t)); +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + // Need to read as-if the number was in little-endian order. + val = byteswap(val); +#endif + return val; +} + +fastfloat_really_inline void write_u64(uint8_t *chars, uint64_t val) { +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + // Need to read as-if the number was in little-endian order. + val = byteswap(val); +#endif + ::memcpy(chars, &val, sizeof(uint64_t)); +} + +// credit @aqrit +fastfloat_really_inline uint32_t parse_eight_digits_unrolled(uint64_t val) { + const uint64_t mask = 0x000000FF000000FF; + const uint64_t mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32) + const uint64_t mul2 = 0x0000271000000001; // 1 + (10000ULL << 32) + val -= 0x3030303030303030; + val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8; + val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32; + return uint32_t(val); +} + +fastfloat_really_inline uint32_t parse_eight_digits_unrolled(const char *chars) noexcept { + return parse_eight_digits_unrolled(read_u64(chars)); +} + +// credit @aqrit +fastfloat_really_inline bool is_made_of_eight_digits_fast(uint64_t val) noexcept { + return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) & + 0x8080808080808080)); +} + +fastfloat_really_inline bool is_made_of_eight_digits_fast(const char *chars) noexcept { + return is_made_of_eight_digits_fast(read_u64(chars)); +} + +struct parsed_number_string { + int64_t exponent; + uint64_t mantissa; + const char *lastmatch; + bool negative; + bool valid; + bool too_many_digits; +}; + + +// Assuming that you use no more than 19 digits, this will +// parse an ASCII string. +fastfloat_really_inline +parsed_number_string parse_number_string(const char *p, const char *pend, parse_options options) noexcept { + const chars_format fmt = options.format; + const char decimal_point = options.decimal_point; + + parsed_number_string answer; + answer.valid = false; + answer.too_many_digits = false; + answer.negative = (*p == '-'); + if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here + ++p; + if (p == pend) { + return answer; + } + if (!is_integer(*p) && (*p != decimal_point)) { // a sign must be followed by an integer or the dot + return answer; + } + } + const char *const start_digits = p; + + uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad) + + while ((p != pend) && is_integer(*p)) { + // a multiplication by 10 is cheaper than an arbitrary integer + // multiplication + i = 10 * i + + uint64_t(*p - '0'); // might overflow, we will handle the overflow later + ++p; + } + const char *const end_of_integer_part = p; + int64_t digit_count = int64_t(end_of_integer_part - start_digits); + int64_t exponent = 0; + if ((p != pend) && (*p == decimal_point)) { + ++p; + // Fast approach only tested under little endian systems + if ((p + 8 <= pend) && is_made_of_eight_digits_fast(p)) { + i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok + p += 8; + if ((p + 8 <= pend) && is_made_of_eight_digits_fast(p)) { + i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok + p += 8; + } + } + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - '0'); + ++p; + i = i * 10 + digit; // in rare cases, this will overflow, but that's ok + } + exponent = end_of_integer_part + 1 - p; + digit_count -= exponent; + } + // we must have encountered at least one integer! + if (digit_count == 0) { + return answer; + } + int64_t exp_number = 0; // explicit exponential part + if ((fmt & chars_format::scientific) && (p != pend) && (('e' == *p) || ('E' == *p))) { + const char * location_of_e = p; + ++p; + bool neg_exp = false; + if ((p != pend) && ('-' == *p)) { + neg_exp = true; + ++p; + } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1) + ++p; + } + if ((p == pend) || !is_integer(*p)) { + if(!(fmt & chars_format::fixed)) { + // We are in error. + return answer; + } + // Otherwise, we will be ignoring the 'e'. + p = location_of_e; + } else { + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - '0'); + if (exp_number < 0x10000) { + exp_number = 10 * exp_number + digit; + } + ++p; + } + if(neg_exp) { exp_number = - exp_number; } + exponent += exp_number; + } + } else { + // If it scientific and not fixed, we have to bail out. + if((fmt & chars_format::scientific) && !(fmt & chars_format::fixed)) { return answer; } + } + answer.lastmatch = p; + answer.valid = true; + + // If we frequently had to deal with long strings of digits, + // we could extend our code by using a 128-bit integer instead + // of a 64-bit integer. However, this is uncommon. + // + // We can deal with up to 19 digits. + if (digit_count > 19) { // this is uncommon + // It is possible that the integer had an overflow. + // We have to handle the case where we have 0.0000somenumber. + // We need to be mindful of the case where we only have zeroes... + // E.g., 0.000000000...000. + const char *start = start_digits; + while ((start != pend) && (*start == '0' || *start == decimal_point)) { + if(*start == '0') { digit_count --; } + start++; + } + if (digit_count > 19) { + answer.too_many_digits = true; + // Let us start again, this time, avoiding overflows. + i = 0; + p = start_digits; + const uint64_t minimal_nineteen_digit_integer{1000000000000000000}; + while((i < minimal_nineteen_digit_integer) && (p != pend) && is_integer(*p)) { + i = i * 10 + uint64_t(*p - '0'); + ++p; + } + if (i >= minimal_nineteen_digit_integer) { // We have a big integers + exponent = end_of_integer_part - p + exp_number; + } else { // We have a value with a fractional component. + p++; // skip the dot + const char *first_after_period = p; + while((i < minimal_nineteen_digit_integer) && (p != pend) && is_integer(*p)) { + i = i * 10 + uint64_t(*p - '0'); + ++p; + } + exponent = first_after_period - p + exp_number; + } + // We have now corrected both exponent and i, to a truncated value + } + } + answer.exponent = exponent; + answer.mantissa = i; + return answer; +} + + +// This should always succeed since it follows a call to parse_number_string +// This function could be optimized. In particular, we could stop after 19 digits +// and try to bail out. Furthermore, we should be able to recover the computed +// exponent from the pass in parse_number_string. +fastfloat_really_inline decimal parse_decimal(const char *p, const char *pend, parse_options options) noexcept { + const char decimal_point = options.decimal_point; + + decimal answer; + answer.num_digits = 0; + answer.decimal_point = 0; + answer.truncated = false; + answer.negative = (*p == '-'); + if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here + ++p; + } + // skip leading zeroes + while ((p != pend) && (*p == '0')) { + ++p; + } + while ((p != pend) && is_integer(*p)) { + if (answer.num_digits < max_digits) { + answer.digits[answer.num_digits] = uint8_t(*p - '0'); + } + answer.num_digits++; + ++p; + } + if ((p != pend) && (*p == decimal_point)) { + ++p; + const char *first_after_period = p; + // if we have not yet encountered a zero, we have to skip it as well + if(answer.num_digits == 0) { + // skip zeros + while ((p != pend) && (*p == '0')) { + ++p; + } + } + // We expect that this loop will often take the bulk of the running time + // because when a value has lots of digits, these digits often + while ((p + 8 <= pend) && (answer.num_digits + 8 < max_digits)) { + uint64_t val = read_u64(p); + if(! is_made_of_eight_digits_fast(val)) { break; } + // We have eight digits, process them in one go! + val -= 0x3030303030303030; + write_u64(answer.digits + answer.num_digits, val); + answer.num_digits += 8; + p += 8; + } + while ((p != pend) && is_integer(*p)) { + if (answer.num_digits < max_digits) { + answer.digits[answer.num_digits] = uint8_t(*p - '0'); + } + answer.num_digits++; + ++p; + } + answer.decimal_point = int32_t(first_after_period - p); + } + // We want num_digits to be the number of significant digits, excluding + // leading *and* trailing zeros! Otherwise the truncated flag later is + // going to be misleading. + if(answer.num_digits > 0) { + // We potentially need the answer.num_digits > 0 guard because we + // prune leading zeros. So with answer.num_digits > 0, we know that + // we have at least one non-zero digit. + const char *preverse = p - 1; + int32_t trailing_zeros = 0; + while ((*preverse == '0') || (*preverse == decimal_point)) { + if(*preverse == '0') { trailing_zeros++; }; + --preverse; + } + answer.decimal_point += int32_t(answer.num_digits); + answer.num_digits -= uint32_t(trailing_zeros); + } + if(answer.num_digits > max_digits) { + answer.truncated = true; + answer.num_digits = max_digits; + } + if ((p != pend) && (('e' == *p) || ('E' == *p))) { + ++p; + bool neg_exp = false; + if ((p != pend) && ('-' == *p)) { + neg_exp = true; + ++p; + } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1) + ++p; + } + int32_t exp_number = 0; // exponential part + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - '0'); + if (exp_number < 0x10000) { + exp_number = 10 * exp_number + digit; + } + ++p; + } + answer.decimal_point += (neg_exp ? -exp_number : exp_number); + } + // In very rare cases, we may have fewer than 19 digits, we want to be able to reliably + // assume that all digits up to max_digit_without_overflow have been initialized. + for(uint32_t i = answer.num_digits; i < max_digit_without_overflow; i++) { answer.digits[i] = 0; } + + return answer; +} +} // namespace fast_float + +#endif + + +#ifndef FASTFLOAT_GENERIC_DECIMAL_TO_BINARY_H +#define FASTFLOAT_GENERIC_DECIMAL_TO_BINARY_H + +/** + * This code is meant to handle the case where we have more than 19 digits. + * + * It is based on work by Nigel Tao (at https://github.com/google/wuffs/) + * who credits Ken Thompson for the design (via a reference to the Go source + * code). + * + * Rob Pike suggested that this algorithm be called "Simple Decimal Conversion". + * + * It is probably not very fast but it is a fallback that should almost never + * be used in real life. Though it is not fast, it is "easily" understood and debugged. + **/ +#include + +namespace fast_float { + +namespace detail { + +// remove all final zeroes +inline void trim(decimal &h) { + while ((h.num_digits > 0) && (h.digits[h.num_digits - 1] == 0)) { + h.num_digits--; + } +} + + + +inline uint32_t number_of_digits_decimal_left_shift(const decimal &h, uint32_t shift) { + shift &= 63; + const static uint16_t number_of_digits_decimal_left_shift_table[65] = { + 0x0000, 0x0800, 0x0801, 0x0803, 0x1006, 0x1009, 0x100D, 0x1812, 0x1817, + 0x181D, 0x2024, 0x202B, 0x2033, 0x203C, 0x2846, 0x2850, 0x285B, 0x3067, + 0x3073, 0x3080, 0x388E, 0x389C, 0x38AB, 0x38BB, 0x40CC, 0x40DD, 0x40EF, + 0x4902, 0x4915, 0x4929, 0x513E, 0x5153, 0x5169, 0x5180, 0x5998, 0x59B0, + 0x59C9, 0x61E3, 0x61FD, 0x6218, 0x6A34, 0x6A50, 0x6A6D, 0x6A8B, 0x72AA, + 0x72C9, 0x72E9, 0x7B0A, 0x7B2B, 0x7B4D, 0x8370, 0x8393, 0x83B7, 0x83DC, + 0x8C02, 0x8C28, 0x8C4F, 0x9477, 0x949F, 0x94C8, 0x9CF2, 0x051C, 0x051C, + 0x051C, 0x051C, + }; + uint32_t x_a = number_of_digits_decimal_left_shift_table[shift]; + uint32_t x_b = number_of_digits_decimal_left_shift_table[shift + 1]; + uint32_t num_new_digits = x_a >> 11; + uint32_t pow5_a = 0x7FF & x_a; + uint32_t pow5_b = 0x7FF & x_b; + const static uint8_t + number_of_digits_decimal_left_shift_table_powers_of_5[0x051C] = { + 5, 2, 5, 1, 2, 5, 6, 2, 5, 3, 1, 2, 5, 1, 5, 6, 2, 5, 7, 8, 1, 2, 5, 3, + 9, 0, 6, 2, 5, 1, 9, 5, 3, 1, 2, 5, 9, 7, 6, 5, 6, 2, 5, 4, 8, 8, 2, 8, + 1, 2, 5, 2, 4, 4, 1, 4, 0, 6, 2, 5, 1, 2, 2, 0, 7, 0, 3, 1, 2, 5, 6, 1, + 0, 3, 5, 1, 5, 6, 2, 5, 3, 0, 5, 1, 7, 5, 7, 8, 1, 2, 5, 1, 5, 2, 5, 8, + 7, 8, 9, 0, 6, 2, 5, 7, 6, 2, 9, 3, 9, 4, 5, 3, 1, 2, 5, 3, 8, 1, 4, 6, + 9, 7, 2, 6, 5, 6, 2, 5, 1, 9, 0, 7, 3, 4, 8, 6, 3, 2, 8, 1, 2, 5, 9, 5, + 3, 6, 7, 4, 3, 1, 6, 4, 0, 6, 2, 5, 4, 7, 6, 8, 3, 7, 1, 5, 8, 2, 0, 3, + 1, 2, 5, 2, 3, 8, 4, 1, 8, 5, 7, 9, 1, 0, 1, 5, 6, 2, 5, 1, 1, 9, 2, 0, + 9, 2, 8, 9, 5, 5, 0, 7, 8, 1, 2, 5, 5, 9, 6, 0, 4, 6, 4, 4, 7, 7, 5, 3, + 9, 0, 6, 2, 5, 2, 9, 8, 0, 2, 3, 2, 2, 3, 8, 7, 6, 9, 5, 3, 1, 2, 5, 1, + 4, 9, 0, 1, 1, 6, 1, 1, 9, 3, 8, 4, 7, 6, 5, 6, 2, 5, 7, 4, 5, 0, 5, 8, + 0, 5, 9, 6, 9, 2, 3, 8, 2, 8, 1, 2, 5, 3, 7, 2, 5, 2, 9, 0, 2, 9, 8, 4, + 6, 1, 9, 1, 4, 0, 6, 2, 5, 1, 8, 6, 2, 6, 4, 5, 1, 4, 9, 2, 3, 0, 9, 5, + 7, 0, 3, 1, 2, 5, 9, 3, 1, 3, 2, 2, 5, 7, 4, 6, 1, 5, 4, 7, 8, 5, 1, 5, + 6, 2, 5, 4, 6, 5, 6, 6, 1, 2, 8, 7, 3, 0, 7, 7, 3, 9, 2, 5, 7, 8, 1, 2, + 5, 2, 3, 2, 8, 3, 0, 6, 4, 3, 6, 5, 3, 8, 6, 9, 6, 2, 8, 9, 0, 6, 2, 5, + 1, 1, 6, 4, 1, 5, 3, 2, 1, 8, 2, 6, 9, 3, 4, 8, 1, 4, 4, 5, 3, 1, 2, 5, + 5, 8, 2, 0, 7, 6, 6, 0, 9, 1, 3, 4, 6, 7, 4, 0, 7, 2, 2, 6, 5, 6, 2, 5, + 2, 9, 1, 0, 3, 8, 3, 0, 4, 5, 6, 7, 3, 3, 7, 0, 3, 6, 1, 3, 2, 8, 1, 2, + 5, 1, 4, 5, 5, 1, 9, 1, 5, 2, 2, 8, 3, 6, 6, 8, 5, 1, 8, 0, 6, 6, 4, 0, + 6, 2, 5, 7, 2, 7, 5, 9, 5, 7, 6, 1, 4, 1, 8, 3, 4, 2, 5, 9, 0, 3, 3, 2, + 0, 3, 1, 2, 5, 3, 6, 3, 7, 9, 7, 8, 8, 0, 7, 0, 9, 1, 7, 1, 2, 9, 5, 1, + 6, 6, 0, 1, 5, 6, 2, 5, 1, 8, 1, 8, 9, 8, 9, 4, 0, 3, 5, 4, 5, 8, 5, 6, + 4, 7, 5, 8, 3, 0, 0, 7, 8, 1, 2, 5, 9, 0, 9, 4, 9, 4, 7, 0, 1, 7, 7, 2, + 9, 2, 8, 2, 3, 7, 9, 1, 5, 0, 3, 9, 0, 6, 2, 5, 4, 5, 4, 7, 4, 7, 3, 5, + 0, 8, 8, 6, 4, 6, 4, 1, 1, 8, 9, 5, 7, 5, 1, 9, 5, 3, 1, 2, 5, 2, 2, 7, + 3, 7, 3, 6, 7, 5, 4, 4, 3, 2, 3, 2, 0, 5, 9, 4, 7, 8, 7, 5, 9, 7, 6, 5, + 6, 2, 5, 1, 1, 3, 6, 8, 6, 8, 3, 7, 7, 2, 1, 6, 1, 6, 0, 2, 9, 7, 3, 9, + 3, 7, 9, 8, 8, 2, 8, 1, 2, 5, 5, 6, 8, 4, 3, 4, 1, 8, 8, 6, 0, 8, 0, 8, + 0, 1, 4, 8, 6, 9, 6, 8, 9, 9, 4, 1, 4, 0, 6, 2, 5, 2, 8, 4, 2, 1, 7, 0, + 9, 4, 3, 0, 4, 0, 4, 0, 0, 7, 4, 3, 4, 8, 4, 4, 9, 7, 0, 7, 0, 3, 1, 2, + 5, 1, 4, 2, 1, 0, 8, 5, 4, 7, 1, 5, 2, 0, 2, 0, 0, 3, 7, 1, 7, 4, 2, 2, + 4, 8, 5, 3, 5, 1, 5, 6, 2, 5, 7, 1, 0, 5, 4, 2, 7, 3, 5, 7, 6, 0, 1, 0, + 0, 1, 8, 5, 8, 7, 1, 1, 2, 4, 2, 6, 7, 5, 7, 8, 1, 2, 5, 3, 5, 5, 2, 7, + 1, 3, 6, 7, 8, 8, 0, 0, 5, 0, 0, 9, 2, 9, 3, 5, 5, 6, 2, 1, 3, 3, 7, 8, + 9, 0, 6, 2, 5, 1, 7, 7, 6, 3, 5, 6, 8, 3, 9, 4, 0, 0, 2, 5, 0, 4, 6, 4, + 6, 7, 7, 8, 1, 0, 6, 6, 8, 9, 4, 5, 3, 1, 2, 5, 8, 8, 8, 1, 7, 8, 4, 1, + 9, 7, 0, 0, 1, 2, 5, 2, 3, 2, 3, 3, 8, 9, 0, 5, 3, 3, 4, 4, 7, 2, 6, 5, + 6, 2, 5, 4, 4, 4, 0, 8, 9, 2, 0, 9, 8, 5, 0, 0, 6, 2, 6, 1, 6, 1, 6, 9, + 4, 5, 2, 6, 6, 7, 2, 3, 6, 3, 2, 8, 1, 2, 5, 2, 2, 2, 0, 4, 4, 6, 0, 4, + 9, 2, 5, 0, 3, 1, 3, 0, 8, 0, 8, 4, 7, 2, 6, 3, 3, 3, 6, 1, 8, 1, 6, 4, + 0, 6, 2, 5, 1, 1, 1, 0, 2, 2, 3, 0, 2, 4, 6, 2, 5, 1, 5, 6, 5, 4, 0, 4, + 2, 3, 6, 3, 1, 6, 6, 8, 0, 9, 0, 8, 2, 0, 3, 1, 2, 5, 5, 5, 5, 1, 1, 1, + 5, 1, 2, 3, 1, 2, 5, 7, 8, 2, 7, 0, 2, 1, 1, 8, 1, 5, 8, 3, 4, 0, 4, 5, + 4, 1, 0, 1, 5, 6, 2, 5, 2, 7, 7, 5, 5, 5, 7, 5, 6, 1, 5, 6, 2, 8, 9, 1, + 3, 5, 1, 0, 5, 9, 0, 7, 9, 1, 7, 0, 2, 2, 7, 0, 5, 0, 7, 8, 1, 2, 5, 1, + 3, 8, 7, 7, 7, 8, 7, 8, 0, 7, 8, 1, 4, 4, 5, 6, 7, 5, 5, 2, 9, 5, 3, 9, + 5, 8, 5, 1, 1, 3, 5, 2, 5, 3, 9, 0, 6, 2, 5, 6, 9, 3, 8, 8, 9, 3, 9, 0, + 3, 9, 0, 7, 2, 2, 8, 3, 7, 7, 6, 4, 7, 6, 9, 7, 9, 2, 5, 5, 6, 7, 6, 2, + 6, 9, 5, 3, 1, 2, 5, 3, 4, 6, 9, 4, 4, 6, 9, 5, 1, 9, 5, 3, 6, 1, 4, 1, + 8, 8, 8, 2, 3, 8, 4, 8, 9, 6, 2, 7, 8, 3, 8, 1, 3, 4, 7, 6, 5, 6, 2, 5, + 1, 7, 3, 4, 7, 2, 3, 4, 7, 5, 9, 7, 6, 8, 0, 7, 0, 9, 4, 4, 1, 1, 9, 2, + 4, 4, 8, 1, 3, 9, 1, 9, 0, 6, 7, 3, 8, 2, 8, 1, 2, 5, 8, 6, 7, 3, 6, 1, + 7, 3, 7, 9, 8, 8, 4, 0, 3, 5, 4, 7, 2, 0, 5, 9, 6, 2, 2, 4, 0, 6, 9, 5, + 9, 5, 3, 3, 6, 9, 1, 4, 0, 6, 2, 5, + }; + const uint8_t *pow5 = + &number_of_digits_decimal_left_shift_table_powers_of_5[pow5_a]; + uint32_t i = 0; + uint32_t n = pow5_b - pow5_a; + for (; i < n; i++) { + if (i >= h.num_digits) { + return num_new_digits - 1; + } else if (h.digits[i] == pow5[i]) { + continue; + } else if (h.digits[i] < pow5[i]) { + return num_new_digits - 1; + } else { + return num_new_digits; + } + } + return num_new_digits; +} + +inline uint64_t round(decimal &h) { + if ((h.num_digits == 0) || (h.decimal_point < 0)) { + return 0; + } else if (h.decimal_point > 18) { + return UINT64_MAX; + } + // at this point, we know that h.decimal_point >= 0 + uint32_t dp = uint32_t(h.decimal_point); + uint64_t n = 0; + for (uint32_t i = 0; i < dp; i++) { + n = (10 * n) + ((i < h.num_digits) ? h.digits[i] : 0); + } + bool round_up = false; + if (dp < h.num_digits) { + round_up = h.digits[dp] >= 5; // normally, we round up + // but we may need to round to even! + if ((h.digits[dp] == 5) && (dp + 1 == h.num_digits)) { + round_up = h.truncated || ((dp > 0) && (1 & h.digits[dp - 1])); + } + } + if (round_up) { + n++; + } + return n; +} + +// computes h * 2^-shift +inline void decimal_left_shift(decimal &h, uint32_t shift) { + if (h.num_digits == 0) { + return; + } + uint32_t num_new_digits = number_of_digits_decimal_left_shift(h, shift); + int32_t read_index = int32_t(h.num_digits - 1); + uint32_t write_index = h.num_digits - 1 + num_new_digits; + uint64_t n = 0; + + while (read_index >= 0) { + n += uint64_t(h.digits[read_index]) << shift; + uint64_t quotient = n / 10; + uint64_t remainder = n - (10 * quotient); + if (write_index < max_digits) { + h.digits[write_index] = uint8_t(remainder); + } else if (remainder > 0) { + h.truncated = true; + } + n = quotient; + write_index--; + read_index--; + } + while (n > 0) { + uint64_t quotient = n / 10; + uint64_t remainder = n - (10 * quotient); + if (write_index < max_digits) { + h.digits[write_index] = uint8_t(remainder); + } else if (remainder > 0) { + h.truncated = true; + } + n = quotient; + write_index--; + } + h.num_digits += num_new_digits; + if (h.num_digits > max_digits) { + h.num_digits = max_digits; + } + h.decimal_point += int32_t(num_new_digits); + trim(h); +} + +// computes h * 2^shift +inline void decimal_right_shift(decimal &h, uint32_t shift) { + uint32_t read_index = 0; + uint32_t write_index = 0; + + uint64_t n = 0; + + while ((n >> shift) == 0) { + if (read_index < h.num_digits) { + n = (10 * n) + h.digits[read_index++]; + } else if (n == 0) { + return; + } else { + while ((n >> shift) == 0) { + n = 10 * n; + read_index++; + } + break; + } + } + h.decimal_point -= int32_t(read_index - 1); + if (h.decimal_point < -decimal_point_range) { // it is zero + h.num_digits = 0; + h.decimal_point = 0; + h.negative = false; + h.truncated = false; + return; + } + uint64_t mask = (uint64_t(1) << shift) - 1; + while (read_index < h.num_digits) { + uint8_t new_digit = uint8_t(n >> shift); + n = (10 * (n & mask)) + h.digits[read_index++]; + h.digits[write_index++] = new_digit; + } + while (n > 0) { + uint8_t new_digit = uint8_t(n >> shift); + n = 10 * (n & mask); + if (write_index < max_digits) { + h.digits[write_index++] = new_digit; + } else if (new_digit > 0) { + h.truncated = true; + } + } + h.num_digits = write_index; + trim(h); +} + +} // namespace detail + +template +adjusted_mantissa compute_float(decimal &d) { + adjusted_mantissa answer; + if (d.num_digits == 0) { + // should be zero + answer.power2 = 0; + answer.mantissa = 0; + return answer; + } + // At this point, going further, we can assume that d.num_digits > 0. + // + // We want to guard against excessive decimal point values because + // they can result in long running times. Indeed, we do + // shifts by at most 60 bits. We have that log(10**400)/log(2**60) ~= 22 + // which is fine, but log(10**299995)/log(2**60) ~= 16609 which is not + // fine (runs for a long time). + // + if(d.decimal_point < -324) { + // We have something smaller than 1e-324 which is always zero + // in binary64 and binary32. + // It should be zero. + answer.power2 = 0; + answer.mantissa = 0; + return answer; + } else if(d.decimal_point >= 310) { + // We have something at least as large as 0.1e310 which is + // always infinite. + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + return answer; + } + static const uint32_t max_shift = 60; + static const uint32_t num_powers = 19; + static const uint8_t decimal_powers[19] = { + 0, 3, 6, 9, 13, 16, 19, 23, 26, 29, // + 33, 36, 39, 43, 46, 49, 53, 56, 59, // + }; + int32_t exp2 = 0; + while (d.decimal_point > 0) { + uint32_t n = uint32_t(d.decimal_point); + uint32_t shift = (n < num_powers) ? decimal_powers[n] : max_shift; + detail::decimal_right_shift(d, shift); + if (d.decimal_point < -decimal_point_range) { + // should be zero + answer.power2 = 0; + answer.mantissa = 0; + return answer; + } + exp2 += int32_t(shift); + } + // We shift left toward [1/2 ... 1]. + while (d.decimal_point <= 0) { + uint32_t shift; + if (d.decimal_point == 0) { + if (d.digits[0] >= 5) { + break; + } + shift = (d.digits[0] < 2) ? 2 : 1; + } else { + uint32_t n = uint32_t(-d.decimal_point); + shift = (n < num_powers) ? decimal_powers[n] : max_shift; + } + detail::decimal_left_shift(d, shift); + if (d.decimal_point > decimal_point_range) { + // we want to get infinity: + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + return answer; + } + exp2 -= int32_t(shift); + } + // We are now in the range [1/2 ... 1] but the binary format uses [1 ... 2]. + exp2--; + constexpr int32_t minimum_exponent = binary::minimum_exponent(); + while ((minimum_exponent + 1) > exp2) { + uint32_t n = uint32_t((minimum_exponent + 1) - exp2); + if (n > max_shift) { + n = max_shift; + } + detail::decimal_right_shift(d, n); + exp2 += int32_t(n); + } + if ((exp2 - minimum_exponent) >= binary::infinite_power()) { + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + return answer; + } + + const int mantissa_size_in_bits = binary::mantissa_explicit_bits() + 1; + detail::decimal_left_shift(d, mantissa_size_in_bits); + + uint64_t mantissa = detail::round(d); + // It is possible that we have an overflow, in which case we need + // to shift back. + if(mantissa >= (uint64_t(1) << mantissa_size_in_bits)) { + detail::decimal_right_shift(d, 1); + exp2 += 1; + mantissa = detail::round(d); + if ((exp2 - minimum_exponent) >= binary::infinite_power()) { + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + return answer; + } + } + answer.power2 = exp2 - binary::minimum_exponent(); + if(mantissa < (uint64_t(1) << binary::mantissa_explicit_bits())) { answer.power2--; } + answer.mantissa = mantissa & ((uint64_t(1) << binary::mantissa_explicit_bits()) - 1); + return answer; +} + +template +adjusted_mantissa parse_long_mantissa(const char *first, const char* last, parse_options options) { + decimal d = parse_decimal(first, last, options); + return compute_float(d); +} + +} // namespace fast_float +#endif + + +#ifndef FASTFLOAT_PARSE_NUMBER_H +#define FASTFLOAT_PARSE_NUMBER_H + +#include +#include +#include +#include +#include + +namespace fast_float { + + +namespace detail { +/** + * Special case +inf, -inf, nan, infinity, -infinity. + * The case comparisons could be made much faster given that we know that the + * strings a null-free and fixed. + **/ +template +from_chars_result parse_infnan(const char *first, const char *last, T &value) noexcept { + from_chars_result answer; + answer.ptr = first; + answer.ec = std::errc(); // be optimistic + bool minusSign = false; + if (*first == '-') { // assume first < last, so dereference without checks; C++17 20.19.3.(7.1) explicitly forbids '+' here + minusSign = true; + ++first; + } + if (last - first >= 3) { + if (fastfloat_strncasecmp(first, "nan", 3)) { + answer.ptr = (first += 3); + value = minusSign ? -std::numeric_limits::quiet_NaN() : std::numeric_limits::quiet_NaN(); + // Check for possible nan(n-char-seq-opt), C++17 20.19.3.7, C11 7.20.1.3.3. At least MSVC produces nan(ind) and nan(snan). + if(first != last && *first == '(') { + for(const char* ptr = first + 1; ptr != last; ++ptr) { + if (*ptr == ')') { + answer.ptr = ptr + 1; // valid nan(n-char-seq-opt) + break; + } + else if(!(('a' <= *ptr && *ptr <= 'z') || ('A' <= *ptr && *ptr <= 'Z') || ('0' <= *ptr && *ptr <= '9') || *ptr == '_')) + break; // forbidden char, not nan(n-char-seq-opt) + } + } + return answer; + } + if (fastfloat_strncasecmp(first, "inf", 3)) { + if ((last - first >= 8) && fastfloat_strncasecmp(first + 3, "inity", 5)) { + answer.ptr = first + 8; + } else { + answer.ptr = first + 3; + } + value = minusSign ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); + return answer; + } + } + answer.ec = std::errc::invalid_argument; + return answer; +} + +template +fastfloat_really_inline void to_float(bool negative, adjusted_mantissa am, T &value) { + uint64_t word = am.mantissa; + word |= uint64_t(am.power2) << binary_format::mantissa_explicit_bits(); + word = negative + ? word | (uint64_t(1) << binary_format::sign_index()) : word; +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + if (std::is_same::value) { + ::memcpy(&value, (char *)&word + 4, sizeof(T)); // extract value at offset 4-7 if float on big-endian + } else { + ::memcpy(&value, &word, sizeof(T)); + } +#else + // For little-endian systems: + ::memcpy(&value, &word, sizeof(T)); +#endif +} + +} // namespace detail + + + +template +from_chars_result from_chars(const char *first, const char *last, + T &value, chars_format fmt /*= chars_format::general*/) noexcept { + return from_chars_advanced(first, last, value, parse_options{fmt}); +} + +template +from_chars_result from_chars_advanced(const char *first, const char *last, + T &value, parse_options options) noexcept { + + static_assert (std::is_same::value || std::is_same::value, "only float and double are supported"); + + + from_chars_result answer; + if (first == last) { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; + } + parsed_number_string pns = parse_number_string(first, last, options); + if (!pns.valid) { + return detail::parse_infnan(first, last, value); + } + answer.ec = std::errc(); // be optimistic + answer.ptr = pns.lastmatch; + // Next is Clinger's fast path. + if (binary_format::min_exponent_fast_path() <= pns.exponent && pns.exponent <= binary_format::max_exponent_fast_path() && pns.mantissa <=binary_format::max_mantissa_fast_path() && !pns.too_many_digits) { + value = T(pns.mantissa); + if (pns.exponent < 0) { value = value / binary_format::exact_power_of_ten(-pns.exponent); } + else { value = value * binary_format::exact_power_of_ten(pns.exponent); } + if (pns.negative) { value = -value; } + return answer; + } + adjusted_mantissa am = compute_float>(pns.exponent, pns.mantissa); + if(pns.too_many_digits) { + if(am != compute_float>(pns.exponent, pns.mantissa + 1)) { + am.power2 = -1; // value is invalid. + } + } + // If we called compute_float>(pns.exponent, pns.mantissa) and we have an invalid power (am.power2 < 0), + // then we need to go the long way around again. This is very uncommon. + if(am.power2 < 0) { am = parse_long_mantissa>(first, last, options); } + detail::to_float(pns.negative, am, value); + return answer; +} + +} // namespace fast_float + +#endif + diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index 4c4bebee44..61ab10f223 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -10,6 +9,7 @@ #include "LocalesUtils.hpp" #include +#include namespace Slic3r { @@ -71,16 +71,9 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G } if (axis != NUM_AXES_WITH_UNKNOWN) { // Try to parse the numeric value. -#ifdef WIN32 double v; - auto [pend, ec] = std::from_chars(++ c, end, v); + auto [pend, ec] = fast_float::from_chars(++ c, end, v); if (pend != c && is_end_of_word(*pend)) { -#else - // The older version of GCC and Clang support std::from_chars just for integers, so strtod we used it instead. - char *pend = nullptr; - double v = strtod(++ c, &pend); - if (pend != nullptr && is_end_of_word(*pend)) { -#endif // The axis value has been parsed correctly. if (axis != UNKNOWN_AXIS) gline.m_axis[int(axis)] = float(v); diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index 72f1421f68..2aae9270dc 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -122,7 +122,9 @@ void CopyrightsDialog::fill_entries() { "AppImage packaging for Linux using AppImageKit" , "2004-2019 Simon Peter and contributors" , "https://appimage.org/" }, { "lib_fts" - , "Forrest Smith" , "https://www.forrestthewoods.com/" } + , "Forrest Smith" , "https://www.forrestthewoods.com/" }, + { "fast_float" + , "Daniel Lemire, João Paulo Magalhaes and contributors", "https://github.com/fastfloat/fast_float" } }; } From d2a185ddb69d4a65910016933ed283cc7fcd6926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Thu, 9 Sep 2021 15:45:31 +0200 Subject: [PATCH 145/151] Optimized export of floating-point value inside emit_axis. Change the behavior of generated G-code commands. Now all redundancy padding zeros are removed, which makes G-code a little bit smaller. --- src/libslic3r/GCodeWriter.cpp | 52 +++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 793a666752..eb45e95c1f 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -7,6 +7,10 @@ #include #include +#ifdef __APPLE__ + #include +#endif + #define XYZF_EXPORT_DIGITS 3 #define E_EXPORT_DIGITS 5 @@ -273,17 +277,47 @@ public: } void emit_axis(const char axis, const double v, size_t digits) { - *ptr_err.ptr ++ = ' '; *ptr_err.ptr ++ = axis; -#ifdef WIN32 - this->ptr_err = std::to_chars(this->ptr_err.ptr, this->buf_end, v, std::chars_format::fixed, digits); + assert(digits <= 6); + static constexpr const std::array pow_10{1, 10, 100, 1000, 10000, 100000, 1000000}; + *ptr_err.ptr++ = ' '; *ptr_err.ptr++ = axis; + + char *base_ptr = this->ptr_err.ptr; + auto v_int = int64_t(std::round(v * pow_10[digits])); + // Older stdlib on macOS doesn't support std::from_chars at all, so it is used boost::spirit::karma::generate instead of it. + // That is a little bit slower than std::to_chars but not much. +#ifdef __APPLE__ + boost::spirit::karma::generate(this->ptr_err.ptr, boost::spirit::karma::int_generator(), v_int); #else - int buf_capacity = int(this->buf_end - this->ptr_err.ptr); - int ret = snprintf(this->ptr_err.ptr, buf_capacity, "%.*lf", int(digits), v); - if (ret <= 0 || ret > buf_capacity) - ptr_err.ec = std::errc::value_too_large; - else - this->ptr_err.ptr = this->ptr_err.ptr + ret; + // this->buf_end minus 1 because we need space for adding the extra decimal point. + this->ptr_err = std::to_chars(this->ptr_err.ptr, this->buf_end - 1, v_int); #endif + size_t writen_digits = (this->ptr_err.ptr - base_ptr) - (v_int < 0 ? 1 : 0); + if (writen_digits < digits) { + // Number is smaller than 10^digits, so that we will pad it with zeros. + size_t remaining_digits = digits - writen_digits; + // Move all newly inserted chars by remaining_digits to allocate space for padding with zeros. + for (char *from_ptr = this->ptr_err.ptr - 1, *to_ptr = from_ptr + remaining_digits; from_ptr >= this->ptr_err.ptr - writen_digits; --to_ptr, --from_ptr) + *to_ptr = *from_ptr; + + memset(this->ptr_err.ptr - writen_digits, '0', remaining_digits); + this->ptr_err.ptr += remaining_digits; + } + + // Move all newly inserted chars by one to allocate space for a decimal point. + for (char *to_ptr = this->ptr_err.ptr, *from_ptr = to_ptr - 1; from_ptr >= this->ptr_err.ptr - digits; --to_ptr, --from_ptr) + *to_ptr = *from_ptr; + + *(this->ptr_err.ptr - digits) = '.'; + for (size_t i = 0; i < digits; ++i) { + if (*this->ptr_err.ptr != '0') + break; + this->ptr_err.ptr--; + } + if (*this->ptr_err.ptr == '.') + this->ptr_err.ptr--; + if ((this->ptr_err.ptr + 1) == base_ptr || *this->ptr_err.ptr == '-') + *(++this->ptr_err.ptr) = '0'; + this->ptr_err.ptr++; } void emit_xy(const Vec2d &point) { From e520454c3e1988c894399f138c863495505dec3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Thu, 9 Sep 2021 12:01:31 +0200 Subject: [PATCH 146/151] Fixed unit tests after the previous commit. --- t/combineinfill.t | 10 +++++++++- tests/fff_print/test_gcodewriter.cpp | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/t/combineinfill.t b/t/combineinfill.t index 925e5b11d4..a19e817a14 100644 --- a/t/combineinfill.t +++ b/t/combineinfill.t @@ -36,7 +36,15 @@ plan tests => 8; $layer_infill{$self->Z} = 1; } } - $layers{$args->{Z}} = 1 if $cmd eq 'G1' && $info->{dist_Z} > 0; + # Previously, all G-code commands had a fixed number of decimal points with means with redundant zeros after decimal points. + # We changed this behavior and got rid of these redundant padding zeros, which caused this test to fail + # because the position in Z-axis is compared as a string, and previously, G-code contained the following two commands: + # "G1 Z5 F5000 ; lift nozzle" + # "G1 Z5.000 F7800.000" + # That has a different Z-axis position from the view of string comparisons of floating-point numbers. + # To correct the computation of the number of printed layers, even in the case of string comparisons of floating-point numbers, + # we filtered out the G-code command with the commend 'lift nozzle'. + $layers{$args->{Z}} = 1 if $cmd eq 'G1' && $info->{dist_Z} && index($info->{comment}, 'lift nozzle') == -1; }); my $layers_with_perimeters = scalar(keys %layer_infill); diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index a4c8aa09a7..15ffaebb48 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -78,13 +78,13 @@ SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") { } } WHEN("set_speed is called to set speed to 1") { - THEN("Output string is G1 F1.000") { - REQUIRE_THAT(writer.set_speed(1.0), Catch::Equals("G1 F1.000\n")); + THEN("Output string is G1 F1") { + REQUIRE_THAT(writer.set_speed(1.0), Catch::Equals("G1 F1\n")); } } WHEN("set_speed is called to set speed to 203.200022") { - THEN("Output string is G1 F203.200") { - REQUIRE_THAT(writer.set_speed(203.200022), Catch::Equals("G1 F203.200\n")); + THEN("Output string is G1 F203.2") { + REQUIRE_THAT(writer.set_speed(203.200022), Catch::Equals("G1 F203.2\n")); } } WHEN("set_speed is called to set speed to 203.200522") { From 4ac013ec9c82d376c762096d6bae31672ebdfc98 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Tue, 7 Sep 2021 15:15:22 +0200 Subject: [PATCH 147/151] Fixed painting gizmos with modifiers below the bed: - When a modifier was below the bed and all the object parts above, it would clip the modifier but not triangulate the cut. - When an object part was below, it would triangulate all modifiers with opaque orange color. Both should now be fixed. --- src/slic3r/GUI/3DScene.cpp | 11 ++++++++-- src/slic3r/GUI/3DScene.hpp | 5 +++++ src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp | 26 +++++++++++------------- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 2d49a8bf38..ba72c432d8 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -409,6 +409,11 @@ GLVolume::GLVolume(float r, float g, float b, float a) set_render_color(color); } +void GLVolume::set_color(const std::array& rgba) +{ + color = rgba; +} + void GLVolume::set_render_color(float r, float g, float b, float a) { render_color = { r, g, b, a }; @@ -458,8 +463,9 @@ void GLVolume::set_render_color() render_color[3] = color[3]; } -void GLVolume::set_color_from_model_volume(const ModelVolume& model_volume) +std::array color_from_model_volume(const ModelVolume& model_volume) { + std::array color; if (model_volume.is_negative_volume()) { color[0] = 0.2f; color[1] = 0.2f; @@ -481,6 +487,7 @@ void GLVolume::set_color_from_model_volume(const ModelVolume& model_volume) color[2] = 1.0f; } color[3] = model_volume.is_model_part() ? 1.f : 0.5f; + return color; } Transform3d GLVolume::world_matrix() const @@ -635,7 +642,7 @@ int GLVolumeCollection::load_object_volume( color[3] = model_volume->is_model_part() ? 1.f : 0.5f; this->volumes.emplace_back(new GLVolume(color)); GLVolume& v = *this->volumes.back(); - v.set_color_from_model_volume(*model_volume); + v.set_color(color_from_model_volume(*model_volume)); #if ENABLE_SMOOTH_NORMALS v.indexed_vertex_array.load_mesh(mesh, true); #else diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 35abe8bd4d..1a85cc41e1 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -39,6 +39,10 @@ class ModelObject; class ModelVolume; enum ModelInstanceEPrintVolumeState : unsigned char; +// Return appropriate color based on the ModelVolume. +std::array color_from_model_volume(const ModelVolume& model_volume); + + // A container for interleaved arrays of 3D vertices and normals, // possibly indexed by triangles and / or quads. class GLIndexedVertexArray { @@ -393,6 +397,7 @@ public: return out; } + void set_color(const std::array& rgba); void set_render_color(float r, float g, float b, float a); void set_render_color(const std::array& rgba); // Sets render color in dependence of current state diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d790e937c7..33399e38d9 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1845,7 +1845,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re volume->extruder_id = extruder_id; volume->is_modifier = !mvs->model_volume->is_model_part(); - volume->set_color_from_model_volume(*mvs->model_volume); + volume->set_color(color_from_model_volume(*mvs->model_volume)); // updates volumes transformations volume->set_instance_transformation(mvs->model_volume->get_object()->instances[mvs->composite_id.instance_id]->get_transformation()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index 18ce9d73c4..b22e72be9e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -152,7 +152,7 @@ void InstancesHider::on_update() canvas->toggle_sla_auxiliaries_visibility(m_show_supports, mo, active_inst); canvas->set_use_clipping_planes(true); // Some objects may be sinking, do not show whatever is below the bed. - canvas->set_clipping_plane(0, ClippingPlane(Vec3d::UnitZ(), 0.)); + canvas->set_clipping_plane(0, ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); canvas->set_clipping_plane(1, ClippingPlane(-Vec3d::UnitZ(), std::numeric_limits::max())); @@ -164,12 +164,7 @@ void InstancesHider::on_update() m_clippers.clear(); for (const TriangleMesh* mesh : meshes) { m_clippers.emplace_back(new MeshClipper); - if (mo->get_instance_min_z(active_inst) < SINKING_Z_THRESHOLD) - m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), 0.)); - else { - m_clippers.back()->set_plane(ClippingPlane::ClipsNothing()); - m_clippers.back()->set_limiting_plane(ClippingPlane::ClipsNothing()); - } + m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); m_clippers.back()->set_mesh(*mesh); } m_old_meshes = meshes; @@ -218,8 +213,16 @@ void InstancesHider::render_cut() const clipper->set_limiting_plane(ClippingPlane::ClipsNothing()); glsafe(::glPushMatrix()); - glsafe(::glColor3f(0.8f, 0.3f, 0.0f)); + if (mv->is_model_part()) + glsafe(::glColor3f(0.8f, 0.3f, 0.0f)); + else { + const std::array& c = color_from_model_volume(*mv); + glsafe(::glColor4f(c[0], c[1], c[2], c[3])); + } + glsafe(::glPushAttrib(GL_DEPTH_TEST)); + glsafe(::glDisable(GL_DEPTH_TEST)); clipper->render_cut(); + glsafe(::glPopAttrib()); glsafe(::glPopMatrix()); ++clipper_id; @@ -385,8 +388,6 @@ void ObjectClipper::on_update() m_active_inst_bb_radius = mo->instance_bounding_box(get_pool()->selection_info()->get_active_instance()).radius(); - //if (has_hollowed && m_clp_ratio != 0.) - // m_clp_ratio = 0.25; } } @@ -407,7 +408,6 @@ void ObjectClipper::render_cut() const const SelectionInfo* sel_info = get_pool()->selection_info(); const ModelObject* mo = sel_info->model_object(); Geometry::Transformation inst_trafo = mo->instances[sel_info->get_active_instance()]->get_transformation(); - const bool sinking = mo->bounding_box().min.z() < SINKING_Z_THRESHOLD; size_t clipper_id = 0; for (const ModelVolume* mv : mo->volumes) { @@ -418,9 +418,7 @@ void ObjectClipper::render_cut() const auto& clipper = m_clippers[clipper_id]; clipper->set_plane(*m_clp); clipper->set_transformation(trafo); - clipper->set_limiting_plane(sinking ? - ClippingPlane(Vec3d::UnitZ(), 0.) - : ClippingPlane::ClipsNothing()); + clipper->set_limiting_plane(ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); glsafe(::glPushMatrix()); glsafe(::glColor3f(1.0f, 0.37f, 0.0f)); clipper->render_cut(); From ae7d6db1d961f85b8e2788d707cef03de7e28fdb Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 13 Sep 2021 10:04:16 +0200 Subject: [PATCH 148/151] Parallelization of the G-code export. Follow-up to 03b60486840f32e32dc54103dc3051f94e79b35a beee18f22991e369b1722a43bbcb692fa0d68af0 b5a007a683124ec3b01b61e892ed8eb39c034248 etc --- src/libslic3r/GCode.cpp | 132 ++++++++++++++++++++++++++++++++-------- src/libslic3r/GCode.hpp | 31 +++++++++- 2 files changed, 134 insertions(+), 29 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 2347225ab7..cebc9136f7 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -35,6 +35,7 @@ #include "SVG.hpp" #include +#include #include @@ -1336,15 +1337,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Reset the cooling buffer internal state (the current position, feed rate, accelerations). m_cooling_buffer->reset(this->writer().get_position()); m_cooling_buffer->set_current_extruder(initial_extruder_id); - // Pair the object layers with the support layers by z, extrude them. - std::vector layers_to_print = collect_layers_to_print(object); - for (const LayerToPrint <p : layers_to_print) { - std::vector lrs; - lrs.emplace_back(std::move(ltp)); - this->process_layer(file, print, lrs, tool_ordering.tools_for_layer(ltp.print_z()), <p == &layers_to_print.back(), - nullptr, *print_object_instance_sequential_active - object.instances().data()); - print.throw_if_canceled(); - } + // Process all layers of a single object instance (sequential mode) with a parallel pipeline: + // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser + // and export G-code into file. + this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file); #ifdef HAS_PRESSURE_EQUALIZER if (m_pressure_equalizer) file.write(m_pressure_equalizer->process("", true)); @@ -1401,14 +1397,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); } - // Extrude the layers. - for (auto &layer : layers_to_print) { - const LayerTools &layer_tools = tool_ordering.tools_for_layer(layer.first); - if (m_wipe_tower && layer_tools.has_wipe_tower) - m_wipe_tower->next_layer(); - this->process_layer(file, print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1)); - print.throw_if_canceled(); - } + // Process all layers of all objects (non-sequential mode) with a parallel pipeline: + // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser + // and export G-code into file. + this->process_layers(print, tool_ordering, print_object_instances_ordering, layers_to_print, file); #ifdef HAS_PRESSURE_EQUALIZER if (m_pressure_equalizer) file.write(m_pressure_equalizer->process("", true)); @@ -1481,6 +1473,88 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato print.throw_if_canceled(); } +// Process all layers of all objects (non-sequential mode) with a parallel pipeline: +// Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser +// and export G-code into file. +void GCode::process_layers( + const Print &print, + const ToolOrdering &tool_ordering, + const std::vector &print_object_instances_ordering, + const std::vector>> &layers_to_print, + GCodeOutputStream &output_stream) +{ + // The pipeline is variable: The vase mode filter is optional. + size_t layer_to_print_idx = 0; + const auto generator = tbb::make_filter(tbb::filter::serial_in_order, + [this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &layer_to_print_idx](tbb::flow_control& fc) -> GCode::LayerResult { + if (layer_to_print_idx == layers_to_print.size()) { + fc.stop(); + return {}; + } else { + const std::pair>& layer = layers_to_print[layer_to_print_idx++]; + const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first); + if (m_wipe_tower && layer_tools.has_wipe_tower) + m_wipe_tower->next_layer(); + print.throw_if_canceled(); + return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1)); + } + }); + const auto spiral_vase = tbb::make_filter(tbb::filter::serial_in_order, + [&spiral_vase = *this->m_spiral_vase.get()](GCode::LayerResult in) -> GCode::LayerResult { + spiral_vase.enable(in.spiral_vase_enable); + return { spiral_vase.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; + }); + const auto cooling = tbb::make_filter(tbb::filter::serial_in_order, + [&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in) -> std::string { + return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush); + }); + const auto output = tbb::make_filter(tbb::filter::serial_in_order, + [&output_stream](std::string s) { output_stream.write(s); } + ); + + // The pipeline elements are joined using const references, thus no copying is performed. + if (m_spiral_vase) + tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output); + else + tbb::parallel_pipeline(12, generator & cooling & output); +} + +// Process all layers of a single object instance (sequential mode) with a parallel pipeline: +// Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser +// and export G-code into file. +void GCode::process_layers( + const Print &print, + const ToolOrdering &tool_ordering, + std::vector layers_to_print, + const size_t single_object_idx, + GCodeOutputStream &output_stream) +{ + // The pipeline is fixed: Neither wipe tower nor vase mode are implemented for sequential print. + size_t layer_to_print_idx = 0; + tbb::parallel_pipeline(12, + tbb::make_filter( + tbb::filter::serial_in_order, + [this, &print, &tool_ordering, &layers_to_print, &layer_to_print_idx, single_object_idx](tbb::flow_control& fc) -> GCode::LayerResult { + if (layer_to_print_idx == layers_to_print.size()) { + fc.stop(); + return {}; + } else { + LayerToPrint &layer = layers_to_print[layer_to_print_idx ++]; + print.throw_if_canceled(); + return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, single_object_idx); + } + }) & + tbb::make_filter( + tbb::filter::serial_in_order, + [&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in) -> std::string { + return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush); + }) & + tbb::make_filter( + tbb::filter::serial_in_order, + [&output_stream](std::string s) { output_stream.write(s); } + )); +} + std::string GCode::placeholder_parser_process(const std::string &name, const std::string &templ, unsigned int current_extruder_id, const DynamicConfig *config_override) { try { @@ -1890,9 +1964,7 @@ namespace Skirt { // In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated. // For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths // and performing the extruder specific extrusions together. -void GCode::process_layer( - // Write into the output file. - GCodeOutputStream &file, +GCode::LayerResult GCode::process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. const std::vector &layers, @@ -1908,11 +1980,6 @@ void 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); - if (layer_tools.extruders.empty()) - // Nothing to extrude. - return; - - // Extract 1st object_layer and support_layer of this set of layers with an equal print_z. const Layer *object_layer = nullptr; const SupportLayer *support_layer = nullptr; for (const LayerToPrint &l : layers) { @@ -1922,6 +1989,12 @@ void GCode::process_layer( support_layer = l.support_layer; } const Layer &layer = (object_layer != nullptr) ? *object_layer : *support_layer; + GCode::LayerResult result { {}, layer.id(), false, last_layer }; + if (layer_tools.extruders.empty()) + // Nothing to extrude. + return result; + + // Extract 1st object_layer and support_layer of this set of layers with an equal print_z. coordf_t print_z = layer.print_z; bool first_layer = layer.id() == 0; unsigned int first_extruder_id = layer_tools.extruders.front(); @@ -1943,7 +2016,7 @@ void GCode::process_layer( break; } } - m_spiral_vase->enable(enable); + result.spiral_vase_enable = enable; // If we're going to apply spiralvase to this layer, disable loop clipping. m_enable_loop_clipping = !enable; } @@ -2285,6 +2358,7 @@ void GCode::process_layer( } } +#if 0 // Apply spiral vase post-processing if this layer contains suitable geometry // (we must feed all the G-code into the post-processor, including the first // bottom non-spiral layers otherwise it will mess with positions) @@ -2308,8 +2382,14 @@ void GCode::process_layer( #endif /* HAS_PRESSURE_EQUALIZER */ file.write(gcode); +#endif + BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z << log_memory_info(); + + result.gcode = std::move(gcode); + result.cooling_buffer_flush = object_layer || last_layer; + return result; } void GCode::apply_print_config(const PrintConfig &print_config) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 1dbc5b70a0..93a11821e4 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -215,9 +215,16 @@ private: static std::vector collect_layers_to_print(const PrintObject &object); static std::vector>> collect_layers_to_print(const Print &print); - void process_layer( - // Write into the output file. - GCodeOutputStream &file, + + struct LayerResult { + std::string gcode; + size_t layer_id; + // Is spiral vase post processing enabled for this layer? + bool spiral_vase_enable { false }; + // Should the cooling buffer content be flushed at the end of this layer? + bool cooling_buffer_flush { false }; + }; + LayerResult process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. const std::vector &layers, @@ -228,6 +235,24 @@ private: // If set to size_t(-1), then print all copies of all objects. // Otherwise print a single copy of a single object. const size_t single_object_idx = size_t(-1)); + // Process all layers of all objects (non-sequential mode) with a parallel pipeline: + // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser + // and export G-code into file. + void process_layers( + const Print &print, + const ToolOrdering &tool_ordering, + const std::vector &print_object_instances_ordering, + const std::vector>> &layers_to_print, + GCodeOutputStream &output_stream); + // Process all layers of a single object instance (sequential mode) with a parallel pipeline: + // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser + // and export G-code into file. + void process_layers( + const Print &print, + const ToolOrdering &tool_ordering, + std::vector layers_to_print, + const size_t single_object_idx, + GCodeOutputStream &output_stream); void set_last_pos(const Point &pos) { m_last_pos = pos; m_last_pos_defined = true; } bool last_pos_defined() const { return m_last_pos_defined; } From 5a95794913a053611c5887ee7460d2da8ed75ac5 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Mon, 13 Sep 2021 10:29:41 +0200 Subject: [PATCH 149/151] OSX specific: Improvements for wxMultiChoiceDialog: Height of a ChoiceListBox will respect to items count This improvement fixed #6926 - Checkbox columns in modal windows are stretched (macOS) --- src/slic3r/GUI/GUI_Factories.cpp | 41 ++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 92ba427b5b..4fec12d148 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -16,6 +16,10 @@ #include #include "slic3r/Utils/FixModelByWin10.hpp" +#ifdef __APPLE__ +#include "wx/dcclient.h" +#include "slic3r/Utils/MacDarkMode.hpp" +#endif namespace Slic3r { @@ -211,7 +215,6 @@ static int GetSelectedChoices( wxArrayInt& selections, const wxString& caption, const wxArrayString& choices) { -#ifdef _WIN32 wxMultiChoiceDialog dialog(nullptr, message, caption, choices); wxGetApp().UpdateDlgDarkUI(&dialog); @@ -219,6 +222,39 @@ static int GetSelectedChoices( wxArrayInt& selections, // deselects the first item which is selected by default dialog.SetSelections(selections); +#ifdef __APPLE__ + // Improvements for ChoiceListBox: Height of control will restect to items count + for (auto child : dialog.GetChildren()) + if (dynamic_cast(child) && !choices.IsEmpty()) { + wxClientDC dc(child); + + int height = dc.GetTextExtent(choices[0]).y; + int width = 0; + for (const auto& string : choices) + width = std::max(width, dc.GetTextExtent(string).x); + + // calculate best size of ListBox + height += 3 * mac_max_scaling_factor(); // extend height by margins + width += 3 * height; // extend width by checkbox width and margins + + // don't make the listbox too tall (limit height to around 10 items) + // but don't make it too small neither + int list_height = wxMax(height * wxMin(wxMax(choices.Count(), 3), 10), 70); + wxSize sz_best = wxSize(width, list_height); + + wxSize sz = child->GetSize(); + child->SetMinSize(sz_best); + + // extend Dialog size, if calculated best size of ListBox is bigger then its size + wxSize dlg_sz = dialog.GetSize(); + if (int delta_x = sz_best.x - sz.x; delta_x > 0) dlg_sz.x += delta_x; + if (int delta_y = sz_best.y - sz.y; delta_y > 0) dlg_sz.y += delta_y; + dialog.SetSize(dlg_sz); + + break; + } +#endif + if (dialog.ShowModal() != wxID_OK) { // NB: intentionally do not clear the selections array here, the caller @@ -229,9 +265,6 @@ static int GetSelectedChoices( wxArrayInt& selections, selections = dialog.GetSelections(); return static_cast(selections.GetCount()); -#else - return wxGetSelectedChoices(selections, message, caption, choices); -#endif } static wxMenu* create_settings_popupmenu(wxMenu* parent_menu, const bool is_object_settings, wxDataViewItem item/*, ModelConfig& config*/) From 880feb3a3d7079fd1f7c6422e70094876084811d Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 13 Sep 2021 10:41:21 +0200 Subject: [PATCH 150/151] GCodeViewer slight opitmization: reuse (move) G-code line indices in G-code viewer. --- src/slic3r/GUI/GCodeViewer.cpp | 6 ++++-- src/slic3r/GUI/GCodeViewer.hpp | 2 +- src/slic3r/GUI/GLCanvas3D.cpp | 5 +---- src/slic3r/GUI/GLCanvas3D.hpp | 3 +-- src/slic3r/GUI/GUI_Preview.cpp | 3 +-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1b2375cf5e..c9e4bef7b7 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -284,7 +284,7 @@ void GCodeViewer::SequentialView::Marker::render() const ImGui::PopStyleVar(); } -void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& filename, const std::vector &lines_ends) +void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& filename, std::vector &&lines_ends) { assert(! m_file.is_open()); if (m_file.is_open()) @@ -577,7 +577,9 @@ void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& // release gpu memory, if used reset(); - m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends); + m_sequential_view.gcode_window.load_gcode(gcode_result.filename, + // Stealing out lines_ends should be safe because this gcode_result is processed only once (see the 1st if in this function). + std::move(const_cast&>(gcode_result.lines_ends))); #if ENABLE_FIX_IMPORTING_COLOR_PRINT_VIEW_INTO_GCODEVIEWER if (wxGetApp().is_gcode_viewer()) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 3509bfbe51..f566c80bd5 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -702,7 +702,7 @@ public: public: GCodeWindow() = default; ~GCodeWindow() { stop_mapping_file(); } - void load_gcode(const std::string& filename, const std::vector &lines_ends); + void load_gcode(const std::string& filename, std::vector &&lines_ends); void reset() { stop_mapping_file(); m_lines_ends.clear(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 33399e38d9..551760ee93 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2092,7 +2092,7 @@ static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& vol_old.finalize_geometry(gl_initialized); } -void GLCanvas3D::load_gcode_preview(const GCodeProcessor::Result& gcode_result) +void GLCanvas3D::load_gcode_preview(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors) { m_gcode_viewer.load(gcode_result, *this->fff_print(), m_initialized); @@ -2100,10 +2100,7 @@ void GLCanvas3D::load_gcode_preview(const GCodeProcessor::Result& gcode_result) m_gcode_viewer.update_shells_color_by_extruder(m_config); _set_warning_notification_if_needed(EWarning::ToolpathOutside); } -} -void GLCanvas3D::refresh_gcode_preview(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors) -{ m_gcode_viewer.refresh(gcode_result, str_tool_colors); set_as_dirty(); request_extra_frame(); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index eb0220cd47..704adb010c 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -722,8 +722,7 @@ public: void reload_scene(bool refresh_immediately, bool force_full_scene_refresh = false); - void load_gcode_preview(const GCodeProcessor::Result& gcode_result); - void refresh_gcode_preview(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors); + void load_gcode_preview(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors); void refresh_gcode_preview_render_paths(); void set_gcode_view_preview_type(GCodeViewer::EViewType type) { return m_gcode_viewer.set_view_type(type); } GCodeViewer::EViewType get_gcode_view_preview_type() const { return m_gcode_viewer.get_view_type(); } diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index eb511177df..f8a10bb8e9 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -943,8 +943,7 @@ void Preview::load_print_as_fff(bool keep_z_range) m_canvas->set_selected_extruder(0); if (gcode_preview_data_valid) { // Load the real G-code preview. - m_canvas->load_gcode_preview(*m_gcode_result); - m_canvas->refresh_gcode_preview(*m_gcode_result, colors); + m_canvas->load_gcode_preview(*m_gcode_result, colors); m_left_sizer->Show(m_bottom_toolbar_panel); m_left_sizer->Layout(); Refresh(); From fda8ef6fce84b1371c1737f8adb82ed937f45c39 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 13 Sep 2021 11:16:13 +0200 Subject: [PATCH 151/151] #5471 - Make the View controls 'sticky' --- src/slic3r/GUI/GCodeViewer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index c9e4bef7b7..9b7d8ec47c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -563,6 +563,8 @@ GCodeViewer::GCodeViewer() set_toolpath_move_type_visible(EMoveType::Extrude, true); #endif // !ENABLE_SEAMS_USING_MODELS + m_extrusions.reset_role_visibility_flags(); + // m_sequential_view.skip_invisible_moves = true; } @@ -736,7 +738,6 @@ void GCodeViewer::reset() m_extruder_ids = std::vector(); m_filament_diameters = std::vector(); m_filament_densities = std::vector(); - m_extrusions.reset_role_visibility_flags(); m_extrusions.reset_ranges(); m_shells.volumes.clear(); m_layers.reset();