diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index c5c5c15222..f80d07418b 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -118,6 +118,13 @@ public: // EReducedDetailMode get_rest_detail_mode() const; void set_rest_detail_mode(EReducedDetailMode mode); + // + // ORCA: with EReducedDetailMode::ShellOnly at rest, draw the walls of one layer in every N, each + // N layers tall, keeping the exposed surfaces of every layer. Choose N from how many layers fit + // in a pixel at the current view and it changes nothing visible. + // + uint32_t get_rest_layer_stride() const; + void set_rest_layer_stride(uint32_t value); 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 5346e3700c..2dca1cd486 100644 --- a/src/libvgcode/src/Settings.hpp +++ b/src/libvgcode/src/Settings.hpp @@ -36,6 +36,10 @@ struct Settings // ORCA: what is left out even at rest, with every layer drawn. Bound whenever the reduced set // above is not. Off draws everything. EReducedDetailMode rest_detail_mode{ EReducedDetailMode::Off }; + // ORCA: in ShellOnly rest mode, the walls of one layer in this many are drawn, that many layers + // tall; the exposed surfaces of every layer stay. Meant to follow how many layers fit in a + // pixel at the current view, so that it changes nothing visible. + uint32_t rest_layer_stride{ 1 }; // // Required update flags // diff --git a/src/libvgcode/src/Shaders.hpp b/src/libvgcode/src/Shaders.hpp index 1450d1a6df..ac4657cfca 100644 --- a/src/libvgcode/src/Shaders.hpp +++ b/src/libvgcode/src/Shaders.hpp @@ -26,6 +26,8 @@ static const char* Segments_Vertex_Shader = "uniform mat4 view_matrix;\n" "uniform mat4 projection_matrix;\n" "uniform vec3 camera_position;\n" +"// ORCA: how many layers each drawn segment stands in for while layers are being skipped\n" +"uniform float height_scale;\n" "uniform samplerBuffer position_tex;\n" "uniform samplerBuffer height_width_angle_tex;\n" "uniform samplerBuffer color_tex;\n" @@ -112,7 +114,9 @@ static const char* Segments_Vertex_Shader = "#endif\n" " float view_right_sign = sign(dot(-camera_view_dir, line_right_dir));\n" " float view_top_sign = sign(dot(-camera_view_dir, line_up_dir));\n" -" float half_height = 0.5 * height_width_angle.x;\n" +" // ORCA: a segment standing in for the skipped layers below it grows downward to cover them\n" +" endpoint_pos -= (height_scale - 1.0) * 0.5 * height_width_angle.x * line_up_dir;\n" +" float half_height = 0.5 * height_scale * height_width_angle.x;\n" " float half_width = 0.5 * height_width_angle.y;\n" " vec3 horizontal_dir = half_width * line_right_dir;\n" " vec3 vertical_dir = half_height * line_up_dir;\n" @@ -133,6 +137,9 @@ static const char* Segments_Vertex_Shader = " pos += sign(height_width_angle.z) * horizontal_dir * cos(abs(height_width_angle.z) * 0.5);\n" " }\n" " }\n" +" // ORCA: the grown first layer must not reach below the bed\n" +" if (height_scale > 1.0)\n" +" pos.z = max(pos.z, 0.0);\n" " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " // ORCA: Apply bias to z-position to avoid z-fighting\n" " eye_position.z += bias;\n" diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index 44e6d94967..06e7530bb8 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -117,6 +117,16 @@ void Viewer::set_rest_detail_mode(EReducedDetailMode mode) m_impl->set_rest_detail_mode(mode); } +uint32_t Viewer::get_rest_layer_stride() const +{ + return m_impl->get_rest_layer_stride(); +} + +void Viewer::set_rest_layer_stride(uint32_t value) +{ + m_impl->set_rest_layer_stride(value); +} + 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 1d52101faa..b1a8849928 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -762,6 +762,7 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_segments_view_matrix_id = glGetUniformLocation(m_segments_shader_id, "view_matrix"); m_uni_segments_projection_matrix_id = glGetUniformLocation(m_segments_shader_id, "projection_matrix"); m_uni_segments_camera_position_id = glGetUniformLocation(m_segments_shader_id, "camera_position"); + m_uni_segments_height_scale_id = glGetUniformLocation(m_segments_shader_id, "height_scale"); m_uni_segments_positions_tex_id = glGetUniformLocation(m_segments_shader_id, "position_tex"); m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex"); m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex"); @@ -898,6 +899,7 @@ void ViewerImpl::reset() m_enabled_options_reduced_count = 0; m_enabled_segments_rest_count = 0; m_shell_bitset = BitSet<>(); + m_exposed_bitset = BitSet<>(); m_settings_used_for_ranges = std::nullopt; @@ -1253,16 +1255,121 @@ struct OccupancyGrid } }; +// Scratch space for close_gaps(), one per worker +struct ClosingScratch +{ + // component label per cell: 0 empty, > 0 a component, WILD a tiny fragment, CONTESTED a cell + // reached by two components' dilations + std::vector labels; + std::vector> frontier; + std::vector> next; + std::vector window_sum; + std::vector raw; + static constexpr int32_t WILD = -1; + static constexpr int32_t CONTESTED = -2; +}; + // Morphological closing with a square window of the given radius: fills gaps up to 2 * radius -// cells wide, so that sparse infill or support reads as the solid area it is part of. Separable -// passes over running window sums keep the cost linear in the rectangle's area. -static void close_gaps(OccupancyGrid& grid, int radius, std::vector& window_sum) +// cells wide, so that sparse infill or support reads as the solid area it is part of. The dilation +// is done per connected component, and a cell two components both reach stays empty, so the gap +// between two objects standing close together is never bridged and both of their facing walls +// stay on the shell. A separable erosion over running window sums then shrinks the result back. +static void close_gaps(OccupancyGrid& grid, int radius, ClosingScratch& scratch) { if (grid.empty() || radius <= 0) return; // the dilated area needs room to grow grid.grow(radius); - const auto pass = [&](bool horizontal, bool dilate) { + const int nx = grid.nx; + const auto idx = [nx](int x, int y) { return static_cast(y) * nx + x; }; + const auto in_rect = [&](int x, int y) { return x >= grid.min_x && x <= grid.max_x && y >= grid.min_y && y <= grid.max_y; }; + std::vector& labels = scratch.labels; + labels.resize(grid.cells.size()); + for (int y = grid.min_y; y <= grid.max_y; ++y) + std::fill_n(&labels[idx(grid.min_x, y)], grid.max_x - grid.min_x + 1, 0); + // the raw cells come back at the end: a closing must never lose one, and the erosion below + // would eat into a wall that faces a contested gap + std::vector& raw = scratch.raw; + raw.resize(grid.cells.size()); + for (int y = grid.min_y; y <= grid.max_y; ++y) + std::copy_n(&grid.at(grid.min_x, y), grid.max_x - grid.min_x + 1, &raw[idx(grid.min_x, y)]); + + // label the 8-connected components of the raw cells; a fragment too small to be a wall does + // not spread and is absorbed by whichever component reaches it + static constexpr size_t TINY = 8; + int32_t next_label = 1; + std::vector>& frontier = scratch.frontier; + frontier.clear(); + for (int y = grid.min_y; y <= grid.max_y; ++y) { + for (int x = grid.min_x; x <= grid.max_x; ++x) { + if (!grid.at(x, y) || labels[idx(x, y)] != 0) + continue; + std::vector>& component = scratch.next; + component.clear(); + component.emplace_back(x, y); + labels[idx(x, y)] = next_label; + for (size_t head = 0; head < component.size(); ++head) { + const auto [cx, cy] = component[head]; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + const int px = cx + dx; + const int py = cy + dy; + if ((dx == 0 && dy == 0) || !in_rect(px, py) || !grid.at(px, py) || labels[idx(px, py)] != 0) + continue; + labels[idx(px, py)] = next_label; + component.emplace_back(px, py); + } + } + } + if (component.size() < TINY) { + for (const auto [cx, cy] : component) + labels[idx(cx, cy)] = ClosingScratch::WILD; + } + else { + frontier.insert(frontier.end(), component.begin(), component.end()); + ++next_label; + } + } + } + + // dilate: each component claims the cells within radius of it, breadth first; a cell already + // claimed by another component is contested and stays empty + for (int step = 0; step < radius; ++step) { + std::vector>& next = scratch.next; + next.clear(); + for (const auto [cx, cy] : frontier) { + const int32_t label = labels[idx(cx, cy)]; + if (label <= 0) + continue; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + const int px = cx + dx; + const int py = cy + dy; + if ((dx == 0 && dy == 0) || !in_rect(px, py)) + continue; + int32_t& other = labels[idx(px, py)]; + if (other == 0 || other == ClosingScratch::WILD) { + other = label; + next.emplace_back(px, py); + } + else if (other != label && other != ClosingScratch::CONTESTED && !grid.at(px, py)) + other = ClosingScratch::CONTESTED; + } + } + } + std::swap(frontier, next); + } + for (int y = grid.min_y; y <= grid.max_y; ++y) { + for (int x = grid.min_x; x <= grid.max_x; ++x) { + if (labels[idx(x, y)] > 0) + grid.at(x, y) = 1; + } + } + + // erode by the same radius, separably; cells outside the rectangle are empty, which is what a + // shrinking erosion has to see + std::vector& window_sum = scratch.window_sum; + const auto erode = [&](bool horizontal) { const int outer_n = horizontal ? grid.max_y - grid.min_y + 1 : grid.max_x - grid.min_x + 1; const int inner_n = horizontal ? grid.max_x - grid.min_x + 1 : grid.max_y - grid.min_y + 1; window_sum.assign(inner_n + 1, 0); @@ -1274,15 +1381,16 @@ static void close_gaps(OccupancyGrid& grid, int radius, std::vector& window window_sum[i + 1] = window_sum[i] + cell(i); for (int i = 0; i < inner_n; ++i) { const int count = window_sum[std::min(inner_n, i + radius + 1)] - window_sum[std::max(0, i - radius)]; - // cells outside the rectangle are empty, which is what a shrinking erosion has to see - cell(i) = dilate ? (count > 0) : (count == 2 * radius + 1); + cell(i) = (count == 2 * radius + 1); } } }; - pass(true, true); - pass(false, true); - pass(true, false); - pass(false, false); + erode(true); + erode(false); + for (int y = grid.min_y; y <= grid.max_y; ++y) { + for (int x = grid.min_x; x <= grid.max_x; ++x) + grid.at(x, y) |= raw[idx(x, y)]; + } } } // namespace @@ -1291,13 +1399,14 @@ static void close_gaps(OccupancyGrid& grid, int radius, std::vector& window // EReducedDetailMode::ShellOnly can leave out everything the walls hide. Each layer is rasterized // into a coarse occupancy grid and closed, so that its footprint is solid whatever the infill; // a cell is then on the shell when it is filled and any of its six neighbours (four in the layer, -// the layer below, the layer above) is not. A segment is kept when at least half of the cells it -// crosses are shell cells: walls run along the shell, infill only touches it at the ends. -// Purely geometric, so it works as well for the wipe tower, whose every segment shares one role, -// as for the objects. +// the layer below, the layer above) is not, and exposed when it is the layer below or above that +// is missing. A segment is kept when at least half of the cells it crosses are shell cells: walls +// run along the shell, infill only touches it at the ends. Purely geometric, so it works as well +// for the wipe tower, whose every segment shares one role, as for the objects. void ViewerImpl::update_shell_bitset() { m_shell_bitset = BitSet<>(m_vertices.size()); + m_exposed_bitset = BitSet<>(m_vertices.size()); if (m_vertices.size() < 2 || m_layers.empty()) return; @@ -1366,13 +1475,15 @@ void ViewerImpl::update_shell_bitset() const OccupancyGrid nothing(nx, ny); - // Classifies the layers in [first_layer, last_layer) and returns the segments kept. Each call - // owns its grids, so the layer range can be split across threads. + // Classifies the layers in [first_layer, last_layer) and returns the segments kept, and among + // them the exposed ones. Each call owns its grids, so the layer range can be split across threads. + struct Kept { std::vector shell; std::vector exposed; }; const auto classify_layers = [&](size_t first_layer, size_t last_layer) { - std::vector kept; + Kept kept; std::vector footprints(3, OccupancyGrid(nx, ny)); OccupancyGrid shell_cells(nx, ny); - std::vector window_sum; + OccupancyGrid exposed_cells(nx, ny); + ClosingScratch scratch; const auto footprint = [&](size_t layer) -> OccupancyGrid& { return footprints[layer % 3]; }; const auto prepare = [&](size_t layer) { OccupancyGrid& g = footprint(layer); @@ -1382,7 +1493,7 @@ void ViewerImpl::update_shell_bitset() if (is_drawn_extrusion(i)) for_each_cell(i, [&](int x, int y) { g.set(x, y); }); } - close_gaps(g, radius, window_sum); + close_gaps(g, radius, scratch); }; if (first_layer > 0) @@ -1396,13 +1507,16 @@ void ViewerImpl::update_shell_bitset() const OccupancyGrid& above = (layer + 1 < layers_count) ? footprint(layer + 1) : nothing; shell_cells.clear(); + exposed_cells.clear(); for (int y = cur.min_y; y <= cur.max_y; ++y) { for (int x = cur.min_x; x <= cur.max_x; ++x) { if (!cur.at(x, y)) continue; - if (!cur.at(x - 1, y) || !cur.at(x + 1, y) || !cur.at(x, y - 1) || !cur.at(x, y + 1) || - !below.at(x, y) || !above.at(x, y)) + const bool exposed = !below.at(x, y) || !above.at(x, y); + if (exposed || !cur.at(x - 1, y) || !cur.at(x + 1, y) || !cur.at(x, y - 1) || !cur.at(x, y + 1)) shell_cells.set(x, y); + if (exposed) + exposed_cells.set(x, y); } } @@ -1412,9 +1526,16 @@ void ViewerImpl::update_shell_bitset() continue; int total = 0; int on_shell = 0; - for_each_cell(i, [&](int x, int y) { ++total; on_shell += shell_cells.at(x, y); }); + int on_exposed = 0; + for_each_cell(i, [&](int x, int y) { + ++total; + on_shell += shell_cells.at(x, y); + on_exposed += exposed_cells.at(x, y); + }); if (2 * on_shell >= total) - kept.push_back(static_cast(i)); + kept.shell.push_back(static_cast(i)); + if (2 * on_exposed >= total) + kept.exposed.push_back(static_cast(i)); } } return kept; @@ -1422,12 +1543,15 @@ void ViewerImpl::update_shell_bitset() const size_t workers = std::clamp(std::thread::hardware_concurrency(), 1, 8); const size_t chunk = std::max(16, (layers_count + workers - 1) / workers); - std::vector>> futures; + std::vector> futures; for (size_t first = 0; first < layers_count; first += chunk) futures.emplace_back(std::async(std::launch::async, classify_layers, first, std::min(layers_count, first + chunk))); for (auto& f : futures) { - for (uint32_t i : f.get()) + const Kept kept = f.get(); + for (uint32_t i : kept.shell) m_shell_bitset.set(i); + for (uint32_t i : kept.exposed) + m_exposed_bitset.set(i); } } #endif // ENABLE_OPENGL_ES @@ -1447,13 +1571,16 @@ void ViewerImpl::update_enabled_entities() std::vector enabled_options_reduced; std::vector enabled_segments_rest; const uint32_t layer_stride = std::max(1, m_settings.reduced_detail_layer_stride); + // Only the shell mode knows which segments are exposed surfaces, so only it can skip layers at + // rest, and in either set only it can keep the surfaces of the layers it skips. + const bool shell_reduced = build_reduced && m_settings.reduced_detail_mode == EReducedDetailMode::ShellOnly; + const bool shell_rest = build_rest && m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly; + const uint32_t rest_stride = shell_rest ? std::max(1, m_settings.rest_layer_stride) : 1; // Whatever else is dropped, both ends of the visible layer range are kept whole: the top is the // surface the user is looking at, and the only layer drawn at full color in top-layer-only // mode; the bottom is exposed whenever the range is cut short. const Interval& layers_range = m_layers.get_view_range(); - if (((build_reduced && m_settings.reduced_detail_mode == EReducedDetailMode::ShellOnly) || - (build_rest && m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly)) && - m_shell_bitset.size != m_vertices.size()) + if ((shell_reduced || shell_rest) && m_shell_bitset.size != m_vertices.size()) update_shell_bitset(); #endif // ENABLE_OPENGL_ES Interval range = m_view_range.get_visible(); @@ -1507,12 +1634,20 @@ void ViewerImpl::update_enabled_entities() #ifndef ENABLE_OPENGL_ES const bool whole_layer = v.layer_id == layers_range[0] || v.layer_id == layers_range[1]; const bool keep_anyway = whole_layer || !v.is_extrusion(); - if (build_rest && !v.is_option() && (keep_anyway || reduced_set_keeps(m_settings.rest_detail_mode, i, v))) - enabled_segments_rest.push_back(static_cast(i)); + const bool exposed = v.is_extrusion() && m_exposed_bitset.size == m_vertices.size() && m_exposed_bitset[i]; + if (build_rest && !v.is_option()) { + const bool skipped = !whole_layer && rest_stride > 1 && (v.layer_id % rest_stride) != 0; + if (skipped ? exposed : (keep_anyway || reduced_set_keeps(m_settings.rest_detail_mode, i, v))) + enabled_segments_rest.push_back(static_cast(i)); + } if (!build_reduced) continue; - if (!whole_layer && (v.layer_id % layer_stride) != 0) + if (!whole_layer && (v.layer_id % layer_stride) != 0) { + // the exposed surfaces of a skipped layer stay, so that a step does not vanish + if (shell_reduced && exposed) + enabled_segments_reduced.push_back(static_cast(i)); continue; + } if (v.is_option()) enabled_options_reduced.push_back(static_cast(i)); else if (keep_anyway || reduced_set_keeps(m_settings.reduced_detail_mode, i, v)) @@ -1774,6 +1909,17 @@ void ViewerImpl::set_rest_detail_mode(EReducedDetailMode mode) m_settings.update_enabled_entities = true; } +void ViewerImpl::set_rest_layer_stride(uint32_t value) +{ + value = std::max(1, value); + if (m_settings.rest_layer_stride == value) + return; + m_settings.rest_layer_stride = value; + // only the shell rest set is built from it + if (m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly) + 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) { @@ -2396,6 +2542,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data())); + glsafe(glUniform1f(m_uni_segments_height_scale_id, active_height_scale())); glsafe(glDisable(GL_CULL_FACE)); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index a658ddc6d8..7b2172033a 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -103,6 +103,8 @@ public: void set_reduced_detail_layer_stride(uint32_t value); EReducedDetailMode get_rest_detail_mode() const { return m_settings.rest_detail_mode; } void set_rest_detail_mode(EReducedDetailMode mode); + uint32_t get_rest_layer_stride() const { return m_settings.rest_layer_stride; } + void set_rest_layer_stride(uint32_t value); float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; } void set_dim_previous_layers_brightness(float value); @@ -327,6 +329,9 @@ private: // computed on demand by update_shell_bitset() for EReducedDetailMode::ShellOnly // BitSet<> m_shell_bitset; + // the subset of those that are exposed from above or below: the surfaces a view from the top + // or bottom sees, kept in every layer even while layers are being skipped + BitSet<> m_exposed_bitset; #endif // ENABLE_OPENGL_ES // // Variables used for toolpaths coloring @@ -366,6 +371,7 @@ private: int m_uni_segments_view_matrix_id{ -1 }; int m_uni_segments_projection_matrix_id{ -1 }; int m_uni_segments_camera_position_id{ -1 }; + int m_uni_segments_height_scale_id{ -1 }; int m_uni_segments_positions_tex_id{ -1 }; int m_uni_segments_height_width_angle_tex_id{ -1 }; int m_uni_segments_colors_tex_id{ -1 }; @@ -533,6 +539,14 @@ private: return m_settings.rest_detail_mode != EReducedDetailMode::Off && m_settings.rest_detail_mode != EReducedDetailMode::LayersOnly; } bool use_rest_set() const { return !use_reduced_set() && build_rest_set(); } + // how many layers each drawn segment of the bound set stands in for + float active_height_scale() const { + if (use_reduced_set()) + return static_cast(std::max(1, m_settings.reduced_detail_layer_stride)); + if (use_rest_set() && m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly) + return static_cast(std::max(1, m_settings.rest_layer_stride)); + return 1.0f; + } size_t active_segments_count() const { return use_reduced_set() ? m_enabled_segments_reduced_count : use_rest_set() ? m_enabled_segments_rest_count : m_enabled_segments_count; } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index d98303d058..3e85866365 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1178,6 +1178,20 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_reduced_detail_layer_stride = static_cast(std::max(1, std::stoi(get_app_config()->get("preview_reduced_detail_layer_stride")))); m_rest_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_rest_detail_mode")); apply_reduced_detail_settings(); + // the median z step between layers, robust to the first layer and to variable layer height + { + std::vector steps; + for (size_t i = 1; i < m_viewer.get_layers_count(); ++i) { + const float step = m_viewer.get_layer_z(i) - m_viewer.get_layer_z(i - 1); + if (step > 0.0f) + steps.push_back(step); + } + m_typical_layer_height = 0.0f; + if (!steps.empty()) { + std::nth_element(steps.begin(), steps.begin() + steps.size() / 2, steps.end()); + m_typical_layer_height = steps[steps.size() / 2]; + } + } // 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")); @@ -1632,6 +1646,8 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (m_viewer.get_extrusion_roles_count() == 0) return; + update_rest_layer_stride(); + render_toolpaths(); float legend_height = 0.0f; @@ -1916,9 +1932,30 @@ void GCodeViewer::update_layers_slider_mode() void GCodeViewer::set_interacting(bool interacting) { + m_interacting = interacting; m_viewer.set_reduced_detail(m_reduced_detail_while_dragging && interacting); } +// ORCA: with the shell drawn at rest, layers thinner than a couple of pixels on screen are merged: +// the walls of one layer in N are drawn N layers tall, which looks the same and costs 1/N. N follows +// the view, from 1 side-on and zoomed in to the cap looking straight down, where the walls are edge-on +// and every layer's exposed surfaces are all there is to see. Changing N rebuilds the sets, so it is +// left alone while the user is dragging. +void GCodeViewer::update_rest_layer_stride() +{ + if (m_interacting || m_rest_detail_mode != libvgcode::EReducedDetailMode::ShellOnly || m_typical_layer_height <= 0.0f) + return; + static constexpr double MERGE_BELOW_PX = 2.0; + static constexpr unsigned int MAX_STRIDE = 64; + const Camera& camera = wxGetApp().plater()->get_camera(); + const double dz = std::abs(camera.get_dir_forward().z()); + const double tilt = std::sqrt(std::max(0.0, 1.0 - dz * dz)); + const double layer_px = static_cast(m_typical_layer_height) * camera.get_zoom() * tilt; + const unsigned int stride = (layer_px * MAX_STRIDE <= MERGE_BELOW_PX) ? MAX_STRIDE : + std::clamp(static_cast(MERGE_BELOW_PX / layer_px), 1u, MAX_STRIDE); + m_viewer.set_rest_layer_stride(stride); +} + // ORCA: libvgcode only builds a reduced set while its mode is not Off, so the preference switch is // folded into the mode it is given. Every setter goes through here. void GCodeViewer::apply_reduced_detail_settings() diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 6ca3007d49..cc55435ea4 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -236,6 +236,11 @@ private: unsigned int m_reduced_detail_layer_stride{ 4 }; libvgcode::EReducedDetailMode m_rest_detail_mode{ libvgcode::EReducedDetailMode::Off }; void apply_reduced_detail_settings(); + // whether the user is dragging or a wheel burst is settling, as told by set_interacting() + bool m_interacting{ false }; + // the print's typical layer height, for how many layers fit in a pixel at the current view + float m_typical_layer_height{ 0.0f }; + void update_rest_layer_stride(); float m_legend_height; PrintEstimatedStatistics m_print_statistics; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 516c231769..3ea675dd30 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -2022,8 +2022,8 @@ void PreferencesDialog::create_items() "Use it when a plate of large objects is slow to draw even when the view is not moving.\n" "Nothing: the full preview.\n" "Internal infill: sparse and solid infill hidden inside the walls is left out.\n" - "Everything but the shell: only the toolpaths on the visible surface of the print are drawn. " - "Holes narrower than 5 mm are treated as solid."), + "Everything but the shell: only the toolpaths on the visible surface of the print are drawn, and layers thinner than a couple of pixels on screen are merged, " + "so that looking from above costs little more than the top surfaces. Holes narrower than 5 mm are treated as solid."), "preview_rest_detail_mode", {_L("Nothing"), _L("Internal infill"), _L("Everything but the shell")}, {"full", "no_infill", "shell"},