From 9bbac19de424b903666f415f07a46ccc5f7646a7 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 25 Mar 2026 22:19:31 -0500 Subject: [PATCH] Part 2.7: Add G-code back-transform and tree support belt floor clipping - Add BeltBackTransform class that inverts the shear/scale matrix and applies it in GCodeWriter::to_machine_coords() so G-code outputs in the machine's physical coordinate space, gated by new belt_gcode_back_transform config option - Extend belt floor clipping to all three tree support pipelines (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon clipping, anti-overhang integration, and belt raft extension layers - Fix tree drop_nodes() belt termination, organic support global Z offset, collision calculation index bug, and first-layer brim/empty layer checks for belt printers two-shot - first build built but didn't plumb to UI. Woah. add pre-slice axis remap, because Y needs to be Z going to change tactic and move based on bbox min switch to per axis snapping per axis swap snap now per object build plate tilt wasn't invalidating slicer settings support upper bound now correct, need to get lower bound corrected axis swapped support termination corrected Z Shear works with and without pre-slice remap now --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/GCode.cpp | 142 +++++++++++++++++++ src/libslic3r/GCode.hpp | 5 + src/libslic3r/GCode/BeltBackTransform.cpp | 150 +++++++++++++++++++++ src/libslic3r/GCode/BeltBackTransform.hpp | 45 +++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 22 +++ src/libslic3r/GCode/GCodeProcessor.hpp | 6 + src/libslic3r/GCodeWriter.cpp | 32 ++++- src/libslic3r/GCodeWriter.hpp | 9 ++ src/libslic3r/Preset.cpp | 6 +- src/libslic3r/Print.cpp | 11 +- src/libslic3r/Print.hpp | 31 +++++ src/libslic3r/PrintConfig.cpp | 60 ++++++++- src/libslic3r/PrintConfig.hpp | 10 ++ src/libslic3r/PrintObject.cpp | 19 ++- src/libslic3r/PrintObjectSlice.cpp | 117 +++++++++++++--- src/libslic3r/Support/TreeModelVolumes.cpp | 2 +- src/libslic3r/Support/TreeSupport.cpp | 93 +++++++++++++ src/libslic3r/Support/TreeSupport3D.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 39 +++++- 20 files changed, 767 insertions(+), 36 deletions(-) create mode 100644 src/libslic3r/GCode/BeltBackTransform.cpp create mode 100644 src/libslic3r/GCode/BeltBackTransform.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 5f9591452f..c2d92ee800 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -208,6 +208,8 @@ set(lisbslic3r_sources GCode/AdaptivePAProcessor.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp + GCode/BeltBackTransform.cpp + GCode/BeltBackTransform.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 5b1330b202..ac04c6dd5b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2432,6 +2432,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Build volume extents for Rev remap mode. BoundingBoxf bbox_bed(print.config().printable_area.values); m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), print.config().printable_height.value)); + // Initialize the back-transform that undoes slicing shear/scale. + m_writer.set_belt_back_transform(print.config()); + // Per-axis origin snap: store config; actual snap is computed + // per-instance in update_origin_snap() called from set_origin(). + m_origin_snap[0] = print.config().belt_origin_snap_x.value; + m_origin_snap[1] = print.config().belt_origin_snap_y.value; + m_origin_snap[2] = print.config().belt_origin_snap_z.value; + m_origin_snap_offset[0] = print.config().belt_origin_offset_x.value; + m_origin_snap_offset[1] = print.config().belt_origin_offset_y.value; + m_origin_snap_offset[2] = print.config().belt_origin_offset_z.value; } // How many times will be change_layer() called? @@ -2544,6 +2554,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); + file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); + file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); + file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); @@ -3248,6 +3261,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } print.throw_if_canceled(); this->set_origin(unscale((*print_object_instance_sequential_active)->shift)); + this->update_origin_snap((*print_object_instance_sequential_active)->print_object, + (*print_object_instance_sequential_active)->shift); // BBS: prime extruder if extruder change happens before this object instance bool prime_extruder = false; @@ -5263,6 +5278,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); if (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5286,6 +5302,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); ExtrusionEntityCollection support_eec; // BBS @@ -5334,6 +5351,7 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + this->update_origin_snap(&instance_to_print.print_object, offset); //FIXME the following code prints regions in the order they are defined, the path is not optimized in any way. auto has_infill = [](const std::vector &by_region) { @@ -5560,6 +5578,130 @@ void GCode::set_origin(const Vec2d &pointf) m_origin = pointf; } +void GCode::update_origin_snap(const PrintObject *obj, const Point &inst_shift) +{ + if (!m_origin_snap[0] && !m_origin_snap[1] && !m_origin_snap[2]) + return; + + // Clear existing snap so to_machine_coords gives raw machine coords for bbox computation. + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, false, 0., 0.); + + // Reconstruct the belt pipeline transform for this object (same as + // PrintObjectSlice.cpp: z_shift * scale * shear * pre_remap). + const auto &cfg = m_config; + Transform3d belt = Transform3d::Identity(); + + // Pre-slice remap + int pre_rx = int(cfg.belt_preslice_remap_x.value); + int pre_ry = int(cfg.belt_preslice_remap_y.value); + int pre_rz = int(cfg.belt_preslice_remap_z.value); + if (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)) { + auto remap_col = [](int r) -> Vec3d { + int a = r % 3; Vec3d c = Vec3d::Zero(); + c[a] = (r < 3) ? 1.0 : -1.0; + return c; + }; + Matrix3d lin; + lin.col(0) = remap_col(pre_rx); + lin.col(1) = remap_col(pre_ry); + lin.col(2) = remap_col(pre_rz); + Transform3d pre = Transform3d::Identity(); + pre.linear() = lin; + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bb(cfg.printable_area.values); + Vec3d vm(bb.max.x(), bb.max.y(), cfg.printable_height.value); + Vec3d tr = Vec3d::Zero(); + if (pre_rx >= 6) tr[0] = vm[pre_rx % 3]; + if (pre_ry >= 6) tr[1] = vm[pre_ry % 3]; + if (pre_rz >= 6) tr[2] = vm[pre_rz % 3]; + pre.translation() = tr; + } + belt = pre * belt; + } + + // Shear + auto shear_f = [](BeltShearMode m, double a) -> double { + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltShearMode::PosCot: return (s > EPSILON) ? c/s : 0.; + case BeltShearMode::NegCot: return (s > EPSILON) ? -c/s : 0.; + case BeltShearMode::PosTan: return (c > EPSILON) ? s/c : 0.; + case BeltShearMode::NegTan: return (c > EPSILON) ? -s/c : 0.; + default: return 0.; + } + }; + struct AS { BeltShearMode m; double a; int f; }; + AS axes[3] = { + {cfg.belt_shear_x.value, cfg.belt_shear_x_angle.value, int(cfg.belt_shear_x_from.value)}, + {cfg.belt_shear_y.value, cfg.belt_shear_y_angle.value, int(cfg.belt_shear_y_from.value)}, + {cfg.belt_shear_z.value, cfg.belt_shear_z_angle.value, int(cfg.belt_shear_z_from.value)}, + }; + Transform3d shear = Transform3d::Identity(); + for (int i = 0; i < 3; ++i) + if (axes[i].m != BeltShearMode::None) { + double f = shear_f(axes[i].m, axes[i].a); + if (std::abs(f) > EPSILON) + shear.matrix()(i, axes[i].f) += f; + } + + // Scale + auto scale_f = [](BeltScaleMode m, double a) -> double { + if (m == BeltScaleMode::None) return 1.; + double r = Geometry::deg2rad(a), s = std::sin(r), c = std::cos(r); + switch (m) { + case BeltScaleMode::InvSin: return (s > EPSILON) ? 1./s : 1.; + case BeltScaleMode::InvCos: return (c > EPSILON) ? 1./c : 1.; + case BeltScaleMode::Sin: return s; + case BeltScaleMode::Cos: return c; + default: return 1.; + } + }; + Transform3d sc = Transform3d::Identity(); + sc.matrix()(0,0) = scale_f(cfg.belt_scale_x.value, cfg.belt_scale_x_angle.value); + sc.matrix()(1,1) = scale_f(cfg.belt_scale_y.value, cfg.belt_scale_y_angle.value); + sc.matrix()(2,2) = scale_f(cfg.belt_scale_z.value, cfg.belt_scale_z_angle.value); + + belt = sc * shear * belt; + + // Z-shift + double zs = (obj->belt_min_z() < 0.) ? -obj->belt_min_z() : 0.; + if (zs > 0.) { + Transform3d zsh = Transform3d::Identity(); + zsh.matrix()(2, 3) = zs; + belt = zsh * belt; + } + + // Full transform: belt * trafo_centered + Transform3d full = belt * obj->trafo_centered(); + + // Instance shift in slicer space + global Z offset + Vec3d shift(unscale(inst_shift.x()), + unscale(inst_shift.y()), + obj->belt_global_z_offset()); + + // Compute this instance's machine-space bbox min + BoundingBoxf3 bb = obj->model_object()->raw_bounding_box(); + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + Vec3d inst_min(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d mc = m_writer.to_machine_coords(full * c + shift); + for (int a = 0; a < 3; ++a) + inst_min[a] = std::min(inst_min[a], mc[a]); + } + + // Update writer snap for each enabled axis + for (int a = 0; a < 3; ++a) + m_writer.set_origin_snap(a, m_origin_snap[a], m_origin_snap_offset[a], inst_min[a]); +} + std::string GCode::preamble() { std::string gcode = m_writer.preamble(); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d379300bad..dbec7d0315 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -493,6 +493,11 @@ private: This affects the input arguments supplied to the extrude*() and travel_to() methods. */ Vec2d m_origin; + // Per-axis origin snap: shift G-code so each object's bbox min = offset. + bool m_origin_snap[3] = {false, false, false}; + double m_origin_snap_offset[3] = {0., 0., 0.}; + // Called when switching instances to recompute the writer's snap for this instance. + void update_origin_snap(const PrintObject *obj, const Point &inst_shift); FullPrintConfig m_config; DynamicConfig m_calib_config; // scaled G-code resolution diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp new file mode 100644 index 0000000000..7cd5bc6b53 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -0,0 +1,150 @@ +#include "BeltBackTransform.hpp" +#include "../Geometry.hpp" +#include + +namespace Slic3r { + +// Keep in sync with PrintObjectSlice.cpp compute_shear_factor (lines ~147-157). +static double compute_shear_factor(BeltShearMode mode, double angle_deg) +{ + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } +} + +// Keep in sync with PrintObjectSlice.cpp compute_scale_factor (lines ~180-192). +static double compute_scale_factor(BeltScaleMode mode, double angle_deg) +{ + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } +} + +bool BeltBackTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) + return false; + + // --- Pre-slice axis remap (same as PrintObjectSlice.cpp) --- + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + // Require at least one active transform to proceed. + bool has_global_shear = config.belt_shear_x_global.value || + config.belt_shear_y_global.value || + config.belt_shear_z_global.value; + if (!has_global_shear && !has_preslice_remap) + return false; + + // Build pre-slice remap matrix. + Transform3d pre_remap = Transform3d::Identity(); + if (has_preslice_remap) { + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; + else if (r < 6) col[axis] = -1.0; + else col[axis] = -1.0; // Rev: max - pos + return col; + }; + + Matrix3d remap_lin; + remap_lin.col(0) = remap_column(pre_rx); + remap_lin.col(1) = remap_column(pre_ry); + remap_lin.col(2) = remap_column(pre_rz); + pre_remap.linear() = remap_lin; + + // Rev mode translation (needs build volume extents). + Vec3d remap_trans = Vec3d::Zero(); + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bbox_bed(config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + config.printable_height.value); + auto add_rev = [&](int r, int out) { + if (r >= 6) remap_trans[out] = vol_max[r % 3]; + }; + add_rev(pre_rx, 0); + add_rev(pre_ry, 1); + add_rev(pre_rz, 2); + } + pre_remap.translation() = remap_trans; + } + + // Build per-axis shear matrix (same as PrintObjectSlice.cpp). + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { config.belt_shear_x.value, config.belt_shear_x_angle.value, int(config.belt_shear_x_from.value) }, + { config.belt_shear_y.value, config.belt_shear_y_angle.value, int(config.belt_shear_y_from.value) }, + { config.belt_shear_z.value, config.belt_shear_z_angle.value, int(config.belt_shear_z_from.value) }, + }; + + Matrix3d shear = Matrix3d::Identity(); + bool has_shear = false; + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) { + shear(row, axes[row].from) += factor; + has_shear = true; + } + } + } + + // Build per-axis scale diagonal matrix (same as PrintObjectSlice.cpp). + double sx = compute_scale_factor(config.belt_scale_x.value, config.belt_scale_x_angle.value); + double sy = compute_scale_factor(config.belt_scale_y.value, config.belt_scale_y_angle.value); + double sz = compute_scale_factor(config.belt_scale_z.value, config.belt_scale_z_angle.value); + + Matrix3d scale = Matrix3d::Identity(); + bool has_scale = (std::abs(sx - 1.) > EPSILON || + std::abs(sy - 1.) > EPSILON || + std::abs(sz - 1.) > EPSILON); + if (has_scale) { + scale(0, 0) = sx; + scale(1, 1) = sy; + scale(2, 2) = sz; + } + + if (!has_shear && !has_scale && !has_preslice_remap) + return false; + + // Forward pipeline: scale * shear * pre_remap (same order as PrintObjectSlice.cpp). + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + combined = combined * pre_remap; + m_inverse = combined.inverse(); + m_active = true; + return true; +} + +Vec3d BeltBackTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp new file mode 100644 index 0000000000..5a75d94dc8 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_BeltBackTransform_hpp_ +#define slic3r_BeltBackTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Reverses the pre-slice remap + shear + scale transforms that +// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code +// coordinates from the sliced (remapped/sheared/scaled) frame back to the +// machine's real coordinate space. +// +// Initialized once from PrintConfig, then applied per-point in +// GCodeWriter::to_machine_coords() before axis remapping. +// +// Active when belt_gcode_back_transform is true AND at least one of: +// - a shear axis has global mode enabled, or +// - a pre-slice axis remap is non-identity. +class BeltBackTransform +{ +public: + BeltBackTransform() = default; + + // Initialize from belt printer config. Rebuilds the same pre-slice remap, + // shear, and scale matrices as PrintObjectSlice.cpp and precomputes the + // affine inverse. Returns true if a non-identity back-transform was computed. + bool init_from_config(const PrintConfig &config); + + // Apply the inverse transform to a point. Returns pos unchanged if + // no back-transform is active. + Vec3d apply(const Vec3d &pos) const; + + // True if a non-identity back-transform is active. + bool is_active() const { return m_active; } + +private: + bool m_active = false; + Transform3d m_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_BeltBackTransform_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8a470eff19..2288c9e670 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3128,6 +3128,28 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers if (boost::starts_with(comment, " belt_scale_z_angle = ")) { try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; } + // Pre-slice axis remap + auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { + if (s == "pos_x") return BeltRemapAxis::PosX; + if (s == "pos_y") return BeltRemapAxis::PosY; + if (s == "pos_z") return BeltRemapAxis::PosZ; + if (s == "neg_x") return BeltRemapAxis::NegX; + if (s == "neg_y") return BeltRemapAxis::NegY; + if (s == "neg_z") return BeltRemapAxis::NegZ; + if (s == "rev_x") return BeltRemapAxis::RevX; + if (s == "rev_y") return BeltRemapAxis::RevY; + if (s == "rev_z") return BeltRemapAxis::RevZ; + return BeltRemapAxis::PosX; + }; + if (boost::starts_with(comment, " belt_preslice_remap_x = ")) { + m_result.belt_preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { + m_result.belt_preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { + m_result.belt_preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f953345f4e..7d08a9cdfd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -246,6 +246,9 @@ class Print; float belt_scale_y_angle{ 45.f }; BeltScaleMode belt_scale_z{ BeltScaleMode::None }; float belt_scale_z_angle{ 45.f }; + BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; + BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; + BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -321,6 +324,9 @@ class Print; belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_z = other.belt_scale_z; belt_scale_z_angle = other.belt_scale_z_angle; + belt_preslice_remap_x = other.belt_preslice_remap_x; + belt_preslice_remap_y = other.belt_preslice_remap_y; + belt_preslice_remap_z = other.belt_preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index da655a6548..b5a6402a14 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -37,18 +37,40 @@ void GCodeWriter::set_build_volume_max(const Vec3d &max) m_build_vol_max = max; } +void GCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void GCodeWriter::set_origin_snap(int axis, bool enable, double offset, double bbox_min) +{ + if (axis >= 0 && axis < 3) { + m_origin_snap[axis] = enable; + m_origin_offset[axis] = offset; + m_origin_bbox_min[axis] = bbox_min; + } +} + Vec3d GCodeWriter::to_machine_coords(const Vec3d &pos) const { if (!is_belt_printer()) return pos; + // Step 1: Undo the shear/scale applied during slicing. + Vec3d p = m_belt_back_transform.apply(pos); + // Step 2: Apply axis remapping for the machine's coordinate convention. // BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z - auto remap = [this, &pos](int r) -> double { + auto remap = [this, &p](int r) -> double { int axis = r % 3; - if (r < 3) return pos[axis]; - if (r < 6) return -pos[axis]; - return m_build_vol_max[axis] - pos[axis]; + if (r < 3) return p[axis]; + if (r < 6) return -p[axis]; + return m_build_vol_max[axis] - p[axis]; }; - return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + // Per-axis origin snap: shift so bbox min on each enabled axis = offset. + for (int i = 0; i < 3; ++i) + if (m_origin_snap[i]) + result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + return result; } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 6074eb247c..2334f5d416 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,6 +8,7 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/BeltBackTransform.hpp" namespace Slic3r { @@ -132,6 +133,10 @@ public: void set_axis_remap(int rx, int ry, int rz); // Set build volume extents for Rev remap mode (max X, Y, Z). void set_build_volume_max(const Vec3d &max); + // Initialize the belt back-transform that undoes slicing shear/scale. + void set_belt_back_transform(const PrintConfig &config); + // Set per-axis origin snap: shifts G-code so bbox min on this axis = offset. + void set_origin_snap(int axis, bool enable, double offset, double bbox_min); // Transform a point from the slicing frame to machine coordinates. Vec3d to_machine_coords(const Vec3d &pos) const; @@ -194,6 +199,10 @@ public: int m_remap_y = 1; int m_remap_z = 2; Vec3d m_build_vol_max = Vec3d::Zero(); + BeltBackTransform m_belt_back_transform; + bool m_origin_snap[3] = {false, false, false}; + double m_origin_offset[3] = {0., 0., 0.}; // target coord for bbox min + double m_origin_bbox_min[3] = {0., 0., 0.}; // computed bbox min in machine space double m_current_speed; bool m_is_first_layer = true; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3c597ac7f2..737ee2d663 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1014,7 +1014,11 @@ static std::vector s_Preset_printer_options { "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0aef12a74b..0b7190e8d1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -103,6 +103,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_origin_snap_x", "belt_origin_offset_x", + "belt_origin_snap_y", "belt_origin_offset_y", + "belt_origin_snap_z", "belt_origin_offset_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -280,6 +283,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. || opt_key == "spiral_mode" + // Build plate tilt changes slicing plane orientation. + || opt_key == "build_plate_tilt_x" + || opt_key == "build_plate_tilt_y" // Belt printer transform options change the mesh geometry before slicing. || opt_key == "belt_printer" || opt_key == "belt_printer_angle" @@ -300,7 +306,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "belt_scale_y" || opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_z" - || opt_key == "belt_scale_z_angle") { + || opt_key == "belt_scale_z_angle" + || opt_key == "belt_preslice_remap_x" + || opt_key == "belt_preslice_remap_y" + || opt_key == "belt_preslice_remap_z") { osteps.emplace_back(posSlice); } else if ( opt_key == "belt_support_floor_offset" diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index d0c050f442..c13a1407c0 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -186,6 +186,36 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(data) {} }; +// Returns the model's raw bounding box with pre-slice axis remap applied. +// When no remap is active, returns the unmodified raw_bounding_box(). +inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig &config) +{ + BoundingBoxf3 bb = model_object.raw_bounding_box(); + int pre_rx = int(config.belt_preslice_remap_x.value); + int pre_ry = int(config.belt_preslice_remap_y.value); + int pre_rz = int(config.belt_preslice_remap_z.value); + if (pre_rx == int(BeltRemapAxis::PosX) && + pre_ry == int(BeltRemapAxis::PosY) && + pre_rz == int(BeltRemapAxis::PosZ)) + return bb; // Identity remap, no change. + auto remap_coord = [](int r, const Vec3d &v) -> double { + int axis = r % 3; + if (r < 3) return v[axis]; + return -v[axis]; + }; + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + BoundingBoxf3 rbb; + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c)); + if (i == 0) rbb = BoundingBoxf3(rc, rc); + else rbb.merge(rc); + } + return rbb; +} + // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -579,6 +609,7 @@ private: double m_belt_min_z { 0.0 }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } + double belt_min_z() const { return m_belt_min_z; } private: diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 96e320ecf4..8c637b7f7c 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6136,9 +6136,63 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output.", BeltRemapAxis::PosX); - add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); - add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + add_belt_remap("belt_preslice_remap_x", "X", + "Before slicing, which model-space axis becomes the slicer's X axis. " + "Use this to re-orient the coordinate system so the slicer's XY plane matches " + "your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " + "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. " + "Default +X: no change.", + BeltRemapAxis::PosX); + add_belt_remap("belt_preslice_remap_y", "Y", + "Before slicing, which model-space axis becomes the slicer's Y axis. " + "The slicer treats Y as one of the two horizontal bed axes. If your physical " + "belt surface runs along the Z axis, map Y to +Z here so the slicer slices " + "along the correct plane. Default +Y: no change.", + BeltRemapAxis::PosY); + add_belt_remap("belt_preslice_remap_z", "Z", + "Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " + "The slicer builds layers upward along this axis. If your printer's layer-stacking " + "direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). " + "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", + BeltRemapAxis::PosZ); + + add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); + add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); + add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); + + def = this->add("belt_gcode_back_transform", coBool); + def->label = L("G-code back-transform"); + def->category = L("Printable space"); + def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " + "coordinates are in the machine's physical coordinate space. " + "Requires at least one shear axis with global mode enabled."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, + const char *axis_label) { + auto def = this->add(key_snap, coBool); + def->label = L(axis_label); + def->category = L("Printable space"); + std::string tip = std::string("Shift G-code output so the object's bounding box minimum on machine ") + + axis_label + " equals the offset value."; + def->tooltip = L(tip); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(key_offset, coFloat); + def->label = L("Offset"); + def->category = L("Printable space"); + def->tooltip = L("Target coordinate for the bounding box minimum on this machine axis."); + def->sidetext = L("mm"); + def->min = -10000; + def->max = 10000; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + }; + add_belt_origin_snap("belt_origin_snap_x", "belt_origin_offset_x", "X"); + add_belt_origin_snap("belt_origin_snap_y", "belt_origin_offset_y", "Y"); + add_belt_origin_snap("belt_origin_snap_z", "belt_origin_offset_z", "Z"); // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 2d76dcc0b5..13202657ef 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1487,9 +1487,19 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) + ((ConfigOptionEnum, belt_preslice_remap_x)) + ((ConfigOptionEnum, belt_preslice_remap_y)) + ((ConfigOptionEnum, belt_preslice_remap_z)) ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionBool, belt_gcode_back_transform)) + ((ConfigOptionBool, belt_origin_snap_x)) + ((ConfigOptionFloat, belt_origin_offset_x)) + ((ConfigOptionBool, belt_origin_snap_y)) + ((ConfigOptionFloat, belt_origin_offset_y)) + ((ConfigOptionBool, belt_origin_snap_z)) + ((ConfigOptionFloat, belt_origin_offset_z)) ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f803cc0369..ba2ed408ce 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3402,9 +3402,16 @@ void PrintObject::update_slicing_parameters() double belt_floor_shear_factor_out = 0.0; int belt_floor_from_axis_out = 1; double belt_floor_z_shift_out = 0.0; - // Belt shear/scale may change the effective Z height. + // Belt shear/scale/pre-remap may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { + BoundingBoxf3 bb = belt_remapped_bbox(*this->model_object(), pcfg); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_height = bb.size().z(); + bool has_z_shear = pcfg.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = pcfg.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { @@ -3435,7 +3442,6 @@ void PrintObject::update_slicing_parameters() double scale_z = compute_scale_factor(pcfg.belt_scale_z.value, pcfg.belt_scale_z_angle.value); if (has_z_shear && std::abs(shear_factor) > EPSILON) { int from = int(pcfg.belt_shear_z_from.value); - BoundingBoxf3 bb = this->model_object()->raw_bounding_box(); double min_rz = std::numeric_limits::max(); double max_rz = std::numeric_limits::lowest(); for (double vz : {bb.min.z(), bb.max.z()}) @@ -3507,8 +3513,15 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); - // Belt shear/scale may change the effective Z height. + // Belt pre-remap/shear/scale may change the effective Z height. if (print_config.belt_printer.value) { + bb = belt_remapped_bbox(model_object, print_config); + bool has_preslice_remap = (int(print_config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(print_config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(print_config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + if (has_preslice_remap) + object_max_z = (float)bb.size().z(); + bool has_z_shear = print_config.belt_shear_z.value != BeltShearMode::None; bool has_z_scale = print_config.belt_scale_z.value != BeltScaleMode::None; if (has_z_shear || has_z_scale) { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 404550da1a..4c767ea0ed 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -143,6 +143,58 @@ static std::vector slice_volumes_inner( params_base.extra_offset = 0; params_base.trafo = object_trafo; if (print_config.belt_printer.value) { + // --- Pre-slice axis remap --- + // Permutes/negates model axes before slicing so the slicer's coordinate + // system matches the physical bed orientation (e.g. XZ bed instead of XY). + int pre_rx = int(print_config.belt_preslice_remap_x.value); + int pre_ry = int(print_config.belt_preslice_remap_y.value); + int pre_rz = int(print_config.belt_preslice_remap_z.value); + + bool has_preslice_remap = (pre_rx != int(BeltRemapAxis::PosX) || + pre_ry != int(BeltRemapAxis::PosY) || + pre_rz != int(BeltRemapAxis::PosZ)); + + if (has_preslice_remap) { + // Build volume extents for Rev mode. + BoundingBoxf bbox_bed(print_config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value); + + // Each remap value selects a source axis and sign. + // The column vector tells the matrix which input axis feeds this output. + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; // +axis + else if (r < 6) col[axis] = -1.0; // -axis + else col[axis] = -1.0; // Rev: max - pos = -(pos - max) + return col; + }; + + Matrix3d remap_lin; + remap_lin.col(0) = remap_column(pre_rx); + remap_lin.col(1) = remap_column(pre_ry); + remap_lin.col(2) = remap_column(pre_rz); + + // Translation for Rev modes: output = max[src] - input[src]. + Vec3d remap_trans = Vec3d::Zero(); + auto add_rev_offset = [&](int r, int out_axis) { + if (r >= 6) { + int src_axis = r % 3; + remap_trans[out_axis] = vol_max[src_axis]; + } + }; + add_rev_offset(pre_rx, 0); + add_rev_offset(pre_ry, 1); + add_rev_offset(pre_rz, 2); + + Transform3d pre_remap = Transform3d::Identity(); + pre_remap.linear() = remap_lin; + pre_remap.translation() = remap_trans; + + params_base.trafo = pre_remap * params_base.trafo; + } + // Build per-axis shear matrix from 3 independent axis configs. auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { double angle_rad = Geometry::deg2rad(angle_deg); @@ -204,11 +256,12 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) { + if (has_shear || has_scale) params_base.trafo = belt_scale * belt_shear * params_base.trafo; - // After the shear/scale transform, the mesh may clip through the - // build plate (Z < 0). Detect this and shift the mesh up. + // After pre-remap/shear/scale, the mesh may clip through the build + // plate (Z < 0). Detect this and shift the mesh up along slicer Z. + if (has_preslice_remap || has_shear || has_scale) { Transform3d combined = params_base.trafo; double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { @@ -893,17 +946,34 @@ void PrintObject::slice() this->slice_volumes(); m_print->throw_if_canceled(); - // After slicing, m_belt_min_z holds the exact post-shear minimum Z - // in trafo_centered space (which includes the ensure_on_bed Z offset). - // The belt surface is at Z=0 in trafo_centered space; after shear it - // becomes Z = sf*Y, and after the z-shift that keeps the mesh above - // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is - // simply the z-shift applied, i.e. max(0, -m_belt_min_z). - // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z - // already includes the ensure_on_bed offset, unlike the min_rz used - // in update_slicing_parameters() which needs that compensation. + // Belt floor Z-shift: where is the belt surface in final slicer space? + // + // The belt surface is at model_Y=0 (XZ belt plane). After the full + // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt + // surface equation in slicer space is: + // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val + // + // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of + // the belt surface in centered-pre-shear slicer space, which is 0 + // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt + // surface away from Z=0 by the centering offset). + // + // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). + // + // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { - m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + // With pre-remap, the belt surface (model_Y=0) may not be at Z=0 in + // centered slicer space — add the remapped bbox min Z to compensate. + // Without pre-remap, the belt surface IS at Z=0 and bb.min.z() is + // already folded into m_belt_min_z, so use 0. + const auto &pcfg = this->print()->config(); + bool has_preslice_remap = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap + ? belt_remapped_bbox(*this->model_object(), pcfg).min.z() : 0.; + m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; } int firstLayerReplacedBy = 0; @@ -989,14 +1059,23 @@ void PrintObject::slice() const auto &za = gaxes[2]; // Z row if (za.global && za.mode != BeltShearMode::None && za.from < 2) { double factor = compute_shear_factor(za.mode, za.angle); - // The Z-shift brought the mesh's lowest sheared vertex to - // Z=0. That vertex's physical Y determines the belt contact - // point. With trafo_z preserved (ensure_on_bed offset), - // min_z = Y_at_contact * factor for bottom-face vertices, - // so: z_offset = center_Y * factor + min_z. + // The global Z offset accounts for the instance's position- + // dependent shear contribution. m_belt_min_z is the minimum Z + // of the mesh after pre_remap + shear + trafo_centered, which + // includes the centering offset on the remapped Z axis. + // Subtract the belt surface's centered Z position so we get + // only the shear-induced contribution (same correction as the + // belt_floor_z_shift fix). + // Same pre-remap guard as belt_floor_z_shift above. + bool has_preslice_remap2 = (int(pcfg.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || + int(pcfg.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || + int(pcfg.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ)); + double belt_surface_z = has_preslice_remap2 + ? belt_remapped_bbox(*this->model_object(), this->print()->config()).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; Point phys = inst_shift; // already has center_offset subtracted double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); - global_z_offset += center_on_axis * factor + m_belt_min_z; + global_z_offset += center_on_axis * factor + shear_min_z; } BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index e270b62aab..ec38ffed73 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -151,7 +151,7 @@ TreeModelVolumes::TreeModelVolumes( double belt_sf = sp2.belt_floor_shear_factor; if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - double bb_min_z = std::abs(print_object.model_object()->raw_bounding_box().min.z()); + double bb_min_z = std::abs(belt_remapped_bbox(*print_object.model_object(), pcfg2).min.z()); double extra_depth = bb_min_z + 10.; int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height)); if (num_extra > 0) { diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 4966a5a139..60e681add9 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1733,6 +1733,99 @@ void TreeSupport::generate() + // Belt floor: extend support below the object's first layer by creating + // additional support layers with geometry copied from the lowest content + // layer and clipped at the belt surface. These layers bypass the tree + // algorithm entirely — they're pure geometry added after draw_circles(). + { + const auto &sp = m_slicing_params; + const auto &pcfg = *m_print_config; + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly + && m_object->support_layer_count() > 0) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Support layer print_z values are in GLOBAL Z (non-organic inherits + // from object layers which include global_z_offset). Use the GLOBAL + // belt_floor_z_shift to match. + const double z_shift = sp.belt_floor_z_shift; + // Find the lowest non-empty, non-brim support layer. + ExPolygons source_areas; + double source_z = 0; + int layers_with_content = 0; + for (size_t i = 0; i < m_object->support_layer_count(); ++i) { + SupportLayer *sl = m_object->get_support_layer(i); + if (sl && !sl->base_areas.empty()) { + layers_with_content++; + if (layers_with_content >= 2) { + source_areas = sl->base_areas; + source_z = sl->print_z; + break; + } + } + } + // Fallback to first content layer. + if (source_areas.empty()) { + for (size_t i = 0; i < m_object->support_layer_count(); ++i) { + SupportLayer *sl = m_object->get_support_layer(i); + if (sl && !sl->base_areas.empty()) { + source_areas = sl->base_areas; + source_z = sl->print_z; + break; + } + } + } + if (!source_areas.empty()) { + BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); + double from_extent = std::abs(bb.min(from_axis)); + double bb_min_z = std::abs(bb.min.z()); + double first_z = m_object->get_support_layer(0)->print_z; + // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed + // distance) + 10mm safety margin. The 10mm is a bodge to avoid + // small cutoff artifacts — ideally computed exactly from belt geometry. + double extra_depth = std::min(from_extent + bb_min_z + 10., std::max(0., first_z)); + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + ExPolygons prev_areas = source_areas; + // Build belt extension layers (lowest Z first). + SupportLayerPtrs belt_ext_layers; + for (int i = num_extra; i >= 1 && !prev_areas.empty(); --i) { + double print_z = first_z - i * sp.layer_height; + if (print_z < -sp.layer_height) continue; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e3); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + ExPolygons clipped = diff_ex(source_areas, Polygons{belt_poly}); + if (clipped.empty()) continue; + SupportLayer *sl = new SupportLayer(0, 0, m_object, sp.layer_height, print_z, -1); + sl->base_areas = clipped; + // Populate area_groups — generate_toolpaths() iterates these, + // not base_areas directly. + for (auto &expoly : sl->base_areas) + sl->area_groups.emplace_back(&expoly, SupportLayer::BaseType, 0); + sl->lslices = clipped; + sl->lslices_bboxes.reserve(clipped.size()); + for (const ExPolygon &ep : clipped) + sl->lslices_bboxes.emplace_back(get_extents(ep)); + belt_ext_layers.push_back(sl); + } + // Insert at the front of support_layers (they're already in Z order). + if (!belt_ext_layers.empty()) { + auto &sl_vec = m_object->support_layers(); + sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end()); + } + } + } + } + profiler.stage_start(STAGE_GENERATE_TOOLPATHS); m_object->print()->set_status(70, _u8L("Generating support")); generate_toolpaths(); diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index b551a42ece..9d35cb2dd8 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3381,7 +3381,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // termination happen inside the belt region and get clipped. // Use the distance from the pre-shear bbox min Z to the part's // post-shear min Z, plus 10mm for base expansion headroom. - double bb_min_z = std::abs(po.model_object()->raw_bounding_box().min.z()); + double bb_min_z = std::abs(belt_remapped_bbox(*po.model_object(), pcfg).min.z()); double extra_depth = bb_min_z + 10.; int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); if (num_extra > 0) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d997fbb7a9..202c28c121 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4432,12 +4432,41 @@ void TabPrinter::build_fff() optgroup->append_line(line); } { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + Line line = { L("Pre-slice axis remap"), + L("Remap model axes before slicing so the slicer's coordinate system matches " + "the physical bed orientation. For belt printers whose bed is NOT in the XY plane, " + "use this to swap axes so layers are stacked in the correct physical direction.") }; + line.append_option(optgroup->get_option("belt_preslice_remap_x")); + line.append_option(optgroup->get_option("belt_preslice_remap_y")); + line.append_option(optgroup->get_option("belt_preslice_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; line.append_option(optgroup->get_option("belt_gcode_remap_x")); line.append_option(optgroup->get_option("belt_gcode_remap_y")); line.append_option(optgroup->get_option("belt_gcode_remap_z")); optgroup->append_line(line); } + optgroup->append_single_option_line("belt_gcode_back_transform"); + { + Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_x")); + line.append_option(optgroup->get_option("belt_origin_offset_x")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Y"), L("Snap object bbox min Y to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_y")); + line.append_option(optgroup->get_option("belt_origin_offset_y")); + optgroup->append_line(line); + } + { + Line line = { L("Origin snap Z"), L("Snap object bbox min Z to offset in G-code output") }; + line.append_option(optgroup->get_option("belt_origin_snap_z")); + line.append_option(optgroup->get_option("belt_origin_offset_z")); + optgroup->append_line(line); + } { Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; line.append_option(optgroup->get_option("belt_support_floor_mode")); @@ -5292,7 +5321,9 @@ void TabPrinter::toggle_options() toggle_line("belt_printer_infinite_y", is_belt); for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", "belt_scale_x", "belt_scale_y", "belt_scale_z", - "belt_gcode_remap_x"}) + "belt_preslice_remap_x", + "belt_gcode_remap_x", "belt_gcode_back_transform", + "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); // Gray out angle/from sub-options when their parent shear/scale mode is None. @@ -5320,6 +5351,10 @@ void TabPrinter::toggle_options() auto scz = m_config->option>("belt_scale_z")->value; toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + toggle_option("belt_origin_offset_x", is_belt && m_config->opt_bool("belt_origin_snap_x")); + toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y")); + toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z")); + toggle_line("belt_support_floor_mode", is_belt); }