Merge upstream main: belt printing, texture displacement color mixing, 3MF component cycle checks, undo blocked during background jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-10-09 07:44:12 -04:00
co-authored by Claude Opus 5.5
227 changed files with 14506 additions and 1079 deletions
+224 -37
View File
@@ -14,6 +14,7 @@
//BBS: add convex hull logic for toolpath check
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "Plater.hpp"
#include "Camera.hpp"
#include "I18N.hpp"
@@ -22,6 +23,8 @@
#include "GLCanvas3D.hpp"
#include "FilamentGroupPopup.hpp"
#include "GLToolbar.hpp"
#include "libslic3r/BeltTransform.hpp"
#include "libslic3r/GCode/MachineFrameTransform.hpp"
#include "MsgDialog.hpp"
#include <boost/container_hash/hash.hpp>
#include "slic3r/GUI/MeshUtils.hpp"
@@ -36,6 +39,7 @@
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLShader.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
#include "libslic3r/Point.hpp"
#include <math.h>
#include "libvgcode/include/Viewer.hpp"
@@ -88,9 +92,11 @@
#include <array>
#include <algorithm>
#include <limits>
#include <sstream>
#include <cmath>
#include <chrono>
#include <Eigen/Geometry>
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/CutUtils.hpp"
@@ -1323,6 +1329,52 @@ std::vector<int> GCodeViewer::get_plater_extruder()
return m_plater_extruder;
}
// Belt printers: compute the full machine->model back-transform from the print
// config, so the "designed" (upright) G-code preview maps each toolpath vertex
// back to Cartesian space. The G-code forward pipeline is (BeltKinematics::
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X the model
// already un-rotated to Cartesian by the back-transform. So the inverse is:
// model = AxisRemap^-1 . MachineFrame^-1
// (origin-snap is a per-instance translation that only shifts position, not
// orientation, so it is intentionally omitted.)
static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
{
if (!cfg.belt_printer.value)
return Transform3d::Identity();
MachineFrameTransform mft;
mft.init_from_config(cfg);
const Transform3d mf_inv = mft.is_active() ? Transform3d(mft.transform().inverse())
: Transform3d::Identity();
// Forward G-code axis remap as an affine matrix (out_i = sign * in[r_i % 3], plus a
// build-volume offset for Rev axes). This is the matrix form of the per-point
// GCodeWriter::apply_axis_remap (row convention: each OUTPUT axis selects an input
// axis + sign) and MUST stay in sync with it. The build-volume max matches what the
// writer is fed in GCode.cpp (printable_area max + printable_height). (Follow-up:
// precompute this matrix once in GCodeWriter and share it with apply_axis_remap to
// remove the parallel encoding.)
Transform3d ar = Transform3d::Identity();
const int rr[3] = { int(cfg.gcode_remap_x.value), int(cfg.gcode_remap_y.value), int(cfg.gcode_remap_z.value) };
if (rr[0] != 0 || rr[1] != 1 || rr[2] != 2) {
BoundingBoxf bbox_bed(cfg.printable_area.values);
const Vec3d vmax(bbox_bed.max.x(), bbox_bed.max.y(), cfg.printable_height.value);
Matrix3d M = Matrix3d::Zero();
Vec3d t = Vec3d::Zero();
for (int i = 0; i < 3; ++i) {
const int axis = rr[i] % 3;
if (rr[i] < 3) M(i, axis) = 1.0;
else if (rr[i] < 6) M(i, axis) = -1.0;
else { M(i, axis) = -1.0; t[i] = vmax[axis]; }
}
ar.linear() = M;
ar.translation() = t;
}
const Transform3d ar_inv = ar.inverse();
return ar_inv * mf_inv;
}
//BBS: always load shell at preview
void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const Print& print, const std::vector<std::string>& str_tool_colors,
const std::vector<std::string>& str_color_print_colors, const BuildVolume& build_volume,
@@ -1330,6 +1382,15 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
{
m_loaded_as_preview = false;
// Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar
// menu item) from the G-code itself. Only BeltGCode writes the belt header, so its tilt
// (gcode_result.belt_tilt_angle, abs of the slicing rotation) says whether this is belt
// G-code; the selected printer does not, for a file opened from disk. The back-transform
// below still reads print.config(), which Plater::load_gcode() fills from the file's own
// config block (GCodeProcessor::export_config_for_render).
m_belt_view_enabled = gcode_result.belt_tilt_angle > 0.f;
m_belt_angle_deg = gcode_result.belt_tilt_angle;
const bool current_top_layer_only = m_viewer.is_top_layer_only_view_range();
const bool required_top_layer_only = get_app_config()->get_bool("seq_top_layer_only");
if (current_top_layer_only != required_top_layer_only)
@@ -1339,8 +1400,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
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")));
// avoid processing if called with the same gcode_result
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) {
// avoid processing if called with the same gcode_result.
// On a belt printer the toolpath geometry fed to libvgcode also depends on the
// designed/raw view state (the back-transform is applied in convert), so the
// same result is converted again only when that view has been toggled.
const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed;
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) {
//BBS: add logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result);
@@ -1381,8 +1446,112 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
return;
}
// convert data from PrusaSlicer format to libvgcode format
libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer);
// convert data from PrusaSlicer format to libvgcode format.
// Belt printers: when the designed (upright) view is active, back-transform
// the toolpath geometry into model/Cartesian space using the general belt
// inverse (handles the mesh rotation, shear and axis remap). When off, the
// raw machine-frame G-code is shown (useful for checking the transform itself).
const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value;
Transform3d belt_inv = (is_belt && m_belt_show_designed)
? compute_belt_back_transform(print.config()) : Transform3d::Identity();
// Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's
// purge-blob advance baked into the machine-Z origin by its G92 Z0 resets). Subtract
// that constant before the linear back-transform so every toolpath maps to the model's
// belt coordinate. Without it the back-transform mixes the offset with the gantry-Y
// term, leaving a per-move designed-Y error that min-corner anchoring cannot remove
// when a bridge/keel move happens to cancel it at the bbox minimum.
if (is_belt && m_belt_show_designed && gcode_result.belt_z_origin != 0.0f)
belt_inv = belt_inv * Transform3d(Eigen::Translation3d(Vec3d(0.0, 0.0, -double(gcode_result.belt_z_origin))));
const bool apply_belt = is_belt && m_belt_show_designed
&& !belt_inv.matrix().isApprox(Transform3d::Identity().matrix());
if (apply_belt) {
// The linear belt back-transform recovers the print's shape and orientation but not
// the per-object placement/lift translation: the object's position on the belt, the
// BeltSliceStrategy min-Z lift, and the centering pre-translate are applied OUTSIDE
// build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse leaves
// them un-undone. Uncorrected, the upright toolpaths float a fixed offset from the
// model shell. Recover the translation generally — independent of the offset's exact
// source or the axis remap — by anchoring the back-transformed object body onto the
// upright model bounding box (the same space the shells render in).
BoundingBoxf3 model_bb;
for (const PrintObject* po : print.objects())
for (const ModelInstance* mi : po->model_object()->instances)
model_bb.merge(po->model_object()->instance_bounding_box(*mi));
// Build the anchor bbox from surface toolpaths only. After the belt_z_origin
// correction the surface back-transforms onto the model, but a few elevated
// features (bridges/overhangs over the chevron gap) are mis-mapped by the linear
// inverse to well outside the model body; if one becomes the bbox minimum it
// drags the min-corner anchor by ~20mm. Drop moves that land clearly outside the
// (correct) model bbox — a geometric filter, not a role guess.
// Tolerance around the model bbox for outlier rejection, and the minimum fraction
// of MOVES (by count) the clip must retain before we trust it over the full bbox.
// Count, not bbox extent: a single far-flung outlier move (e.g. a belt-entry purge
// that maps to y~0) inflates the full bbox so much that a size-fraction test would
// wrongly reject the clip and re-include the outlier, dragging the min-corner anchor
// by ~the offset. By count the clip keeps ~100% of moves here and ~0% only when the
// object is genuinely offset from the belt entry (the real fall-back-to-full case).
constexpr double ANCHOR_CLIP_MARGIN_MM = 10.0;
constexpr double ANCHOR_CLIP_MIN_KEEP = 0.5;
BoundingBoxf3 tp_bb_clip, tp_bb_full;
size_t n_filtered = 0, n_clip = 0;
const double y_lo = model_bb.defined ? model_bb.min.y() - ANCHOR_CLIP_MARGIN_MM : -1e30;
const double y_hi = model_bb.defined ? model_bb.max.y() + ANCHOR_CLIP_MARGIN_MM : 1e30;
// The anchor aligns the toolpath body onto the object-only model bbox, so it must be
// built from OBJECT-body toolpaths only. Support, skirt, brim and wipe-tower extrusions
// are not part of the model mesh and extend past it (support especially reaches well
// beyond the object on a belt), so including them drags tp_bb.min and shifts the whole
// back-transformed g-code off the mesh — but only when those features are present. That
// is the "g-code shifts vs mesh as soon as supports are enabled" bug. Filter them out.
auto is_object_body = [](ExtrusionRole r) {
return r != erSupportMaterial && r != erSupportMaterialInterface
&& r != erSupportTransition
&& r != erSkirt && r != erBrim && r != erWipeTower;
};
auto accumulate = [&](bool body_only) {
tp_bb_clip = BoundingBoxf3();
tp_bb_full = BoundingBoxf3();
n_filtered = 0;
n_clip = 0;
for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves)
if (mv.type == EMoveType::Extrude && mv.layer_id >= 1 // skip layer-0 prime/skirt
&& (!body_only || is_object_body(mv.extrusion_role))) {
++n_filtered;
const Vec3d p = belt_inv * mv.position.cast<double>();
tp_bb_full.merge(p);
if (p.y() >= y_lo && p.y() <= y_hi) {
tp_bb_clip.merge(p);
++n_clip;
}
}
};
accumulate(/*body_only=*/true);
// Fallback: a plate with no object-body extrusions above layer 0 (e.g. a
// support-only object) would leave tp_bb undefined and silently skip the
// anchor. Re-accumulate over ALL extrude moves so the min-corner anchor is
// still computed rather than letting the preview float by the placement offset.
if (n_filtered == 0)
accumulate(/*body_only=*/false);
// Use the clipped bbox when it still holds the bulk of the moves (outliers removed).
// If the object sits far from the belt entry the toolpaths are grossly offset and the
// clip drops most of them — fall back to the full bbox so the min-corner anchor still
// recovers that gross translation rather than breaking.
const BoundingBoxf3& tp_bb =
(tp_bb_clip.defined && n_filtered > 0 &&
double(n_clip) >= ANCHOR_CLIP_MIN_KEEP * double(n_filtered)) ? tp_bb_clip : tp_bb_full;
if (model_bb.defined && tp_bb.defined) {
// Anchor the back-transformed toolpath body onto the upright model bbox by
// its MIN corner. (Center anchoring was tried and regressed when the toolpath
// and model bounding boxes differ in extent.) With the belt_z_origin
// correction above and the outlier-robust bbox below, the surface overlaps the
// shell to well under a millimetre when the object is at the belt entry; an
// object placed elsewhere in global-rotation mode still carries the placement
// translation, which this min-corner step recovers.
const Vec3d d = model_bb.min - tp_bb.min;
belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv;
}
}
libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer,
apply_belt ? &belt_inv : nullptr);
//#define ENABLE_DATA_EXPORT 1
//#if ENABLE_DATA_EXPORT
@@ -1480,6 +1649,26 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_viewer.reset_default_extrusion_roles_colors();
m_viewer.load(std::move(data));
// Belt printers: libvgcode labels a layer with the height of its toolpaths, which
// on a tilted layer is wherever its last extrusion happened to end, and the layer
// slider looks its labels and the colour-change ticks up in that list assuming it
// increases. Give it the layers' print Z instead (the slicer's layer Z, which
// increases along the belt), numbered the way libvgcode::convert() numbers the
// layers: consecutively over the moves that exist.
m_belt_layer_zs.clear();
if (is_belt) {
unsigned int src_layer_id = std::numeric_limits<unsigned int>::max();
for (size_t i = 1; i < gcode_result.moves.size(); ++ i) {
const GCodeProcessorResult::MoveVertex &mv = gcode_result.moves[i];
if (mv.layer_id != src_layer_id) {
src_layer_id = mv.layer_id;
m_belt_layer_zs.emplace_back(double(mv.print_z));
}
}
if (m_belt_layer_zs.size() != m_viewer.get_layers_count())
m_belt_layer_zs.clear();
}
// #if !VGCODE_ENABLE_COG_AND_TOOL_MARKERS
// const size_t vertices_count = m_viewer.get_vertices_count();
// m_cog.reset();
@@ -1513,13 +1702,24 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
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())
m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box);
if (wxGetApp().is_editor()) {
if (is_belt) {
// The moves are machine-frame coordinates (Z is belt travel), so the per-move
// test inside all_paths_inside() can never pass on a belt. Judge the
// back-transformed box instead, with room for the designed view's min-corner
// anchor, which is only accurate to a fraction of a millimetre.
BoundingBoxf3 bed = wxGetApp().plater()->build_volume().bounding_volume();
bed.offset(1.);
m_contained_in_bed = !m_paths_bounding_box.defined || (bed.contains(m_paths_bounding_box.min) && bed.contains(m_paths_bounding_box.max));
} else
m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box);
}
m_extruders_count = gcode_result.filaments_count;
//BBS: move the id to the end of reset
m_last_result_id = gcode_result.id;
m_last_belt_show_designed = m_belt_show_designed;
m_gcode_result = &gcode_result;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
@@ -1550,8 +1750,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_custom_gcode_per_print_z = gcode_result.custom_gcode_per_print_z;
m_max_print_height = gcode_result.printable_height;
m_machine_frame_transform_active = gcode_result.machine_frame_transform_active;
m_z_offset = gcode_result.z_offset;
// load_toolpaths(gcode_result, build_volume, exclude_bounding_box);
// ORCA: Apply the default view type now that we know how many tools the G-code actually uses.
@@ -1737,6 +1939,7 @@ void GCodeViewer::reset()
//BBS: should also reset the result id
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id;
m_last_result_id = -1;
m_belt_layer_zs.clear();
//BBS: add only gcode mode
m_only_gcode_in_preview = false;
@@ -1745,6 +1948,7 @@ void GCodeViewer::reset()
m_paths_bounding_box = BoundingBoxf3();
m_max_bounding_box = BoundingBoxf3();
m_max_print_height = 0.0f;
m_machine_frame_transform_active = false;
m_z_offset = 0.0f;
m_extruders_count = 0;
m_filament_diameters = std::vector<float>();
@@ -3015,7 +3219,14 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
void GCodeViewer::render_toolpaths()
{
const Camera& camera = wxGetApp().plater()->get_camera();
const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast<Matrix4f>(camera.get_view_matrix().matrix().cast<float>()));
Matrix4f view = camera.get_view_matrix().matrix().cast<float>();
// Belt "designed" (upright) view is produced by back-transforming the toolpath
// GEOMETRY into model space at load time (see load_as_gcode ->
// compute_belt_back_transform -> libvgcode::convert). The camera is left untouched
// here; transforming the view as well would double-apply the inverse. Baking the
// transform into the geometry (not the camera) also keeps the bed and toolpaths in
// the same frame so they stay aligned.
const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view);
const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast<Matrix4f>(camera.get_projection_matrix().matrix().cast<float>()));
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size);
@@ -3590,27 +3801,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
}
};
auto link_filament_group_wiki = [&](const std::string& label) {
ImVec2 wiki_part_size = ImGui::CalcTextSize(label.c_str());
ImColor HyperColor = ImColor(0, 150, 136, 255); // ORCA match color
ImGui::PushStyleColor(ImGuiCol_Text, HyperColor.Value);
imgui.text(label.c_str());
ImGui::PopStyleColor();
// ORCA use underline to match hyperlink style
ImVec2 lineEnd = ImGui::GetItemRectMax();
lineEnd.y -= 2.0f;
ImVec2 lineStart = lineEnd;
lineStart.x = ImGui::GetItemRectMin().x;
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, HyperColor);
// click behavior
if (ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), true)) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
open_filament_group_wiki();
}
}
};
auto draw_dash_line = [&](ImDrawList* draw_list, int dash_length = 5, int gap_length = 3) {
ImVec2 p1 = ImGui::GetCursorScreenPos();
ImVec2 p2 = ImVec2(p1.x + ImGui::GetContentRegionAvail().x, p1.y);
@@ -3684,7 +3874,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
else
tips_count = 5;
float AMS_container_height = ams_item_height + line_height * tips_count + line_height;
float AMS_container_height = ams_item_height + line_height * tips_count + line_height + window_padding * 2.f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // this shold be 0 since its child of gcodeviewer
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(window_padding * 3, 0));
@@ -3699,10 +3889,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
ImGui::Dummy({window_padding, window_padding});
ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(1.0f,1.0f,1.0f,0.6f));
imgui.bold_text(_u8L("Filament Grouping"));
ImGui::SameLine();
std::string tip_str = _u8L("Why this grouping");
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(tip_str.c_str()).x);
link_filament_group_wiki(tip_str);
ImGui::Separator();
ImGui::PopStyleColor();
ImGui::Dummy({window_padding, window_padding});
@@ -3807,6 +3993,9 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
}
ImGui::Dummy({window_padding, window_padding});
GLGizmoUtils::render_wiki_guide_button(*wxGetApp().plater()->get_current_canvas3D(), m_scale,"https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
ImGui::SameLine();
if (!is_optimal_group) {
link_text_set_to_optional(_u8L("Set to Optimal"));
ImGui::SameLine();
@@ -3815,10 +4004,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
}
link_text(_u8L("Regroup filament"));
ImGui::SameLine();
std::string wiki_str = _u8L("Wiki Guide"); // ORCA
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(wiki_str.c_str()).x);
link_filament_group_wiki(wiki_str);
ImGui::Dummy({window_padding, window_padding});
ImGui::EndChild();
}
@@ -5417,6 +5603,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab
render_legend_color_arr_recommen(window_padding);
legend_height = ImGui::GetCurrentWindow()->Size.y;
imgui.end();
ImGui::PopStyleColor(7);