belt: render the G-code preview in model (Cartesian) space

On a belt printer the emitted G-code is in the machine frame (45-deg sheared,
axis-remapped, scaled), so the toolpath preview shows the print as a sheared
slab floating off the bed. Map each toolpath vertex back to model/Cartesian
space for the "designed" view.

The back-transform is the inverse of the full G-code forward pipeline
(BeltGCodeWriter::to_machine_coords):
  model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
built from config, so it handles any rotation / shear / scale / axis-remap
combination, not just plain 45-deg belt slicing. Computed in load_as_gcode()
from print.config() and applied per-vertex inside libvgcode::convert (display
position only; layer_id, times and the volumetric/flow math keep the raw
machine values, so the layer slider and stats are unaffected).

- Toggle with the existing "Show designed view" checkbox / hotkey B; off shows
  the raw machine-frame G-code (useful for debugging the transform itself).
  Defaults to on.
- Belt printers skip the same-result-id load cache so the upright view applies
  and the toggle takes effect even when the G-code is unchanged.
- The object extrusions (layer_id >= 1) are anchored to the belt entry to drop
  the constant machine-origin offset (start-G-code belt advance) that the linear
  back-transform alone does not capture; start-G-code prime lines are excluded
  so they don't steal the anchor.
This commit is contained in:
Tommaso Bianchi
2026-06-06 16:00:37 +02:00
parent 2d69f6e17c
commit 695a1f897a
4 changed files with 112 additions and 17 deletions

View File

