Compare commits

...
Author SHA1 Message Date
Hanif Koh 8b2ae48e4a Keep the Exposed Band of a Sub-Cell Step Beside the Neighbouring Layer's Outer Wall
On a shallow surface the band of a layer that the layer above leaves exposed is
narrower than a grid cell, so whether the layer is the topmost occupant of any
cell flickers from layer to layer and the band's segments, inner walls and solid
infill hidden by role, came and went in a dashed ring.

That band is always in the same place: the strip just outside the outer wall of
the layer above, or of the layer below for the underside of an overhang. The
classifier now keeps the outer-wall cell map of the previous and next layers as
well as the current one, rotating like the footprints, and keeps any segment
whose midpoint lies within a line and a half, or a cell if larger, of a
neighbouring layer's outer wall. It can only add segments, so no hole is opened
by it; what it over-keeps is the covered strip under the neighbour's wall.
2026-09-23 12:53:58 +08:00
Hanif Koh 8095904bc2 Catch Only Allocation and Thread Failures Around the Shell Classification
The fallback that marks every segment as shell caught everything, which would
also have hidden a logic error. It now catches what the classification can
really throw on a huge print: std::bad_alloc from its grids and std::system_error
from a worker thread that cannot be launched.
2026-09-23 12:34:56 +08:00
Hanif Koh b7c86befe1 Keep the Surfaces the Roles Name in the Shell-Only Drag Mode
The shell test asks whether at least half of the cells a segment crosses lie on
the footprint's boundary. On a curved wall the boundary is a staircase of cells,
and an outer-wall segment that runs past an inside corner of that staircase
crosses cells whose four neighbours are all filled, so a short segment there
failed the test and vanished, leaving a hole with the inner wall showing through
it. A shallow dome does the same to its top skin: one layer's ring lies within
the same cells as the next layer's, so it is neither a boundary nor the topmost
occupant, and the bridges under it showed through.

Outer walls and the top and bottom skins are the surface by definition, so the
mode now keeps them whatever the grid says, and the grid decides only for the
roles that can be either: inner walls, the prime tower, supports, skirt and brim.
2026-09-23 12:34:56 +08:00
Hanif Koh 526a91d323 Add a Shell-Only Drag Mode That Keeps the Visible Surface of the Print
Outer walls is a role filter, which loses the prime tower, whose every segment
shares one role, and every top and bottom surface between the range's ends. A
fifth mode, Shell only, keeps what a geometric classification marks as the
visible surface of the print, of one layer in every N, plus whatever a view from
straight above or below sees of the layers it skips, so that a step does not
vanish.

The classification runs once per load, the first time the mode is needed, and
is purely geometric: each layer is rasterized into a half-millimetre occupancy
grid and closed so that sparse infill reads as solid, a 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. The closing dilates each connected component
on its own and leaves a cell two components reach empty, so the gap between two
close objects is never bridged. The first inner wall beside an outer wall is
kept as well, since the step of a sloped surface is narrower than a cell, and
the interior infill roles and gap fill are never taken, whatever the geometry
says. The layers are split across up to eight workers, and an allocation
failure on a huge print falls back to keeping everything but the hidden infill.

Separate from the other modes so that it can be dropped on its own.
2026-09-23 12:14:16 +08:00
Hanif Koh 493896260e Let the User Choose What the Preview Draws While Dragging
The solid model is one answer to a preview that cannot keep up with a drag; a
reviewer asked for the two that keep the drag view made of toolpaths. The
checkbox is now a combobox, "Simplify preview while dragging", with Off, Solid
model, Skip layers and Outer walls, and a spin, "Draw one layer in every N",
for the toolpath modes. Off is the default, and with it nothing is built.

Skip layers keeps one layer in every N. Outer walls keeps the outer and overhang
perimeters of those layers, so the prime tower and supports, whose segments have
other roles, drop out of it. Both keep the bottom and top layers of the visible
range whole, as the solid model does, so the faces the range cuts open stay what
was printed there.

libvgcode takes the choice as EReducedDetailMode and a stride; the sets are
rebuilt when either changes, and drawing from the reduced one is still a buffer
binding. A mode change reaches the loaded preview at once, as the checkbox did.
2026-09-23 12:14:16 +08:00
Hanif Koh ca83497bb6 Release the Reduced Set When Unused and Share the Drag Check With the Scene Cache
The reduced index buffers were only re-uploaded while the preference was on, so
the last set stayed allocated until the next load once it was switched off, and
their size was missing from get_used_gpu_memory(). They are now uploaded on every
rebuild, empty when nothing was built, and counted.

The scene cache and the solid model both asked whether the user was dragging,
with different lists: the cache knew about gizmos and the rectangle selection,
the solid model about the navigator and the sliders. One is_user_interacting()
now answers both.

The tooltip says that negative volumes are not cut out of the solid model, since
load_shells() drops every non-model-part volume, and the preferences handler
keeps the dimming comment with the branch it documents.
2026-09-23 12:13:49 +08:00
Hanif Koh 6a985744c4 Offer the Sliced Objects as a Solid Model While Dragging
The G-code preview draws every toolpath segment as an instanced box, and its frame cost is
linear in the number of segments drawn. On a plate of large objects that is enough that
dragging the camera or a preview slider cannot keep up, and no amount of per-segment work
changes that; only drawing fewer segments does.

The objects themselves are cheaper: a mesh costs its triangles once, however many layers it
has, and the preview already loads the objects as shells for its translucent ghost. A new
preference, "Only render solid model when dragging" (off by default), draws those shells
opaque in their filament colours instead of the toolpaths while the user drags.

The visible layer range still holds. The shells are cut at the range's top and bottom through
the gouraud shader's z range, and libvgcode keeps a second index buffer holding just the
range's bottom and top layers, filled in the same walk as the full one, which is drawn
afterwards so that it caps the cut with what was really printed there. Switching between the
two sets is a buffer binding, never a rebuild. The prime tower is added to the shells from its
sliced mesh while the preference is on, positioned as the print placed it; it keeps its opaque
colour and so stays out of the translucent ghost. Supports have no mesh and are not shown.

