diff --git a/docs/HLSD/gcode-preview-dragging.md b/docs/HLSD/gcode-preview-dragging.md index d2935f84e9..600c7f5cd6 100644 --- a/docs/HLSD/gcode-preview-dragging.md +++ b/docs/HLSD/gcode-preview-dragging.md @@ -9,7 +9,7 @@ let go. | Preference | Values | Effect | |---|---|---| -| `preview_reduced_detail_mode` | `off`, `solid`, `layers`, `outer_walls` | what is drawn while dragging | +| `preview_reduced_detail_mode` | `off`, `solid`, `layers`, `outer_walls`, `shell` | what is drawn while dragging | | `preview_reduced_detail_layer_stride` | 1–20 | one layer in every N is kept by the toolpath modes | libvgcode (`src/libvgcode`) builds and binds the reduced toolpath set, `GCodeViewer` maps the @@ -35,6 +35,10 @@ are the faces the range cuts open, and the top is what the user is looking at. - `LayersOnly` (`layers`) keeps every role of one layer in every stride. - `OuterWallsOnly` (`outer_walls`) keeps the outer and overhang perimeters of one layer in every stride. The prime tower and supports have other roles and are left out. +- `ShellOnly` (`shell`) keeps what the shell extraction below marks as visible surface, of one + layer in every stride, plus whatever a view from above or below sees of the skipped layers, so + that a step does not vanish. It is the only mode that knows the prime tower's outside from its + inside. ## The solid model @@ -51,6 +55,43 @@ and `load_shells()` drops every non-model-part volume, so a negative volume is n A plate whose shells are not loaded keeps drawing toolpaths, since the solid model would leave only the end layers. +## Shell extraction + +`ViewerImpl::update_shell_bitset()` classifies every extrusion segment once per load, on demand +the first time the shell mode needs it, and records the result in four bit sets. It is purely +geometric so that the wipe tower, whose every segment shares one role, works as well as the +objects. + +Each layer is rasterized into a coarse 2D **occupancy grid** over the print's footprint: cells +are 0.5 mm, or coarser so that the grid is at most 1024 cells across. The footprint is then +**closed** with a radius of 2.5 mm so that sparse infill and support read as the solid area they +belong to, while holes wider than 5 mm stay open. The closing dilates each 8-connected component +separately and leaves a cell that two components both reach empty, so the gap between two objects +standing close together is never bridged and both of their facing walls stay on the shell; +fragments under eight cells do not spread and are absorbed by whatever reaches them. A separable +erosion shrinks the result back, and the raw cells are OR-ed in again so that a closing never +loses one. + +A cell is a **shell cell** when it is filled and any of its six neighbours, four in the layer, +one below, one above, is not. A segment is on the shell when at least half of the cells it +crosses are shell cells: walls run along the shell, infill only touches it at the ends. The +interior infill roles and gap fill are excluded regardless, since short infill segments hugging a +wall would otherwise pass by the thousand. + +Two refinements keep sloped surfaces closed: + +- **Near-shell inner walls.** The step between one layer's outer wall and the next is often + narrower than a cell. An inner wall (`Perimeter`) segment whose midpoint lies within a line and + a half of an outer or overhang perimeter of the same layer is kept as well. +- **Top and bottom visibility.** The same pass records the highest and lowest layer occupying + each cell over the whole print. A segment whose layer is the topmost occupant of any cell it + crosses is visible from above, and likewise from below with the lowest. These segments are kept + even when their layer is skipped by the stride. + +The layer range is split across up to eight `std::async` workers, each owning its grids. An +allocation failure on a huge print falls back to marking every segment as shell, which leaves out +only the hidden infill roles. + ## Deciding that the user is dragging `GLCanvas3D::_update_preview_interaction()` runs at the top of every preview frame, before the diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index bbc05d0c4e..0cdb4f176a 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -208,7 +208,7 @@ void AppConfig::set_defaults() // what the preview draws while the user drags it, and one layer in how many the toolpath modes keep { const std::string mode = get("preview_reduced_detail_mode"); - if (mode != "off" && mode != "solid" && mode != "layers" && mode != "outer_walls") + if (mode != "off" && mode != "solid" && mode != "layers" && mode != "outer_walls" && mode != "shell") set("preview_reduced_detail_mode", "off"); int stride = 4; try { diff --git a/src/libvgcode/include/Types.hpp b/src/libvgcode/include/Types.hpp index ee4d555ffe..6c45622669 100644 --- a/src/libvgcode/include/Types.hpp +++ b/src/libvgcode/include/Types.hpp @@ -172,6 +172,8 @@ enum class EReducedDetailMode : uint8_t LayersOnly, // one layer in every stride, outer walls only OuterWallsOnly, + // one layer in every stride, only the segments on the visible surface of the print + ShellOnly, COUNT }; diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index 92b0b25c62..0890a03afe 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -17,6 +17,10 @@ #include #include #include +#include +#include +#include +#include namespace libvgcode { @@ -895,6 +899,10 @@ void ViewerImpl::reset() m_enabled_options_reduced_count = 0; m_enabled_segments_reduced_tex_size = 0; m_enabled_options_reduced_tex_size = 0; + m_shell_bitset = BitSet<>(); + m_near_shell_bitset = BitSet<>(); + m_top_visible_bitset = BitSet<>(); + m_bottom_visible_bitset = BitSet<>(); m_settings_used_for_ranges = std::nullopt; @@ -1184,17 +1192,494 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) } #ifndef ENABLE_OPENGL_ES -bool ViewerImpl::reduced_set_keeps(const PathVertex& v) const +// what can never be a visible surface whatever the geometry says: short infill segments hugging +// a wall would otherwise pass the geometric test by the thousand +static bool is_hidden_in_shell(EGCodeExtrusionRole role) +{ + return role == EGCodeExtrusionRole::InternalInfill || + role == EGCodeExtrusionRole::SolidInfill || + role == EGCodeExtrusionRole::InternalBridgeInfill || + role == EGCodeExtrusionRole::GapFill; +} + +bool ViewerImpl::reduced_set_keeps(size_t i, const PathVertex& v) const { switch (m_settings.reduced_detail_mode) { case EReducedDetailMode::OuterWallsOnly: return v.role == EGCodeExtrusionRole::ExternalPerimeter || v.role == EGCodeExtrusionRole::OverhangPerimeter; + case EReducedDetailMode::ShellOnly: + // the first inner wall fills the step of a sloped surface between one layer's outer wall + // and the next, too narrow for the grid to see; whatever is the visible top or bottom of a + // step stays whatever its role + return (!is_hidden_in_shell(v.role) && m_shell_bitset[i]) || m_near_shell_bitset[i] || + m_top_visible_bitset[i] || m_bottom_visible_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, static_cast(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); + } +}; + +// 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, so that sparse infill 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 close objects is never bridged. +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 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); + 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)]; + cell(i) = (count == 2 * radius + 1); + } + } + }; + 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 + +// Classifies the extrusion segments for EReducedDetailMode::ShellOnly from a coarse occupancy grid +// per layer: a closed footprint cell is on the shell when any of its six neighbours is empty, and a +// segment is kept when at least half of the cells it crosses are. Purely geometric, so the wipe +// tower works as well as the objects. The same pass records the highest and lowest layer occupying +// each cell, which tells what a view from above or below sees; see docs/HLSD/gcode-preview-dragging.md. +void ViewerImpl::update_shell_bitset() +{ + m_shell_bitset = BitSet<>(m_vertices.size()); + m_near_shell_bitset = BitSet<>(m_vertices.size()); + m_top_visible_bitset = BitSet<>(m_vertices.size()); + m_bottom_visible_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_index = [nx](int x, int y) { return static_cast(y) * nx + x; }; + 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, plus the + // highest and lowest of these layers occupying each cell. Each call owns its grids, so the layer + // range can be split across threads. + static constexpr int32_t NO_LAYER = -1; + struct Kept { + std::vector shell; + std::vector near_shell; + std::vector top; + std::vector bottom; + // the rectangle of cells these layers touched, inclusive; empty while min > max + int min_x{ 0 }; + int min_y{ 0 }; + int max_x{ -1 }; + int max_y{ -1 }; + }; + const size_t cells_count = static_cast(nx) * static_cast(ny); + const auto classify_layers = [&](size_t first_layer, size_t last_layer) { + Kept kept; + kept.top.assign(cells_count, NO_LAYER); + kept.bottom.assign(cells_count, NO_LAYER); + std::vector footprints(3, OccupancyGrid(nx, ny)); + OccupancyGrid shell_cells(nx, ny); + // the outer wall segments of the layer, by every cell they cross + std::unordered_map> outer_walls_by_cell; + ClosingScratch scratch; + 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, scratch); + }; + + 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(); + if (!cur.empty()) { + kept.min_x = (kept.max_x < kept.min_x) ? cur.min_x : std::min(kept.min_x, cur.min_x); + kept.min_y = (kept.max_y < kept.min_y) ? cur.min_y : std::min(kept.min_y, cur.min_y); + kept.max_x = std::max(kept.max_x, cur.max_x); + kept.max_y = std::max(kept.max_y, cur.max_y); + } + 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; + // layers come in ascending order, so the first occupant is the lowest + int32_t& top = kept.top[cell_index(x, y)]; + int32_t& bottom = kept.bottom[cell_index(x, y)]; + top = static_cast(layer); + if (bottom == NO_LAYER) + bottom = static_cast(layer); + if (!below.at(x, y) || !above.at(x, y) || + !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); + } + } + const auto [first, last] = layer_segments(layer); + outer_walls_by_cell.clear(); + for (size_t i = first; i < last; ++i) { + const EGCodeExtrusionRole role = m_vertices[i].role; + if (is_drawn_extrusion(i) && (role == EGCodeExtrusionRole::ExternalPerimeter || role == EGCodeExtrusionRole::OverhangPerimeter)) + for_each_cell(i, [&](int x, int y) { outer_walls_by_cell[cell_index(x, y)].push_back(static_cast(i)); }); + } + // an inner wall segment is the first inner wall when its midpoint lies within a line + // and a half of an outer wall segment of the same layer + const auto beside_outer_wall = [&](size_t i) { + const Vec3& a = m_vertices[i].position; + const Vec3& b = m_vertices[i + 1].position; + const float mx = 0.5f * (a[0] + b[0]); + const float my = 0.5f * (a[1] + b[1]); + const float reach = 1.5f * m_vertices[i].width; + const auto [cx, cy] = cell_of(mx, my); + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + const auto it = outer_walls_by_cell.find(cell_index(cx + dx, cy + dy)); + if (it == outer_walls_by_cell.end()) + continue; + for (uint32_t o : it->second) { + const Vec3& p = m_vertices[o].position; + const Vec3& q = m_vertices[o + 1].position; + const float ex = q[0] - p[0]; + const float ey = q[1] - p[1]; + const float len2 = ex * ex + ey * ey; + const float t = (len2 > 0.0f) ? std::clamp(((mx - p[0]) * ex + (my - p[1]) * ey) / len2, 0.0f, 1.0f) : 0.0f; + const float ddx = mx - (p[0] + t * ex); + const float ddy = my - (p[1] + t * ey); + if (ddx * ddx + ddy * ddy <= reach * reach) + return true; + } + } + } + return false; + }; + + 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.shell.push_back(static_cast(i)); + if (m_vertices[i].role == EGCodeExtrusionRole::Perimeter && beside_outer_wall(i)) + kept.near_shell.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))); + std::vector top_layer(cells_count, NO_LAYER); + std::vector bottom_layer(cells_count, NO_LAYER); + for (auto& f : futures) { + const Kept kept = f.get(); + for (uint32_t i : kept.shell) + m_shell_bitset.set(i); + for (uint32_t i : kept.near_shell) + m_near_shell_bitset.set(i); + for (int y = kept.min_y; y <= kept.max_y; ++y) { + for (int x = kept.min_x; x <= kept.max_x; ++x) { + const size_t c = cell_index(x, y); + if (kept.top[c] == NO_LAYER) + continue; + top_layer[c] = std::max(top_layer[c], kept.top[c]); + bottom_layer[c] = (bottom_layer[c] == NO_LAYER) ? kept.bottom[c] : std::min(bottom_layer[c], kept.bottom[c]); + } + } + } + + // A segment is visible from straight above when its layer is the topmost occupant of any of its + // cells, and from below likewise with the bottommost: the exposed band of a sloped surface is + // narrower than the infill chords that fill it, so touching it is what counts. + struct Visible { std::vector top; std::vector bottom; }; + const auto find_visible = [&](size_t first_layer, size_t last_layer) { + Visible visible; + for (size_t layer = first_layer; layer < last_layer; ++layer) { + 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_top = 0; + int on_bottom = 0; + for_each_cell(i, [&](int x, int y) { + ++total; + on_top += top_layer[cell_index(x, y)] == static_cast(layer); + on_bottom += bottom_layer[cell_index(x, y)] == static_cast(layer); + }); + if (on_top > 0) + visible.top.push_back(static_cast(i)); + if (on_bottom > 0) + visible.bottom.push_back(static_cast(i)); + } + } + return visible; + }; + std::vector> visible_futures; + for (size_t first = 0; first < layers_count; first += chunk) + visible_futures.emplace_back(std::async(std::launch::async, find_visible, first, std::min(layers_count, first + chunk))); + for (auto& f : visible_futures) { + const Visible visible = f.get(); + for (uint32_t i : visible.top) + m_top_visible_bitset.set(i); + for (uint32_t i : visible.bottom) + m_bottom_visible_bitset.set(i); + } +} #endif // ENABLE_OPENGL_ES + void ViewerImpl::update_enabled_entities() { if (m_vertices.empty()) @@ -1212,6 +1697,21 @@ void ViewerImpl::update_enabled_entities() std::vector enabled_segments_reduced; std::vector enabled_options_reduced; const Interval& layers_range = m_layers.get_view_range(); + // the shell is classified once per load, the first time it is needed + const bool shell_reduced = reduced_mode == EReducedDetailMode::ShellOnly; + if (shell_reduced && m_shell_bitset.size != m_vertices.size()) { + try { + update_shell_bitset(); + } + catch (...) { + // out of memory on a huge print: take everything for shell, which leaves out only the hidden infill + m_shell_bitset = BitSet<>(m_vertices.size()); + m_shell_bitset.setAll(); + m_near_shell_bitset = BitSet<>(m_vertices.size()); + m_top_visible_bitset = BitSet<>(m_vertices.size()); + m_bottom_visible_bitset = BitSet<>(m_vertices.size()); + } + } #endif // ENABLE_OPENGL_ES Interval range = m_view_range.get_visible(); @@ -1266,10 +1766,15 @@ void ViewerImpl::update_enabled_entities() const bool end_layer = v.layer_id == layers_range[0] || v.layer_id == layers_range[1]; if (end_layer) (v.is_option() ? enabled_options_reduced : enabled_segments_reduced).push_back(static_cast(i)); - else if (reduced_mode != EReducedDetailMode::EndLayersOnly && (v.layer_id % layer_stride) == 0) { - if (v.is_option()) - enabled_options_reduced.push_back(static_cast(i)); - else if (!v.is_extrusion() || reduced_set_keeps(v)) + else if (reduced_mode != EReducedDetailMode::EndLayersOnly) { + if ((v.layer_id % layer_stride) == 0) { + if (v.is_option()) + enabled_options_reduced.push_back(static_cast(i)); + else if (!v.is_extrusion() || reduced_set_keeps(i, v)) + enabled_segments_reduced.push_back(static_cast(i)); + } + // the surfaces of a skipped layer that either side can see stay, so that a step does not vanish + else if (shell_reduced && v.is_extrusion() && (m_top_visible_bitset[i] || m_bottom_visible_bitset[i])) enabled_segments_reduced.push_back(static_cast(i)); } } @@ -1869,6 +2374,12 @@ size_t ViewerImpl::get_used_cpu_memory() const ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t); ret += STDVEC_MEMSIZE(m_colors_scratch, float); ret += m_valid_lines_bitset.size_in_bytes_cpu(); +#ifndef ENABLE_OPENGL_ES + ret += m_shell_bitset.size_in_bytes_cpu(); + ret += m_near_shell_bitset.size_in_bytes_cpu(); + ret += m_top_visible_bitset.size_in_bytes_cpu(); + ret += m_bottom_visible_bitset.size_in_bytes_cpu(); +#endif // ENABLE_OPENGL_ES ret += m_height_range.size_in_bytes_cpu(); ret += m_width_range.size_in_bytes_cpu(); ret += m_speed_range.size_in_bytes_cpu(); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index c998a0ec72..ef6ab7c35c 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -317,6 +317,17 @@ private: // Variables used for toolpaths visibiliity // BitSet<> m_valid_lines_bitset; +#ifndef ENABLE_OPENGL_ES + // + // Extrusion segments classified by update_shell_bitset() for EReducedDetailMode::ShellOnly: on + // the visible surface, the first inner wall beside an outer wall, visible from straight above, + // visible from straight below + // + BitSet<> m_shell_bitset; + BitSet<> m_near_shell_bitset; + BitSet<> m_top_visible_bitset; + BitSet<> m_bottom_visible_bitset; +#endif // ENABLE_OPENGL_ES // // Variables used for toolpaths coloring // @@ -510,8 +521,9 @@ private: // 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_mode != EReducedDetailMode::Off; } - // Whether an extrusion segment belongs to the reduced set under the current mode - bool reduced_set_keeps(const PathVertex& v) const; + // Whether the extrusion 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(); struct ActiveSet { size_t count{ 0 }; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index d2983604fe..e39c7c25e0 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1975,6 +1975,8 @@ libvgcode::EReducedDetailMode GCodeViewer::reduced_detail_mode_from_string(const return libvgcode::EReducedDetailMode::LayersOnly; if (mode == "outer_walls") return libvgcode::EReducedDetailMode::OuterWallsOnly; + if (mode == "shell") + return libvgcode::EReducedDetailMode::ShellOnly; return libvgcode::EReducedDetailMode::Off; } diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index b43d86b724..95a08b2ace 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -364,7 +364,7 @@ public: // while the user drags the camera or a slider, draw the reduced set, if the preference asks for one void set_interacting(bool interacting); bool is_reduced_detail() const { return m_viewer.is_reduced_detail(); } - // the preference's string value: "off", "solid", "layers" or "outer_walls" + // the preference's string value: "off", "solid", "layers", "outer_walls" or "shell" void set_reduced_detail_mode(const std::string& mode); void set_reduced_detail_layer_stride(unsigned int value); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 928fd12a16..e165765549 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -688,7 +688,7 @@ wxBoxSizer *PreferencesDialog::create_item_input(wxString title, wxString title2 // the reduced-detail modes that keep one layer in every N, so the stride applies static bool reduced_detail_mode_skips_layers(const std::string& mode) { - return mode == "layers" || mode == "outer_walls"; + return mode == "layers" || mode == "outer_walls" || mode == "shell"; } wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString title2, wxString side_label, wxString tooltip, std::string param, int min, int max, std::function onchange, const wxString wiki_url) @@ -2040,10 +2040,12 @@ void PreferencesDialog::create_items() "with its bottom and top layers drawn as toolpaths. Supports are not shown, and negative volumes are not cut out.\n" "Skip layers: the toolpaths of one layer in every N, set below.\n" "Outer walls: only the outer walls of one layer in every N. The prime tower and supports are left out.\n" + "Shell only: only the toolpaths on the visible surface of the print, including the prime tower, of one layer in every N. " + "Removes the most; holes narrower than 5 mm are treated as solid.\n" "The bottom and top of the visible layer range are always drawn whole."), "preview_reduced_detail_mode", - {_L("Off"), _L("Solid model"), _L("Skip layers"), _L("Outer walls")}, - {"off", "solid", "layers", "outer_walls"}, + {_L("Off"), _L("Solid model"), _L("Skip layers"), _L("Outer walls"), _L("Shell only")}, + {"off", "solid", "layers", "outer_walls", "shell"}, // apply the new mode immediately to the currently loaded preview [this](std::string value) { if (m_reduced_detail_layer_stride_input)