@@ -24,6 +24,8 @@
#include "GLToolbar.hpp"
#include "GUI_Preview.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/BeltTransform.hpp"
#include "libslic3r/GCode/MachineFrameTransform.hpp"
#include "libslic3r/Layer.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "MsgDialog.hpp"
@@ -1118,6 +1120,51 @@ 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 (BeltGCodeWriter::
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if
// gcode_back_transform (write already un-rotated to Cartesian) else BeltForward(
// model). So the inverse is:
// model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
// All parts are config-driven affines -> handles any rotation/shear/scale/axis-
// remap combination. (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();
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();
Transform3d bf_inv = Transform3d::Identity();
if (!cfg.gcode_back_transform.value)
bf_inv = BeltTransformPipeline::build_forward_transform(cfg).inverse();
return bf_inv * 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,
@@ -1130,8 +1177,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
if (current_top_layer_only != required_top_layer_only)
m_viewer.toggle_top_layer_only_view_range();
// 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.
// Belt printers are exempt: the toolpath geometry fed to libvgcode depends on
// the current designed/raw view state (back-transform applied in convert), so
// re-running the conversion is required for the upright view and for toggling
// it (hotkey B) to take effect even when the G-code itself is unchanged.
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && !print.config().belt_printer.value) {
//BBS: add logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result);
@@ -1172,8 +1223,18 @@ 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 any mesh rotation + shear + axis remap). When off, the raw
// machine-frame G-code is shown (useful for debugging the transform itself).
const bool is_belt = print.config().belt_printer.value;
const Transform3d belt_inv = (is_belt && m_belt_show_designed)
? compute_belt_back_transform(print.config()) : Transform3d::Identity();
const bool apply_belt = is_belt && m_belt_show_designed
&& !belt_inv.matrix().isApprox(Transform3d::Identity().matrix());
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
@@ -2303,12 +2364,12 @@ void GCodeViewer::render_toolpaths()
{
const Camera& camera = wxGetApp().plater()->get_camera();
Matrix4f view = camera.get_view_matrix().matrix().cast<float>();
// Belt "designed" view: apply the precomputed inverse of the full belt
// shear+scale transform so toolpaths appear upright (as originally designed)
// instead of transformed on the belt.
if (m_belt_show_designed && m_belt_view_enabled) {
view = (camera.get_view_matrix() * m_belt_inverse_transform).matrix().cast<float>();
}
// Belt "designed" (upright) view is now produced by back-transforming the
// toolpath GEOMETRY into model space at load time (see load_as_gcode ->
// libvgcode::convert with m_belt_inverse_transform). The camera is therefore
// left untouched here; transforming the view as well would double-apply the
// inverse. Keeping m_belt_inverse_transform on 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

View File

@@ -245,7 +245,8 @@ mutable bool m_no_render_path { false };
bool m_belt_view_enabled = false;
float m_belt_angle_deg = 0.f;
bool m_belt_show_designed = false; // Toggle: show designed (upright) view via inverse shear
bool m_belt_show_designed = true; // Toggle: designed (upright, back-transformed) view by default;
// turn off (hotkey B) to inspect the raw machine-frame G-code.
Transform3d m_belt_inverse_transform{Transform3d::Identity()};
libvgcode::Viewer m_viewer;

View File

@@ -189,10 +189,22 @@ Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode)
}
GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector<std::string>& str_tool_colors,
const std::vector<std::string>& str_color_print_colors, const Viewer& viewer)
const std::vector<std::string>& str_color_print_colors, const Viewer& viewer,
const Slic3r::Transform3d* belt_xform)
{
GCodeInputData ret;
// Belt printers: optionally map each vertex DISPLAY position from machine
// (G-code) space back to model/Cartesian space using the general belt
// back-transform (handles any mesh rotation + shear + axis remap, not just
// 45 deg). Only the rendered position is transformed; layer_id, times and
// the volumetric/flow math below keep the original machine-space values.
auto xform_pos = [belt_xform](const Slic3r::Vec3f& v) -> Vec3 {
if (belt_xform != nullptr)
return convert(Slic3r::Vec3f((*belt_xform * v.cast<double>()).cast<float>()));
return convert(v);
};
// collect tool colors
ret.tools_colors.reserve(str_tool_colors.size());
for (const std::string& color : str_tool_colors) {
@@ -221,7 +233,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
// equal to the current one with the exception of the position, which should match the previous move position,
// and the times, which are set to zero
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
const libvgcode::PathVertex vertex = { convert(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
@@ -229,7 +241,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
/* ORCA: Add Jerk visualization support */ curr.jerk };
#else
const libvgcode::PathVertex vertex = { convert(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
@@ -242,7 +254,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
}
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
const libvgcode::PathVertex vertex = { convert(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate,
const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature,
result.filament_densities[curr.extruder_id] * curr.mm3_per_mm * (curr.position - prev.position).norm(),
convert(curr.extrusion_role), curr_type, static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
@@ -251,7 +263,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
/* ORCA: Add Jerk visualization support */ curr.jerk };
#else
const libvgcode::PathVertex vertex = { convert(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate,
const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), curr.time,
@@ -263,6 +275,26 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
}
ret.vertices.shrink_to_fit();
// Belt designed view: the linear back-transform recovers the correct shape
// and orientation, but not the constant machine-frame origin offset baked
// into the G-code (e.g. the start-G-code belt advance + a G92 reset leaves a
// ~20 mm Z residual). Anchor the lowest extrusion to the belt entry (Y=0) so
// the toolpaths sit on the bed under the model shell. Independent of the
// offset's source, so it stays general across machines.
if (belt_xform != nullptr && !ret.vertices.empty()) {
// Anchor on the OBJECT extrusions only (layer_id >= 1): the start-G-code
// prime lines (layer 0) print at the belt origin, while the object prints
// after the start-G-code belt advance, so anchoring on the global min
// would lock onto the prime and leave the object offset.
float min_y = std::numeric_limits<float>::max();
for (const PathVertex& v : ret.vertices)
if (v.type == EMoveType::Extrude && v.layer_id >= 1 && v.position[1] < min_y)
min_y = v.position[1];
if (min_y != std::numeric_limits<float>::max() && std::abs(min_y) > 1e-3f)
for (PathVertex& v : ret.vertices)
v.position[1] -= min_y;
}
ret.spiral_vase_mode = result.spiral_vase_mode;
return ret;

View File

@@ -71,7 +71,8 @@ extern Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode
// mapping from Slic3r::GCodeProcessorResult to libvgcode::GCodeInputData
extern GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector<std::string>& str_tool_colors,
const std::vector<std::string>& str_color_print_colors, const Viewer& viewer);
const std::vector<std::string>& str_color_print_colors, const Viewer& viewer,
const Slic3r::Transform3d* belt_xform = nullptr);
// mapping from Slic3r::Print to libvgcode::GCodeInputData
extern GCodeInputData convert(const Slic3r::Print& print, const std::vector<std::string>& str_tool_colors,