Keep Facing Walls on the Shell and Merge Sub-Pixel Layers at Rest

Two cubes standing a few millimetres apart lost the walls that face each
other: the gap-closing that makes sparse infill read as solid bridged the gap
as well, so the footprint became one block and those walls were no longer on
its boundary. The closing now dilates each connected component on its own and
leaves a cell two components both reach empty, so no gap between objects is
ever bridged, and the raw cells are put back after the erosion, which would
otherwise eat into a wall that faces such a gap. On a two-cube fixture with a
2 mm gap every facing wall segment is kept where none was before.

Looking from above, every layer's walls project onto the same outline, yet
each cost a segment. With the shell drawn at rest the walls of one layer in N
are now drawn, N layers tall through a shader uniform so the wall stays solid,
while the exposed top and bottom surfaces of every layer stay so that no step
disappears. N follows the view: how many layers fit in two pixels at the
current zoom and tilt, from 1 side-on and close up to 64 straight down, and it
is left alone while the user is dragging since a change rebuilds the sets. The
same taller drawing applies to the dragging set, which stops it looking
striped, and in shell mode the dragging set keeps the exposed surfaces of the
layers it skips too. The first layer's grown boxes are clamped at the bed.

On the tall fixture with the shell at rest, against 334 ms at full detail:

  default view    37585 segments   85 ms   (was 215797 / 181 ms)
  from above      28682 segments   58 ms

The classification now records which shell segments are exposed from above or
below, in a second bit set alongside the first.
This commit is contained in:
Hanif Koh
2026-09-10 18:09:22 +08:00
parent e0275c4d69
commit f2143d3c91
9 changed files with 265 additions and 34 deletions
+7
View File
@@ -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);
//
+4
View File
@@ -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
//
+8 -1
View File
@@ -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"
+10
View File
@@ -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);
+178 -31
View File
@@ -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<int32_t> labels;
std::vector<std::pair<int, int>> frontier;
std::vector<std::pair<int, int>> next;
std::vector<int> window_sum;
std::vector<uint8_t> 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<int>& 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<size_t>(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<int32_t>& 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<uint8_t>& 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<std::pair<int, int>>& 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<std::pair<int, int>>& 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<std::pair<int, int>>& 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<int>& 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<int>& 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<int>& 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<uint32_t> shell; std::vector<uint32_t> exposed; };
const auto classify_layers = [&](size_t first_layer, size_t last_layer) {
std::vector<uint32_t> kept;
Kept kept;
std::vector<OccupancyGrid> footprints(3, OccupancyGrid(nx, ny));
OccupancyGrid shell_cells(nx, ny);
std::vector<int> 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<uint32_t>(i));
kept.shell.push_back(static_cast<uint32_t>(i));
if (2 * on_exposed >= total)
kept.exposed.push_back(static_cast<uint32_t>(i));
}
}
return kept;
@@ -1422,12 +1543,15 @@ void ViewerImpl::update_shell_bitset()
const size_t workers = std::clamp<size_t>(std::thread::hardware_concurrency(), 1, 8);
const size_t chunk = std::max<size_t>(16, (layers_count + workers - 1) / workers);
std::vector<std::future<std::vector<uint32_t>>> futures;
std::vector<std::future<Kept>> 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<uint32_t> enabled_options_reduced;
std::vector<uint32_t> enabled_segments_rest;
const uint32_t layer_stride = std::max<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(i));
continue;
}
if (v.is_option())
enabled_options_reduced.push_back(static_cast<uint32_t>(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<uint32_t>(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));
+14
View File
@@ -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<float>(std::max<uint32_t>(1, m_settings.reduced_detail_layer_stride));
if (use_rest_set() && m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly)
return static_cast<float>(std::max<uint32_t>(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;
}
+37
View File
@@ -1178,6 +1178,20 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_reduced_detail_layer_stride = static_cast<unsigned int>(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<float> 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<double>(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<unsigned int>(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()
+5
View File
@@ -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;
+2 -2
View File
@@ -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"},