mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-23 17:02:39 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b2ae48e4a | ||
|
|
8095904bc2 | ||
|
|
b7c86befe1 | ||
|
|
526a91d323 | ||
|
|
493896260e | ||
|
|
ca83497bb6 | ||
|
|
6a985744c4 |
@@ -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` | 1–20 | one layer in every N is kept by the toolpath modes |
|
||||
|
||||
libvgcode (`src/libvgcode`) builds and binds the reduced toolpath set, `GCodeViewer` maps the
|
||||
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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
//
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user