Merge upstream/main into belt-printer

Brings belt-printer up to main 4b4a261787. Resolutions:

- G-code header (#15897, #15915): main moved the header, config and
  thumbnail block later in _do_export; write_belt_header() moves with it,
  still after the thumbnails and outside the BTT_TFT gate.
- _extrude: first-layer acceleration keeps the per-path first-layer plane
  test with main's cached nozzle index (#16028); main's set_speed out-param
  form (#16108) everywhere else.
- GCodeWriter (#16108): the arc-to-polyline fallback for machine mappings
  that cannot express G2/G3 now runs in the out-param extrude_arc_to_xy,
  which is the overload GCode calls, and appends to the caller's string.
- GCodeProcessorResult: the belt fields join main's forwarding assign.
- Clipper2 (#15969): belt arrange helpers take Slic3r::Point; the tree
  support join types lose their ClipperLib qualifier.
- CLI arrange (#15837): belt printers still reserve no wipe tower.
- Wipe tower options (#15841): the two new sparse-layer toggles are hidden
  for belt printers like the rest of the tower options.
- Keyboard shortcuts (#15706): main's registry replaces the old key switch;
  the belt view toggle is re-registered in the next commit.
- Print::process: the belt purge-plan undo runs before main's SliceStarted
  event.
- scripts/filament_id_snapshot.json: deleted on main (a77209af8f).
- Includes and appended tests: union of both sides.
This commit is contained in:
harrierpigeon
2026-10-04 19:20:33 -05:00
11471 changed files with 2992690 additions and 2720441 deletions
+258 -45
View File
@@ -17,6 +17,7 @@
#include "Plater.hpp"
#include "Camera.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "GUI_Utils.hpp"
#include "GUI.hpp"
#include "GLCanvas3D.hpp"
@@ -29,12 +30,61 @@
#include "libslic3r/Layer.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "MsgDialog.hpp"
#include <boost/container_hash/hash.hpp>
#include "slic3r/GUI/MeshUtils.hpp"
#include <string>
#include "libvgcode/include/Types.hpp"
#include <vector>
#include <utility>
#include <cstdio>
#include "libslic3r/Technologies.hpp"
#include <imgui.h>
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLShader.hpp"
#include "libslic3r/Point.hpp"
#include <math.h>
#include "libvgcode/include/Viewer.hpp"
#include "libvgcode/include/PathVertex.hpp"
#include <cstddef>
#include <cstring>
#include "slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp"
#include <cassert>
#include <cstdint>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/constants.hpp>
#include "slic3r/GUI/IMSlider.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Preset.hpp"
#include <exception>
#include <iterator>
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libvgcode/include/GCodeInputData.hpp"
#include "libvgcode/include/ColorRange.hpp"
#include <optional>
#include "libslic3r_version.h"
#include <wx/busycursor.h>
#include "libslic3r/Slicing.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "libslic3r/Color.hpp"
#include <map>
#include "libslic3r/PrintBase.hpp"
#include <wx/event.h>
#include <wx/string.h>
#include <wx/slider.h>
#include "libslic3r/PrintConfig.hpp"
#include "slic3r/GUI/Event.hpp"
#include <string_view>
#include <functional>
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui/imgui_internal.h>
#include <glad/gl.h>
#include <boost/functional/hash.hpp>
#include <boost/log/trivial.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/nowide/cstdio.hpp>
@@ -105,6 +155,31 @@ static std::string get_view_type_string(libvgcode::EViewType view_type)
return "";
}
// ORCA: Stable, locale independent names used to persist a view type in the application config.
// Keep these in sync with the entries of libvgcode::EViewType exposed in the preview combo box.
static const std::vector<std::pair<std::string, libvgcode::EViewType>>& view_type_config_map()
{
static const std::vector<std::pair<std::string, libvgcode::EViewType>> map = {
{ "summary", libvgcode::EViewType::Summary },
{ "feature_type", libvgcode::EViewType::FeatureType },
{ "color_print", libvgcode::EViewType::ColorPrint },
{ "speed", libvgcode::EViewType::Speed },
{ "actual_speed", libvgcode::EViewType::ActualSpeed },
{ "acceleration", libvgcode::EViewType::Acceleration },
{ "jerk", libvgcode::EViewType::Jerk },
{ "height", libvgcode::EViewType::Height },
{ "width", libvgcode::EViewType::Width },
{ "volumetric_flow_rate", libvgcode::EViewType::VolumetricFlowRate },
{ "actual_volumetric_flow_rate", libvgcode::EViewType::ActualVolumetricFlowRate },
{ "layer_time_linear", libvgcode::EViewType::LayerTimeLinear },
{ "layer_time_logarithmic", libvgcode::EViewType::LayerTimeLogarithmic },
{ "fan_speed", libvgcode::EViewType::FanSpeed },
{ "temperature", libvgcode::EViewType::Temperature },
{ "pressure_advance", libvgcode::EViewType::PressureAdvance },
};
return map;
}
// Find an index of a value in a sorted vector, which is in <z-eps, z+eps>.
// Returns -1 if there is no such member.
static int find_close_layer_idx(const std::vector<double> &zs, double &z, double eps)
@@ -998,13 +1073,17 @@ void GCodeViewer::SequentialView::GCodeWindow::stop_mapping_file()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": finished mapping file " << m_filename;
}
}
void GCodeViewer::SequentialView::render(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
void GCodeViewer::SequentialView::render_marker(const bool has_render_path, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type)
{
if (has_render_path && m_show_marker) {
if (has_render_path && m_show_marker)
// marker.set_world_offset(current_offset);
marker.render(canvas_width, canvas_height, view_type);
}
void GCodeViewer::SequentialView::render_overlay(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
{
if (has_render_path && m_show_marker)
marker.render_position_window(viewer, canvas_width, canvas_height, view_type);
}
//float bottom = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_height();
// BBS
@@ -1088,9 +1167,7 @@ void GCodeViewer::init(ConfigOptionMode mode, PresetBundle* preset_bundle)
// Default view type at first slice.
// May be overridden in load() once we know how many tools are actually used in the G-code.
m_nozzle_nums = preset_bundle ? preset_bundle->get_printer_extruder_count() : 1;
auto it = std::find(view_type_items.begin(), view_type_items.end(), libvgcode::EViewType::FeatureType);
m_view_type_sel = (it != view_type_items.end()) ? std::distance(view_type_items.begin(), it) : 0;
set_view_type(libvgcode::EViewType::FeatureType);
apply_default_view_type();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished");
}
@@ -1111,6 +1188,73 @@ void GCodeViewer::set_scale(float scale)
}
}
// ORCA: Preview default view type preference, see "preview_default_view_type" in the application config.
std::string GCodeViewer::view_type_to_config_name(libvgcode::EViewType type)
{
for (const auto& [name, value] : view_type_config_map()) {
if (value == type)
return name;
}
return std::string();
}
bool GCodeViewer::view_type_from_config_name(const std::string& name, libvgcode::EViewType& type)
{
for (const auto& [config_name, value] : view_type_config_map()) {
if (config_name == name) {
type = value;
return true;
}
}
return false;
}
std::vector<std::pair<std::string, std::string>> GCodeViewer::default_view_type_choices()
{
std::vector<std::pair<std::string, std::string>> choices = {
{ "auto", _u8L("Automatic") },
{ "last", _u8L("Last used") },
};
for (const auto& [name, type] : view_type_config_map())
choices.push_back({ name, get_view_type_string(type) });
return choices;
}
void GCodeViewer::select_view_type(libvgcode::EViewType type)
{
auto it = std::find(view_type_items.begin(), view_type_items.end(), type);
m_view_type_sel = (it != view_type_items.end()) ? static_cast<int>(std::distance(view_type_items.begin(), it)) : 0;
set_view_type(type);
}
// ORCA: Pick the view type the preview opens with, following the "preview_default_view_type" preference:
// a fixed view type, the one the user picked last ("last"), or the automatic choice ("auto", the default)
// which shows Filament for multi material prints and Line Type for single material ones.
// The default is only (re)applied when it actually changes, so a view type picked by hand survives a reslice.
void GCodeViewer::apply_default_view_type()
{
const std::string preference = wxGetApp().app_config->get("preview_default_view_type");
std::string key = preference;
libvgcode::EViewType type = libvgcode::EViewType::FeatureType;
if (preference == "last") {
if (!view_type_from_config_name(wxGetApp().app_config->get("preview_last_view_type"), type))
type = libvgcode::EViewType::FeatureType;
}
else if (!view_type_from_config_name(preference, type)) {
// "auto", or an unknown value written by a newer version
const bool multi_material = m_viewer.get_used_extruders_count() > 1;
type = multi_material ? libvgcode::EViewType::ColorPrint : libvgcode::EViewType::FeatureType;
key = multi_material ? "auto_multi_material" : "auto_single_material";
}
if (m_applied_default_view_type_key == key)
return;
m_applied_default_view_type_key = key;
select_view_type(type);
}
void GCodeViewer::update_by_mode(ConfigOptionMode mode)
{
view_type_items.clear();
@@ -1517,6 +1661,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
libvgcode::EGCodeExtrusionRole::SupportTransition, libvgcode::EGCodeExtrusionRole::Mixed
});
m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast<double>(), libvgcode::convert(bbox[1]).cast<double>());
m_max_bounding_box = m_paths_bounding_box;
if (wxGetApp().is_editor()) {
if (is_belt) {
@@ -1571,27 +1716,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
// load_toolpaths(gcode_result, build_volume, exclude_bounding_box);
// ORCA: Apply smart default view type when extruder count changes.
// Multi-color: ColorPrint (Filament), Single-color: FeatureType (Line Type).
// User selections persist within same extruder count, defaults reapply on count change.
int current_count = m_viewer.get_used_extruders_count();
if (current_count > 1) {
if (m_last_extruder_count_default_applied != 2) {
auto it = std::find(view_type_items.begin(), view_type_items.end(), libvgcode::EViewType::ColorPrint);
if (it != view_type_items.end())
m_view_type_sel = std::distance(view_type_items.begin(), it);
set_view_type(libvgcode::EViewType::ColorPrint);
m_last_extruder_count_default_applied = 2;
}
} else {
if (m_last_extruder_count_default_applied != 1) {
auto it = std::find(view_type_items.begin(), view_type_items.end(), libvgcode::EViewType::FeatureType);
if (it != view_type_items.end())
m_view_type_sel = std::distance(view_type_items.begin(), it);
set_view_type(libvgcode::EViewType::FeatureType);
m_last_extruder_count_default_applied = 1;
}
}
// ORCA: Apply the default view type now that we know how many tools the G-code actually uses.
apply_default_view_type();
// BBS: data for rendering color arrangement recommendation
m_nozzle_nums = print.config().option<ConfigOptionFloats>("nozzle_diameter")->values.size();
@@ -1799,16 +1925,88 @@ void GCodeViewer::reset()
}
//BBS: GUI refactor: add canvas width and height
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
{
glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height);
if (m_viewer.get_extrusion_roles().empty())
if (m_viewer.get_extrusion_roles_count() == 0)
return;
render_toolpaths();
auto current = m_viewer.get_view_visible_range();
auto endpoints = m_viewer.get_view_full_range();
m_sequential_view.m_show_marker = m_sequential_view.m_show_marker || (current.back() != endpoints.back() && !m_no_render_path);
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type());
}
size_t GCodeViewer::shadow_casters_signature() const
{
size_t hash = m_viewer.get_vertices_count();
const libvgcode::Interval& visible = m_viewer.get_view_visible_range();
const libvgcode::Interval& layers = m_viewer.get_layers_view_range();
for (size_t value : { size_t(visible[0]), size_t(visible[1]), size_t(layers[0]), size_t(layers[1]) })
boost::hash_combine(hash, value);
for (double value : { m_paths_bounding_box.min.x(), m_paths_bounding_box.min.y(), m_paths_bounding_box.min.z(),
m_paths_bounding_box.max.x(), m_paths_bounding_box.max.y(), m_paths_bounding_box.max.z() })
boost::hash_combine(hash, value);
boost::hash_combine(hash, m_viewer.is_top_layer_only_view_range());
for (size_t i = 0; i < libvgcode::GCODE_EXTRUSION_ROLES_COUNT; ++i)
boost::hash_combine(hash, m_viewer.is_extrusion_role_visible(libvgcode::EGCodeExtrusionRole(i)));
boost::hash_combine(hash, m_viewer.is_option_visible(libvgcode::EOptionType::Travels));
boost::hash_combine(hash, m_viewer.is_option_visible(libvgcode::EOptionType::Wipes));
for (float value : m_clipping_plane)
boost::hash_combine(hash, value);
return hash;
}
void GCodeViewer::render_shadow_casters(const Transform3d& light_view_matrix, const Transform3d& light_projection_matrix, const Vec3d& light_position)
{
if (!has_data())
return;
m_viewer.render_shadow_casters(
libvgcode::convert(static_cast<Matrix4f>(light_view_matrix.matrix().cast<float>())),
libvgcode::convert(static_cast<Matrix4f>(light_projection_matrix.matrix().cast<float>())),
libvgcode::convert(static_cast<Vec3f>(light_position.cast<float>())));
}
void GCodeViewer::set_shadow_map(int texture_unit, const Transform3d& light_view_projection, float intensity, float texel_size)
{
m_viewer.set_shadow_map(texture_unit,
libvgcode::convert(static_cast<Matrix4f>(light_view_projection.matrix().cast<float>())),
intensity, texel_size);
}
void GCodeViewer::set_light_top_dir(const Vec3d& direction)
{
m_viewer.set_light_top_dir(libvgcode::convert(static_cast<Vec3f>(direction.cast<float>())));
}
void GCodeViewer::set_tone(float exposure, float saturation)
{
m_viewer.set_tone(exposure, saturation);
}
void GCodeViewer::set_clipping_plane(const ClippingPlane& plane)
{
// Flipped to match ClippingPlane::distance().
const Vec3f normal = -plane.get_normal().cast<float>();
m_clipping_plane = plane.is_active() ?
std::array<float, 4>{ normal.x(), normal.y(), normal.z(), float(plane.get_offset()) } :
std::array<float, 4>{ 0.0f, 0.0f, 0.0f, 1.0f };
m_viewer.set_clipping_plane(m_clipping_plane);
}
void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin)
{
if (m_viewer.get_extrusion_roles().empty())
return;
float legend_height = 0.0f;
render_legend(legend_height, canvas_width, canvas_height, right_margin);
@@ -1817,16 +2015,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
m_user_mode = wxGetApp().get_mode();
}
//BBS fixed bottom_margin for space to render horiz slider
int bottom_margin = SLIDER_BOTTOM_MARGIN * GCODE_VIEWER_SLIDER_SCALE;
auto current = m_viewer.get_view_visible_range();
auto endpoints = m_viewer.get_view_full_range();
m_sequential_view.m_show_marker = m_sequential_view.m_show_marker || (current.back() != endpoints.back() && !m_no_render_path);
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
// BBS fixed buttom margin. m_moves_slider.pos_y
m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type());
m_sequential_view.render_overlay(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, sequential_view_height(canvas_height), right_margin * m_scale, m_viewer.get_view_type());
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
if (is_legend_shown()) {
@@ -1867,6 +2056,14 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
render_slider(canvas_width, canvas_height);
}
int GCodeViewer::sequential_view_height(int canvas_height) const
{
//BBS fixed bottom_margin for space to render horiz slider
const int bottom_margin = SLIDER_BOTTOM_MARGIN * GCODE_VIEWER_SLIDER_SCALE;
// BBS fixed buttom margin. m_moves_slider.pos_y
return canvas_height - bottom_margin * m_scale;
}
#define ENABLE_CALIBRATION_THUMBNAIL_OUTPUT 0
#if ENABLE_CALIBRATION_THUMBNAIL_OUTPUT
static void debug_calibration_output_thumbnail(const ThumbnailData& thumbnail_data)
@@ -3597,6 +3794,12 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::pair<ColorRGBA, std::pair<double, double>>> ret;
ret.reserve(custom_gcode_per_print_z.size());
// Loop invariant, but built lazily: this lambda runs once per extruder on every frame
// and most prints reach neither colour change below, so fetching it up front would cost
// more than the per-item fetch it replaces.
std::vector<float> zs;
bool zs_built = false;
for (const auto& item : custom_gcode_per_print_z) {
if (extruder_id + 1 != static_cast<unsigned char>(item.extruder))
continue;
@@ -3604,7 +3807,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (item.type != ColorChange)
continue;
const std::vector<float> zs = m_viewer.get_layers_zs();
if (!zs_built) {
zs = m_viewer.get_layers_zs();
zs_built = true;
}
auto lower_b = std::lower_bound(zs.begin(), zs.end(),
static_cast<float>(item.print_z - epsilon()));
if (lower_b == zs.end())
@@ -3625,16 +3831,18 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
return ret;
};
// Whole sentences: the bare "up to"/"above"/"from"/"to" these used to be glued from gave a
// translator no context, and left the unit and the numbers stuck in English word order.
auto upto_label = [](double z) {
char buf[64];
::sprintf(buf, "%.2f", z);
return _u8L("up to") + " " + std::string(buf) + " " + _u8L("mm");
return format(_u8L("up to %1% mm"), buf);
};
auto above_label = [](double z) {
char buf[64];
::sprintf(buf, "%.2f", z);
return _u8L("above") + " " + std::string(buf) + " " + _u8L("mm");
return format(_u8L("above %1% mm"), buf);
};
auto fromto_label = [](double z1, double z2) {
@@ -3642,7 +3850,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf1, "%.2f", z1);
char buf2[64];
::sprintf(buf2, "%.2f", z2);
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
return format(_u8L("from %1% to %2% mm"), buf1, buf2);
};
auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
@@ -3727,6 +3935,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
m_view_type_sel = i;
set_view_type(view_type_items[m_view_type_sel]);
reset_visible(view_type_items[m_view_type_sel]);
// ORCA: remember the pick so the "Last used" preview default can restore it
const std::string view_type_name = view_type_to_config_name(view_type_items[m_view_type_sel]);
if (!view_type_name.empty())
wxGetApp().app_config->set("preview_last_view_type", view_type_name);
update_moves_slider();
#if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
imgui.set_requires_extra_frame();
@@ -4751,6 +4963,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
// ORCA: Get layer Zs as doubles
std::vector<double> layer_zs = get_layers_zs();
// loop invariant, same reason as the layer Zs above
const std::vector<float> layer_times = m_viewer.get_layers_estimated_times();
for (Slic3r::CustomGCode::Item custom_gcode : custom_gcode_per_print_z) {
ImGui::Dummy({window_padding, window_padding});
@@ -4770,7 +4984,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
imgui.text(buf);
ImGui::SameLine(max_len * 1.5);
std::vector<float> layer_times = m_viewer.get_layers_estimated_times();
float custom_gcode_time = 0;
if (layer > 0)
{
@@ -4819,7 +5032,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::string print_str = _u8L("Model printing time");
std::string total_str = _u8L("Total time");
float max_len = window_padding + 2 * ImGui::GetStyle().ItemSpacing.x;
if (m_viewer.get_layers_estimated_times().empty())
if (m_viewer.get_layers_count() == 0)
max_len += ImGui::CalcTextSize(total_str.c_str()).x;
else {
if (m_viewer.get_view_type() == libvgcode::EViewType::FeatureType)