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); }