Dragging is the camera, the navigator or either slider being held; a slider reports it from
ImGui's active id, since its dirty flag is raised and consumed inside one frame. A wheel step
holds the solid model for a 150 ms settle time, with the frame that restores the toolpaths
scheduled for when it runs out. The switch is decided at the top of the frame, before the
cached scene is consulted, and a frame that switches redraws the scene. A drag cut short by
focus or capture loss is ended explicitly, and a button release wakes the idle loop, since on
some platforms no idle event follows it until the next input.

With the preference off, nothing is built and the preview is unchanged.
2026-09-23 12:13:49 +08:00
16 changed files with 1241 additions and 17 deletions
+119
View File
@@ -0,0 +1,119 @@
# G-code preview while dragging
The sliced preview draws every toolpath segment of the plate as an instanced box. On a large
plate that is tens of millions of segments, and the frame is GPU-bound: the cost is the number of
instances drawn, not anything the CPU does per frame. Dragging the camera over such a plate cannot
keep up. The `preview_reduced_detail_mode` preference (*Graphics > G-code Preview*, off by
default) lets the preview draw less while the user drags and put the full toolpaths back when they
let go.
| Preference | Values | Effect |
|---|---|---|
| `preview_reduced_detail_mode` | `off`, `solid`, `layers`, `outer_walls`, `shell` | what is drawn while dragging |
| `preview_reduced_detail_layer_stride` | 120 | one layer in every N is kept by the toolpath modes |
libvgcode (`src/libvgcode`) builds and binds the reduced toolpath set, `GCodeViewer` maps the
preferences onto it and draws the solid model, and `GLCanvas3D` decides when the user is dragging.
The OpenGL ES path keeps a single set and ignores the preference.
## Two sets, one walk
`ViewerImpl::update_enabled_entities()` walks the visible vertex range once and fills two segment
index buffers side by side: the **full** set and the **reduced** set (segments and options).
Building them together is what makes switching free: starting or ending a drag is a buffer
binding, never a rebuild. A change of mode or stride does rebuild. Nothing is built while the mode
is off, and the reduced buffers are then uploaded empty so that the last set does not stay
allocated.
Whatever the mode leaves out, the bottom and top layers of the visible range are kept whole: they
are the faces the range cuts open, and the top is what the user is looking at.
### Modes
- `EndLayersOnly` (`solid` in the preference) keeps only the two end layers. `GCodeViewer` then
draws the sliced objects and the prime tower as opaque solids, see below.
- `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
The preview already loads the sliced objects as shells for its translucent ghost.
`GCodeViewer::render_solid_model()` draws those shells opaque, in their filament colors, with the
`gouraud` shader, whose z range cuts them to the visible layer range. The two toolpath layers of
the reduced set are drawn afterwards and cap the cut with what was really printed there. The
shells hold only the objects, so while this mode is on the prime tower is added from its sliced
mesh, positioned as the print placed it. It is added or removed on its own when the mode changes,
without reloading the objects, keeps its opaque color so that it never appears among the
translucent shells, and stays out of their bounding box. Supports have no mesh and are not shown,
and `load_shells()` drops every non-model-part volume, so a negative volume is not cut out.
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-wall segments.** 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. So is any segment,
whatever its role, whose midpoint lies within that reach, or a cell if larger, of an outer wall
of the layer above or below: that strip is the exposed band of the step, which the cell tests
cannot see when it is narrower than a cell.
- **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
canvas decides whether to reuse its cached scene, so that the switch lands in that frame. Dragging
is `GLCanvas3D::is_user_interacting()`, the same answer the scene cache reads: the camera, the
navigator, a gizmo, the rectangle selection or either slider being held. A slider reports this from
ImGui's active id rather than its dirty flag, which is raised and consumed inside one frame. A
wheel step has no duration, so it holds the reduced set for a 150 ms settle time instead, and the
frame that restores the toolpaths is scheduled for when that time runs out, since the render timer
only wakes the idle loop. A drag cut short by focus or capture loss is ended explicitly, and a
button release wakes the idle loop, because on some platforms nothing else would until the next
input.
## Reused scene frames
`GLCanvas3D` keeps its last scene pass for frames that only rebuild the overlay (`SceneCache`). Its
key covers the canvas size, the camera and hover state, not what the toolpath sets draw, so a frame
that reuses the scene must never be one on which the set is switched.
`_update_preview_interaction()` therefore reports whether the bound set changed, and a frame on
which it did redraws the scene. The canvas neither captures nor reuses the scene while the user
drags, so no reduced frame outlives a drag, and the frame that ends a wheel's settle time is
requested as a full frame.
+15
View File
@@ -205,6 +205,21 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1");
// 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" && mode != "shell")
set("preview_reduced_detail_mode", "off");
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);
+19
View File
@@ -158,6 +158,25 @@ enum class EGCodeExtrusionRole : uint8_t
static constexpr std::size_t GCODE_EXTRUSION_ROLES_COUNT = static_cast<std::size_t>(EGCodeExtrusionRole::COUNT);
//
// What the reduced set, drawn while the user is dragging, holds in place of the full toolpaths
//
enum class EReducedDetailMode : uint8_t
{
// nothing: no reduced set is built
Off,
// only the bottom and top layers of the visible range, for a caller that draws the print
// itself some other way
EndLayersOnly,
// one layer in every stride, every role kept
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
};
//
// Option types
//
+12
View File
@@ -114,6 +114,18 @@ public:
//
bool is_dim_previous_layers() const;
void set_dim_previous_layers(bool value);
//
// The reduced set drawn while the user drags: what the mode keeps, one layer in every stride
// for the toolpath modes, and always the bottom and top layers of the visible range. While a
// mode is set it is built alongside the full set, so set_reduced_detail() rebuilds nothing.
// Ignored on the OpenGL ES path.
//
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);
void set_reduced_detail(bool value);
bool is_reduced_detail() const;
float get_dim_previous_layers_brightness() const;
void set_dim_previous_layers_brightness(float value);
//
+5
View File
@@ -25,6 +25,11 @@ struct Settings
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
float dim_previous_layers_brightness{ 0.4f };
bool spiral_vase_mode{ false };
// what the reduced set holds, one layer in every reduced_detail_layer_stride for the toolpath
// modes, and whether it is drawn. Ignored on the OpenGL ES path.
EReducedDetailMode reduced_detail_mode{ EReducedDetailMode::Off };
uint32_t reduced_detail_layer_stride{ 4 };
bool reduced_detail{ false };
//
// Required update flags
//
+30
View File
@@ -92,6 +92,36 @@ bool Viewer::is_dim_previous_layers() const
return m_impl->is_dim_previous_layers();
}
void Viewer::set_reduced_detail(bool value)
{
m_impl->set_reduced_detail(value);
}
bool Viewer::is_reduced_detail() const
{
return m_impl->is_reduced_detail();
}
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);
}
void Viewer::set_dim_previous_layers(bool value)
{
m_impl->set_dim_previous_layers(value);
+650 -8
View File
@@ -17,6 +17,11 @@
#include <algorithm>
#include <cmath>
#include <numeric>
#include <cfloat>
#include <future>
#include <system_error>
#include <thread>
#include <unordered_map>
namespace libvgcode {
@@ -899,9 +904,21 @@ void ViewerImpl::reset()
#else
m_enabled_segments_count = 0;
m_enabled_options_count = 0;
m_enabled_segments_reduced_count = 0;
m_enabled_options_reduced_count = 0;
m_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;
delete_textures(m_enabled_options_reduced_tex_id);
delete_buffers(m_enabled_options_reduced_buf_id);
delete_textures(m_enabled_segments_reduced_tex_id);
delete_buffers(m_enabled_segments_reduced_buf_id);
delete_textures(m_enabled_options_tex_id);
delete_buffers(m_enabled_options_buf_id);
delete_textures(m_enabled_segments_tex_id);
@@ -1161,6 +1178,17 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
glsafe(glGenTextures(1, &m_enabled_options_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
// create (but do not fill) the reduced counterparts of the two buffers above
glsafe(glGenBuffers(1, &m_enabled_segments_reduced_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id));
glsafe(glGenTextures(1, &m_enabled_segments_reduced_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_tex_id));
glsafe(glGenBuffers(1, &m_enabled_options_reduced_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id));
glsafe(glGenTextures(1, &m_enabled_options_reduced_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_reduced_tex_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture));
#endif // ENABLE_OPENGL_ES
@@ -1172,6 +1200,523 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
update_colors();
}
#ifndef ENABLE_OPENGL_ES
static bool is_outer_wall(EGCodeExtrusionRole role)
{
return role == EGCodeExtrusionRole::ExternalPerimeter || role == EGCodeExtrusionRole::OverhangPerimeter;
}
// what is a visible surface by definition, whatever the grid says: the outer walls and the top
// and bottom skins. A step between one layer and the next is often narrower than a cell, so the
// grid alone would drop the odd segment of them on a curve and show what lies behind
static bool is_surface_by_role(EGCodeExtrusionRole role)
{
return is_outer_wall(role) || role == EGCodeExtrusionRole::TopSolidInfill || role == EGCodeExtrusionRole::BottomSurface ||
role == EGCodeExtrusionRole::BridgeInfill || role == EGCodeExtrusionRole::Ironing;
}
// 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 is_outer_wall(v.role);
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_surface_by_role(v.role) || (!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<uint8_t> 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<size_t>(nx) * static_cast<size_t>(ny), 0) {}
bool empty() const { return min_x > max_x; }
uint8_t at(int x, int y) const { return cells[static_cast<size_t>(y) * nx + x]; }
uint8_t& at(int x, int y) { return cells[static_cast<size_t>(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<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);
}
};
// 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, 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<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);
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<float>(MAX_CELLS));
const int radius = static_cast<int>(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<float>(margin) * cell;
const float origin_y = min_y - static_cast<float>(margin) * cell;
const int nx = static_cast<int>((max_x - min_x) / cell) + 1 + 2 * margin;
const int ny = static_cast<int>((max_y - min_y) / cell) + 1 + 2 * margin;
const auto cell_index = [nx](int x, int y) { return static_cast<size_t>(y) * nx + x; };
const auto cell_of = [&](float x, float y) {
const int cx = std::clamp(static_cast<int>((x - origin_x) / cell), margin, nx - 1 - margin);
const int cy = std::clamp(static_cast<int>((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<int>(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<float>(s) / static_cast<float>(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<uint32_t> shell;
std::vector<uint32_t> near_shell;
std::vector<int32_t> top;
std::vector<int32_t> 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<size_t>(nx) * static_cast<size_t>(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<OccupancyGrid> footprints(3, OccupancyGrid(nx, ny));
OccupancyGrid shell_cells(nx, ny);
// the outer wall segments of a layer, by every cell they cross; kept for the layer below
// and above as well, since the exposed band of a step lies just outside their walls
using WallMap = std::unordered_map<size_t, std::vector<uint32_t>>;
std::vector<WallMap> wall_maps(3);
const WallMap no_walls;
ClosingScratch scratch;
const auto footprint = [&](size_t layer) -> OccupancyGrid& { return footprints[layer % 3]; };
const auto walls = [&](size_t layer) -> WallMap& { return wall_maps[layer % 3]; };
const auto prepare = [&](size_t layer) {
OccupancyGrid& g = footprint(layer);
g.clear();
WallMap& w = walls(layer);
w.clear();
const auto [first, last] = layer_segments(layer);
for (size_t i = first; i < last; ++i) {
if (!is_drawn_extrusion(i))
continue;
for_each_cell(i, [&](int x, int y) { g.set(x, y); });
if (is_outer_wall(m_vertices[i].role))
for_each_cell(i, [&](int x, int y) { w[cell_index(x, y)].push_back(static_cast<uint32_t>(i)); });
}
close_gaps(g, radius, scratch);
};
// whether the midpoint of the segment starting at vertex i lies within reach of an outer
// wall segment listed in the map
const auto beside_wall = [&](size_t i, const WallMap& map, float reach) {
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 auto [cx, cy] = cell_of(mx, my);
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
const auto it = map.find(cell_index(cx + dx, cy + dy));
if (it == map.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;
};
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<int32_t>(layer);
if (bottom == NO_LAYER)
bottom = static_cast<int32_t>(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);
const WallMap& walls_below = (layer > 0) ? walls(layer - 1) : no_walls;
const WallMap& walls_cur = walls(layer);
const WallMap& walls_above = (layer + 1 < layers_count) ? walls(layer + 1) : no_walls;
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<uint32_t>(i));
// an inner wall segment is the first inner wall when its midpoint lies within a line
// and a half of an outer wall of the same layer
const float reach = 1.5f * m_vertices[i].width;
if (m_vertices[i].role == EGCodeExtrusionRole::Perimeter && beside_wall(i, walls_cur, reach)) {
kept.near_shell.push_back(static_cast<uint32_t>(i));
continue;
}
// the exposed band of a step is the strip just outside the outer wall of the layer
// above or below, whatever role fills it; a step narrower than a cell is invisible
// to the grid, so the reach is at least a cell
if (beside_wall(i, walls_above, std::max(reach, cell)) || beside_wall(i, walls_below, std::max(reach, cell)))
kept.near_shell.push_back(static_cast<uint32_t>(i));
}
}
return kept;
};
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<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)));
std::vector<int32_t> top_layer(cells_count, NO_LAYER);
std::vector<int32_t> 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<uint32_t> top; std::vector<uint32_t> 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<int32_t>(layer);
on_bottom += bottom_layer[cell_index(x, y)] == static_cast<int32_t>(layer);
});
if (on_top > 0)
visible.top.push_back(static_cast<uint32_t>(i));
if (on_bottom > 0)
visible.bottom.push_back(static_cast<uint32_t>(i));
}
}
return visible;
};
std::vector<std::future<Visible>> 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())
@@ -1179,6 +1724,39 @@ void ViewerImpl::update_enabled_entities()
std::vector<uint32_t> enabled_segments;
std::vector<uint32_t> enabled_options;
#ifndef ENABLE_OPENGL_ES
// the reduced set is filled by the same walk, so switching to it costs no rebuild. Whatever the
// mode leaves out, the bottom and top layers of the visible range are kept whole: they are the
// surfaces the range cuts open
const EReducedDetailMode reduced_mode = m_settings.reduced_detail_mode;
const bool build_reduced = reduced_mode != EReducedDetailMode::Off;
const uint32_t layer_stride = std::max<uint32_t>(1, m_settings.reduced_detail_layer_stride);
std::vector<uint32_t> enabled_segments_reduced;
std::vector<uint32_t> 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()) {
const auto fallback = [this]() {
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());
};
try {
update_shell_bitset();
}
catch (const std::bad_alloc&) {
// out of memory on a huge print: take everything for shell, which leaves out only the hidden infill
fallback();
}
catch (const std::system_error&) {
// a worker thread could not be launched
fallback();
}
}
#endif // ENABLE_OPENGL_ES
Interval range = m_view_range.get_visible();
// when top layer only visualization is enabled, we need to render
@@ -1226,6 +1804,25 @@ void ViewerImpl::update_enabled_entities()
enabled_options.push_back(static_cast<uint32_t>(i));
else
enabled_segments.push_back(static_cast<uint32_t>(i));
#ifndef ENABLE_OPENGL_ES
if (build_reduced) {
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<uint32_t>(i));
else if (reduced_mode != EReducedDetailMode::EndLayersOnly) {
if ((v.layer_id % layer_stride) == 0) {
if (v.is_option())
enabled_options_reduced.push_back(static_cast<uint32_t>(i));
else if (!v.is_extrusion() || reduced_set_keeps(i, v))
enabled_segments_reduced.push_back(static_cast<uint32_t>(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<uint32_t>(i));
}
}
#endif // ENABLE_OPENGL_ES
}
#ifdef ENABLE_OPENGL_ES
@@ -1254,6 +1851,23 @@ void ViewerImpl::update_enabled_entities()
else
glsafe(glBufferData(GL_TEXTURE_BUFFER, 0, nullptr, GL_STATIC_DRAW));
m_enabled_segments_reduced_count = enabled_segments_reduced.size();
m_enabled_options_reduced_count = enabled_options_reduced.size();
m_enabled_segments_reduced_tex_size = enabled_segments_reduced.size() * sizeof(uint32_t);
m_enabled_options_reduced_tex_size = enabled_options_reduced.size() * sizeof(uint32_t);
// uploaded even when nothing was built, so that the last reduced set is released as soon as
// the preference is switched off
assert(m_enabled_segments_reduced_buf_id > 0);
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id));
glsafe(glBufferData(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_tex_size,
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, m_enabled_options_reduced_tex_size,
enabled_options_reduced.empty() ? nullptr : enabled_options_reduced.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
#endif // ENABLE_OPENGL_ES
@@ -1461,6 +2075,24 @@ void ViewerImpl::toggle_top_layer_only_view_range()
update_colors_texture();
}
// Either changes which vertices land in the reduced set, so the sets are 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<uint32_t>(1, value);
if (m_settings.reduced_detail_layer_stride == value)
return;
m_settings.reduced_detail_layer_stride = value;
m_settings.update_enabled_entities = true;
}
// ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to
void ViewerImpl::set_dim_previous_layers(bool value)
{
@@ -1815,6 +2447,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();
@@ -1854,6 +2492,8 @@ size_t ViewerImpl::get_used_gpu_memory() const
ret += m_colors_tex_size;
ret += m_enabled_segments_tex_size;
ret += m_enabled_options_tex_size;
ret += m_enabled_segments_reduced_tex_size;
ret += m_enabled_options_reduced_tex_size;
#endif // ENABLE_OPENGL_ES
return ret;
}
@@ -2070,7 +2710,8 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
#ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_segments_count() == 0)
#else
if (m_enabled_segments_count == 0)
const ActiveSet segments = active_segments();
if (segments.count == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2138,10 +2779,10 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, segments.tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, segments.buf_id));
m_segment_template.render(m_enabled_segments_count);
m_segment_template.render(segments.count);
#endif // ENABLE_OPENGL_ES
if (curr_cull_face)
@@ -2167,7 +2808,8 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
#ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_options_count() == 0)
#else
if (m_enabled_options_count == 0)
const ActiveSet options = active_options();
if (options.count == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2225,10 +2867,10 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, options.tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, options.buf_id));
m_option_template.render(m_enabled_options_count);
m_option_template.render(options.count);
#endif // ENABLE_OPENGL_ES
if (!curr_cull_face)
+59
View File
@@ -109,6 +109,21 @@ public:
// 0.0 = black
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
void set_dim_previous_layers(bool value);
//
// Draw from the reduced set; it is already built, so this is just a buffer binding.
//
void set_reduced_detail(bool value) {
#ifdef ENABLE_OPENGL_ES
// no reduced set is built on OpenGL ES
value = false;
#endif // ENABLE_OPENGL_ES
m_settings.reduced_detail = value;
}
bool is_reduced_detail() const { return m_settings.reduced_detail; }
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);
@@ -320,6 +335,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
//
@@ -499,6 +525,15 @@ private:
unsigned int m_enabled_options_tex_id{ 0 };
size_t m_enabled_options_count{ 0 };
//
// OpenGL buffers to store the reduced set drawn while Settings::reduced_detail is set
//
unsigned int m_enabled_segments_reduced_buf_id{ 0 };
unsigned int m_enabled_segments_reduced_tex_id{ 0 };
size_t m_enabled_segments_reduced_count{ 0 };
unsigned int m_enabled_options_reduced_buf_id{ 0 };
unsigned int m_enabled_options_reduced_tex_id{ 0 };
size_t m_enabled_options_reduced_count{ 0 };
//
// Caches for size of data sent to gpu, in bytes
//
size_t m_positions_tex_size{ 0 };
@@ -506,6 +541,30 @@ private:
size_t m_colors_tex_size{ 0 };
size_t m_enabled_segments_tex_size{ 0 };
size_t m_enabled_options_tex_size{ 0 };
size_t m_enabled_segments_reduced_tex_size{ 0 };
size_t m_enabled_options_reduced_tex_size{ 0 };
// The set the next draw reads from: the reduced one while dragging, if one is built.
bool use_reduced_set() const { return m_settings.reduced_detail && m_settings.reduced_detail_mode != EReducedDetailMode::Off; }
// 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 };
unsigned int buf_id{ 0 };
unsigned int tex_id{ 0 };
};
ActiveSet active_segments() const {
if (use_reduced_set())
return { m_enabled_segments_reduced_count, m_enabled_segments_reduced_buf_id, m_enabled_segments_reduced_tex_id };
return { m_enabled_segments_count, m_enabled_segments_buf_id, m_enabled_segments_tex_id };
}
ActiveSet active_options() const {
if (use_reduced_set())
return { m_enabled_options_reduced_count, m_enabled_options_reduced_buf_id, m_enabled_options_reduced_tex_id };
return { m_enabled_options_count, m_enabled_options_buf_id, m_enabled_options_tex_id };
}
#endif // ENABLE_OPENGL_ES
//
+154 -1
View File
@@ -1267,6 +1267,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
if (current_top_layer_only != required_top_layer_only)
m_viewer.toggle_top_layer_only_view_range();
read_reduced_detail_preferences();
// 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")));
@@ -1660,6 +1662,7 @@ void GCodeViewer::reset_shell()
{
m_shells.volumes.clear();
m_shells.print_id = -1;
m_shells.with_wipe_tower = false;
m_shell_bounding_box = BoundingBoxf3();
}
@@ -1696,7 +1699,12 @@ void GCodeViewer::reset()
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
{
glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height);
// while dragging in the solid model mode, the objects stand in for their toolpaths, cut to the
// visible layer range; the toolpath set then holds only the range's bottom and top layers
if (m_viewer.is_reduced_detail() && solid_model_enabled())
render_solid_model(canvas_width, canvas_height);
else
render_shells(canvas_width, canvas_height);
if (m_viewer.get_extrusion_roles_count() == 0)
return;
@@ -2019,6 +2027,68 @@ void GCodeViewer::update_layers_slider_mode()
// TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder);
}
void GCodeViewer::set_interacting(bool interacting)
{
// with no shells to stand in for the toolpaths, the solid model would leave only the end layers
const bool usable = !solid_model_enabled() || !m_shells.volumes.empty();
m_viewer.set_reduced_detail(interacting && usable);
}
void GCodeViewer::read_reduced_detail_preferences()
{
m_reduced_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_reduced_detail_mode"));
m_reduced_detail_layer_stride = static_cast<unsigned int>(std::max(1, std::stoi(get_app_config()->get("preview_reduced_detail_layer_stride"))));
apply_reduced_detail_settings();
}
void GCodeViewer::apply_reduced_detail_settings()
{
m_viewer.set_reduced_detail_mode(m_reduced_detail_mode);
m_viewer.set_reduced_detail_layer_stride(m_reduced_detail_layer_stride);
}
void GCodeViewer::set_reduced_detail_mode(const std::string& mode)
{
const bool was_solid = solid_model_enabled();
m_reduced_detail_mode = reduced_detail_mode_from_string(mode);
apply_reduced_detail_settings();
reload_shells_if_solid_model_changed(was_solid);
}
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 == "solid")
return libvgcode::EReducedDetailMode::EndLayersOnly;
if (mode == "layers")
return libvgcode::EReducedDetailMode::LayersOnly;
if (mode == "outer_walls")
return libvgcode::EReducedDetailMode::OuterWallsOnly;
if (mode == "shell")
return libvgcode::EReducedDetailMode::ShellOnly;
return libvgcode::EReducedDetailMode::Off;
}
void GCodeViewer::reload_shells_if_solid_model_changed(bool was_enabled)
{
if (was_enabled == solid_model_enabled() || m_shells.print_id == -1)
return;
// only the prime tower comes and goes with the mode: a full reload would drop the shells
// whenever the print has moved on since they were loaded, leaving the solid model nothing to draw
if (wxGetApp().plater() == nullptr)
return;
// the shells are loaded from the current plate's print, which is not the plater's own
const Print& print = wxGetApp().plater()->get_partplate_list().get_current_fff_print();
if (static_cast<int>(print.id().id) != m_shells.print_id)
return;
update_shell_wipe_tower(print, m_gl_data_initialized);
}
void GCodeViewer::set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range)
{
m_viewer.set_layers_view_range(static_cast<uint32_t>(layers_z_range[0]), static_cast<uint32_t>(layers_z_range[1]));
@@ -2343,7 +2413,11 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const
void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": initialized=%1%, force_previewing=%2%")%initialized %force_previewing;
// the shells can load before the first G-code does, so the preferences are read here as well
read_reduced_detail_preferences();
if ((print.id().id == m_shells.print_id)&&(print.get_modified_count() == m_shells.print_modify_count)) {
// the prime tower comes and goes on its own, without reloading the objects
update_shell_wipe_tower(print, initialized);
//BBS: update force previewing logic
if (force_previewing)
m_shells.previewing = force_previewing;
@@ -2452,10 +2526,45 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
m_shells.print_id = print.id().id;
m_shells.print_modify_count = print.get_modified_count();
m_shells.previewing = true;
update_shell_wipe_tower(print, initialized);
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": shell loaded, id change to %1%, modify_count %2%, object count %3%, glvolume count %4%")
% m_shells.print_id % m_shells.print_modify_count % object_count %m_shells.volumes.volumes.size();
}
// The prime tower as it was sliced, so that the solid model shows what the print shows. It keeps its
// opaque colour, so it never appears among the translucent shells, and stays out of their bounding box.
void GCodeViewer::update_shell_wipe_tower(const Print& print, bool initialized)
{
const bool with_wipe_tower = solid_model_enabled() && print.is_step_done(psWipeTower) && print.wipe_tower_data().wipe_tower_mesh_data;
if (with_wipe_tower == m_shells.with_wipe_tower)
return;
m_shells.with_wipe_tower = with_wipe_tower;
GLVolumePtrs& volumes = m_shells.volumes.volumes;
if (!with_wipe_tower) {
volumes.erase(std::remove_if(volumes.begin(), volumes.end(), [](GLVolume* volume) {
if (!volume->is_wipe_tower)
return false;
delete volume;
return true;
}), volumes.end());
return;
}
const PrintConfig& config = print.config();
const int plate_idx = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin();
const float x = static_cast<float>(config.wipe_tower_x.get_at(plate_idx) + plate_origin.x());
const float y = static_cast<float>(config.wipe_tower_y.get_at(plate_idx) + plate_origin.y());
const size_t first_new = volumes.size();
m_shells.volumes.load_real_wipe_tower_preview(1000 + plate_idx, x, y, print.wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh,
print.wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, true,
static_cast<float>(config.wipe_tower_rotation_angle), false, initialized);
for (size_t i = first_new; i < volumes.size(); ++i) {
volumes[i]->zoom_to_volumes = false;
volumes[i]->force_native_color = true;
volumes[i]->set_render_color();
}
}
void GCodeViewer::render_toolpaths()
{
const Camera& camera = wxGetApp().plater()->get_camera();
@@ -2656,6 +2765,50 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height)
glsafe(::glDepthMask(GL_TRUE));
}
// The sliced objects and the prime tower drawn opaque, in their filament colours, cut to the
// visible layer range by the shader's z range. The toolpaths of the range's bottom and top layers
// are drawn afterwards and cap the cut.
void GCodeViewer::render_solid_model(int canvas_width, int canvas_height)
{
if (m_shells.volumes.empty())
return;
// gouraud_light has no z range, so it could not cut the model
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
if (shader == nullptr)
return;
const libvgcode::Interval& layers = m_viewer.get_layers_view_range();
const float z_top = m_viewer.get_layer_z(layers[1]) - m_z_offset + 0.001f;
const float z_bottom = (layers[0] > 0) ? m_viewer.get_layer_z(layers[0] - 1) - m_z_offset - 0.001f : -FLT_MAX;
std::vector<float> alphas;
alphas.reserve(m_shells.volumes.volumes.size());
for (GLVolume* volume : m_shells.volumes.volumes) {
alphas.push_back(volume->color.a());
volume->color.a(1.0f);
volume->set_render_color();
}
m_shells.volumes.set_z_range(z_bottom, z_top);
// gouraud also clips by this plane, which nothing else sets on the shells
m_shells.volumes.set_clipping_plane(ClippingPlane::ClipsNothing().get_data());
shader->start_using();
// the 3D view leaves its shadow settings on the shared program
shader->set_uniform("shadow_intensity", 0.0f);
const Camera& camera = wxGetApp().plater()->get_camera();
shader->set_uniform("z_far", camera.get_far_z());
shader->set_uniform("z_near", camera.get_near_z());
m_shells.volumes.render(GLVolumeCollection::ERenderType::Opaque, false, camera.get_view_matrix(), camera.get_projection_matrix(), {canvas_width, canvas_height});
shader->stop_using();
m_shells.volumes.set_z_range(-FLT_MAX, FLT_MAX);
size_t k = 0;
for (GLVolume* volume : m_shells.volumes.volumes) {
volume->color.a(alphas[k++]);
volume->set_render_color();
}
}
//BBS
void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const {
if (!show)
+24 -1
View File
@@ -174,6 +174,8 @@ public:
int print_id{-1};
int print_modify_count{-1};
bool previewing{false};
// the prime tower was loaded with the objects, for the solid model
bool with_wipe_tower{false};
};
//BBS
ConflictResultOpt m_conflict_result;
@@ -234,6 +236,18 @@ private:
bool m_legend_visible{ true };
bool m_legend_enabled{ true };
// the reduced-detail preferences, pushed to libvgcode by apply_reduced_detail_settings()
libvgcode::EReducedDetailMode m_reduced_detail_mode{ libvgcode::EReducedDetailMode::Off };
unsigned int m_reduced_detail_layer_stride{ 4 };
void read_reduced_detail_preferences();
void apply_reduced_detail_settings();
static libvgcode::EReducedDetailMode reduced_detail_mode_from_string(const std::string& mode);
// in the solid model mode, the sliced objects are drawn as solid shapes instead of toolpaths
bool solid_model_enabled() const { return m_reduced_detail_mode == libvgcode::EReducedDetailMode::EndLayersOnly; }
void render_solid_model(int canvas_width, int canvas_height);
// the prime tower is only among the shells for the solid model, so it is added or removed when that changes
void reload_shells_if_solid_model_changed(bool was_enabled);
void update_shell_wipe_tower(const Print& print, bool initialized);
float m_legend_height;
PrintEstimatedStatistics m_print_statistics;
@@ -291,7 +305,7 @@ public:
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void render_calibration_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
bool has_data() const { return !m_viewer.get_extrusion_roles().empty(); }
bool has_data() const { return m_viewer.get_extrusion_roles_count() != 0; }
bool can_export_toolpaths() const;
std::vector<int> get_plater_extruder();
@@ -363,6 +377,15 @@ public:
void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); }
float get_dim_previous_layers_brightness() const { return m_viewer.get_dim_previous_layers_brightness(); }
// whether the mouse is holding either slider's handle
bool is_slider_dragging() const { return m_layers_slider->is_dragging() || m_moves_slider->is_dragging(); }
// 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", "outer_walls" or "shell"
void set_reduced_detail_mode(const std::string& mode);
void set_reduced_detail_layer_stride(unsigned int value);
void set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range);
bool is_legend_shown() const { return m_legend_visible && m_legend_enabled; }
+64 -4
View File
@@ -2054,6 +2054,11 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
const bool overlay_tick = m_fps_overlay_tick;
m_fps_overlay_tick = false;
// Whether the preview draws its reduced set is decided before the cached scene is consulted,
// since switching changes what the scene pass draws.
if (m_canvas_type == ECanvasType::CanvasPreview && m_render_preview && m_gcode_viewer.has_data() && _update_preview_interaction())
scene_dirty = true;
// An overlay-only frame reuses the last scene pass. The overlay is rebuilt either way, and drawn
// below once it is known whether the frame differs from the one on screen.
const bool reuse_scene = !scene_dirty && _can_reuse_cached_scene(camera);
@@ -3235,9 +3240,16 @@ void GLCanvas3D::bind_event_handlers()
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
// a drag cut short never sees its button release, which would leave the reduced set drawn
if (m_canvas_type == CanvasPreview && m_mouse.dragging && m_gcode_viewer.is_reduced_detail())
mouse_up_cleanup();
render();
evt.Skip();
});
m_canvas->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
if (m_canvas_type == CanvasPreview && m_mouse.dragging && m_gcode_viewer.is_reduced_detail())
mouse_up_cleanup();
});
m_event_handlers_bound = true;
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -3310,6 +3322,17 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
m_overlay_dirty |= imgui_requires_extra_frame;
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh();
// the render timer only wakes the idle loop; the frame that puts the preview's toolpaths back
// after a wheel burst has to be asked for here, once the settle time is really up
if (m_preview_settle_pending) {
const auto now = std::chrono::steady_clock::now();
if (now >= m_preview_interaction_until) {
m_preview_settle_pending = false;
m_dirty = true;
}
else // the timer fired early
schedule_extra_frame(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(m_preview_interaction_until - now).count()) + 1);
}
if (!m_dirty && !m_overlay_dirty)
return;
@@ -3840,6 +3863,10 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
return;
}
// only a wheel the panels did not take moves the camera
if (m_canvas_type == CanvasPreview)
note_preview_interaction();
#ifdef __WXMSW__
// For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad,
// if the event is not allowed to be passed further.
@@ -3940,6 +3967,11 @@ void GLCanvas3D::on_fps_overlay_timer(wxTimerEvent& evt)
wxWakeUpIdle();
}
void GLCanvas3D::note_preview_interaction()
{
m_preview_interaction_until = std::chrono::steady_clock::now() + std::chrono::milliseconds(150);
}
void GLCanvas3D::schedule_extra_frame(int milliseconds)
{
// Schedule idle event right now
@@ -5556,6 +5588,9 @@ void GLCanvas3D::mouse_up_cleanup()
m_mouse.ignore_left_up = false;
m_mouse.ignore_right_up = false;
m_dirty = true;
// the frame that follows a release puts the preview's toolpaths back, and on some platforms
// no idle event follows a button release until the next input
wxWakeUpIdle();
if (m_canvas->HasCapture())
m_canvas->ReleaseMouse();
@@ -7702,13 +7737,20 @@ bool GLCanvas3D::_is_scene_cacheable() const
return false;
#endif
// The scene follows the cursor during a drag, under a gizmo that draws at the cursor, and while
// the cursor is on the layer height bar, where the object shader draws a band at its height.
// The scene follows the cursor while the user drags, under a gizmo that draws at the cursor, and
// while the cursor is on the layer height bar, where the object shader draws a band at its height.
const GLGizmoBase* gizmo = m_gizmos.get_current();
const bool cursor_on_layers_bar = is_layers_editing_enabled() &&
m_layers_editing.bar_rect_contains(*this, (float)m_mouse.position.x(), (float)m_mouse.position.y());
return !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging() &&
(gizmo == nullptr || !gizmo->render_follows_cursor()) && !cursor_on_layers_bar;
return !is_user_interacting() && (gizmo == nullptr || !gizmo->render_follows_cursor()) && !cursor_on_layers_bar;
}
// Whether the user is holding something that moves the scene: the camera, the navigator, a gizmo,
// the rectangle selection or a preview slider.
bool GLCanvas3D::is_user_interacting() const
{
return m_mouse.dragging || m_navigator_dragging || m_gizmos.is_dragging() || m_rectangle_selection.is_dragging() ||
m_gcode_viewer.is_slider_dragging();
}
bool GLCanvas3D::_is_frame_skipping_enabled() const
@@ -8699,6 +8741,24 @@ void GLCanvas3D::_render_wireframe_overlay()
shader->stop_using();
}
// The reduced set is drawn while the camera, the navigator or either slider is dragged. A wheel
// step has no duration, so it holds the reduced set for a settle time instead, and the frame that
// restores the full toolpaths is scheduled for when that time runs out. Returns whether what the scene
// pass draws changed, since a frame that reuses the cached scene would hide the change.
bool GLCanvas3D::_update_preview_interaction()
{
const auto now = std::chrono::steady_clock::now();
const bool settling = now < m_preview_interaction_until;
const bool dragging = is_user_interacting();
const bool was_reduced = m_gcode_viewer.is_reduced_detail();
m_gcode_viewer.set_interacting(dragging || settling);
if (settling && !dragging && m_gcode_viewer.is_reduced_detail()) {
m_preview_settle_pending = true;
schedule_extra_frame(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(m_preview_interaction_until - now).count()) + 1);
}
return m_gcode_viewer.is_reduced_detail() != was_reduced;
}
//BBS: GUI refactor: add canvas size as parameters
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
{
+11
View File
@@ -649,6 +649,10 @@ private:
ECursorType m_cursor_type;
GLSelectionRectangle m_rectangle_selection;
bool m_navigator_dragging{ false };
// until when a wheel step keeps the preview's reduced set drawn
std::chrono::time_point<std::chrono::steady_clock> m_preview_interaction_until{};
// whether the frame that restores the toolpaths once that time is up is still owed
bool m_preview_settle_pending{ false };
//BBS:add plate related logic
mutable std::vector<int> m_hover_volume_idxs;
@@ -1215,6 +1219,10 @@ public:
void msw_rescale() { m_gcode_viewer.invalidate_legend(); }
void request_extra_frame() { m_extra_frame_requested = true; }
// whether the user is holding the camera, the navigator, a gizmo, the rectangle selection or a preview slider
bool is_user_interacting() const;
// a wheel step is over before the next frame, so it holds the preview's reduced set for a settle time
void note_preview_interaction();
void schedule_extra_frame(int milliseconds);
@@ -1362,6 +1370,9 @@ private:
//BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height);
void _render_gcode_overlay(int canvas_width, int canvas_height);
// decides whether the preview draws its reduced set this frame and returns whether what the scene
// pass draws changed; runs before the cached scene is consulted
bool _update_preview_interaction();
//BBS: render a plane for assemble
void _render_plane() const;
void _render_selection();
+7
View File
@@ -483,6 +483,11 @@ void IMSlider::draw_background_and_groove(const ImRect& bg_rect, const ImRect& g
ImGui::RenderFrame(groove.Min, groove.Max, groove_col, false, 0.5 * groove.GetWidth());
}
bool IMSlider::is_dragging() const
{
return GImGui != nullptr && m_imgui_id != 0 && GImGui->ActiveId == m_imgui_id && GImGui->IO.MouseDown[0];
}
bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int v_max, const ImVec2& size, float scale)
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
@@ -491,6 +496,7 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size);
@@ -883,6 +889,7 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size);
+5
View File
@@ -118,6 +118,9 @@ public:
//BBS update scroll value changed
bool is_dirty() { return m_dirty; }
// whether the mouse is holding this slider's handle, read from ImGui's active id rather than
// from the dirty flag, which is raised and consumed inside a single frame
bool is_dragging() const;
void set_as_dirty(bool dirty = true) { m_dirty = dirty; }
bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; }
void reset_post_tick_event(bool val = false) {
@@ -182,6 +185,8 @@ private:
int m_higher_value;
int m_one_layer_value; // ORCA
bool m_dirty = false;
// the ImGui id of the slider widget, as of its last render
unsigned int m_imgui_id = 0;
bool m_render_as_disabled{ false };
+65 -2
View File
@@ -322,7 +322,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<wxString> vlist, std::vector<std::string> config_name_index, const wxString wiki_url)
wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, std::function<void(std::string)> onchange, const wxString wiki_url)
{
assert(vlist.size() == config_name_index.size());
unsigned int current_index = 0;
@@ -338,8 +338,9 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index);
//// 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();
});
@@ -684,6 +685,12 @@ wxBoxSizer *PreferencesDialog::create_item_input(wxString title, wxString title2
return m_sizer;
}
// 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" || 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<void(int)> onchange, const wxString wiki_url)
{
auto tip = tooltip.IsEmpty() ? title : tooltip; // auto fill tooltips with title if its empty
@@ -698,6 +705,11 @@ wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString tit
m_dim_previous_layers_brightness_input = input;
input->Enable(app_config->get_bool("preview_dim_previous_layers"));
}
// only the toolpath modes skip layers
else if (param == "preview_reduced_detail_layer_stride") {
m_reduced_detail_layer_stride_input = input;
input->Enable(reduced_detail_mode_skips_layers(app_config->get("preview_reduced_detail_mode")));
}
m_sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL);
@@ -2045,6 +2057,57 @@ void PreferencesDialog::create_items()
"preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues);
g_sizer->Add(item_preview_view_type);
auto item_reduced_detail_mode = create_item_combobox(
_L("Simplify preview while dragging"),
_L("What the sliced preview draws while you drag the camera or a preview slider, or zoom with the mouse wheel, so that large prints stay responsive. "
"The full toolpaths are restored as soon as you let go.\n"
"Off: the full toolpaths.\n"
"Solid model: the sliced objects and the prime tower as solid shapes in their filament colors, cut to the visible layer range, "
"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"), _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)
m_reduced_detail_layer_stride_input->Enable(reduced_detail_mode_skips_layers(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."),
"preview_reduced_detail_layer_stride",
1,
20,
// 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<unsigned int>(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."),
+2 -1
View File
@@ -80,6 +80,7 @@ public:
::CheckBox * m_skip_identical_frames_checkbox = {nullptr};
::TextInput *m_backup_interval_textinput = {nullptr};
::SpinInput *m_dim_previous_layers_brightness_input = {nullptr};
::SpinInput *m_reduced_detail_layer_stride_input = {nullptr};
::ComboBox * m_network_version_combo = {nullptr};
std::vector<NetworkLibraryVersionInfo> m_available_versions;
@@ -93,7 +94,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<wxString> vlist, std::function<void(wxString)> onchange = {}, const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, std::function<void(std::string)> 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<wxString> vlist);