From 2d69f6e17c56c7506520be03b0f1ea595ddd98a8 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 6 Jun 2026 16:00:37 +0200 Subject: [PATCH 1/4] belt: expose MachineFrameTransform's composed matrix Add a const accessor for the shear*scale transform so the G-code viewer can build the machine->model back-transform for the upright belt preview. --- src/libslic3r/GCode/MachineFrameTransform.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 425fe3004b..f5e71398da 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -33,6 +33,11 @@ public: bool is_active() const { return m_active; } + // The composed shear*scale transform (identity when inactive). Exposed so the + // G-code viewer can build the machine->model back-transform for the upright + // ("designed") belt preview. + const Transform3d& transform() const { return m_transform; } + private: bool m_active = false; Transform3d m_transform = Transform3d::Identity(); From 695a1f897a828d6e24b44559662fd79b7cb49567 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 6 Jun 2026 16:00:37 +0200 Subject: [PATCH 2/4] 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. --- src/slic3r/GUI/GCodeViewer.cpp | 81 ++++++++++++++++--- src/slic3r/GUI/GCodeViewer.hpp | 3 +- src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp | 42 ++++++++-- src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp | 3 +- 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1f9741e92b..963730e093 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -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 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& str_tool_colors, const std::vector& 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(); - // 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(); - } + // 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(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index ca79a82584..d5173b02e5 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -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; diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index 5b5c7da076..ffc549755b 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -189,10 +189,22 @@ Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode) } GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector& str_tool_colors, - const std::vector& str_color_print_colors, const Viewer& viewer) + const std::vector& 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()).cast())); + 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(curr.gcode_id), static_cast(curr.layer_id), static_cast(curr.extruder_id), static_cast(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(curr.gcode_id), static_cast(curr.layer_id), static_cast(curr.extruder_id), static_cast(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(curr.gcode_id), static_cast(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(curr.gcode_id), static_cast(curr.layer_id), static_cast(curr.extruder_id), static_cast(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::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::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; diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp index 35b5bd7b44..872f297ae6 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp @@ -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& str_tool_colors, - const std::vector& str_color_print_colors, const Viewer& viewer); + const std::vector& 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& str_tool_colors, From 3fc3b8a8aedae1042cfe8f8166c51f3c0b25351f Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 6 Jun 2026 19:39:57 +0200 Subject: [PATCH 3/4] belt: anchor the designed-view G-code preview onto the model bounding box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The belt designed (upright) preview back-transforms the machine-frame G-code into model space with the linear belt inverse. That inverse 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 cannot undo them. The result was a constant offset (~20 mm on the belt-advance axis) of the toolpaths from the model shell, on every model. Recover the missing translation generally — independent of the offset's exact source or the axis remap — by anchoring the back-transformed object body (extrusions on layer_id >= 1, i.e. excluding the layer-0 prime/skirt) onto the upright model bounding box, the same space the shells render in, and folding that translation into the belt inverse before converting to libvgcode. Replaces the previous Y=0 anchoring in LibVGCodeWrapper, which pinned the toolpaths to the belt entry rather than to the model and so left the offset in place for any object not sitting at the origin. --- src/slic3r/GUI/GCodeViewer.cpp | 28 ++++++++++++++++++- src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp | 23 +++------------ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 963730e093..9baced5d78 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1229,10 +1229,36 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // 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) + 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()); + 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)); + BoundingBoxf3 tp_bb; + for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) // skip layer-0 prime/skirt + tp_bb.merge(Vec3d(belt_inv * mv.position.cast())); + if (model_bb.defined && tp_bb.defined) { + const Vec3d d = model_bb.min - tp_bb.min; + belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; + BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor: model_bb.min=(" + << model_bb.min.x() << "," << model_bb.min.y() << "," << model_bb.min.z() + << ") tp_bb.min=(" << tp_bb.min.x() << "," << tp_bb.min.y() << "," << tp_bb.min.z() + << ") d=(" << d.x() << "," << d.y() << "," << d.z() << ")"; + } + } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, apply_belt ? &belt_inv : nullptr); diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index ffc549755b..015947e9bb 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -275,25 +275,10 @@ 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::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::max() && std::abs(min_y) > 1e-3f) - for (PathVertex& v : ret.vertices) - v.position[1] -= min_y; - } + // Note: the belt designed-view anchoring (recovering the per-object placement/ + // lift translation the linear back-transform cannot) is folded into belt_xform + // by the caller (GCodeViewer::load_as_gcode), which anchors onto the upright + // model bounding box. Nothing extra to do here. ret.spiral_vase_mode = result.spiral_vase_mode; From 8593d66a3998de8e87886251493f6d10bbc26eef Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sun, 7 Jun 2026 09:57:12 +0200 Subject: [PATCH 4/4] belt: correct the designed-view preview's belt-Z origin and reject mis-mapped outliers The Cartesian designed-view preview over-extended the toolpaths past the model shell by a height-proportional amount (up to ~20mm tall parts), most visibly on long multi-part prints; compact parts like a calibration cube looked fine. Two coupled causes: - Belt start G-code that primes with a Z advance and a 'G92 Z0' reset leaves a constant machine-Z origin in the GCodeProcessor, so move positions are stored as gcode_Z + origin. The linear back-transform mixes that constant with the gantry-Y term, leaving a per-move designed-Y error that min-corner anchoring cannot cancel when an elevated move (e.g. a bridge) happens to cancel it at the bbox minimum. Expose GCodeProcessorResult::belt_z_origin (the m_origin[Z] left by the start G-code) and subtract it before the back-transform. - Elevated features (bridges/overhangs) are mis-mapped by the linear inverse to outside the model body; build the anchor bbox only from moves within model_bb +/- 10mm, with a fallback to the full bbox when the clip would drop the bulk (object placed away from the belt entry) so the gross-offset case still anchors. Preview-only; G-code output is unchanged. --- src/libslic3r/GCode/GCodeProcessor.cpp | 7 ++++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 ++++ src/slic3r/GUI/GCodeViewer.cpp | 47 ++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 003c0af05a..b15736ef57 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4933,6 +4933,13 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line) if (line.has_z()) { m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor; any_found = true; + // Belt only: the start G-code's purge-blob advance + G92 Z0 resets leave a constant + // machine-Z origin offset here; the designed-view back-transform subtracts it so + // toolpaths map to the model's belt coordinate (gcode Z). Gated on belt_tilt_angle + // (set from the belt header, parsed before the body) so non-belt G-code processing + // is byte-identical — no unconditional work on the shared path. + if (m_result.belt_tilt_angle != 0.f) + m_result.belt_z_origin = m_origin[Z]; } if (line.has_e()) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 3047c005ed..8fcf2e0c43 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -248,6 +248,11 @@ class Print; // Belt printer: physical tilt magnitude (deg) parsed from the slicing-rotation // header comment; used to enable the preview's belt view. float belt_tilt_angle{ 0.f }; + // Belt printer: machine-Z origin offset (mm) left in m_origin[Z] by the start + // G-code (purge-blob belt advance + G92 Z0 resets). Move positions are stored + // as gcode_Z + this offset, so the designed-view back-transform must subtract it + // to recover the model's belt coordinate. + float belt_z_origin{ 0.f }; RemapAxis preslice_remap_x{ RemapAxis::PosX }; RemapAxis preslice_remap_y{ RemapAxis::PosY }; RemapAxis preslice_remap_z{ RemapAxis::PosZ }; @@ -317,6 +322,7 @@ class Print; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; belt_tilt_angle = other.belt_tilt_angle; + belt_z_origin = other.belt_z_origin; preslice_remap_x = other.preslice_remap_x; preslice_remap_y = other.preslice_remap_y; preslice_remap_z = other.preslice_remap_z; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9baced5d78..1263e3fed6 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1231,6 +1231,14 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const const bool is_belt = 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) { @@ -1246,17 +1254,42 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const for (const PrintObject* po : print.objects()) for (const ModelInstance* mi : po->model_object()->instances) model_bb.merge(po->model_object()->instance_bounding_box(*mi)); - BoundingBoxf3 tp_bb; + // 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. + BoundingBoxf3 tp_bb_clip, tp_bb_full; + const double y_lo = model_bb.defined ? model_bb.min.y() - 10.0 : -1e30; + const double y_hi = model_bb.defined ? model_bb.max.y() + 10.0 : 1e30; for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) - if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) // skip layer-0 prime/skirt - tp_bb.merge(Vec3d(belt_inv * mv.position.cast())); + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) { // skip layer-0 prime/skirt + const Vec3d p = belt_inv * mv.position.cast(); + tp_bb_full.merge(p); + if (p.y() >= y_lo && p.y() <= y_hi) + tp_bb_clip.merge(p); + } + // Use the clipped bbox only when it still holds the bulk of the body (outliers + // removed). If the object sits far from the belt entry the toolpaths are grossly + // offset and the clip would drop 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 && tp_bb_full.defined && + tp_bb_clip.size().y() >= 0.5 * tp_bb_full.size().y()) ? 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; - BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor: model_bb.min=(" - << model_bb.min.x() << "," << model_bb.min.y() << "," << model_bb.min.z() - << ") tp_bb.min=(" << tp_bb.min.x() << "," << tp_bb.min.y() << "," << tp_bb.min.z() - << ") d=(" << d.x() << "," << d.y() << "," << d.z() << ")"; + BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] z_origin=" << gcode_result.belt_z_origin + << " model_bb.y=[" << model_bb.min.y() << "," << model_bb.max.y() + << "] tp_bb.y=[" << tp_bb.min.y() << "," << tp_bb.max.y() << "] d.y=" << d.y(); } } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer,