diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 0f54d2f4dd..0ac3597347 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.03.02.67", + "version": "02.03.02.68", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index ce39146042..0c3a976c0e 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -89,9 +89,9 @@ "1" ], "belt_printer": "1", - "belt_shear_z": "pos_tan", - "belt_shear_z_angle": "45", - "belt_scale_y": "inv_cos", + "belt_slice_rotation": "x", + "belt_slice_rotation_angle": "45", + "belt_slice_rotation_global": "1", "gcode_shear_z": "pos_tan", "gcode_scale_y": "inv_cos", "build_plate_tilt_x": "45", diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 767d280579..a89b20bcec 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -53,6 +53,10 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) 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); + // Slicing rotation configs + file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str()); + file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value); + file.write_format("; belt_slice_rotation_global = %d\n", print.config().belt_slice_rotation_global.value ? 1 : 0); file.write_format("; belt_mesh_transform_order = %s\n", full_cfg.opt_serialize("belt_mesh_transform_order").c_str()); // Pre-slice remap configs file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str()); @@ -104,7 +108,10 @@ void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)) || (m_config.belt_shear_z_global.value - && m_config.belt_shear_z.value != BeltShearMode::None); + && m_config.belt_shear_z.value != BeltShearMode::None) + || (m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON); if (use_global && m_config.belt_printer.value) { auto *belt_writer = dynamic_cast(m_writer.get()); if (belt_writer) { diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 1987d0127a..b2a8f5c470 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -57,17 +57,39 @@ Vec3d BeltGCodeWriter::to_cartesian(const Vec3d &pos) const Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1+2: To Cartesian (back_transform + axis_remap). - Vec3d result = to_cartesian(pos); + Vec3d after_back = m_belt_back_transform.apply(pos); + Vec3d result = apply_axis_remap(after_back); + Vec3d after_remap = result; // Step 3: Per-axis origin snap (computed in the Cartesian frame). for (int i = 0; i < 3; ++i) if (m_origin_snap[i]) result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); + Vec3d after_snap = result; // Step 4: Machine-frame transform (gcode_shear / gcode_scale / post_gcode_remap) // applied LAST so it acts as a global linear transform on the placed coords. // Order matters: putting it before origin_snap would feed sheared bbox corners // into the snap's per-object min calculation, mis-normalizing non-cubic geometries // (the corners of the original bbox aren't extreme points of the sheared shape). - return m_machine_frame_transform.apply(result); + Vec3d final = m_machine_frame_transform.apply(result); + + // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z + // crosses an integer mm boundary) to keep the log volume manageable while + // still capturing one sample per ~5 layers. Shows the full pipeline so + // Case A vs Case B can be compared step-by-step. + static thread_local int s_last_logged_z = std::numeric_limits::min(); + int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm + if (z_bucket != s_last_logged_z) { + s_last_logged_z = z_bucket; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] to_machine_coords" + << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" + << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" + << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" + << " after_snap=(" << after_snap.x() << "," << after_snap.y() << "," << after_snap.z() << ")" + << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" + << " mft_active=" << m_machine_frame_transform.is_active() + << " back_active=" << m_belt_back_transform.is_active(); + } + return final; } // ---- Overridden movement methods ------------------------------------------ diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 850a4ad221..81c4c06f1e 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -1,6 +1,9 @@ #include "BeltSliceStrategy.hpp" +#include "Model.hpp" #include +#include +#include #include namespace Slic3r { @@ -14,9 +17,10 @@ std::unique_ptr BeltSliceStrategy::create(const PrintConfig & BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) { - m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); - m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); - m_order = config.belt_mesh_transform_order.value; + m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); + m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); + m_rotation = BeltTransformPipeline::build_rotation_matrix(config, &m_has_rotation); + m_order = config.belt_mesh_transform_order.value; } void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, @@ -26,27 +30,83 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, { // ScaleThenShear: applied to a point, scale runs first then shear (m_shear * m_scale). // ShearThenScale: applied to a point, shear runs first then scale (m_scale * m_shear). - if (m_has_shear || m_has_scale) { - Transform3d belt_xform = Transform3d::Identity(); - belt_xform.linear() = (m_order == BeltTransformOrder::ScaleThenShear) + // Rotation (if active) is applied AFTER shear/scale, matching build_forward_transform. + if (m_has_shear || m_has_scale || m_has_rotation) { + Matrix3d shear_scale = (m_order == BeltTransformOrder::ScaleThenShear) ? Matrix3d(m_shear * m_scale) : Matrix3d(m_scale * m_shear); + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = Matrix3d(m_rotation * shear_scale); trafo = belt_xform * trafo; } // Z-shift — detect if mesh clips below build plate after transforms. - if (has_remap || m_has_shear || m_has_scale) { + // Each mesh vertex must be brought into object space via mv->get_matrix() + // before applying the full trafo (which is in object space). Missing this + // step on assemblies (where per-volume get_matrix() positions each volume + // within the object) causes min_z to be computed against mesh-local vertex + // coordinates rather than object-space coordinates, so volumes translated + // along the slicer's Z axis are silently excluded from the bound check. + if (has_remap || m_has_shear || m_has_scale || m_has_rotation) { + // [BELT-DEBUG] Capture the incoming trafo for diagnostic logging. + // This is the slicer-frame transform AFTER belt_xform but BEFORE z_shift. + const Transform3d trafo_pre_shift = trafo; + auto log_mat = [](const Matrix3d &m) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," + << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," + << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; + return ss.str(); + }; + auto log_vec3 = [](const Vec3d &v) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; + return ss.str(); + }; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] apply_to_trafo enter" + << " has_shear=" << m_has_shear + << " has_scale=" << m_has_scale + << " has_rotation=" << m_has_rotation + << " has_remap=" << has_remap + << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) + << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) + << " volumes=" << model_volumes.size(); + double min_z = std::numeric_limits::max(); + int vol_idx = 0; for (const ModelVolume *mv : model_volumes) { - if (!mv->is_model_part()) continue; - for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = trafo * v.cast(); + if (!mv->is_model_part()) { ++vol_idx; continue; } + Transform3d vol_trafo = trafo * mv->get_matrix(); + // [BELT-DEBUG] Per-volume bbox in mesh-frame and post-trafo slicer-frame. + const auto &its = mv->mesh().its; + Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + double vol_min_z = std::numeric_limits::max(); + for (const stl_vertex &v : its.vertices) { + Vec3d vm = v.cast(); + mesh_min = mesh_min.cwiseMin(vm); + mesh_max = mesh_max.cwiseMax(vm); + Vec3d pt = vol_trafo * vm; + slicer_min = slicer_min.cwiseMin(pt); + slicer_max = slicer_max.cwiseMax(pt); + vol_min_z = std::min(vol_min_z, pt.z()); min_z = std::min(min_z, pt.z()); } + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] vol[" << vol_idx + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) + << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) + << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) + << " vol_min_z=" << vol_min_z; + ++vol_idx; } double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z - << " z_shift=" << belt_z_shift_val; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] combined min_z=" << min_z + << " z_shift_val=" << belt_z_shift_val; if (belt_z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); z_shift.matrix()(2, 3) = belt_z_shift_val; @@ -54,10 +114,13 @@ void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, } if (out_belt_min_z) { double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; - BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_min_z tid=" << std::this_thread::get_id() + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; *out_belt_min_z = new_val; } + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] apply_to_trafo exit" + << " final_trafo.linear=" << log_mat(trafo.linear()) + << " final_trafo.translation=" << log_vec3(trafo.translation()); } } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index c2362e3552..6009155c7b 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -34,11 +34,13 @@ public: private: explicit BeltSliceStrategy(const PrintConfig &config); - bool m_has_shear = false; - bool m_has_scale = false; - Matrix3d m_shear = Matrix3d::Identity(); - Matrix3d m_scale = Matrix3d::Identity(); - BeltTransformOrder m_order = BeltTransformOrder::ScaleThenShear; + bool m_has_shear = false; + bool m_has_scale = false; + bool m_has_rotation = false; + Matrix3d m_shear = Matrix3d::Identity(); + Matrix3d m_scale = Matrix3d::Identity(); + Matrix3d m_rotation = Matrix3d::Identity(); + BeltTransformOrder m_order = BeltTransformOrder::ScaleThenShear; }; } // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 6c0838b2eb..0d1ae41295 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -95,23 +95,47 @@ Matrix3d BeltTransformPipeline::build_scale_matrix(const PrintConfig &config, bo return scale; } +Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out) +{ + BeltRotationAxis axis = config.belt_slice_rotation.value; + double angle_deg = config.belt_slice_rotation_angle.value; + bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON; + if (has_rot_out) *has_rot_out = active; + if (!active) + return Matrix3d::Identity(); + double angle_rad = Geometry::deg2rad(angle_deg); + Vec3d unit_axis; + switch (axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: return Matrix3d::Identity(); + } + return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); +} + Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) { - Transform3d pre_remap = build_preslice_remap(config); + Transform3d pre_remap = build_preslice_remap(config); bool shear_active = false; Matrix3d shear = build_shear_matrix(config, &shear_active); bool scale_active = false; Matrix3d scale = build_scale_matrix(config, &scale_active); + bool rot_active = false; + Matrix3d rot = build_rotation_matrix(config, &rot_active); // Match the mesh-side ordering selected by belt_mesh_transform_order so // BeltBackTransform inverts the same composition that BeltSliceStrategy // applied to the mesh. // ScaleThenShear: applied to p, scale runs first then shear (shear * scale). // ShearThenScale: applied to p, shear runs first then scale (scale * shear). - Transform3d combined = Transform3d::Identity(); - combined.linear() = (config.belt_mesh_transform_order.value == BeltTransformOrder::ScaleThenShear) + // Rotation is applied AFTER shear/scale: rot * shear_scale * pre_remap. + Matrix3d shear_scale = (config.belt_mesh_transform_order.value == BeltTransformOrder::ScaleThenShear) ? Matrix3d(shear * scale) : Matrix3d(scale * shear); + + Transform3d combined = Transform3d::Identity(); + combined.linear() = Matrix3d(rot * shear_scale); combined = combined * pre_remap; return combined; } @@ -166,7 +190,7 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract Z-axis shear/scale + per-axis scale + transform order from config. + // Extract Z-axis shear/scale + per-axis scale + transform order + rotation from config. BeltShearMode z_shear_mode; double z_shear_angle; BeltScaleMode z_scale_mode; @@ -175,6 +199,8 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( BeltScaleMode from_scale_mode; // scale on the shear's source axis double from_scale_angle; BeltTransformOrder order; + BeltRotationAxis rot_axis; + double rot_angle; if constexpr (std::is_same_v) { z_shear_mode = config.belt_shear_z.value; @@ -183,6 +209,8 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( z_scale_angle = config.belt_scale_z_angle.value; z_shear_from = int(config.belt_shear_z_from.value); order = config.belt_mesh_transform_order.value; + rot_axis = config.belt_slice_rotation.value; + rot_angle = config.belt_slice_rotation_angle.value; if (z_shear_from == 0) { from_scale_mode = config.belt_scale_x.value; from_scale_angle = config.belt_scale_x_angle.value; @@ -212,12 +240,18 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( auto *opt = config.template option>(key); return opt ? opt->value : BeltTransformOrder::ScaleThenShear; }; + auto get_rot_axis = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltRotationAxis::None; + }; z_shear_mode = get_shear("belt_shear_z"); z_shear_angle = get_float("belt_shear_z_angle"); z_scale_mode = get_scale("belt_scale_z"); z_scale_angle = get_float("belt_scale_z_angle"); z_shear_from = get_axis("belt_shear_z_from"); order = get_order("belt_mesh_transform_order"); + rot_axis = get_rot_axis("belt_slice_rotation"); + rot_angle = get_float("belt_slice_rotation_angle"); if (z_shear_from == 0) { from_scale_mode = get_scale("belt_scale_x"); from_scale_angle = get_float("belt_scale_x_angle"); @@ -227,10 +261,11 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( } } - bool has_z_shear = z_shear_mode != BeltShearMode::None; - bool has_z_scale = z_scale_mode != BeltScaleMode::None; + bool has_z_shear = z_shear_mode != BeltShearMode::None; + bool has_z_scale = z_scale_mode != BeltScaleMode::None; + bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; - if (!has_z_shear && !has_z_scale) + if (!has_z_shear && !has_z_scale && !has_rotation) return result; double shear_factor = has_z_shear @@ -267,6 +302,51 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( result.floor_params.shear_factor = effective_shear; result.floor_params.from_axis = from; result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + } else if (has_rotation) { + // Rotation-only path (no Z-shear): sweep 8 bbox corners through R. + double angle_rad = Geometry::deg2rad(rot_angle); + Vec3d unit_axis; + switch (rot_axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: unit_axis = Vec3d::UnitX(); break; + } + Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); + double min_rz = std::numeric_limits::max(); + double max_rz = std::numeric_limits::lowest(); + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? bb.max.x() : bb.min.x(), + (i & 2) ? bb.max.y() : bb.min.y(), + (i & 4) ? bb.max.z() : bb.min.z()); + double z = (R * c).z(); + min_rz = std::min(min_rz, z); + max_rz = std::max(max_rz, z); + } + // Optional Z-scale still applies multiplicatively if both are set. + result.object_height = (max_rz - min_rz) * (has_z_scale ? scale_z : 1.0); + + // Belt floor in slicer-frame is the image of z_machine = 0 under R. + // R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s + // R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s + // R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (rot_axis) { + case BeltRotationAxis::X: + result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.; + result.floor_params.from_axis = 1; // Y + break; + case BeltRotationAxis::Y: + result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.; + result.floor_params.from_axis = 0; // X + break; + case BeltRotationAxis::Z: + default: + result.floor_params.shear_factor = 0.0; + result.floor_params.from_axis = 1; + break; + } + result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { result.object_height = original_height * scale_z; } diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index f15a9995c3..1ce47a2e31 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -92,6 +92,12 @@ public: std::abs(sz - 1.) > EPSILON; } + static bool has_rotation(const PrintConfig &config) + { + return config.belt_slice_rotation.value != BeltRotationAxis::None && + std::abs(config.belt_slice_rotation_angle.value) > EPSILON; + } + // ---- Matrix builders -------------------------------------------------- // Build the pre-slice axis remap transform (includes Rev-mode translation). @@ -105,6 +111,11 @@ public: // Also sets has_scale_out if non-null. static Matrix3d build_scale_matrix(const PrintConfig &config, bool *has_scale_out = nullptr); + // Build the 3x3 rotation matrix from belt_slice_rotation* config. + // Returns Identity if rotation axis is None or angle is ~0. + // Also sets has_rot_out if non-null. + static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr); + // Combined forward transform. Shear/scale order is selected by // belt_mesh_transform_order so the result matches what BeltSliceStrategy // applied to the mesh (BeltBackTransform inverts this). diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index 23327aea7a..10866c57f5 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -84,12 +84,12 @@ FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) // -------- Resolve Auto ------------------------------------------------- FirstLayerPlaneMode mode = config.first_layer_plane.value; if (mode == FirstLayerPlaneMode::Auto) { - if (config.belt_printer.value && - config.belt_shear_z.value != BeltShearMode::None) { - mode = FirstLayerPlaneMode::BeltShear; - } else { - mode = FirstLayerPlaneMode::XY; - } + bool belt_affine_active = config.belt_printer.value && + (config.belt_shear_z.value != BeltShearMode::None || + (config.belt_slice_rotation.value != BeltRotationAxis::None && + std::abs(config.belt_slice_rotation_angle.value) > EPSILON)); + mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine + : FirstLayerPlaneMode::XY; } m_mode = mode; @@ -130,7 +130,7 @@ FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) m_active = true; return; - case FirstLayerPlaneMode::BeltShear: { + case FirstLayerPlaneMode::BeltAffine: { // Compute the slicing-frame plane that maps to machine_Z = user_offset // under the gcode axis remap (and optional back-transform). MachineZAffine mz = compute_machine_z_affine(config); diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 893c46e52b..654afe6262 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -15,9 +15,12 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) bool has_global_shear = config.belt_shear_x_global.value || config.belt_shear_y_global.value || config.belt_shear_z_global.value; + bool has_global_rotation = config.belt_slice_rotation_global.value + && config.belt_slice_rotation.value != BeltRotationAxis::None; bool has_preslice_global = config.belt_preslice_global.value || config.preslice_remap_global.value; - if (!has_global_shear && !has_preslice_global && !BeltTransformPipeline::has_preslice_remap(config)) + if (!has_global_shear && !has_global_rotation && !has_preslice_global + && !BeltTransformPipeline::has_preslice_remap(config)) return false; // Build the forward pipeline (scale * shear * pre_remap) and store its inverse. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b6875f6117..8a5acdd95a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1329,6 +1329,7 @@ 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_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global", "belt_mesh_transform_order", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 11b8c4d99c..eb4e7b0bed 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1536,6 +1536,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // so each gets independent layer Z values. bool belt_force_separate = m_config.belt_printer.value && ( (m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) + || (m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON) || m_config.belt_preslice_global.value || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config))); model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); @@ -1629,6 +1632,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (belt_instances_shifted && m_config.belt_printer.value && ((m_config.belt_shear_z_global.value && m_config.belt_shear_z.value != BeltShearMode::None) + || (m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON) || m_config.belt_preslice_global.value || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)))) { for (PrintObject *object : m_objects) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5edfb1b0f0..ab6c122fd3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -326,6 +326,14 @@ static t_config_enum_values s_keys_map_BeltAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltAxis) +static t_config_enum_values s_keys_map_BeltRotationAxis { + { "none", int(BeltRotationAxis::None) }, + { "x", int(BeltRotationAxis::X) }, + { "y", int(BeltRotationAxis::Y) }, + { "z", int(BeltRotationAxis::Z) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRotationAxis) + static t_config_enum_values s_keys_map_BeltTransformOrder { { "scale_then_shear", int(BeltTransformOrder::ScaleThenShear) }, { "shear_then_scale", int(BeltTransformOrder::ShearThenScale) }, @@ -374,7 +382,10 @@ static t_config_enum_values s_keys_map_FirstLayerPlaneMode { { "xy", int(FirstLayerPlaneMode::XY) }, { "yz", int(FirstLayerPlaneMode::YZ) }, { "xz", int(FirstLayerPlaneMode::XZ) }, - { "belt_shear", int(FirstLayerPlaneMode::BeltShear) }, + { "belt_affine", int(FirstLayerPlaneMode::BeltAffine) }, + // Back-compat alias: pre-rotation builds serialised this mode as + // "belt_shear". Accept it on parse so old 3MFs / presets keep loading. + { "belt_shear", int(FirstLayerPlaneMode::BeltAffine) }, }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FirstLayerPlaneMode) @@ -6429,9 +6440,9 @@ void PrintConfigDef::init_fff_params() add_belt_axis_enum ("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z, comExpert); add_belt_shear_global("belt_shear_y_global", "Global"); - add_belt_shear_mode ("belt_shear_z", "Function", BeltShearMode::PosTan); - add_belt_shear_angle("belt_shear_z_angle", "Angle"); - add_belt_axis_enum ("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_shear_mode ("belt_shear_z", "Function", BeltShearMode::None, comExpert); + add_belt_shear_angle("belt_shear_z_angle", "Angle", comExpert); + add_belt_axis_enum ("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y, comExpert); add_belt_shear_global("belt_shear_z_global", "Global", true); // Per-axis scale controls for belt printer @@ -6463,12 +6474,47 @@ void PrintConfigDef::init_fff_params() add_belt_scale_mode ("belt_scale_x", "Function", BeltScaleMode::None, comExpert); add_belt_scale_angle("belt_scale_x_angle", "Angle", comExpert); - add_belt_scale_mode ("belt_scale_y", "Function", BeltScaleMode::None); - add_belt_scale_angle("belt_scale_y_angle", "Angle"); + add_belt_scale_mode ("belt_scale_y", "Function", BeltScaleMode::None, comExpert); + add_belt_scale_angle("belt_scale_y_angle", "Angle", comExpert); add_belt_scale_mode ("belt_scale_z", "Function", BeltScaleMode::None, comExpert); add_belt_scale_angle("belt_scale_z_angle", "Angle", comExpert); + // Global slicing rotation (alternative to per-axis shear+scale). + def = this->add("belt_slice_rotation", coEnum); + def->label = L("Slicing rotation axis"); + def->category = L("Printable space"); + def->tooltip = L("Rotate the mesh by this axis before slicing. Use this for an " + "isometric (no shear distortion) belt slicing transform. " + "Mutually exclusive with the per-axis shear/scale controls " + "in the UI; the pipeline composes them if both are set in JSON."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "x", "y", "z"}; + def->enum_labels = {L("None"), L("X"), L("Y"), L("Z")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltRotationAxis::None)); + + def = this->add("belt_slice_rotation_angle", coFloat); + def->label = L("Slicing rotation angle"); + def->category = L("Printable space"); + def->tooltip = L("Magnitude of the slicing rotation, in degrees. Positive values " + "rotate counter-clockwise looking down the positive axis."); + def->sidetext = L("°"); + def->min = -180.; + def->max = 180.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("belt_slice_rotation_global", coBool); + def->label = L("Global"); + def->category = L("Printable space"); + def->tooltip = L("Treat the slicing rotation as part of the global forward transform " + "that BeltBackTransform inverts before the machine-frame remap. " + "Required for rotation-mode belt printers; mirrors belt_shear_z_global. " + "Defaults to on because virtually all rotation-mode printers need it."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + auto add_belt_transform_order = [this](const char *key, const char *label, const char *tooltip) { auto def = this->add(key, coEnum); def->label = L(label); @@ -6597,7 +6643,8 @@ void PrintConfigDef::init_fff_params() // First-layer plane: which surface defines "first layer" for fan / speed / // accel decisions. On belt printers the slicing-frame layer 0 is a tilted // slab that no longer corresponds to the physical first printed layer. - // Auto picks BeltShear when belt_shear_z != None, otherwise XY (legacy). + // Auto picks BeltAffine when any belt-side affine transform is active + // (Z shear or slicing rotation), otherwise XY (legacy). def = this->add("first_layer_plane", coEnum); def->label = L("First layer plane"); def->category = L("Printable space"); @@ -6605,14 +6652,15 @@ void PrintConfigDef::init_fff_params() "first-layer settings (no fan, slow speed, initial-layer accel/jerk, " "deferred temperature drop). On belt printers a single slicing layer " "contains paths at many machine-Z values, so layer-index based detection " - "fails. Auto resolves to Belt shear plane when belt_shear_z is non-None, " - "otherwise XY (legacy). Pick XY explicitly to opt out and force the " - "legacy slicing-layer-0 detection."); + "fails. Auto resolves to Belt affine plane when any belt-side affine " + "transform (Z shear or slicing rotation) is active, otherwise XY (legacy). " + "Pick XY explicitly to opt out and force the legacy slicing-layer-0 " + "detection."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"auto", "xy", "yz", "xz", "belt_shear"}; - def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt shear plane")}; + def->enum_values = {"auto", "xy", "yz", "xz", "belt_affine"}; + def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt affine plane")}; def->mode = comExpert; - def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltShear)); + def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltAffine)); def = this->add("first_layer_plane_offset", coFloat); def->label = L("Belt plane offset"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3536f6d909..a6c6f55394 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -194,6 +194,17 @@ enum class BeltAxis Z = 2, }; +// Axis around which the mesh is rotated before slicing, when +// `belt_slice_rotation` is set. None disables the rotation stage. +// Distinct from BeltAxis because BeltAxis carries no "None" semantics. +enum class BeltRotationAxis +{ + None = 0, + X = 1, + Y = 2, + Z = 3, +}; + // Order in which the belt shear and scale matrices are composed. // ScaleThenShear: applied to a point p, the result is shear(scale(p)). // ShearThenScale: applied to a point p, the result is scale(shear(p)). @@ -231,8 +242,9 @@ enum class BeltSupportZOffsetMode // // Auto resolves to: // - XY (inactive, legacy behavior) for non-belt printers and for belt -// printers with no Z-axis shear. -// - BeltShear for belt printers with belt_shear_z != None. +// printers with no active belt-side transform. +// - BeltAffine for belt printers with any active belt-side affine +// transform (Z shear, slicing rotation, or both). // // XY is also used as an explicit "opt out" mode that forces legacy // per-layer first-layer detection even on belt printers. @@ -242,7 +254,7 @@ enum class FirstLayerPlaneMode XY, YZ, XZ, - BeltShear, + BeltAffine, // formerly BeltShear; renamed to reflect rotation support }; enum SupportMaterialPattern { @@ -607,6 +619,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltTransformOrder) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) @@ -1579,6 +1592,12 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionEnum, belt_scale_z)) ((ConfigOptionFloat, belt_scale_z_angle)) + // Global mesh rotation as an alternative to per-axis shear/scale (isometric + // slicing transform). Composes with shear in the pipeline math; UI gates + // them as mutually exclusive. + ((ConfigOptionEnum, belt_slice_rotation)) + ((ConfigOptionFloat, belt_slice_rotation_angle)) + ((ConfigOptionBool, belt_slice_rotation_global)) ((ConfigOptionEnum, preslice_remap_x)) ((ConfigOptionEnum, preslice_remap_y)) ((ConfigOptionEnum, preslice_remap_z)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 07ba2c63af..a0d2b18992 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -174,12 +174,15 @@ static std::vector slice_volumes_inner( belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z); } // If only remap (no belt), still need z-shift detection. + // Compose per-volume mv->get_matrix() so assembly volumes contribute their + // actual object-space positions — see the matching note in BeltSliceStrategy. if (has_remap && !print_config.belt_printer.value) { double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; + Transform3d vol_trafo = params_base.trafo * mv->get_matrix(); for (const stl_vertex &v : mv->mesh().its.vertices) { - Vec3d pt = params_base.trafo * v.cast(); + Vec3d pt = vol_trafo * v.cast(); min_z = std::min(min_z, pt.z()); } } @@ -322,17 +325,35 @@ static std::vector> slices_to_regions( } } else { zs_complex.reserve(zs.size()); - // region.bbox is computed in pre-belt-shear slicer space (see PrintApply.cpp::trafo_for_bbox). - // When belt transforms are active, layer Z values are in post-shear/scale/remap space, + // region.bbox is computed in pre-belt-transform slicer space (see PrintApply.cpp::trafo_for_bbox). + // When belt transforms are active, layer Z values are in post-rotation/shear/scale/remap space, // so the Z components of region.bbox aren't comparable to z. Skipping the Z filter here // pushes those layers into the parallel_for path below, which handles multi-volume // clipping per layer without relying on the bbox Z range. const bool bbox_z_in_layer_frame = !(print_config.belt_printer.value && (BeltTransformPipeline::has_shear(print_config) || BeltTransformPipeline::has_scale(print_config) + || BeltTransformPipeline::has_rotation(print_config) || BeltTransformPipeline::has_preslice_remap(print_config))); + // Belt-transform addendum: with bbox-Z untrusted, the simple path's + // "first model_part wins" logic drops subsequent volumes' slices unless + // they XY-overlap with the first. Assemblies whose volumes are stacked + // or side-by-side in pre-transform Z (different bbox.z ranges) thus lose + // the volumes that originally sat outside the first volume's Z range — + // showing up as truncation at the top or bottom of the assembly. Force + // every layer in a multi-volume range through the parallel_for path, + // which correctly merges all volumes per layer. + int num_model_parts = 0; + for (const PrintObjectRegions::VolumeRegion &vr : layer_range.volume_regions) + if (vr.model_volume->is_model_part()) + ++num_model_parts; + const bool force_complex_for_belt = !bbox_z_in_layer_frame && num_model_parts > 1; for (; z_idx < zs.size() && zs[z_idx] < layer_range.layer_height_range.second; ++ z_idx) { float z = zs[z_idx]; + if (force_complex_for_belt) { + zs_complex.push_back({ z_idx, z }); + continue; + } int idx_first_printable_region = -1; bool complex = false; for (int idx_region = 0; idx_region < int(layer_range.volume_regions.size()); ++ idx_region) { @@ -949,7 +970,81 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name << " instances=" << this->instances().size() << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; - double global_z_offset = 0.; + + // Per-object Z-shift compensation, applied regardless of global mode. + // + // BeltSliceStrategy::apply_to_trafo lifts the mesh by max(0, -m_belt_min_z) + // so the slicer can slice with slicer_z >= 0. BeltBackTransform inverts + // build_forward_transform() which DOES NOT include this per-object + // Z-shift (it's not known until vertex scan time). Result: G-code + // coords emerge offset by the un-undone Z-shift — for shear that's a + // pure machine_z lift; for rotation it leaks into both machine_y and + // machine_z because the inverse rotation couples slicer_z back into + // both axes. Compensating layer.print_z by shear_min_z here makes the + // back-transform produce correct machine-frame coordinates whether or + // not any global mode is active. + // + // (The original global-mode-only application of shear_min_z was sized + // for cube-vs-inverted-cone-tip differentiation; the same compensation + // is what fixes assemblies and rotation-mode parts where z_shift > 0.) + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + double shear_min_z = m_belt_min_z - belt_surface_z; + double global_z_offset = shear_min_z; + + // Centering correction: trafo_centered pretranslates by + // -m_center_offset.{x,y}. Under the belt forward transform, the + // Y component of that pretranslate couples into slicer-Z (shear: + // tan*c.y, rotation: sin*c.y). BeltBackTransform inverts the + // rotation/shear but doesn't undo centering, so this Z component + // leaks into machine output as a position offset whenever + // m_center_offset != 0. When a user moves a volume within an + // assembly such that the combined bbox center shifts, this shows + // up as a small Z translation in the print. Compensate by adding + // the Z component of the centering through the forward transform. + { + Transform3d T_fwd = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d c_off(unscale(m_center_offset.x()), + unscale(m_center_offset.y()), + 0.); + double centering_z_corr = (T_fwd.linear() * c_off).z(); + global_z_offset += centering_z_corr; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] centering correction" + << " obj=" << this->model_object()->name + << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" + << " centering_z_corr=" << centering_z_corr + << " (added to global_z_offset)"; + } + + // [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared + // side-by-side. Lays out every value that feeds into the final layer + // print_z adjustment. + { + BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] slice() per-object summary" + << " obj=" << this->model_object()->name + << " n_volumes=" << this->model_object()->volumes.size() + << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" + << " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")" + << " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")" + << " m_center_offset=(" << unscale(m_center_offset.x()) << "," << unscale(m_center_offset.y()) << ")" + << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" + << " m_belt_min_z=" << m_belt_min_z + << " belt_surface_z=" << belt_surface_z + << " shear_min_z=" << shear_min_z; + // Per-volume bbox + get_matrix translation so order/composition is visible. + int vi = 0; + for (const ModelVolume *mv : this->model_object()->volumes) { + if (!mv->is_model_part()) { ++vi; continue; } + BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] vol[" << vi + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" + << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" + << " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")"; + ++vi; + } + } if (pcfg.belt_preslice_global.value) { // Global pre-slice mode: compute full correction c = (T.linear() - I) * d @@ -957,18 +1052,7 @@ void PrintObject::slice() Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; - - // Per-object shape contribution: BeltSliceStrategy::apply_to_trafo - // lifts the mesh by max(0, -m_belt_min_z) to keep slicer-frame Z - // above 0. Two objects at the same bed position but different - // m_belt_min_z (e.g. cube vs inverted-cone tip) otherwise end up - // at the same print_z, which causes the inverted-cone tip to - // start on the same layer as the cube's lowest sheared corner. - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - double shear_min_z = m_belt_min_z - belt_surface_z; - - global_z_offset = c.z() + shear_min_z; + global_z_offset += c.z(); BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ") new=(" << c.x() << "," << c.y() << ")"; @@ -999,10 +1083,7 @@ void PrintObject::slice() Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - double shear_min_z = m_belt_min_z - belt_surface_z; - global_z_offset += c.z() + shear_min_z; + global_z_offset += c.z(); BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ") new=(" << c.x() << "," << c.y() << ")"; @@ -1012,6 +1093,19 @@ void PrintObject::slice() << " shear_min_z=" << shear_min_z << " (m_belt_min_z=" << m_belt_min_z << ")"; } + // Slicing rotation in global mode: bed-position-dependent Z offset. + // For R(α, X): c.z = sin(α)*d.y so objects at different bed-Y + // values print at different machine Z values along the inclined belt. + if (pcfg.belt_slice_rotation_global.value + && pcfg.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(pcfg.belt_slice_rotation_angle.value) > EPSILON) { + Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); + Vec3d c = T.linear() * d - d; + global_z_offset += c.z(); + m_belt_global_xy_correction = Vec2d(c.x(), c.y()); + } + // Pre-slice remap global mode: when on, the remap accounts for the // instance bed position. The Z component of the correction // (R - I) * d shifts layer print_z so e.g. a Y↔Z swap with an @@ -1030,6 +1124,15 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(warning) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; + // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z + // and where the first / last layer end up post-adjustment. + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] global_z_offset breakdown" + << " obj=" << this->model_object()->name + << " shear_min_z=" << shear_min_z + << " total_global_z_offset=" << global_z_offset + << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" + << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) + << " n_layers=" << m_layers.size(); if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) layer->print_z += global_z_offset; @@ -1038,6 +1141,12 @@ void PrintObject::slice() // layer print_z, so belt_floor_z_shift must match. m_slicing_params.belt_floor_z_shift += global_z_offset; } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] post-adjustment" + << " first_layer.print_z=" << m_layers.front()->print_z + << " last_layer.print_z=" << m_layers.back()->print_z + << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; + } if (!m_layers.empty()) { BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z << " last_layer_z=" << m_layers.back()->print_z diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 93dde99522..470f1d93cb 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4462,7 +4462,23 @@ void TabPrinter::build_fff() belt_og->append_single_option_line("belt_printer"); belt_og->append_single_option_line("belt_printer_angle"); belt_og->append_single_option_line("belt_printer_infinite_y"); - // Per-axis shear: group mode + angle + source on one row per axis + // Mesh rotate (default belt-side transform): isometric, no distortion. + // Shown above the per-axis shear/scale rows because most users should + // pick rotation; shear/scale is the expert escape hatch. + { + Line line = { L("Mesh rotate"), + L("Global mesh rotation applied before slicing. Isometric " + "(no distortion); the back-transform inverts it before the " + "machine-frame remap. Default belt transform. Mutually " + "exclusive with per-axis shear/scale in the UI.") }; + line.append_option(belt_og->get_option("belt_slice_rotation")); + line.append_option(belt_og->get_option("belt_slice_rotation_angle")); + line.append_option(belt_og->get_option("belt_slice_rotation_global")); + belt_og->append_line(line); + } + // Per-axis shear/scale: expert escape hatch for non-rigid belt transforms + // (BlackBelt-style 1/sin scaling, asymmetric belt geometries, etc.). + // Group mode + angle + source on one row per axis. { Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; line.append_option(belt_og->get_option("belt_shear_x")); @@ -5594,17 +5610,19 @@ void TabPrinter::toggle_options() bool expert_or_above = (m_mode >= comExpert); toggle_line("belt_printer_angle", is_belt); toggle_line("belt_printer_infinite_y", is_belt); - // Mesh shear/scale: only shear Z and scale Y are shown in Advanced; the others - // are Expert-only. Each Line packs all its options on one row, so toggle the - // Line here in addition to the per-option mode gating. + // Mesh rotate: advanced (visible by default in belt mode). + toggle_line("belt_slice_rotation", is_belt); + // Mesh shear/scale: expert-only (the rigid rotation above is the + // primary belt transform; shear/scale is reserved for power users + // matching BlackBelt-style 1/sin scale or asymmetric belt geometries). toggle_line("belt_shear_x", is_belt && expert_or_above); toggle_line("belt_shear_y", is_belt && expert_or_above); - toggle_line("belt_shear_z", is_belt); + toggle_line("belt_shear_z", is_belt && expert_or_above); toggle_line("belt_scale_x", is_belt && expert_or_above); - toggle_line("belt_scale_y", is_belt); + toggle_line("belt_scale_y", is_belt && expert_or_above); toggle_line("belt_scale_z", is_belt && expert_or_above); - for (auto el : {"belt_mesh_transform_order", - "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) + toggle_line("belt_mesh_transform_order", is_belt && expert_or_above); + for (auto el : {"belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) toggle_line(el, is_belt); // Remap, back-transform, and global mesh-transforms toggles are gated by belt @@ -5619,31 +5637,59 @@ void TabPrinter::toggle_options() // preslice_remap_global: superseded by belt_preslice_global toggle_option("preslice_remap_global", is_belt && !belt_global); + // Mutual exclusion (UI only): slicing rotation and per-axis shear/scale are + // alternatives in the printer panel. The pipeline math composes them; this + // just disables the inactive set so users pick one path or the other. + auto rot_axis = m_config->option>("belt_slice_rotation")->value; + bool rotation_active = is_belt + && rot_axis != BeltRotationAxis::None + && std::abs(m_config->opt_float("belt_slice_rotation_angle")) > 1e-9; + bool shear_or_scale_active = is_belt && ( + m_config->option>("belt_shear_x")->value != BeltShearMode::None || + m_config->option>("belt_shear_y")->value != BeltShearMode::None || + m_config->option>("belt_shear_z")->value != BeltShearMode::None || + m_config->option>("belt_scale_x")->value != BeltScaleMode::None || + m_config->option>("belt_scale_y")->value != BeltScaleMode::None || + m_config->option>("belt_scale_z")->value != BeltScaleMode::None); + bool allow_shear_scale = !rotation_active; + bool allow_rotation = !shear_or_scale_active; + + // Disable shear/scale mode dropdowns when rotation is active. + for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", + "belt_scale_x", "belt_scale_y", "belt_scale_z", + "belt_mesh_transform_order"}) + toggle_option(el, is_belt && allow_shear_scale); + + // Disable rotation controls when any shear/scale is active. + toggle_option("belt_slice_rotation", is_belt && allow_rotation); + toggle_option("belt_slice_rotation_angle", is_belt && allow_rotation && rot_axis != BeltRotationAxis::None); + toggle_option("belt_slice_rotation_global", is_belt && allow_rotation && rot_axis != BeltRotationAxis::None); + // Gray out angle/from sub-options when their parent shear/scale mode is None. // Per-axis globals are superseded when belt_preslice_global is on. auto sx = m_config->option>("belt_shear_x")->value; - toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); - toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); - toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None && !belt_global); + toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None && !belt_global && allow_shear_scale); auto sy = m_config->option>("belt_shear_y")->value; - toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); - toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); - toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None && !belt_global); + toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None && !belt_global && allow_shear_scale); auto sz = m_config->option>("belt_shear_z")->value; - toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); - toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); - toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None && !belt_global); + toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None && allow_shear_scale); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None && !belt_global && allow_shear_scale); auto scx = m_config->option>("belt_scale_x")->value; - toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); + toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None && allow_shear_scale); auto scy = m_config->option>("belt_scale_y")->value; - toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None); + toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None && allow_shear_scale); auto scz = m_config->option>("belt_scale_z")->value; - toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None && allow_shear_scale); // Machine-frame transforms: shown only in belt mode. // Mirror the Advanced/Expert split used for mesh shear/scale.