Compare commits

..
Author SHA1 Message Date
Hanif Koh dd593317fd Stop Malformed Network Responses from Crashing the App
Duet, MKS and UltiMaker parsed print host replies with boost read_json
inside the HTTP completion callback with no try, so an HTML or truncated
reply threw out of the Physical Printer Test button and terminated the app,
or killed the upload queue thread. The five identical copies of the parser
(ESP3D's and Flashforge's were unused) are replaced by one shared
PrintHost::get_err_code_from_body that reports a non-JSON reply as an error.
The upload queue now catches a failing job per job, so one bad upload no
longer leaves later jobs queued forever.

Flashforge read material station slots with nlohmann value(), which throws
on off-type fields or non-object entries. The parsing moves into
Flashforge::parse_material_slots, which reads fields leniently with the
existing try_parse_json_int and skips bad entries.

UserManager::parse_json parsed the payload before its try block; the parse
now happens inside it.
2026-09-28 13:00:58 +08:00
33 changed files with 202 additions and 663 deletions
-80
View File
@@ -1,80 +0,0 @@
# 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`, `shell` | what is drawn while dragging |
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 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.
- `ShellOnly` (`shell`) keeps every role but the sparse infill, internal solid infill, internal
bridge infill and gap fill, which lie under the skins or between the walls. Walls, top and
bottom surfaces, bridges, supports and the prime tower are drawn whole, so from outside the
print looks as it does at rest. The roles come from
the slicer, so the classification is exact wherever the slicer's is; what it cannot express is
the inside of the prime tower and of a support, which share one role with their outside and are
kept whole. A profile with no top or bottom shell layers, or no walls, leaves the infill on the
surface, so `GCodeViewer` turns the hiding off for such a print when it loads it, from the
print's default region settings; per-object overrides are not consulted.
## 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.
## 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.
-7
View File
@@ -205,13 +205,6 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty()) if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1"); set("seq_top_layer_only", "1");
// what the preview draws while the user drags it
{
const std::string mode = get("preview_reduced_detail_mode");
if (mode != "off" && mode != "solid" && mode != "shell")
set("preview_reduced_detail_mode", "off");
}
// ORCA: darken the layers the preview layer slider is not scrubbed to // ORCA: darken the layers the preview layer slider is not scrubbed to
if (get("preview_dim_previous_layers").empty()) if (get("preview_dim_previous_layers").empty())
set_bool("preview_dim_previous_layers", false); set_bool("preview_dim_previous_layers", false);
-15
View File
@@ -158,21 +158,6 @@ enum class EGCodeExtrusionRole : uint8_t
static constexpr std::size_t GCODE_EXTRUSION_ROLES_COUNT = static_cast<std::size_t>(EGCodeExtrusionRole::COUNT); 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,
// every role but the infill under the skins and the gap fill: what can be seen from outside
ShellOnly,
COUNT
};
// //
// Option types // Option types
// //
-13
View File
@@ -114,19 +114,6 @@ public:
// //
bool is_dim_previous_layers() const; bool is_dim_previous_layers() const;
void set_dim_previous_layers(bool value); void set_dim_previous_layers(bool value);
//
// The reduced set drawn while the user drags: what the mode keeps, 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);
// Whether the shell mode hides the infill roles; off for a profile that leaves them on the
// surface, with no top or bottom shell or no walls.
bool get_reduced_detail_hide_infill() const;
void set_reduced_detail_hide_infill(bool value);
void set_reduced_detail(bool value);
bool is_reduced_detail() const;
float get_dim_previous_layers_brightness() const; float get_dim_previous_layers_brightness() const;
void set_dim_previous_layers_brightness(float value); void set_dim_previous_layers_brightness(float value);
// //
-5
View File
@@ -25,11 +25,6 @@ struct Settings
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black // ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
float dim_previous_layers_brightness{ 0.4f }; float dim_previous_layers_brightness{ 0.4f };
bool spiral_vase_mode{ false }; bool spiral_vase_mode{ false };
// what the reduced set holds and whether it is drawn. Ignored on the OpenGL ES path.
EReducedDetailMode reduced_detail_mode{ EReducedDetailMode::Off };
// whether the shell mode may hide the infill roles: false when the profile leaves them on the surface
bool reduced_detail_hide_infill{ true };
bool reduced_detail{ false };
// //
// Required update flags // Required update flags
// //
+1 -6
View File
@@ -34,10 +34,6 @@ static const char* Segments_Vertex_Shader =
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not // ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
// world_position, so the caster would write a depth the receiver never looks up. // world_position, so the caster would write a depth the receiver never looks up.
"uniform float bias_scale;\n" "uniform float bias_scale;\n"
// draw the instances last to first, top layers before the ones they hide, so that early depth
// rejection discards most of the hidden fragments; set when the camera looks down on the print
"uniform int reverse_order;\n"
"uniform int instance_count;\n"
"in int vertex_id;\n" "in int vertex_id;\n"
"out vec3 color;\n" "out vec3 color;\n"
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" "// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
@@ -63,8 +59,7 @@ static const char* Segments_Vertex_Shader =
" return top_diffuse + front_diffuse + top_specular;\n" " return top_diffuse + front_diffuse + top_specular;\n"
"}\n" "}\n"
"void main() {\n" "void main() {\n"
" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n" " int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n"
" int id_a = int(texelFetch(segment_index_tex, instance).r);\n"
" int id_b = id_a + 1;\n" " int id_b = id_a + 1;\n"
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n" " vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n" " vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
-30
View File
@@ -92,36 +92,6 @@ bool Viewer::is_dim_previous_layers() const
return m_impl->is_dim_previous_layers(); 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);
}
bool Viewer::get_reduced_detail_hide_infill() const
{
return m_impl->get_reduced_detail_hide_infill();
}
void Viewer::set_reduced_detail_hide_infill(bool value)
{
m_impl->set_reduced_detail_hide_infill(value);
}
void Viewer::set_dim_previous_layers(bool value) void Viewer::set_dim_previous_layers(bool value)
{ {
m_impl->set_dim_previous_layers(value); m_impl->set_dim_previous_layers(value);
+8 -114
View File
@@ -17,7 +17,6 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <numeric> #include <numeric>
#include <cfloat>
namespace libvgcode { namespace libvgcode {
@@ -764,8 +763,6 @@ void ViewerImpl::init(const std::string& opengl_context_version)
m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex"); m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex");
m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex"); m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex");
m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex"); m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex");
m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order");
m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count");
// ORCA: realistic view // ORCA: realistic view
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map"); m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp"); m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
@@ -902,17 +899,9 @@ void ViewerImpl::reset()
#else #else
m_enabled_segments_count = 0; m_enabled_segments_count = 0;
m_enabled_options_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_settings_used_for_ranges = std::nullopt; 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_textures(m_enabled_options_tex_id);
delete_buffers(m_enabled_options_buf_id); delete_buffers(m_enabled_options_buf_id);
delete_textures(m_enabled_segments_tex_id); delete_textures(m_enabled_segments_tex_id);
@@ -1172,17 +1161,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
glsafe(glGenTextures(1, &m_enabled_options_tex_id)); glsafe(glGenTextures(1, &m_enabled_options_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, 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(glBindBuffer(GL_TEXTURE_BUFFER, 0));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture));
#endif // ENABLE_OPENGL_ES #endif // ENABLE_OPENGL_ES
@@ -1194,18 +1172,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
update_colors(); update_colors();
} }
#ifndef ENABLE_OPENGL_ES
// the roles that lie under a skin or between walls, never seen from outside the print
static bool is_hidden_in_shell(EGCodeExtrusionRole role)
{
return role == EGCodeExtrusionRole::InternalInfill ||
role == EGCodeExtrusionRole::SolidInfill ||
role == EGCodeExtrusionRole::InternalBridgeInfill ||
role == EGCodeExtrusionRole::GapFill;
}
#endif // ENABLE_OPENGL_ES
void ViewerImpl::update_enabled_entities() void ViewerImpl::update_enabled_entities()
{ {
if (m_vertices.empty()) if (m_vertices.empty())
@@ -1213,17 +1179,6 @@ void ViewerImpl::update_enabled_entities()
std::vector<uint32_t> enabled_segments; std::vector<uint32_t> enabled_segments;
std::vector<uint32_t> enabled_options; 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 bool hide_infill = m_settings.reduced_detail_hide_infill;
std::vector<uint32_t> enabled_segments_reduced;
std::vector<uint32_t> enabled_options_reduced;
const Interval& layers_range = m_layers.get_view_range();
#endif // ENABLE_OPENGL_ES
Interval range = m_view_range.get_visible(); Interval range = m_view_range.get_visible();
// when top layer only visualization is enabled, we need to render // when top layer only visualization is enabled, we need to render
@@ -1271,20 +1226,6 @@ void ViewerImpl::update_enabled_entities()
enabled_options.push_back(static_cast<uint32_t>(i)); enabled_options.push_back(static_cast<uint32_t>(i));
else else
enabled_segments.push_back(static_cast<uint32_t>(i)); 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::ShellOnly) {
if (v.is_option())
enabled_options_reduced.push_back(static_cast<uint32_t>(i));
else if (!v.is_extrusion() || !hide_infill || !is_hidden_in_shell(v.role))
enabled_segments_reduced.push_back(static_cast<uint32_t>(i));
}
}
#endif // ENABLE_OPENGL_ES
} }
#ifdef ENABLE_OPENGL_ES #ifdef ENABLE_OPENGL_ES
@@ -1313,23 +1254,6 @@ void ViewerImpl::update_enabled_entities()
else else
glsafe(glBufferData(GL_TEXTURE_BUFFER, 0, nullptr, GL_STATIC_DRAW)); 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)); glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
#endif // ENABLE_OPENGL_ES #endif // ENABLE_OPENGL_ES
@@ -1537,23 +1461,6 @@ void ViewerImpl::toggle_top_layer_only_view_range()
update_colors_texture(); update_colors_texture();
} }
// Both decide 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_hide_infill(bool value)
{
if (m_settings.reduced_detail_hide_infill == value)
return;
m_settings.reduced_detail_hide_infill = value;
m_settings.update_enabled_entities = true;
}
// ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to // ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to
void ViewerImpl::set_dim_previous_layers(bool value) void ViewerImpl::set_dim_previous_layers(bool value)
{ {
@@ -1947,8 +1854,6 @@ size_t ViewerImpl::get_used_gpu_memory() const
ret += m_colors_tex_size; ret += m_colors_tex_size;
ret += m_enabled_segments_tex_size; ret += m_enabled_segments_tex_size;
ret += m_enabled_options_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 #endif // ENABLE_OPENGL_ES
return ret; return ret;
} }
@@ -2165,8 +2070,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
#ifdef ENABLE_OPENGL_ES #ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_segments_count() == 0) if (m_texture_data.get_enabled_segments_count() == 0)
#else #else
const ActiveSet segments = active_segments(); if (m_enabled_segments_count == 0)
if (segments.count == 0)
#endif // ENABLE_OPENGL_ES #endif // ENABLE_OPENGL_ES
return; return;
@@ -2186,15 +2090,6 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data()));
glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data())); glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data()));
// The segments come in print order, bottom layer first. Seen from above, that is back to front,
// and every hidden fragment is shaded before the one that covers it. Drawing them last to first
// lets the depth test reject the hidden ones instead. The camera looks down when the world's
// up axis points towards it, which is the view matrix's (2, 2) entry being positive.
const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f;
glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0));
#ifndef ENABLE_OPENGL_ES
glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast<int>(segments.count)));
#endif // ENABLE_OPENGL_ES
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades // ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
// with the lookup off. // with the lookup off.
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit)); glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
@@ -2243,10 +2138,10 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3)); glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, segments.tex_id)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, segments.buf_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id));
m_segment_template.render(segments.count); m_segment_template.render(m_enabled_segments_count);
#endif // ENABLE_OPENGL_ES #endif // ENABLE_OPENGL_ES
if (curr_cull_face) if (curr_cull_face)
@@ -2272,8 +2167,7 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
#ifdef ENABLE_OPENGL_ES #ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_options_count() == 0) if (m_texture_data.get_enabled_options_count() == 0)
#else #else
const ActiveSet options = active_options(); if (m_enabled_options_count == 0)
if (options.count == 0)
#endif // ENABLE_OPENGL_ES #endif // ENABLE_OPENGL_ES
return; return;
@@ -2331,10 +2225,10 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3)); glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, options.tex_id)); glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, options.buf_id)); glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id));
m_option_template.render(options.count); m_option_template.render(m_enabled_options_count);
#endif // ENABLE_OPENGL_ES #endif // ENABLE_OPENGL_ES
if (!curr_cull_face) if (!curr_cull_face)
-47
View File
@@ -109,21 +109,6 @@ public:
// 0.0 = black // 0.0 = black
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; } bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
void set_dim_previous_layers(bool value); 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);
bool get_reduced_detail_hide_infill() const { return m_settings.reduced_detail_hide_infill; }
void set_reduced_detail_hide_infill(bool value);
float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; } float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; }
void set_dim_previous_layers_brightness(float value); void set_dim_previous_layers_brightness(float value);
@@ -377,8 +362,6 @@ private:
int m_uni_segments_height_width_angle_tex_id{ -1 }; int m_uni_segments_height_width_angle_tex_id{ -1 };
int m_uni_segments_colors_tex_id{ -1 }; int m_uni_segments_colors_tex_id{ -1 };
int m_uni_segments_segment_index_tex_id{ -1 }; int m_uni_segments_segment_index_tex_id{ -1 };
int m_uni_segments_reverse_order_id{ -1 };
int m_uni_segments_instance_count_id{ -1 };
int m_uni_segments_shadow_map_id{ -1 }; int m_uni_segments_shadow_map_id{ -1 };
int m_uni_segments_shadow_light_vp_id{ -1 }; int m_uni_segments_shadow_light_vp_id{ -1 };
int m_uni_segments_shadow_intensity_id{ -1 }; int m_uni_segments_shadow_intensity_id{ -1 };
@@ -516,15 +499,6 @@ private:
unsigned int m_enabled_options_tex_id{ 0 }; unsigned int m_enabled_options_tex_id{ 0 };
size_t m_enabled_options_count{ 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 // Caches for size of data sent to gpu, in bytes
// //
size_t m_positions_tex_size{ 0 }; size_t m_positions_tex_size{ 0 };
@@ -532,27 +506,6 @@ private:
size_t m_colors_tex_size{ 0 }; size_t m_colors_tex_size{ 0 };
size_t m_enabled_segments_tex_size{ 0 }; size_t m_enabled_segments_tex_size{ 0 };
size_t m_enabled_options_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; }
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 #endif // ENABLE_OPENGL_ES
// //
+1 -146
View File
@@ -1267,8 +1267,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
if (current_top_layer_only != required_top_layer_only) if (current_top_layer_only != required_top_layer_only)
m_viewer.toggle_top_layer_only_view_range(); m_viewer.toggle_top_layer_only_view_range();
read_reduced_detail_preferences();
// ORCA: darken the layers the preview layer slider is not scrubbed to // 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(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"))); m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
@@ -1492,10 +1490,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
// BBS: data for rendering color arrangement recommendation // BBS: data for rendering color arrangement recommendation
m_nozzle_nums = print.config().option<ConfigOptionFloats>("nozzle_diameter")->values.size(); m_nozzle_nums = print.config().option<ConfigOptionFloats>("nozzle_diameter")->values.size();
// the shell drag mode hides the infill only where the profile covers it: with no top or bottom
// shell, or no walls, the infill is the surface. Per-object settings are not consulted.
const PrintRegionConfig& region = print.default_region_config();
m_viewer.set_reduced_detail_hide_infill(region.top_shell_layers.value > 0 && region.bottom_shell_layers.value > 0 && region.wall_loops.value > 0);
// Orca hack: Hide filament group for non-bbl printers // Orca hack: Hide filament group for non-bbl printers
if (!print.is_BBL_printer()) m_nozzle_nums = 1; if (!print.is_BBL_printer()) m_nozzle_nums = 1;
std::vector<int> filament_maps = print.get_filament_maps(); std::vector<int> filament_maps = print.get_filament_maps();
@@ -1666,7 +1660,6 @@ void GCodeViewer::reset_shell()
{ {
m_shells.volumes.clear(); m_shells.volumes.clear();
m_shells.print_id = -1; m_shells.print_id = -1;
m_shells.with_wipe_tower = false;
m_shell_bounding_box = BoundingBoxf3(); m_shell_bounding_box = BoundingBoxf3();
} }
@@ -1703,12 +1696,7 @@ void GCodeViewer::reset()
void GCodeViewer::render_scene(int canvas_width, int canvas_height) void GCodeViewer::render_scene(int canvas_width, int canvas_height)
{ {
glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glEnable(GL_DEPTH_TEST));
// while dragging in the solid model mode, the objects stand in for their toolpaths, cut to the render_shells(canvas_width, canvas_height);
// 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) if (m_viewer.get_extrusion_roles_count() == 0)
return; return;
@@ -2031,56 +2019,6 @@ void GCodeViewer::update_layers_slider_mode()
// TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder); // 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"));
apply_reduced_detail_settings();
}
void GCodeViewer::apply_reduced_detail_settings()
{
m_viewer.set_reduced_detail_mode(m_reduced_detail_mode);
}
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);
}
libvgcode::EReducedDetailMode GCodeViewer::reduced_detail_mode_from_string(const std::string& mode)
{
if (mode == "solid")
return libvgcode::EReducedDetailMode::EndLayersOnly;
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) 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])); m_viewer.set_layers_view_range(static_cast<uint32_t>(layers_z_range[0]), static_cast<uint32_t>(layers_z_range[1]));
@@ -2405,11 +2343,7 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const
void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing) 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; 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)) { 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 //BBS: update force previewing logic
if (force_previewing) if (force_previewing)
m_shells.previewing = force_previewing; m_shells.previewing = force_previewing;
@@ -2518,45 +2452,10 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
m_shells.print_id = print.id().id; m_shells.print_id = print.id().id;
m_shells.print_modify_count = print.get_modified_count(); m_shells.print_modify_count = print.get_modified_count();
m_shells.previewing = true; 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%") 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(); % 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() void GCodeViewer::render_toolpaths()
{ {
const Camera& camera = wxGetApp().plater()->get_camera(); const Camera& camera = wxGetApp().plater()->get_camera();
@@ -2757,50 +2656,6 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height)
glsafe(::glDepthMask(GL_TRUE)); 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 //BBS
void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const { void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const {
if (!show) if (!show)
+1 -22
View File
@@ -174,8 +174,6 @@ public:
int print_id{-1}; int print_id{-1};
int print_modify_count{-1}; int print_modify_count{-1};
bool previewing{false}; bool previewing{false};
// the prime tower was loaded with the objects, for the solid model
bool with_wipe_tower{false};
}; };
//BBS //BBS
ConflictResultOpt m_conflict_result; ConflictResultOpt m_conflict_result;
@@ -236,17 +234,6 @@ private:
bool m_legend_visible{ true }; bool m_legend_visible{ true };
bool m_legend_enabled{ 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 };
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; float m_legend_height;
PrintEstimatedStatistics m_print_statistics; PrintEstimatedStatistics m_print_statistics;
@@ -304,7 +291,7 @@ public:
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // 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_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); // 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_count() != 0; } bool has_data() const { return !m_viewer.get_extrusion_roles().empty(); }
bool can_export_toolpaths() const; bool can_export_toolpaths() const;
std::vector<int> get_plater_extruder(); std::vector<int> get_plater_extruder();
@@ -376,14 +363,6 @@ public:
void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); } 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(); } 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" or "shell"
void set_reduced_detail_mode(const std::string& mode);
void set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range); 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; } bool is_legend_shown() const { return m_legend_visible && m_legend_enabled; }
+4 -64
View File
@@ -2084,11 +2084,6 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
const bool overlay_tick = m_fps_overlay_tick; const bool overlay_tick = m_fps_overlay_tick;
m_fps_overlay_tick = false; 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 // 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. // 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); const bool reuse_scene = !scene_dirty && _can_reuse_cached_scene(camera);
@@ -3282,16 +3277,9 @@ void GLCanvas3D::bind_event_handlers()
if (m_selection_edit.kind != SelectionEdit::None) if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit(); finish_selection_edit();
ImGui::SetWindowFocus(nullptr); 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(); render();
evt.Skip(); 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_event_handlers_bound = true;
m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this); m_canvas->Bind(wxEVT_GESTURE_PAN, &GLCanvas3D::on_gesture, this);
@@ -3367,17 +3355,6 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
m_overlay_dirty |= imgui_requires_extra_frame; m_overlay_dirty |= imgui_requires_extra_frame;
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT #endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh(); 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) if (!m_dirty && !m_overlay_dirty)
return; return;
@@ -3908,10 +3885,6 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
return; return;
} }
// only a wheel the panels did not take moves the camera
if (m_canvas_type == CanvasPreview)
note_preview_interaction();
#ifdef __WXMSW__ #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, // 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. // if the event is not allowed to be passed further.
@@ -4012,11 +3985,6 @@ void GLCanvas3D::on_fps_overlay_timer(wxTimerEvent& evt)
wxWakeUpIdle(); 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) void GLCanvas3D::schedule_extra_frame(int milliseconds)
{ {
// Schedule idle event right now // Schedule idle event right now
@@ -5663,9 +5631,6 @@ void GLCanvas3D::mouse_up_cleanup()
m_mouse.ignore_left_up = false; m_mouse.ignore_left_up = false;
m_mouse.ignore_right_up = false; m_mouse.ignore_right_up = false;
m_dirty = true; 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()) if (m_canvas->HasCapture())
m_canvas->ReleaseMouse(); m_canvas->ReleaseMouse();
@@ -7814,20 +7779,13 @@ bool GLCanvas3D::_is_scene_cacheable() const
return false; return false;
#endif #endif
// The scene follows the cursor while the user drags, under a gizmo that draws at the cursor, and // The scene follows the cursor during a drag, under a gizmo that draws at the cursor, and while
// while the cursor is on the layer height bar, where the object shader draws a band at its height. // 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 GLGizmoBase* gizmo = m_gizmos.get_current();
const bool cursor_on_layers_bar = is_layers_editing_enabled() && 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()); m_layers_editing.bar_rect_contains(*this, (float)m_mouse.position.x(), (float)m_mouse.position.y());
return !is_user_interacting() && (gizmo == nullptr || !gizmo->render_follows_cursor()) && !cursor_on_layers_bar; return !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging() &&
} (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 bool GLCanvas3D::_is_frame_skipping_enabled() const
@@ -8818,24 +8776,6 @@ void GLCanvas3D::_render_wireframe_overlay()
shader->stop_using(); 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 //BBS: GUI refactor: add canvas size as parameters
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height) void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
{ {
-11
View File
@@ -656,10 +656,6 @@ private:
ECursorType m_cursor_type; ECursorType m_cursor_type;
GLSelectionRectangle m_rectangle_selection; GLSelectionRectangle m_rectangle_selection;
bool m_navigator_dragging{ false }; 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 //BBS:add plate related logic
mutable std::vector<int> m_hover_volume_idxs; mutable std::vector<int> m_hover_volume_idxs;
@@ -1226,10 +1222,6 @@ public:
void msw_rescale() { m_gcode_viewer.invalidate_legend(); } void msw_rescale() { m_gcode_viewer.invalidate_legend(); }
void request_extra_frame() { m_extra_frame_requested = true; } 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); void schedule_extra_frame(int milliseconds);
@@ -1380,9 +1372,6 @@ private:
//BBS: GUI refactor: add canvas size as parameters //BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height); void _render_gcode(int canvas_width, int canvas_height);
void _render_gcode_overlay(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 //BBS: render a plane for assemble
void _render_plane() const; void _render_plane() const;
void _render_selection(); void _render_selection();
-7
View File
@@ -483,11 +483,6 @@ 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()); 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) 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(); ImGuiWindow* window = ImGui::GetCurrentWindow();
@@ -496,7 +491,6 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
ImGuiContext& context = *GImGui; ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id); const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size); const ImRect draw_region(pos, pos + size);
@@ -889,7 +883,6 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
ImGuiContext& context = *GImGui; ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id); const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size); const ImRect draw_region(pos, pos + size);
-5
View File
@@ -118,9 +118,6 @@ public:
//BBS update scroll value changed //BBS update scroll value changed
bool is_dirty() { return m_dirty; } 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; } void set_as_dirty(bool dirty = true) { m_dirty = dirty; }
bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; } bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; }
void reset_post_tick_event(bool val = false) { void reset_post_tick_event(bool val = false) {
@@ -185,8 +182,6 @@ private:
int m_higher_value; int m_higher_value;
int m_one_layer_value; // ORCA int m_one_layer_value; // ORCA
bool m_dirty = false; 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 }; bool m_render_as_disabled{ false };
+2 -29
View File
@@ -322,7 +322,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too
return sizer; 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, std::function<void(std::string)> onchange, 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, const wxString wiki_url)
{ {
assert(vlist.size() == config_name_index.size()); assert(vlist.size() == config_name_index.size());
unsigned int current_index = 0; unsigned int current_index = 0;
@@ -338,9 +338,8 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index); auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index);
//// save config //// save config
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index, onchange](wxCommandEvent& e) { combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index](wxCommandEvent& e) {
app_config->set(param, config_name_index[e.GetSelection()]); app_config->set(param, config_name_index[e.GetSelection()]);
if (onchange != nullptr) onchange(config_name_index[e.GetSelection()]);
e.Skip(); e.Skip();
}); });
@@ -2045,32 +2044,6 @@ void PreferencesDialog::create_items()
"preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues); "preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues);
g_sizer->Add(item_preview_view_type); 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"
"Shell only: every layer without its sparse infill, internal solid infill and gap fill, which lie under the walls and skins. "
"Walls, top and bottom surfaces, bridges, supports and the prime tower are drawn whole, so the print looks the same from outside.\n"
"The bottom and top of the visible layer range are always drawn whole."),
"preview_reduced_detail_mode",
{_L("Off"), _L("Solid model"), _L("Shell only")},
{"off", "solid", "shell"},
// apply the new mode immediately to the currently loaded preview
[](std::string value) {
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_reduced_detail_mode(value);
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
);
g_sizer->Add(item_reduced_detail_mode);
auto item_dim_previous_layers = create_item_checkbox( auto item_dim_previous_layers = create_item_checkbox(
_L("Dim lower layers"), _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."), _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."),
+1 -1
View File
@@ -93,7 +93,7 @@ public:
wxBoxSizer *create_item_title(wxString title); wxBoxSizer *create_item_title(wxString title);
wxBoxSizer *create_item_label(wxString label, const wxString tooltip = "", const wxString wiki_url = ""); 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::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, std::function<void(std::string)> 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_region_combobox(wxString title, wxString tooltip); wxBoxSizer *create_item_region_combobox(wxString title, wxString tooltip);
wxBoxSizer *create_item_language_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); wxBoxSizer *create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector<wxString> vlist);
+5 -4
View File
@@ -31,14 +31,15 @@ int UserManager::parse_json(std::string payload)
{ {
bool restored_json = false; bool restored_json = false;
json j; json j;
json j_pre = json::parse(payload);
if (j_pre.empty()) {
return -1;
}
//bind/unbind //bind/unbind
try { try {
json j_pre = json::parse(payload);
if (j_pre.empty()) {
return -1;
}
if (j_pre.contains("bind")) { if (j_pre.contains("bind")) {
if (j_pre["bind"].contains("command")) { if (j_pre["bind"].contains("command")) {
-9
View File
@@ -274,13 +274,4 @@ bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionTyp
return res; return res;
} }
int Duet::get_err_code_from_body(const std::string &body) const
{
pt::ptree root;
std::istringstream iss (body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
} }
-1
View File
@@ -40,7 +40,6 @@ private:
ConnectionType connect(wxString &msg) const; ConnectionType connect(wxString &msg) const;
void disconnect(ConnectionType connectionType) const; void disconnect(ConnectionType connectionType) const;
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const; bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const;
int get_err_code_from_body(const std::string &body) const;
}; };
} }
-9
View File
@@ -146,15 +146,6 @@ bool ESP3D::start_print(wxString& msg, const std::string& filename) const
return ret; return ret;
} }
int ESP3D::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
std::istringstream iss(body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
// ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour // ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour
std::string ESP3D::get_short_name(const std::string& filename) const std::string ESP3D::get_short_name(const std::string& filename) const
{ {
-1
View File
@@ -33,7 +33,6 @@ private:
std::string m_console_port; std::string m_console_port;
bool start_print(wxString& msg, const std::string& filename) const; bool start_print(wxString& msg, const std::string& filename) const;
int get_err_code_from_body(const std::string& body) const;
std::string get_short_name(const std::string& filename) const; std::string get_short_name(const std::string& filename) const;
std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const; std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const;
}; };
+25 -15
View File
@@ -510,12 +510,22 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg)) if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
return false; return false;
const auto parsed = json::parse(body, nullptr, false, true); if (!parse_material_slots(body, slots, supports_material_station)) {
if (parsed.is_discarded()) {
msg = _(L("Flashforge returned an invalid JSON response.")); msg = _(L("Flashforge returned an invalid JSON response."));
return false; return false;
} }
return true;
}
bool Flashforge::parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station)
{
slots.clear();
const auto parsed = json::parse(body, nullptr, false, true);
if (parsed.is_discarded())
return false;
const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed; const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] : const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json(); detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
@@ -542,12 +552,21 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
if (supports_material_station != nullptr) if (supports_material_station != nullptr)
*supports_material_station = reports_material_station; *supports_material_station = reports_material_station;
// Fields are read leniently: firmware may send numbers as strings or flags as numbers.
for (const auto& slot : slot_infos) { for (const auto& slot : slot_infos) {
if (!slot.is_object())
continue;
FlashforgeMaterialSlot info; FlashforgeMaterialSlot info;
info.slot_id = slot.value("slotId", static_cast<int>(slots.size()) + 1); info.slot_id = static_cast<int>(slots.size()) + 1;
info.has_filament = slot.value("hasFilament", false); if (const auto it = slot.find("slotId"); it != slot.end())
info.material_name = slot.value("materialName", std::string()); try_parse_json_int(*it, info.slot_id);
info.material_color = slot.value("materialColor", std::string()); int has_filament = 0;
if (const auto it = slot.find("hasFilament"); it != slot.end() && try_parse_json_int(*it, has_filament))
info.has_filament = has_filament != 0;
if (const auto it = slot.find("materialName"); it != slot.end() && it->is_string())
info.material_name = it->get<std::string>();
if (const auto it = slot.find("materialColor"); it != slot.end() && it->is_string())
info.material_color = it->get<std::string>();
slots.emplace_back(std::move(info)); slots.emplace_back(std::move(info));
} }
@@ -670,13 +689,4 @@ std::string Flashforge::extract_host_name() const
return out; return out;
} }
int Flashforge::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
std::istringstream iss(body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
} // namespace Slic3r } // namespace Slic3r
+2 -1
View File
@@ -45,6 +45,8 @@ public:
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
std::string get_host() const override { return m_host; } std::string get_host() const override { return m_host; }
bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const; bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const;
// Parses a local API "detail" reply. Returns false when the body is not valid JSON.
static bool parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station);
static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3); static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3);
private: private:
@@ -68,7 +70,6 @@ private:
bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const; bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const;
std::string make_http_url(const std::string& path) const; std::string make_http_url(const std::string& path) const;
std::string extract_host_name() const; std::string extract_host_name() const;
int get_err_code_from_body(const std::string &body) const;
bool connect(wxString& msg) const; bool connect(wxString& msg) const;
bool start_print(wxString& msg, const std::string& filename) const; bool start_print(wxString& msg, const std::string& filename) const;
}; };
-9
View File
@@ -141,13 +141,4 @@ bool MKS::start_print(wxString& msg, const std::string& filename) const
return ret; return ret;
} }
int MKS::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
std::istringstream iss(body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
} // Slic3r } // Slic3r
-1
View File
@@ -34,7 +34,6 @@ private:
std::string get_upload_url(const std::string& filename) const; std::string get_upload_url(const std::string& filename) const;
bool start_print(wxString& msg, const std::string& filename) const; bool start_print(wxString& msg, const std::string& filename) const;
int get_err_code_from_body(const std::string& body) const;
}; };
} }
+23 -1
View File
@@ -3,10 +3,13 @@
#include <vector> #include <vector>
#include <thread> #include <thread>
#include <exception> #include <exception>
#include <sstream>
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <wx/string.h> #include <wx/string.h>
#include <wx/app.h> #include <wx/app.h>
@@ -172,6 +175,20 @@ std::string moonraker_error_reason(const std::string &body)
} // namespace } // namespace
int PrintHost::get_err_code_from_body(const std::string &body)
{
boost::property_tree::ptree root;
std::istringstream iss(body);
try {
boost::property_tree::read_json(iss, root);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "PrintHost: response is not valid JSON: " << ex.what();
return -1;
}
return root.get<int>("err", 0);
}
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
{ {
if (status != 0) { if (status != 0) {
@@ -303,7 +320,12 @@ void PrintHostJobQueue::priv::bg_thread_main()
% job.cancelled; % job.cancelled;
if (! job.cancelled) { if (! job.cancelled) {
perform_job(std::move(job)); // A failing job must not stop the worker, or later jobs would stay queued forever.
try {
perform_job(std::move(job));
} catch (const std::exception &e) {
emit_error(e.what());
}
} }
remove_source(); remove_source();
+2
View File
@@ -87,6 +87,8 @@ public:
static PrintHost* get_print_host(DynamicPrintConfig *config); static PrintHost* get_print_host(DynamicPrintConfig *config);
static std::string get_print_host_webui(DynamicPrintConfig *config); static std::string get_print_host_webui(DynamicPrintConfig *config);
// Reads the "err" field of a JSON reply, 0 when absent. Returns -1 when the body is not valid JSON.
static int get_err_code_from_body(const std::string &body);
//Support for cloud webui login //Support for cloud webui login
virtual bool is_cloud() const { return false; } virtual bool is_cloud() const { return false; }
-9
View File
@@ -654,13 +654,4 @@ bool UltiMaker::start_print(wxString &msg, const std::string &filename, Connecti
return res; return res;
} }
int UltiMaker::get_err_code_from_body(const std::string &body) const
{
pt::ptree root;
std::istringstream iss (body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
} }
-1
View File
@@ -64,7 +64,6 @@ private:
void set_auth(Http& http) const; void set_auth(Http& http) const;
void disconnect(ConnectionType connectionType) const; void disconnect(ConnectionType connectionType) const;
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const; bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const;
int get_err_code_from_body(const std::string &body) const;
}; };
} }
+1
View File
@@ -25,6 +25,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_cloud_metadata.cpp test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp test_plugin_audit.cpp
test_shortcuts.cpp test_shortcuts.cpp
test_user_manager.cpp
../fff_print/test_helpers.cpp ../fff_print/test_helpers.cpp
) )
+103
View File
@@ -1,8 +1,12 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <string>
#include <vector>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "slic3r/Utils/PrintHost.hpp" #include "slic3r/Utils/PrintHost.hpp"
#include "slic3r/Utils/Flashforge.hpp"
using namespace Slic3r; using namespace Slic3r;
@@ -46,6 +50,14 @@ std::string moonraker_error(int code, const std::string& message, const std::str
constexpr const char* k_busy_file_403 = constexpr const char* k_busy_file_403 =
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON"; R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
// Replies a print host can send instead of JSON: a proxy or login page, nothing, a cut-off body.
const std::vector<std::string> non_json_replies = {
"<html><body>proxy login required</body></html>",
"",
"{\"err\":",
"{\"detail\":{\"matlStationInfo\":{\"slotInfos\":[{\"slotId\":1,",
};
} // namespace } // namespace
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]") TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
@@ -211,3 +223,94 @@ TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect"); CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
} }
} }
TEST_CASE("Print host error code is read from a JSON reply", "[PrintHost]")
{
CHECK(PrintHost::get_err_code_from_body(R"({"err":0})") == 0);
CHECK(PrintHost::get_err_code_from_body(R"({"err":2})") == 2);
CHECK(PrintHost::get_err_code_from_body(R"({"status":"ok"})") == 0);
}
TEST_CASE("Print host error code reports a reply that is not JSON as an error", "[PrintHost]")
{
const std::string body = GENERATE(from_range(non_json_replies));
int err = 0;
REQUIRE_NOTHROW(err = PrintHost::get_err_code_from_body(body));
CHECK(err != 0);
}
TEST_CASE("Print host error code tolerates a wrongly typed err field", "[PrintHost]")
{
const std::string body = GENERATE(as<std::string>{}, R"({"err":"busy"})", R"({"err":{"code":1}})", R"([1,2])");
CHECK_NOTHROW(PrintHost::get_err_code_from_body(body));
}
TEST_CASE("Flashforge material slots are read from a well-formed reply", "[PrintHost][Flashforge]")
{
const std::string body = R"({"code":0,"detail":{"hasMatlStation":true,"matlStationInfo":{"slotCnt":2,"slotInfos":[
{"slotId":1,"hasFilament":true,"materialName":"PLA","materialColor":"#FFFFFF"},
{"slotId":2,"hasFilament":false,"materialName":"","materialColor":""}]}}})";
std::vector<FlashforgeMaterialSlot> slots;
bool supports_station = false;
REQUIRE(Flashforge::parse_material_slots(body, slots, &supports_station));
CHECK(supports_station);
REQUIRE(slots.size() == 2);
CHECK(slots[0].slot_id == 1);
CHECK(slots[0].has_filament);
CHECK(slots[0].material_name == "PLA");
CHECK(slots[0].material_color == "#FFFFFF");
CHECK(slots[1].slot_id == 2);
CHECK_FALSE(slots[1].has_filament);
}
TEST_CASE("Flashforge material slots accept numbers as strings and flags as numbers", "[PrintHost][Flashforge]")
{
const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[
{"slotId":"3","hasFilament":1,"materialName":null,"materialColor":7}]}}})";
std::vector<FlashforgeMaterialSlot> slots;
REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr));
REQUIRE(slots.size() == 1);
CHECK(slots[0].slot_id == 3);
CHECK(slots[0].has_filament);
CHECK(slots[0].material_name.empty());
CHECK(slots[0].material_color.empty());
}
TEST_CASE("Flashforge material slots skip entries that are not objects", "[PrintHost][Flashforge]")
{
const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[5,"slot",null,[],
{"slotId":4,"hasFilament":true,"materialName":"PETG"}]}}})";
std::vector<FlashforgeMaterialSlot> slots;
REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr));
REQUIRE(slots.size() == 1);
CHECK(slots[0].slot_id == 4);
CHECK(slots[0].material_name == "PETG");
}
TEST_CASE("Flashforge material slots tolerate slot info that is not a list", "[PrintHost][Flashforge]")
{
const std::string body = GENERATE(as<std::string>{},
R"({"detail":{"matlStationInfo":{"slotInfos":5}}})",
R"({"detail":{"matlStationInfo":{"slotInfos":"none"}}})",
R"({"detail":{"matlStationInfo":7}})",
R"({"detail":"offline"})");
std::vector<FlashforgeMaterialSlot> slots;
bool ok = false;
REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr));
CHECK(ok);
CHECK(slots.empty());
}
TEST_CASE("Flashforge material slots reject a reply that is not JSON", "[PrintHost][Flashforge]")
{
const std::string body = GENERATE(from_range(non_json_replies));
std::vector<FlashforgeMaterialSlot> slots;
bool ok = true;
REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr));
CHECK_FALSE(ok);
CHECK(slots.empty());
}
+23
View File
@@ -0,0 +1,23 @@
#include <catch2/catch_all.hpp>
#include <string>
#include "slic3r/GUI/UserManager.hpp"
using namespace Slic3r;
TEST_CASE("User message that is not JSON is rejected without throwing", "[UserManager]")
{
const std::string payload = GENERATE(as<std::string>{}, "not json", "", "<html></html>", "{\"bind\":");
UserManager manager;
int result = 0;
REQUIRE_NOTHROW(result = manager.parse_json(payload));
CHECK(result == -1);
}
TEST_CASE("User message without a successful bind is ignored", "[UserManager]")
{
const std::string payload = GENERATE(as<std::string>{}, "{}", R"({"bind":{"command":"unbind"}})", R"({"bind":"bind"})", "[1]");
UserManager manager;
CHECK(manager.parse_json(payload) == -1);
}