diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index f2b87d4030..3d6d23f32c 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -202,10 +202,30 @@ void AppConfig::set_defaults() if (get("seq_top_layer_only").empty()) set("seq_top_layer_only", "1"); - // ORCA: darken the layers the preview layer slider is not scrubbed to + // ORCA: simplify the preview while the user is dragging: what is left out and one layer in how many is kept if (get("preview_reduced_detail_while_dragging").empty()) set_bool("preview_reduced_detail_while_dragging", false); + { + const std::string mode = get("preview_reduced_detail_mode"); + if (mode != "layers" && mode != "no_infill" && mode != "shell") + set("preview_reduced_detail_mode", "no_infill"); + } + + if (get("preview_reduced_detail_layer_stride").empty()) + set("preview_reduced_detail_layer_stride", "4"); + else { + int stride = 4; + try { + stride = std::stoi(get("preview_reduced_detail_layer_stride")); + } + catch (...) { + stride = 4; + } + set("preview_reduced_detail_layer_stride", std::to_string(std::max(1, std::min(stride, 20)))); + } + + // 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/Types.hpp b/src/libvgcode/include/Types.hpp index 2187238ef0..0a3d4fe15e 100644 --- a/src/libvgcode/include/Types.hpp +++ b/src/libvgcode/include/Types.hpp @@ -158,6 +158,23 @@ enum class EGCodeExtrusionRole : uint8_t static constexpr std::size_t GCODE_EXTRUSION_ROLES_COUNT = static_cast(EGCodeExtrusionRole::COUNT); +// +// ORCA: what the reduced toolpath set drawn while the user is dragging leaves out, on top of +// keeping only one layer in every Viewer::get_reduced_detail_layer_stride() layers +// +enum class EReducedDetailMode : uint8_t +{ + // no reduced set is built and Viewer::set_reduced_detail() has no effect + Off, + // every role is kept, only layers are skipped + LayersOnly, + // the interior infill roles are left out + NoInternalInfill, + // only the segments on the visible surface of the print are kept + ShellOnly, + COUNT +}; + // // Option types // diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index 6624577de6..5ddee75533 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -99,14 +99,18 @@ public: bool is_dim_previous_layers() const; void set_dim_previous_layers(bool value); // - // ORCA: draw the preview from a reduced set of entities - the interior infill roles dropped and - // one layer in every set_reduced_detail_layer_stride() kept, always keeping the top of the - // visible layer range. Meant to be held only while the user drags the camera or a slider: the - // reduced set is built alongside the full one, so toggling it never rebuilds anything. - // Has no effect on the OpenGL ES path. + // ORCA: draw the preview from a reduced set of entities: one layer in every + // get_reduced_detail_layer_stride() layers is kept, and on top of that the mode decides which + // roles or segments are left out. The bottom and top of the visible layer range are always kept + // whole. Meant to be held only while the user drags the camera or a slider: the reduced set is + // built alongside the full one, so toggling it never rebuilds anything. Nothing is built while + // the mode is EReducedDetailMode::Off. Has no effect on the OpenGL ES path. // void set_reduced_detail(bool value); bool is_reduced_detail() const; + EReducedDetailMode get_reduced_detail_mode() const; + void set_reduced_detail_mode(EReducedDetailMode mode); + uint32_t get_reduced_detail_layer_stride() const; void set_reduced_detail_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 eb7d5929ac..0f3011f4d2 100644 --- a/src/libvgcode/src/Settings.hpp +++ b/src/libvgcode/src/Settings.hpp @@ -26,11 +26,12 @@ struct Settings float dim_previous_layers_brightness{ 0.4f }; bool spiral_vase_mode{ false }; // ORCA: while the user drags the camera or a slider, the preview can be drawn from a reduced - // set of entities: the interior infill roles dropped and one layer in every - // reduced_detail_layer_stride kept. The reduced sets are built alongside the full ones in - // update_enabled_entities(), so turning this on and off costs nothing but a buffer binding. + // set of entities: one layer in every reduced_detail_layer_stride kept, and on top of that + // whatever reduced_detail_mode leaves out. The reduced sets are built alongside the full ones + // in update_enabled_entities(), so holding reduced_detail costs nothing but a buffer binding. // Ignored on the OpenGL ES path, which keeps a single set of entities. bool reduced_detail{ false }; + EReducedDetailMode reduced_detail_mode{ EReducedDetailMode::Off }; uint32_t reduced_detail_layer_stride{ 4 }; // // Required update flags diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index 5cfa55304b..883abf8df7 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -87,6 +87,21 @@ bool Viewer::is_reduced_detail() const return m_impl->is_reduced_detail(); } +EReducedDetailMode Viewer::get_reduced_detail_mode() const +{ + return m_impl->get_reduced_detail_mode(); +} + +void Viewer::set_reduced_detail_mode(EReducedDetailMode mode) +{ + m_impl->set_reduced_detail_mode(mode); +} + +uint32_t Viewer::get_reduced_detail_layer_stride() const +{ + return m_impl->get_reduced_detail_layer_stride(); +} + void Viewer::set_reduced_detail_layer_stride(uint32_t value) { m_impl->set_reduced_detail_layer_stride(value); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index a1cfb5d592..91e9ad1958 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include namespace libvgcode { @@ -893,6 +896,7 @@ void ViewerImpl::reset() m_enabled_options_count = 0; m_enabled_segments_reduced_count = 0; m_enabled_options_reduced_count = 0; + m_shell_bitset = BitSet<>(); m_settings_used_for_ranges = std::nullopt; @@ -1170,14 +1174,254 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) } #ifndef ENABLE_OPENGL_ES -// ORCA: the roles that sit inside the part and are hidden by its walls from every angle. Dropping -// them is the least visible half of the reduced set built below. +// ORCA: the roles that sit inside the part and are hidden by its walls from every angle static bool is_interior_infill(EGCodeExtrusionRole role) { return role == EGCodeExtrusionRole::InternalInfill || role == EGCodeExtrusionRole::SolidInfill || role == EGCodeExtrusionRole::InternalBridgeInfill; } + +bool ViewerImpl::reduced_set_keeps(size_t i, const PathVertex& v) const +{ + switch (m_settings.reduced_detail_mode) { + case EReducedDetailMode::NoInternalInfill: return !is_interior_infill(v.role); + case EReducedDetailMode::ShellOnly: return m_shell_bitset[i]; + default: return true; + } +} + +namespace { + +// A 2D occupancy grid over the print's footprint, one byte per cell. Only the rectangle a layer +// touches is ever cleared or scanned, so a grid the size of the whole print costs no more than +// the layer needs. +struct OccupancyGrid +{ + int nx{ 0 }; + int ny{ 0 }; + std::vector cells; + // bounding rectangle of the set cells, inclusive; empty while min > max + int min_x{ 0 }; + int min_y{ 0 }; + int max_x{ -1 }; + int max_y{ -1 }; + + OccupancyGrid(int nx, int ny) : nx(nx), ny(ny), cells(static_cast(nx) * static_cast(ny), 0) {} + + bool empty() const { return min_x > max_x; } + uint8_t at(int x, int y) const { return cells[static_cast(y) * nx + x]; } + uint8_t& at(int x, int y) { return cells[static_cast(y) * nx + x]; } + + void set(int x, int y) { + at(x, y) = 1; + if (empty()) { + min_x = max_x = x; + min_y = max_y = y; + } + else { + min_x = std::min(min_x, x); + max_x = std::max(max_x, x); + min_y = std::min(min_y, y); + max_y = std::max(max_y, y); + } + } + + void clear() { + for (int y = min_y; y <= max_y; ++y) + std::fill_n(&at(min_x, y), max_x - min_x + 1, uint8_t(0)); + min_x = min_y = 0; + max_x = max_y = -1; + } + + // grow the bounding rectangle by r cells, staying inside the grid + void grow(int r) { + if (empty()) + return; + min_x = std::max(0, min_x - r); + min_y = std::max(0, min_y - r); + max_x = std::min(nx - 1, max_x + r); + max_y = std::min(ny - 1, max_y + r); + } +}; + +// 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) +{ + 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 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); + for (int o = 0; o < outer_n; ++o) { + const auto cell = [&](int i) -> uint8_t& { + return horizontal ? grid.at(grid.min_x + i, grid.min_y + o) : grid.at(grid.min_x + o, grid.min_y + i); + }; + for (int i = 0; i < inner_n; ++i) + 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); + } + } + }; + pass(true, true); + pass(false, true); + pass(true, false); + pass(false, false); +} + +} // namespace + +// ORCA: mark the extrusion segments that lie on the visible surface of the print, so that +// 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. +void ViewerImpl::update_shell_bitset() +{ + m_shell_bitset = BitSet<>(m_vertices.size()); + if (m_vertices.size() < 2 || m_layers.empty()) + return; + + float min_x = FLT_MAX; + float min_y = FLT_MAX; + float max_x = -FLT_MAX; + float max_y = -FLT_MAX; + for (const PathVertex& v : m_vertices) { + if (!v.is_extrusion()) + continue; + min_x = std::min(min_x, v.position[0]); + min_y = std::min(min_y, v.position[1]); + max_x = std::max(max_x, v.position[0]); + max_y = std::max(max_y, v.position[1]); + } + if (min_x > max_x) + return; + + // Half a millimetre separates a wall from the wall behind it; a print too large for that at + // 1024 cells across gets coarser cells rather than a bigger grid. Gaps of up to 5 mm read as + // solid: wide enough to swallow sparse infill, narrow enough to leave real holes open. + static constexpr int MAX_CELLS = 1024; + const float cell = std::max(0.5f, std::max(max_x - min_x, max_y - min_y) / static_cast(MAX_CELLS)); + const int radius = static_cast(std::ceil(2.5f / cell)); + // room for the closing to grow into, plus the neighbour lookups + const int margin = radius + 2; + const float origin_x = min_x - static_cast(margin) * cell; + const float origin_y = min_y - static_cast(margin) * cell; + const int nx = static_cast((max_x - min_x) / cell) + 1 + 2 * margin; + const int ny = static_cast((max_y - min_y) / cell) + 1 + 2 * margin; + + const auto cell_of = [&](float x, float y) { + const int cx = std::clamp(static_cast((x - origin_x) / cell), margin, nx - 1 - margin); + const int cy = std::clamp(static_cast((y - origin_y) / cell), margin, ny - 1 - margin); + return std::make_pair(cx, cy); + }; + + // calls f(cx, cy) once per cell the segment starting at vertex i passes through + const auto for_each_cell = [&](size_t i, auto&& f) { + const Vec3& a = m_vertices[i].position; + const Vec3& b = m_vertices[i + 1].position; + const float dx = b[0] - a[0]; + const float dy = b[1] - a[1]; + const int steps = static_cast(std::sqrt(dx * dx + dy * dy) / (0.5f * cell)) + 1; + int last_x = -1; + int last_y = -1; + for (int s = 0; s <= steps; ++s) { + const float t = static_cast(s) / static_cast(steps); + const auto [cx, cy] = cell_of(a[0] + t * dx, a[1] + t * dy); + if (cx != last_x || cy != last_y) { + f(cx, cy); + last_x = cx; + last_y = cy; + } + } + }; + + const size_t layers_count = m_layers.count(); + // the segments of a layer: [first, last), where segment i runs from vertex i to vertex i + 1 + const auto layer_segments = [&](size_t layer) { + const size_t first = m_layer_first_vertex[layer]; + const size_t last = (layer + 1 < layers_count) ? m_layer_first_vertex[layer + 1] : m_vertices.size() - 1; + return std::make_pair(first, std::min(last, m_vertices.size() - 1)); + }; + const auto is_drawn_extrusion = [&](size_t i) { return m_vertices[i].is_extrusion() && m_valid_lines_bitset[i]; }; + + 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. + const auto classify_layers = [&](size_t first_layer, size_t last_layer) { + std::vector kept; + std::vector footprints(3, OccupancyGrid(nx, ny)); + OccupancyGrid shell_cells(nx, ny); + std::vector window_sum; + const auto footprint = [&](size_t layer) -> OccupancyGrid& { return footprints[layer % 3]; }; + const auto prepare = [&](size_t layer) { + OccupancyGrid& g = footprint(layer); + g.clear(); + const auto [first, last] = layer_segments(layer); + for (size_t i = first; i < last; ++i) { + if (is_drawn_extrusion(i)) + for_each_cell(i, [&](int x, int y) { g.set(x, y); }); + } + close_gaps(g, radius, window_sum); + }; + + if (first_layer > 0) + prepare(first_layer - 1); + prepare(first_layer); + for (size_t layer = first_layer; layer < last_layer; ++layer) { + if (layer + 1 < layers_count) + prepare(layer + 1); + const OccupancyGrid& below = (layer > 0) ? footprint(layer - 1) : nothing; + const OccupancyGrid& cur = footprint(layer); + const OccupancyGrid& above = (layer + 1 < layers_count) ? footprint(layer + 1) : nothing; + + shell_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)) + shell_cells.set(x, y); + } + } + + const auto [first, last] = layer_segments(layer); + for (size_t i = first; i < last; ++i) { + if (!is_drawn_extrusion(i)) + continue; + int total = 0; + int on_shell = 0; + for_each_cell(i, [&](int x, int y) { ++total; on_shell += shell_cells.at(x, y); }); + if (2 * on_shell >= total) + kept.push_back(static_cast(i)); + } + } + return kept; + }; + + 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; + 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()) + m_shell_bitset.set(i); + } +} #endif // ENABLE_OPENGL_ES void ViewerImpl::update_enabled_entities() @@ -1189,12 +1433,17 @@ void ViewerImpl::update_enabled_entities() std::vector enabled_options; #ifndef ENABLE_OPENGL_ES // ORCA: the reduced sets are filled by the same walk, so switching to them costs no rebuild. + const bool build_reduced = m_settings.reduced_detail_mode != EReducedDetailMode::Off; std::vector enabled_segments_reduced; std::vector enabled_options_reduced; const uint32_t layer_stride = std::max(1, m_settings.reduced_detail_layer_stride); - // Whatever else is dropped, the top of the visible layer range is kept whole: it is the surface - // the user is looking at, and in top-layer-only mode it is the only layer drawn at full color. - const uint32_t kept_layer = m_layers.get_view_range()[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 && + m_shell_bitset.size != m_vertices.size()) + update_shell_bitset(); #endif // ENABLE_OPENGL_ES Interval range = m_view_range.get_visible(); @@ -1245,11 +1494,14 @@ void ViewerImpl::update_enabled_entities() enabled_segments.push_back(static_cast(i)); #ifndef ENABLE_OPENGL_ES - if (v.layer_id != kept_layer && (v.layer_id % layer_stride) != 0) + if (!build_reduced) + continue; + const bool whole_layer = v.layer_id == layers_range[0] || v.layer_id == layers_range[1]; + if (!whole_layer && (v.layer_id % layer_stride) != 0) continue; if (v.is_option()) enabled_options_reduced.push_back(static_cast(i)); - else if (!(v.is_extrusion() && is_interior_infill(v.role))) + else if (whole_layer || !v.is_extrusion() || reduced_set_keeps(i, v)) enabled_segments_reduced.push_back(static_cast(i)); #endif // ENABLE_OPENGL_ES } @@ -1283,15 +1535,17 @@ void ViewerImpl::update_enabled_entities() m_enabled_segments_reduced_count = enabled_segments_reduced.size(); m_enabled_options_reduced_count = enabled_options_reduced.size(); - 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)); + 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)); + 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 @@ -1471,8 +1725,16 @@ void ViewerImpl::toggle_top_layer_only_view_range() update_colors_texture(); } -// ORCA: how many layers the reduced set keeps one of. Changing it changes which vertices land in -// the reduced set, so the sets have to be rebuilt. +// ORCA: what the reduced set leaves out, and how many layers it keeps one of. Either changes which +// vertices land in the reduced set, so the sets have to be rebuilt. +void ViewerImpl::set_reduced_detail_mode(EReducedDetailMode mode) +{ + if (m_settings.reduced_detail_mode == mode) + return; + m_settings.reduced_detail_mode = mode; + m_settings.update_enabled_entities = true; +} + void ViewerImpl::set_reduced_detail_layer_stride(uint32_t value) { value = std::max(1, value); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index f9764fa408..421e1bc7ed 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -97,6 +97,9 @@ public: // void set_reduced_detail(bool value) { m_settings.reduced_detail = value; } bool is_reduced_detail() const { return m_settings.reduced_detail; } + EReducedDetailMode get_reduced_detail_mode() const { return m_settings.reduced_detail_mode; } + void set_reduced_detail_mode(EReducedDetailMode mode); + uint32_t get_reduced_detail_layer_stride() const { return m_settings.reduced_detail_layer_stride; } void set_reduced_detail_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); @@ -316,6 +319,13 @@ private: // Variables used for toolpaths visibiliity // BitSet<> m_valid_lines_bitset; +#ifndef ENABLE_OPENGL_ES + // + // ORCA: bit set for the extrusion segments that lie on the visible surface of the print, + // computed on demand by update_shell_bitset() for EReducedDetailMode::ShellOnly + // + BitSet<> m_shell_bitset; +#endif // ENABLE_OPENGL_ES // // Variables used for toolpaths coloring // @@ -505,25 +515,33 @@ private: 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 only while the user is dragging. + // The set the next draw reads from: the reduced one only while the user is dragging, and only + // if a reduced set is being built at all. + bool use_reduced_set() const { + return m_settings.reduced_detail && m_settings.reduced_detail_mode != EReducedDetailMode::Off; + } size_t active_segments_count() const { - return m_settings.reduced_detail ? m_enabled_segments_reduced_count : m_enabled_segments_count; + return use_reduced_set() ? m_enabled_segments_reduced_count : m_enabled_segments_count; } unsigned int active_segments_buf_id() const { - return m_settings.reduced_detail ? m_enabled_segments_reduced_buf_id : m_enabled_segments_buf_id; + return use_reduced_set() ? m_enabled_segments_reduced_buf_id : m_enabled_segments_buf_id; } unsigned int active_segments_tex_id() const { - return m_settings.reduced_detail ? m_enabled_segments_reduced_tex_id : m_enabled_segments_tex_id; + return use_reduced_set() ? m_enabled_segments_reduced_tex_id : m_enabled_segments_tex_id; } size_t active_options_count() const { - return m_settings.reduced_detail ? m_enabled_options_reduced_count : m_enabled_options_count; + return use_reduced_set() ? m_enabled_options_reduced_count : m_enabled_options_count; } unsigned int active_options_buf_id() const { - return m_settings.reduced_detail ? m_enabled_options_reduced_buf_id : m_enabled_options_buf_id; + return use_reduced_set() ? m_enabled_options_reduced_buf_id : m_enabled_options_buf_id; } unsigned int active_options_tex_id() const { - return m_settings.reduced_detail ? m_enabled_options_reduced_tex_id : m_enabled_options_tex_id; + return use_reduced_set() ? m_enabled_options_reduced_tex_id : m_enabled_options_tex_id; } + + // Whether the segment starting at vertex i belongs to the reduced set under the current mode + bool reduced_set_keeps(size_t i, const PathVertex& v) const; + void update_shell_bitset(); #endif // ENABLE_OPENGL_ES void update_view_full_range(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b13f65ce7c..c5f79863b0 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1172,8 +1172,13 @@ 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(); - // ORCA: darken the layers the preview layer slider is not scrubbed to + // ORCA: simplify the preview while the user is dragging m_reduced_detail_while_dragging = get_app_config()->get_bool("preview_reduced_detail_while_dragging"); + m_reduced_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_reduced_detail_mode")); + m_reduced_detail_layer_stride = static_cast(std::max(1, std::stoi(get_app_config()->get("preview_reduced_detail_layer_stride")))); + apply_reduced_detail_settings(); + + // 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"))); @@ -1908,13 +1913,44 @@ void GCodeViewer::update_layers_slider_mode() // TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder); } -bool GCodeViewer::set_interacting(bool interacting) +void GCodeViewer::set_interacting(bool interacting) { - const bool reduced = m_reduced_detail_while_dragging && interacting; - if (reduced == m_viewer.is_reduced_detail()) - return false; - m_viewer.set_reduced_detail(reduced); - return true; + m_viewer.set_reduced_detail(m_reduced_detail_while_dragging && interacting); +} + +// 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() +{ + m_viewer.set_reduced_detail_mode(m_reduced_detail_while_dragging ? m_reduced_detail_mode : libvgcode::EReducedDetailMode::Off); + m_viewer.set_reduced_detail_layer_stride(m_reduced_detail_layer_stride); +} + +void GCodeViewer::set_reduced_detail_while_dragging(bool value) +{ + m_reduced_detail_while_dragging = value; + apply_reduced_detail_settings(); +} + +void GCodeViewer::set_reduced_detail_mode(const std::string& mode) +{ + m_reduced_detail_mode = reduced_detail_mode_from_string(mode); + apply_reduced_detail_settings(); +} + +void GCodeViewer::set_reduced_detail_layer_stride(unsigned int value) +{ + m_reduced_detail_layer_stride = std::max(1u, value); + apply_reduced_detail_settings(); +} + +libvgcode::EReducedDetailMode GCodeViewer::reduced_detail_mode_from_string(const std::string& mode) +{ + if (mode == "layers") + return libvgcode::EReducedDetailMode::LayersOnly; + if (mode == "shell") + return libvgcode::EReducedDetailMode::ShellOnly; + return libvgcode::EReducedDetailMode::NoInternalInfill; } void GCodeViewer::set_layers_z_range(const std::array& layers_z_range) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index f8ebe8d9fc..2dcfb2a634 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -230,8 +230,11 @@ private: bool m_legend_visible{ true }; bool m_legend_enabled{ true }; - // ORCA: whether the reduced-detail-while-dragging preference is on + // ORCA: the reduced-detail-while-dragging preferences, pushed to libvgcode by apply_reduced_detail_settings() bool m_reduced_detail_while_dragging{ false }; + libvgcode::EReducedDetailMode m_reduced_detail_mode{ libvgcode::EReducedDetailMode::NoInternalInfill }; + unsigned int m_reduced_detail_layer_stride{ 4 }; + void apply_reduced_detail_settings(); float m_legend_height; PrintEstimatedStatistics m_print_statistics; @@ -342,10 +345,14 @@ public: float get_dim_previous_layers_brightness() const { return m_viewer.get_dim_previous_layers_brightness(); } // ORCA: while the user drags the camera or a slider, draw the preview from libvgcode's reduced - // toolpath set. Returns true when the detail level actually changed, so the caller can queue the - // frame that puts the full detail back. - bool set_interacting(bool interacting); - void set_reduced_detail_while_dragging(bool value) { m_reduced_detail_while_dragging = value; } + // toolpath set, if the preference asks for one + void set_interacting(bool interacting); + bool is_reduced_detail() const { return m_viewer.is_reduced_detail(); } + void set_reduced_detail_while_dragging(bool value); + // the preference's string value: "layers", "no_infill" or "shell" + void set_reduced_detail_mode(const std::string& mode); + void set_reduced_detail_layer_stride(unsigned int value); + static libvgcode::EReducedDetailMode reduced_detail_mode_from_string(const std::string& mode); void set_layers_z_range(const std::array& layers_z_range); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 275eb2d9cf..8bd1e66397 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3240,6 +3240,12 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt) m_dirty |= imgui_requires_extra_frame; #endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh(); + // ORCA: the render timer only wakes the idle loop; the frame that puts the preview's full detail + // back after a wheel burst has to be asked for here, once the settle time is really up + if (m_preview_settle_pending && std::chrono::steady_clock::now() >= m_preview_interaction_until) { + m_preview_settle_pending = false; + m_dirty = true; + } if (!m_dirty) return; @@ -3945,6 +3951,9 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) evt.SetY(evt.GetY() * scale); #endif + if (m_canvas_type == CanvasPreview) + note_preview_interaction(); + if (wxGetApp().imgui()->update_mouse_data(evt)) { if (m_canvas_type == CanvasPreview) { IMSlider* m_layers_slider = get_gcode_viewer().get_layers_slider(); @@ -4051,6 +4060,11 @@ void GLCanvas3D::on_set_color_timer(wxTimerEvent& evt) } +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 @@ -8513,11 +8527,19 @@ void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height) IMSlider *layers_slider = m_gcode_viewer.get_layers_slider(); IMSlider *moves_slider = m_gcode_viewer.get_moves_slider(); - // ORCA: dragging the camera or either slider is the only time the preview has to keep up with - // continuous input, so that is when the reduced toolpath set earns its visible coarseness. - // A change of detail level needs one more frame to draw the result of the change. - if (m_gcode_viewer.set_interacting(m_mouse.dragging || layers_slider->is_dirty() || moves_slider->is_dirty())) - request_extra_frame(); + // ORCA: dragging the camera, the navigator or either slider is when the preview has to keep up + // with continuous input, so that is when the reduced toolpath set earns its visible coarseness. + // A wheel step has no duration, so it holds the reduced set for a settle time instead, and the + // frame that restores the full detail is scheduled for when that time runs out. The level is + // chosen before the draw, so a change lands in this very frame. + 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(); + 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); + } m_gcode_viewer.render(canvas_width, canvas_height, SLIDER_RIGHT_MARGIN * GCODE_VIEWER_SLIDER_SCALE); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 84dbd5d652..b427fb634e 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -591,6 +591,10 @@ private: ECursorType m_cursor_type; GLSelectionRectangle m_rectangle_selection; bool m_navigator_dragging{ false }; + // ORCA: until when a discrete preview interaction (a wheel step) keeps the reduced toolpath set bound + std::chrono::time_point m_preview_interaction_until{}; + // whether the frame that restores the full detail 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; @@ -1138,6 +1142,9 @@ public: void msw_rescale() { m_gcode_viewer.invalidate_legend(); } void request_extra_frame() { m_extra_frame_requested = true; } + // ORCA: a wheel step is over before the next frame, so it holds the reduced preview for a short + // settle time instead: a burst of steps stays cheap and the full frame lands once they stop + void note_preview_interaction(); void schedule_extra_frame(int milliseconds); diff --git a/src/slic3r/GUI/IMSlider.cpp b/src/slic3r/GUI/IMSlider.cpp index c008963646..3ac12f3c1d 100644 --- a/src/slic3r/GUI/IMSlider.cpp +++ b/src/slic3r/GUI/IMSlider.cpp @@ -482,6 +482,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(); @@ -490,6 +495,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); @@ -882,6 +888,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..599ab8c82d 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; } + // ORCA: whether the mouse is currently 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; + // ORCA: 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 6229ce207e..b92d302b9a 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -318,7 +318,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too return sizer; } -wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index, const wxString wiki_url) +wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index, std::function onchange, const wxString wiki_url) { assert(vlist.size() == config_name_index.size()); unsigned int current_index = 0; @@ -333,9 +333,16 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index); + // ORCA: this one is only meaningful while the simplification it configures is enabled + if (param == "preview_reduced_detail_mode") { + m_reduced_detail_mode_combo = combobox; + combobox->Enable(app_config->get_bool("preview_reduced_detail_while_dragging")); + } + //// save config - combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index](wxCommandEvent& e) { + combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index, onchange](wxCommandEvent& e) { app_config->set(param, config_name_index[e.GetSelection()]); + if (onchange != nullptr) onchange(config_name_index[e.GetSelection()]); e.Skip(); }); @@ -700,11 +707,15 @@ wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString tit auto input = new SpinInput(m_parent, wxEmptyString, side_label, wxDefaultPosition, DESIGN_INPUT_SIZE, wxSP_ARROW_KEYS, min, max, stoi(app_config->get(param))); input->SetToolTip(tip); - // ORCA: this one is only meaningful while the dimming it controls is enabled + // ORCA: these are only meaningful while the option they belong to is enabled if (param == "preview_dim_previous_layers_brightness") { m_dim_previous_layers_brightness_input = input; input->Enable(app_config->get_bool("preview_dim_previous_layers")); } + else if (param == "preview_reduced_detail_layer_stride") { + m_reduced_detail_layer_stride_input = input; + input->Enable(app_config->get_bool("preview_reduced_detail_while_dragging")); + } m_sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL); @@ -1058,6 +1069,10 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too } // ORCA: apply the reduced-detail preference immediately to the currently loaded preview else if (param == "preview_reduced_detail_while_dragging") { + if (m_reduced_detail_mode_combo) + m_reduced_detail_mode_combo->Enable(app_config->get_bool(param)); + if (m_reduced_detail_layer_stride_input) + m_reduced_detail_layer_stride_input->Enable(app_config->get_bool(param)); if (Plater* plater = wxGetApp().plater()) { if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) { canvas->get_gcode_viewer().set_reduced_detail_while_dragging(app_config->get_bool(param)); @@ -1950,11 +1965,57 @@ void PreferencesDialog::create_items() auto item_reduced_detail_while_dragging = create_item_checkbox( _L("Simplify preview while dragging"), - _L("While dragging the camera or a preview slider, draw only part of the toolpaths so that large prints stay responsive. Internal infill is left out and only every fourth layer is drawn, and the full detail is restored as soon as you let go."), + _L("While dragging the camera or a preview slider, or zooming with the mouse wheel, draw only part of the toolpaths so that large prints stay responsive. " + "The two options below choose what is left out. The full detail is restored as soon as you let go."), "preview_reduced_detail_while_dragging" ); g_sizer->Add(item_reduced_detail_while_dragging); + auto item_reduced_detail_mode = create_item_combobox( + _L("Simplification"), + _L("What the simplified preview leaves out while dragging, on top of skipping layers.\n" + "Skip layers only: every toolpath of the drawn layers is kept.\n" + "Skip internal infill: sparse and solid infill hidden inside the walls is left out.\n" + "Shell only: only the toolpaths on the visible surface of the print are drawn, including the outside of the prime tower. " + "Removes the most, and holes narrower than 5 mm are treated as solid."), + "preview_reduced_detail_mode", + {_L("Skip layers only"), _L("Skip internal infill"), _L("Shell only")}, + {"layers", "no_infill", "shell"}, + // ORCA: apply the new mode immediately to the currently loaded preview + [](std::string value) { + if (Plater* plater = wxGetApp().plater()) { + if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) { + canvas->get_gcode_viewer().set_reduced_detail_mode(value); + canvas->set_as_dirty(); + canvas->request_extra_frame(); + } + } + } + ); + g_sizer->Add(item_reduced_detail_mode); + + auto item_reduced_detail_layer_stride = create_item_spinctrl( + _L("Draw one layer in every"), + "", + _L("layers"), + _L("How many layers the simplified preview keeps one of while dragging. 1 draws every layer, 4 draws every fourth. " + "The bottom and top of the visible layer range are always drawn whole."), + "preview_reduced_detail_layer_stride", + 1, + 20, + // ORCA: apply the new stride immediately to the currently loaded preview + [](int value) { + if (Plater* plater = wxGetApp().plater()) { + if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) { + canvas->get_gcode_viewer().set_reduced_detail_layer_stride(static_cast(value)); + canvas->set_as_dirty(); + canvas->request_extra_frame(); + } + } + } + ); + g_sizer->Add(item_reduced_detail_layer_stride); + 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."), diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index 94340ce619..96b20c4b7a 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -73,6 +73,8 @@ public: ::CheckBox * m_bambu_cloud_checkbox = {nullptr}; ::TextInput *m_backup_interval_textinput = {nullptr}; ::SpinInput *m_dim_previous_layers_brightness_input = {nullptr}; + ::ComboBox * m_reduced_detail_mode_combo = {nullptr}; + ::SpinInput *m_reduced_detail_layer_stride_input = {nullptr}; ::ComboBox * m_network_version_combo = {nullptr}; std::vector m_available_versions; @@ -86,7 +88,7 @@ public: wxBoxSizer *create_item_title(wxString title); wxBoxSizer *create_item_label(wxString label, const wxString tooltip = "", const wxString wiki_url = ""); wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::function onchange = {}, const wxString wiki_url = ""); - wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index, const wxString wiki_url = ""); + wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector vlist, std::vector config_name_index, std::function onchange = {}, const wxString wiki_url = ""); wxBoxSizer *create_item_region_combobox(wxString title, wxString tooltip); wxBoxSizer *create_item_language_combobox(wxString title, wxString tooltip); wxBoxSizer *create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector vlist);