diff --git a/docs/HLSD/gcode-preview-dragging.md b/docs/HLSD/gcode-preview-dragging.md new file mode 100644 index 0000000000..0567e52082 --- /dev/null +++ b/docs/HLSD/gcode-preview-dragging.md @@ -0,0 +1,62 @@ +# G-code preview while dragging + +The sliced preview draws every toolpath segment of the plate as an instanced box. On a large +plate that is tens of millions of segments, and the frame is GPU-bound: the cost is the number of +instances drawn, not anything the CPU does per frame. Dragging the camera over such a plate cannot +keep up. With the `preview_solid_model_while_dragging` preference (*Graphics > G-code Preview*, +off by default), the preview draws the sliced objects as solid meshes while the user drags and +puts the toolpaths back when they let go. A mesh costs its triangles once, however many layers it +has. + +`GCodeViewer` draws the solid model, libvgcode (`src/libvgcode`) keeps the toolpaths that cap it, +and `GLCanvas3D` decides when the user is dragging. The OpenGL ES path ignores the preference. + +## The solid model + +The preview already loads the sliced objects as shells for its translucent ghost. +`GCodeViewer::render_solid_model()` draws those shells opaque, in their filament colours, with the +`gouraud` shader, whose z range cuts them to the visible layer range. The shells hold only the +objects, so while the preference is on the prime tower is added from its sliced mesh, positioned +as the print placed it. It is added or removed on its own when the preference changes, without +reloading the objects, keeps its opaque colour so that it never appears among the translucent +shells, and stays out of their bounding box. Supports have no mesh and are not shown. + +A plate whose shells are not loaded keeps drawing toolpaths, since the solid model would leave +only the end layers. + +## End-layer set + +The cut faces of the solid model are capped with what was really printed there: the toolpaths of +the bottom and top layers of the visible range. While the preference is on, +`ViewerImpl::update_enabled_entities()` fills a second, **reduced** index buffer holding just those +two layers, in the same walk that fills the full one. Building both together is what makes +switching free: starting or ending a drag is a buffer binding, never a rebuild. + +## Deciding that the user is dragging + +`GLCanvas3D::_update_preview_interaction()` runs at the top of every preview frame, before the +canvas decides whether to reuse its cached scene, so that the switch lands in that frame. Dragging +is the camera, the navigator or either slider being held; a slider reports this from ImGui's active +id rather than its dirty flag, which is raised and consumed inside one frame. A wheel step has no +duration, so it holds the solid model for a 150 ms settle time instead, and the frame that restores +the toolpaths is scheduled for when that time runs out, since the render timer only wakes the idle +loop. A drag cut short by focus or capture loss is ended explicitly, and a button release wakes the +idle loop, because on some platforms nothing else would until the next input. + +## Reused scene frames + +`GLCanvas3D` keeps its last scene pass for frames that only rebuild the overlay (`SceneCache`). Its +key covers the canvas size, the camera and hover state, not what the toolpath sets draw, so a frame +that reuses the scene must never be one on which the solid model is switched. +`_update_preview_interaction()` therefore reports whether the bound set changed, and a frame on +which it did redraws the scene. The canvas neither captures nor reuses the scene while the user +drags, so no solid-model frame outlives a drag, and the frame that ends a wheel's settle time is +requested as a full frame. + +## Per-frame lookups + +The segment template draws its box from 8 corners through an index buffer, so the vertex shader +runs at most once per corner. `get_estimated_time_at()`, which the tool marker tooltip calls every +frame, starts from the running time at the first vertex of the vertex's layer, kept per layer at +load, and adds only that layer's vertices. The sum runs in vertex order, so it matches a full +accumulation exactly while costing memory per layer rather than per vertex. diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index c8a2c4984d..37284194a5 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -205,6 +205,10 @@ void AppConfig::set_defaults() if (get("seq_top_layer_only").empty()) set("seq_top_layer_only", "1"); + // draw the sliced objects instead of their toolpaths while the user drags the preview + if (get("preview_solid_model_while_dragging").empty()) + set_bool("preview_solid_model_while_dragging", false); + // ORCA: darken the layers the preview layer slider is not scrubbed to if (get("preview_dim_previous_layers").empty()) set_bool("preview_dim_previous_layers", false); diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index 5141245d96..883db9a1bf 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -98,6 +98,15 @@ public: // bool is_dim_previous_layers() const; void set_dim_previous_layers(bool value); + // + // The reduced set holds only the bottom and top layers of the visible range, for a caller that + // draws the print itself some other way while the user drags. While enabled it is built + // alongside the full set, so set_reduced_detail() rebuilds nothing. Ignored on the OpenGL ES path. + // + void set_reduced_detail_enabled(bool value); + bool is_reduced_detail_enabled() const; + void set_reduced_detail(bool value); + bool is_reduced_detail() const; float get_dim_previous_layers_brightness() const; void set_dim_previous_layers_brightness(float value); // diff --git a/src/libvgcode/src/Settings.hpp b/src/libvgcode/src/Settings.hpp index b3aa371c4a..93b7aa02dc 100644 --- a/src/libvgcode/src/Settings.hpp +++ b/src/libvgcode/src/Settings.hpp @@ -25,6 +25,10 @@ struct Settings // ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black float dim_previous_layers_brightness{ 0.4f }; bool spiral_vase_mode{ false }; + // whether the reduced set (the visible range's end layers) is built, and whether it is drawn. + // Ignored on the OpenGL ES path. + bool reduced_detail_enabled{ false }; + bool reduced_detail{ false }; // // Required update flags // diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index eb606598e9..f95703bdfe 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -77,6 +77,26 @@ bool Viewer::is_dim_previous_layers() const return m_impl->is_dim_previous_layers(); } +void Viewer::set_reduced_detail(bool value) +{ + m_impl->set_reduced_detail(value); +} + +bool Viewer::is_reduced_detail() const +{ + return m_impl->is_reduced_detail(); +} + +void Viewer::set_reduced_detail_enabled(bool value) +{ + m_impl->set_reduced_detail_enabled(value); +} + +bool Viewer::is_reduced_detail_enabled() const +{ + return m_impl->is_reduced_detail_enabled(); +} + void Viewer::set_dim_previous_layers(bool value) { m_impl->set_dim_previous_layers(value); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index 77fca69fc5..41ef90531b 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -877,7 +877,7 @@ void ViewerImpl::reset() m_vertices_colors.clear(); // swap rather than clear: these are sized by the print, and a reset means the memory // should go back, not sit reserved until the next load - for (std::vector& times : m_cumulative_times) + for (std::vector& times : m_layer_start_times) std::vector().swap(times); std::vector().swap(m_layer_first_vertex); std::vector().swap(m_colors_scratch); @@ -891,9 +891,15 @@ void ViewerImpl::reset() #else m_enabled_segments_count = 0; m_enabled_options_count = 0; + m_enabled_segments_reduced_count = 0; + m_enabled_options_reduced_count = 0; m_settings_used_for_ranges = std::nullopt; + delete_textures(m_enabled_options_reduced_tex_id); + delete_buffers(m_enabled_options_reduced_buf_id); + delete_textures(m_enabled_segments_reduced_tex_id); + delete_buffers(m_enabled_segments_reduced_buf_id); delete_textures(m_enabled_options_tex_id); delete_buffers(m_enabled_options_buf_id); delete_textures(m_enabled_segments_tex_id); @@ -1002,8 +1008,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) m_tool_colors = std::move(gcode_data.tools_colors); m_color_print_colors = std::move(gcode_data.color_print_colors); m_vertices_colors.resize(m_vertices.size()); - for (std::vector& times : m_cumulative_times) - times.resize(m_vertices.size()); m_settings.spiral_vase_mode = gcode_data.spiral_vase_mode; @@ -1014,9 +1018,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) for (size_t j = 0; j < TIME_MODES_COUNT; ++j) { m_total_time[j] += v.times[j]; - // the running total up to and including this vertex is exactly what - // get_estimated_time_at() has to return for it - m_cumulative_times[j][i] = m_total_time[j]; if (v.type == EMoveType::Travel) m_travels_time[j] += v.times[j]; } @@ -1071,6 +1072,23 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) } for (size_t i = m_layer_first_vertex.size() - 1; i > 0; --i) m_layer_first_vertex[i - 1] = std::min(m_layer_first_vertex[i - 1], m_layer_first_vertex[i]); + + // the running time at each layer's first vertex, summed in vertex order so that + // get_estimated_time_at() matches a full accumulation exactly + std::array running{}; + for (std::vector& times : m_layer_start_times) + times.assign(m_layer_first_vertex.size(), 0.0f); + size_t layer = 0; + for (size_t i = 0; i <= m_vertices.size(); ++i) { + for (; layer < m_layer_first_vertex.size() && m_layer_first_vertex[layer] == i; ++layer) { + for (size_t j = 0; j < TIME_MODES_COUNT; ++j) + m_layer_start_times[j][layer] = running[j]; + } + if (i < m_vertices.size()) { + for (size_t j = 0; j < TIME_MODES_COUNT; ++j) + running[j] += m_vertices[i].times[j]; + } + } } if (!m_layers.empty()) @@ -1141,6 +1159,17 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) glsafe(glGenTextures(1, &m_enabled_options_tex_id)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id)); + // create (but do not fill) the reduced counterparts of the two buffers above + glsafe(glGenBuffers(1, &m_enabled_segments_reduced_buf_id)); + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id)); + glsafe(glGenTextures(1, &m_enabled_segments_reduced_tex_id)); + glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_tex_id)); + + glsafe(glGenBuffers(1, &m_enabled_options_reduced_buf_id)); + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id)); + glsafe(glGenTextures(1, &m_enabled_options_reduced_tex_id)); + glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_reduced_tex_id)); + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture)); #endif // ENABLE_OPENGL_ES @@ -1159,6 +1188,14 @@ void ViewerImpl::update_enabled_entities() std::vector enabled_segments; std::vector enabled_options; +#ifndef ENABLE_OPENGL_ES + // the reduced set is filled by the same walk, so switching to it costs no rebuild. It keeps the + // bottom and top layers of the visible range: the surfaces the range cuts open + const bool build_reduced = m_settings.reduced_detail_enabled; + std::vector enabled_segments_reduced; + std::vector enabled_options_reduced; + const Interval& layers_range = m_layers.get_view_range(); +#endif // ENABLE_OPENGL_ES Interval range = m_view_range.get_visible(); // when top layer only visualization is enabled, we need to render @@ -1206,6 +1243,11 @@ void ViewerImpl::update_enabled_entities() enabled_options.push_back(static_cast(i)); else enabled_segments.push_back(static_cast(i)); + +#ifndef ENABLE_OPENGL_ES + if (build_reduced && (v.layer_id == layers_range[0] || v.layer_id == layers_range[1])) + (v.is_option() ? enabled_options_reduced : enabled_segments_reduced).push_back(static_cast(i)); +#endif // ENABLE_OPENGL_ES } #ifdef ENABLE_OPENGL_ES @@ -1234,6 +1276,21 @@ void ViewerImpl::update_enabled_entities() else glsafe(glBufferData(GL_TEXTURE_BUFFER, 0, nullptr, GL_STATIC_DRAW)); + m_enabled_segments_reduced_count = enabled_segments_reduced.size(); + m_enabled_options_reduced_count = enabled_options_reduced.size(); + + if (build_reduced) { + assert(m_enabled_segments_reduced_buf_id > 0); + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id)); + glsafe(glBufferData(GL_TEXTURE_BUFFER, enabled_segments_reduced.size() * sizeof(uint32_t), + enabled_segments_reduced.empty() ? nullptr : enabled_segments_reduced.data(), GL_STATIC_DRAW)); + + assert(m_enabled_options_reduced_buf_id > 0); + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id)); + glsafe(glBufferData(GL_TEXTURE_BUFFER, enabled_options_reduced.size() * sizeof(uint32_t), + enabled_options_reduced.empty() ? nullptr : enabled_options_reduced.data(), GL_STATIC_DRAW)); + } + glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0)); #endif // ENABLE_OPENGL_ES @@ -1412,6 +1469,14 @@ void ViewerImpl::toggle_top_layer_only_view_range() update_colors_texture(); } +void ViewerImpl::set_reduced_detail_enabled(bool value) +{ + if (m_settings.reduced_detail_enabled == value) + return; + m_settings.reduced_detail_enabled = value; + m_settings.update_enabled_entities = true; +} + // ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to void ViewerImpl::set_dim_previous_layers(bool value) { @@ -1545,9 +1610,18 @@ void ViewerImpl::set_view_visible_range(Interval::value_type min, Interval::valu float ViewerImpl::get_estimated_time_at(size_t id) const { const size_t mode = static_cast(m_settings.time_mode); - if (mode >= TIME_MODES_COUNT || id >= m_cumulative_times[mode].size()) + if (mode >= TIME_MODES_COUNT || id >= m_vertices.size()) return 0.0f; - return m_cumulative_times[mode][id]; + size_t first = 0; + float time = 0.0f; + const size_t layer = static_cast(m_vertices[id].layer_id); + if (layer < m_layer_first_vertex.size() && m_layer_first_vertex[layer] <= id) { + first = m_layer_first_vertex[layer]; + time = m_layer_start_times[mode][layer]; + } + for (size_t i = first; i <= id; ++i) + time += m_vertices[i].times[mode]; + return time; } Color ViewerImpl::get_vertex_color(const PathVertex& v) const @@ -1752,7 +1826,7 @@ size_t ViewerImpl::get_used_cpu_memory() const ret += sizeof(m_extrusion_roles_colors); ret += sizeof(m_options_colors); ret += STDVEC_MEMSIZE(m_vertices, PathVertex); - for (const std::vector& times : m_cumulative_times) + for (const std::vector& times : m_layer_start_times) ret += STDVEC_MEMSIZE(times, float); ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t); ret += STDVEC_MEMSIZE(m_colors_scratch, float); @@ -1821,10 +1895,8 @@ void ViewerImpl::update_view_full_range() const bool travels_visible = m_settings.options_visibility[size_t(EOptionType::Travels)]; const bool wipes_visible = m_settings.options_visibility[size_t(EOptionType::Wipes)]; - // Every vertex before m_layer_first_vertex[layers_range[0]] has a smaller layer_id, so the - // loop below would skip all of them on its first condition alone. Starting there turns a scan - // from vertex 0 on every slider tick into a scan of the visible part only; what the loop - // settles on is unchanged. + // every vertex before m_layer_first_vertex[layers_range[0]] has a smaller layer_id, so the loop + // below would skip all of them anyway auto first_it = m_vertices.begin(); if (layers_range[0] < m_layer_first_vertex.size()) first_it += m_layer_first_vertex[layers_range[0]]; @@ -2014,7 +2086,8 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec #ifdef ENABLE_OPENGL_ES if (m_texture_data.get_enabled_segments_count() == 0) #else - if (m_enabled_segments_count == 0) + const ActiveSet segments = active_segments(); + if (segments.count == 0) #endif // ENABLE_OPENGL_ES return; @@ -2073,10 +2146,10 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id)); glsafe(glActiveTexture(GL_TEXTURE3)); - glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id)); - glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id)); + glsafe(glBindTexture(GL_TEXTURE_BUFFER, segments.tex_id)); + glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, segments.buf_id)); - m_segment_template.render(m_enabled_segments_count); + m_segment_template.render(segments.count); #endif // ENABLE_OPENGL_ES if (curr_cull_face) @@ -2102,7 +2175,8 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project #ifdef ENABLE_OPENGL_ES if (m_texture_data.get_enabled_options_count() == 0) #else - if (m_enabled_options_count == 0) + const ActiveSet options = active_options(); + if (options.count == 0) #endif // ENABLE_OPENGL_ES return; @@ -2160,10 +2234,10 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id)); glsafe(glActiveTexture(GL_TEXTURE3)); - glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id)); - glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id)); + glsafe(glBindTexture(GL_TEXTURE_BUFFER, options.tex_id)); + glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, options.buf_id)); - m_option_template.render(m_enabled_options_count); + m_option_template.render(options.count); #endif // ENABLE_OPENGL_ES if (!curr_cull_face) diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index e52317643d..cfc86e255a 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -91,6 +91,19 @@ public: // 0.0 = black bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; } void set_dim_previous_layers(bool value); + // + // Draw from the reduced set; it is already built, so this is just a buffer binding. + // + void set_reduced_detail(bool value) { +#ifdef ENABLE_OPENGL_ES + // no reduced set is built on OpenGL ES + value = false; +#endif // ENABLE_OPENGL_ES + m_settings.reduced_detail = value; + } + bool is_reduced_detail() const { return m_settings.reduced_detail; } + bool is_reduced_detail_enabled() const { return m_settings.reduced_detail_enabled; } + void set_reduced_detail_enabled(bool value); float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; } void set_dim_previous_layers_brightness(float value); @@ -234,24 +247,17 @@ private: // std::array m_total_time{ 0.0f, 0.0f }; // - // Running sum of the vertex estimated times, one entry per vertex for each time mode. - // get_estimated_time_at() answers from this instead of re-accumulating the whole vertex - // array, which it was doing once per frame from the tool marker tooltip. The sums are - // built by the same left-to-right addition the accumulate performed, so the value handed - // back is bit-identical, float rounding included. + // Running sum of the vertex estimated times at each layer's first vertex, for each time mode, + // so that get_estimated_time_at() only accumulates the vertices of one layer. // - std::array, TIME_MODES_COUNT> m_cumulative_times; + std::array, TIME_MODES_COUNT> m_layer_start_times; // // For each layer L, the index of the first vertex whose layer_id is >= L (m_vertices.size() - // if there is none). Every vertex before it is guaranteed to belong to an earlier layer, so - // the scan in update_view_full_range() can start there rather than at vertex 0. Derived from - // the vertices themselves rather than from Layers, which buckets an out-of-order vertex into - // the layer that happens to be open, so this stays exact whatever order the vertices arrive in. + // if there is none). Derived from the vertices, so it stays exact whatever order they arrive in. // std::vector m_layer_first_vertex; // - // Scratch buffer for update_colors_texture(), kept alive so that a slider drag does not - // allocate and free one float per vertex of the print on every step. + // Scratch buffer for update_colors_texture(), kept alive across slider steps // std::vector m_colors_scratch; // @@ -481,6 +487,15 @@ private: unsigned int m_enabled_options_tex_id{ 0 }; size_t m_enabled_options_count{ 0 }; // + // OpenGL buffers to store the reduced set drawn while Settings::reduced_detail is set + // + unsigned int m_enabled_segments_reduced_buf_id{ 0 }; + unsigned int m_enabled_segments_reduced_tex_id{ 0 }; + size_t m_enabled_segments_reduced_count{ 0 }; + unsigned int m_enabled_options_reduced_buf_id{ 0 }; + unsigned int m_enabled_options_reduced_tex_id{ 0 }; + size_t m_enabled_options_reduced_count{ 0 }; + // // Caches for size of data sent to gpu, in bytes // size_t m_positions_tex_size{ 0 }; @@ -488,6 +503,25 @@ private: size_t m_colors_tex_size{ 0 }; size_t m_enabled_segments_tex_size{ 0 }; size_t m_enabled_options_tex_size{ 0 }; + + // The set the next draw reads from: the reduced one while dragging, if one is built. + bool use_reduced_set() const { return m_settings.reduced_detail && m_settings.reduced_detail_enabled; } + struct ActiveSet + { + size_t count{ 0 }; + unsigned int buf_id{ 0 }; + unsigned int tex_id{ 0 }; + }; + ActiveSet active_segments() const { + if (use_reduced_set()) + return { m_enabled_segments_reduced_count, m_enabled_segments_reduced_buf_id, m_enabled_segments_reduced_tex_id }; + return { m_enabled_segments_count, m_enabled_segments_buf_id, m_enabled_segments_tex_id }; + } + ActiveSet active_options() const { + if (use_reduced_set()) + return { m_enabled_options_reduced_count, m_enabled_options_reduced_buf_id, m_enabled_options_reduced_tex_id }; + return { m_enabled_options_count, m_enabled_options_buf_id, m_enabled_options_tex_id }; + } #endif // ENABLE_OPENGL_ES void update_view_full_range(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 19eff76965..4d315b3129 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1177,6 +1177,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const if (current_top_layer_only != required_top_layer_only) m_viewer.toggle_top_layer_only_view_range(); + read_solid_model_preference(); + // ORCA: darken the layers the preview layer slider is not scrubbed to m_viewer.set_dim_previous_layers(get_app_config()->get_bool("preview_dim_previous_layers")); m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness"))); @@ -1589,6 +1591,7 @@ void GCodeViewer::reset_shell() { m_shells.volumes.clear(); m_shells.print_id = -1; + m_shells.with_wipe_tower = false; m_shell_bounding_box = BoundingBoxf3(); } @@ -1625,7 +1628,12 @@ void GCodeViewer::reset() void GCodeViewer::render_scene(int canvas_width, int canvas_height) { glsafe(::glEnable(GL_DEPTH_TEST)); - render_shells(canvas_width, canvas_height); + // while dragging with the solid model on, the objects stand in for their toolpaths, cut to the + // visible layer range; the toolpath set then holds only the range's bottom and top layers + if (m_viewer.is_reduced_detail()) + render_solid_model(canvas_width, canvas_height); + else + render_shells(canvas_width, canvas_height); if (m_viewer.get_extrusion_roles_count() == 0) return; @@ -1925,6 +1933,41 @@ void GCodeViewer::update_layers_slider_mode() // TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder); } +void GCodeViewer::set_interacting(bool interacting) +{ + // with no shells to stand in for the toolpaths, the solid model would leave only the end layers + m_viewer.set_reduced_detail(m_solid_model_while_dragging && interacting && !m_shells.volumes.empty()); +} + +void GCodeViewer::set_solid_model_while_dragging(bool value) +{ + const bool was_enabled = m_solid_model_while_dragging; + m_solid_model_while_dragging = value; + m_viewer.set_reduced_detail_enabled(value); + reload_shells_if_solid_model_changed(was_enabled); +} + +void GCodeViewer::read_solid_model_preference() +{ + m_solid_model_while_dragging = get_app_config()->get_bool("preview_solid_model_while_dragging"); + m_viewer.set_reduced_detail_enabled(m_solid_model_while_dragging); +} + +void GCodeViewer::reload_shells_if_solid_model_changed(bool was_enabled) +{ + if (was_enabled == m_solid_model_while_dragging || m_shells.print_id == -1) + return; + // only the prime tower comes and goes with the mode: a full reload would drop the shells + // whenever the print has moved on since they were loaded, leaving the solid model nothing to draw + if (wxGetApp().plater() == nullptr) + return; + // the shells are loaded from the current plate's print, which is not the plater's own + const Print& print = wxGetApp().plater()->get_partplate_list().get_current_fff_print(); + if (static_cast(print.id().id) != m_shells.print_id) + return; + update_shell_wipe_tower(print, m_gl_data_initialized); +} + void GCodeViewer::set_layers_z_range(const std::array& layers_z_range) { m_viewer.set_layers_view_range(static_cast(layers_z_range[0]), static_cast(layers_z_range[1])); @@ -2249,7 +2292,11 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": initialized=%1%, force_previewing=%2%")%initialized %force_previewing; + // the shells can load before the first G-code does, so the preferences are read here as well + read_solid_model_preference(); if ((print.id().id == m_shells.print_id)&&(print.get_modified_count() == m_shells.print_modify_count)) { + // the prime tower comes and goes on its own, without reloading the objects + update_shell_wipe_tower(print, initialized); //BBS: update force previewing logic if (force_previewing) m_shells.previewing = force_previewing; @@ -2358,10 +2405,45 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p m_shells.print_id = print.id().id; m_shells.print_modify_count = print.get_modified_count(); m_shells.previewing = true; + update_shell_wipe_tower(print, initialized); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": shell loaded, id change to %1%, modify_count %2%, object count %3%, glvolume count %4%") % m_shells.print_id % m_shells.print_modify_count % object_count %m_shells.volumes.volumes.size(); } +// The prime tower as it was sliced, so that the solid model shows what the print shows. It keeps its +// opaque colour, so it never appears among the translucent shells, and stays out of their bounding box. +void GCodeViewer::update_shell_wipe_tower(const Print& print, bool initialized) +{ + const bool with_wipe_tower = m_solid_model_while_dragging && print.is_step_done(psWipeTower) && print.wipe_tower_data().wipe_tower_mesh_data; + if (with_wipe_tower == m_shells.with_wipe_tower) + return; + m_shells.with_wipe_tower = with_wipe_tower; + GLVolumePtrs& volumes = m_shells.volumes.volumes; + if (!with_wipe_tower) { + volumes.erase(std::remove_if(volumes.begin(), volumes.end(), [](GLVolume* volume) { + if (!volume->is_wipe_tower) + return false; + delete volume; + return true; + }), volumes.end()); + return; + } + const PrintConfig& config = print.config(); + const int plate_idx = print.get_plate_index(); + const Vec3d plate_origin = print.get_plate_origin(); + const float x = static_cast(config.wipe_tower_x.get_at(plate_idx) + plate_origin.x()); + const float y = static_cast(config.wipe_tower_y.get_at(plate_idx) + plate_origin.y()); + const size_t first_new = volumes.size(); + m_shells.volumes.load_real_wipe_tower_preview(1000 + plate_idx, x, y, print.wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh, + print.wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, true, + static_cast(config.wipe_tower_rotation_angle), false, initialized); + for (size_t i = first_new; i < volumes.size(); ++i) { + volumes[i]->zoom_to_volumes = false; + volumes[i]->force_native_color = true; + volumes[i]->set_render_color(); + } +} + void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); @@ -2562,6 +2644,50 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height) glsafe(::glDepthMask(GL_TRUE)); } +// The sliced objects and the prime tower drawn opaque, in their filament colours, cut to the +// visible layer range by the shader's z range. The toolpaths of the range's bottom and top layers +// are drawn afterwards and cap the cut. +void GCodeViewer::render_solid_model(int canvas_width, int canvas_height) +{ + if (m_shells.volumes.empty()) + return; + // gouraud_light has no z range, so it could not cut the model + GLShaderProgram* shader = wxGetApp().get_shader("gouraud"); + if (shader == nullptr) + return; + + const libvgcode::Interval& layers = m_viewer.get_layers_view_range(); + const float z_top = m_viewer.get_layer_z(layers[1]) - m_z_offset + 0.001f; + const float z_bottom = (layers[0] > 0) ? m_viewer.get_layer_z(layers[0] - 1) - m_z_offset - 0.001f : -FLT_MAX; + + std::vector alphas; + alphas.reserve(m_shells.volumes.volumes.size()); + for (GLVolume* volume : m_shells.volumes.volumes) { + alphas.push_back(volume->color.a()); + volume->color.a(1.0f); + volume->set_render_color(); + } + m_shells.volumes.set_z_range(z_bottom, z_top); + // gouraud also clips by this plane, which nothing else sets on the shells + m_shells.volumes.set_clipping_plane(ClippingPlane::ClipsNothing().get_data()); + + shader->start_using(); + // the 3D view leaves its shadow settings on the shared program + shader->set_uniform("shadow_intensity", 0.0f); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("z_far", camera.get_far_z()); + shader->set_uniform("z_near", camera.get_near_z()); + m_shells.volumes.render(GLVolumeCollection::ERenderType::Opaque, false, camera.get_view_matrix(), camera.get_projection_matrix(), {canvas_width, canvas_height}); + shader->stop_using(); + + m_shells.volumes.set_z_range(-FLT_MAX, FLT_MAX); + size_t k = 0; + for (GLVolume* volume : m_shells.volumes.volumes) { + volume->color.a(alphas[k++]); + volume->set_render_color(); + } +} + //BBS void GCodeViewer::render_all_plates_stats(const std::vector& gcode_result_list, bool show /*= true*/) const { if (!show) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 9b82a81b2f..efc956ff1a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -174,6 +174,8 @@ public: int print_id{-1}; int print_modify_count{-1}; bool previewing{false}; + // the prime tower was loaded with the objects, for the solid model + bool with_wipe_tower{false}; }; //BBS ConflictResultOpt m_conflict_result; @@ -233,6 +235,13 @@ private: bool m_legend_visible{ true }; bool m_legend_enabled{ true }; + // while dragging, the sliced objects are drawn as solid shapes instead of toolpaths + bool m_solid_model_while_dragging{ false }; + void read_solid_model_preference(); + void render_solid_model(int canvas_width, int canvas_height); + // the prime tower is only among the shells for the solid model, so it is added or removed when that changes + void reload_shells_if_solid_model_changed(bool was_enabled); + void update_shell_wipe_tower(const Print& print, bool initialized); float m_legend_height; PrintEstimatedStatistics m_print_statistics; @@ -283,7 +292,7 @@ public: // void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // void render_calibration_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); - bool has_data() const { return !m_viewer.get_extrusion_roles().empty(); } + bool has_data() const { return m_viewer.get_extrusion_roles_count() != 0; } bool can_export_toolpaths() const; std::vector get_plater_extruder(); @@ -345,6 +354,11 @@ public: void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); } float get_dim_previous_layers_brightness() const { return m_viewer.get_dim_previous_layers_brightness(); } + // while the user drags the camera or a slider, draw the solid model, if the preference asks for it + void set_interacting(bool interacting); + bool is_reduced_detail() const { return m_viewer.is_reduced_detail(); } + void set_solid_model_while_dragging(bool value); + void set_layers_z_range(const std::array& layers_z_range); bool is_legend_shown() const { return m_legend_visible && m_legend_enabled; } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 190eeedbb4..b80120991a 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2054,6 +2054,11 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) const bool overlay_tick = m_fps_overlay_tick; m_fps_overlay_tick = false; + // Whether the preview draws the solid model is decided before the cached scene is consulted, + // since switching changes what the scene pass draws. + if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview && m_gcode_viewer.has_data() && _update_preview_interaction()) + scene_dirty = true; + // An overlay-only frame reuses the last scene pass. The overlay is rebuilt either way, and drawn // below once it is known whether the frame differs from the one on screen. const bool reuse_scene = !scene_dirty && _can_reuse_cached_scene(camera); @@ -3233,9 +3238,16 @@ void GLCanvas3D::bind_event_handlers() if (m_selection_edit.kind != SelectionEdit::None) finish_selection_edit(); ImGui::SetWindowFocus(nullptr); + // a drag cut short never sees its button release, which would leave the solid model drawn + if (m_canvas_type == CanvasPreview && m_mouse.dragging && m_gcode_viewer.is_reduced_detail()) + mouse_up_cleanup(); render(); evt.Skip(); }); + m_canvas->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + if (m_canvas_type == CanvasPreview && m_mouse.dragging && m_gcode_viewer.is_reduced_detail()) + mouse_up_cleanup(); + }); m_event_handlers_bound = true; m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this); @@ -3308,6 +3320,17 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt) m_overlay_dirty |= imgui_requires_extra_frame; #endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh(); + // the render timer only wakes the idle loop; the frame that puts the preview's toolpaths back + // after a wheel burst has to be asked for here, once the settle time is really up + if (m_preview_settle_pending) { + const auto now = std::chrono::steady_clock::now(); + if (now >= m_preview_interaction_until) { + m_preview_settle_pending = false; + m_dirty = true; + } + else // the timer fired early + schedule_extra_frame(static_cast(std::chrono::duration_cast(m_preview_interaction_until - now).count()) + 1); + } if (!m_dirty && !m_overlay_dirty) return; @@ -3838,6 +3861,10 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) return; } + // only a wheel the panels did not take moves the camera + if (m_canvas_type == CanvasPreview) + note_preview_interaction(); + #ifdef __WXMSW__ // For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad, // if the event is not allowed to be passed further. @@ -3938,6 +3965,11 @@ void GLCanvas3D::on_fps_overlay_timer(wxTimerEvent& evt) wxWakeUpIdle(); } +void GLCanvas3D::note_preview_interaction() +{ + m_preview_interaction_until = std::chrono::steady_clock::now() + std::chrono::milliseconds(150); +} + void GLCanvas3D::schedule_extra_frame(int milliseconds) { // Schedule idle event right now @@ -5554,6 +5586,9 @@ void GLCanvas3D::mouse_up_cleanup() m_mouse.ignore_left_up = false; m_mouse.ignore_right_up = false; m_dirty = true; + // the frame that follows a release puts the preview's toolpaths back, and on some platforms + // no idle event follows a button release until the next input + wxWakeUpIdle(); if (m_canvas->HasCapture()) m_canvas->ReleaseMouse(); @@ -8738,6 +8773,26 @@ void GLCanvas3D::_render_wireframe_overlay() shader->stop_using(); } +// The solid model is drawn while the camera, the navigator or either slider is dragged. A wheel +// step has no duration, so it holds the solid model for a settle time instead, and the frame that +// restores the toolpaths is scheduled for when that time runs out. Returns whether what the scene +// pass draws changed, since a frame that reuses the cached scene would hide the change. +bool GLCanvas3D::_update_preview_interaction() +{ + IMSlider* layers_slider = m_gcode_viewer.get_layers_slider(); + IMSlider* moves_slider = m_gcode_viewer.get_moves_slider(); + const auto now = std::chrono::steady_clock::now(); + const bool settling = now < m_preview_interaction_until; + const bool dragging = m_mouse.dragging || m_navigator_dragging || layers_slider->is_dragging() || moves_slider->is_dragging(); + const bool was_reduced = m_gcode_viewer.is_reduced_detail(); + m_gcode_viewer.set_interacting(dragging || settling); + if (settling && !dragging && m_gcode_viewer.is_reduced_detail()) { + m_preview_settle_pending = true; + schedule_extra_frame(static_cast(std::chrono::duration_cast(m_preview_interaction_until - now).count()) + 1); + } + return m_gcode_viewer.is_reduced_detail() != was_reduced; +} + //BBS: GUI refactor: add canvas size as parameters void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height) { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index ad5188f2f3..8f999cc11e 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -649,6 +649,10 @@ private: ECursorType m_cursor_type; GLSelectionRectangle m_rectangle_selection; bool m_navigator_dragging{ false }; + // until when a wheel step keeps the preview's solid model drawn + std::chrono::time_point m_preview_interaction_until{}; + // whether the frame that restores the toolpaths once that time is up is still owed + bool m_preview_settle_pending{ false }; //BBS:add plate related logic mutable std::vector m_hover_volume_idxs; @@ -1215,6 +1219,8 @@ public: void msw_rescale() { m_gcode_viewer.invalidate_legend(); } void request_extra_frame() { m_extra_frame_requested = true; } + // a wheel step is over before the next frame, so it holds the preview's solid model for a settle time + void note_preview_interaction(); void schedule_extra_frame(int milliseconds); @@ -1361,6 +1367,9 @@ private: //BBS: GUI refactor: add canvas size as parameters void _render_gcode(int canvas_width, int canvas_height); void _render_gcode_overlay(int canvas_width, int canvas_height); + // decides whether the preview draws its solid model this frame and returns whether what the scene + // pass draws changed; runs before the cached scene is consulted + bool _update_preview_interaction(); //BBS: render a plane for assemble void _render_plane() const; void _render_selection(); diff --git a/src/slic3r/GUI/IMSlider.cpp b/src/slic3r/GUI/IMSlider.cpp index fa777b6a37..9df41e2aa7 100644 --- a/src/slic3r/GUI/IMSlider.cpp +++ b/src/slic3r/GUI/IMSlider.cpp @@ -483,6 +483,11 @@ void IMSlider::draw_background_and_groove(const ImRect& bg_rect, const ImRect& g ImGui::RenderFrame(groove.Min, groove.Max, groove_col, false, 0.5 * groove.GetWidth()); } +bool IMSlider::is_dragging() const +{ + return GImGui != nullptr && m_imgui_id != 0 && GImGui->ActiveId == m_imgui_id && GImGui->IO.MouseDown[0]; +} + bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int v_max, const ImVec2& size, float scale) { ImGuiWindow* window = ImGui::GetCurrentWindow(); @@ -491,6 +496,7 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int ImGuiContext& context = *GImGui; const ImGuiID id = window->GetID(str_id); + m_imgui_id = id; const ImVec2 pos = window->DC.CursorPos; const ImRect draw_region(pos, pos + size); @@ -883,6 +889,7 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower ImGuiContext& context = *GImGui; const ImGuiID id = window->GetID(str_id); + m_imgui_id = id; const ImVec2 pos = window->DC.CursorPos; const ImRect draw_region(pos, pos + size); diff --git a/src/slic3r/GUI/IMSlider.hpp b/src/slic3r/GUI/IMSlider.hpp index d28a65c1ff..7a3182d7a4 100644 --- a/src/slic3r/GUI/IMSlider.hpp +++ b/src/slic3r/GUI/IMSlider.hpp @@ -118,6 +118,9 @@ public: //BBS update scroll value changed bool is_dirty() { return m_dirty; } + // whether the mouse is holding this slider's handle, read from ImGui's active id rather than + // from the dirty flag, which is raised and consumed inside a single frame + bool is_dragging() const; void set_as_dirty(bool dirty = true) { m_dirty = dirty; } bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; } void reset_post_tick_event(bool val = false) { @@ -182,6 +185,8 @@ private: int m_higher_value; int m_one_layer_value; // ORCA bool m_dirty = false; + // the ImGui id of the slider widget, as of its last render + unsigned int m_imgui_id = 0; bool m_render_as_disabled{ false }; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index be4bfcfa63..6b67a718c6 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1049,6 +1049,16 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too } } // ORCA: apply the preview dimming change immediately to the currently loaded preview + // apply the solid model preference immediately to the currently loaded preview + else if (param == "preview_solid_model_while_dragging") { + if (Plater* plater = wxGetApp().plater()) { + if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) { + canvas->get_gcode_viewer().set_solid_model_while_dragging(app_config->get_bool(param)); + canvas->set_as_dirty(); + canvas->request_extra_frame(); + } + } + } else if (param == "preview_dim_previous_layers") { if (m_dim_previous_layers_brightness_input) m_dim_previous_layers_brightness_input->Enable(app_config->get_bool(param)); @@ -2019,6 +2029,15 @@ void PreferencesDialog::create_items() //// GRAPHICS > G-code Preview g_sizer->Add(create_item_title(_L("G-code Preview")), 1, wxEXPAND); + auto item_solid_model_while_dragging = create_item_checkbox( + _L("Only render solid model when dragging"), + _L("While dragging the camera or a preview slider, or zooming with the mouse wheel, draw the sliced objects and the prime tower as solid shapes " + "in their filament colours instead of toolpaths, so that large prints stay responsive. They are cut to the visible layer range, with its bottom " + "and top layers drawn as toolpaths. Supports are not shown. The toolpaths are restored as soon as you let go."), + "preview_solid_model_while_dragging" + ); + g_sizer->Add(item_solid_model_while_dragging); + auto item_dim_previous_layers = create_item_checkbox( _L("Dim lower layers"), _L("When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."),