diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 19765b8d4e..96b3138832 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -88,7 +88,6 @@ "belt_printer": "1", "belt_slice_rotation": "x", "belt_slice_rotation_angle": "45", - "belt_slice_rotation_global": "1", "build_plate_tilt_x": "45", "purge_in_prime_tower": "0", "scan_first_layer": "0", diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index 60ef096de0..aacd36e118 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -91,7 +91,6 @@ "belt_printer": "1", "belt_slice_rotation": "x", "belt_slice_rotation_angle": "45", - "belt_slice_rotation_global": "1", "build_plate_tilt_x": "45", "purge_in_prime_tower": "0", "scan_first_layer": "0", diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json index 2a392b4f60..96d38ac31a 100644 --- a/resources/profiles/Printcepts/machine/fdm_belt_common.json +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -91,7 +91,6 @@ "belt_printer": "1", "belt_slice_rotation": "x", "belt_slice_rotation_angle": "45", - "belt_slice_rotation_global": "1", "build_plate_tilt_x": "45", "purge_in_prime_tower": "0", "scan_first_layer": "0", diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index a78c0cbfa6..1726fc6eac 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,13 +25,6 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) // for the physical tilt the G-code viewer uses to enable belt view. 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); - // Pre-slice remap configs - file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str()); - file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str()); - file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str()); - file.write_format("; preslice_remap_global = %d\n", print.config().preslice_remap_global.value ? 1 : 0); - file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0); // Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt // tilt angle (or belt_frame_tilt_angle when decoupled). file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0); @@ -40,26 +33,10 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/) { - // Global pre-slice mode: adjust origin using computed correction. - // Transform the origin through the belt pipeline so that - // back_transform(T * origin) = origin (correct machine position). - // - // Flags that trigger this path: - // belt_preslice_global — full pipeline (rotation * remap) is global - // preslice_remap_global — only the pre-slice remap is global - // belt_slice_rotation_global — slicing rotation treated as global (matches - // the per-instance Z-offset added in PrintObjectSlice.cpp) - // The XY origin adjustment uses the FULL forward transform, because the - // back_transform applied during G-code emission is always the inverse of - // the full pipeline. - bool use_global = m_config.belt_preslice_global.value - || (m_config.preslice_remap_global.value - && BeltTransformPipeline::has_preslice_remap(m_config)) - || (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) - return; + // Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform + // the origin through the belt pipeline so that back_transform(T * origin) = + // origin (correct machine position). The back_transform applied during + // G-code emission is the inverse of the forward transform. // Adjust origin: transform through belt forward pipeline so that // the back-transform correctly recovers model-space positions. diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 8892b4e5de..8aac729f1b 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -14,12 +14,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, const ModelVolumePtrs &model_volumes, double *out_belt_min_z) { - // 1. Standalone pre-slice axis remap (works without belt mode). - const bool has_remap = BeltTransformPipeline::has_preslice_remap(config); - if (has_remap) - trafo = BeltTransformPipeline::build_preslice_remap(config) * trafo; - - // 2. Belt rotation — the sole mesh-side belt transform (matching + // 1. Belt rotation — the sole mesh-side belt transform (matching // BeltTransformPipeline::build_forward_transform). Only active in // belt-printer mode. bool has_rotation = false; @@ -32,10 +27,10 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, } } - if (!has_remap && !has_rotation) + if (!has_rotation) return; - // 3. Z-shift — detect if the mesh clips below the build plate after the + // 2. Z-shift — detect if the mesh clips below the build plate after the // transforms and lift it. 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 on assemblies (where per-volume get_matrix() positions @@ -43,6 +38,20 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // coordinates rather than object-space coordinates, so volumes translated along // the slicer's Z axis would be silently excluded from the bound check. + // + // The lift is measured to the lowest point of the SUPPORT region, not of the + // mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis + // coordinate) runs below every vertex, and under the leading end of an + // overhang it lies below the lowest vertex by up to the overhang's length + // times the shear. Supports have to reach that floor, and every support + // generator works in layers at z >= 0, so z = 0 has to be the lowest floor + // point under the footprint. The layers between it and the first vertex + // come out empty, which belt slicing already tolerates (the bottom corner + // of a tilted part is a point). Vertices on the belt have z == floor, so + // for a part resting on the belt this is simply the floor at its leading + // extreme, less the frame margin (see BeltTransformPipeline::frame_margin). + BeltTransformPipeline::BeltFloorParams floor; + const bool has_floor = BeltTransformPipeline::floor_shear(config, floor); double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { if (!mv->is_model_part()) continue; @@ -52,8 +61,12 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, Vec3d vm = v.cast(); Vec3d pt = vol_trafo * vm; min_z = std::min(min_z, pt.z()); + if (has_floor) + min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y())); } } + if (has_floor && min_z != std::numeric_limits::max()) + min_z -= BeltTransformPipeline::frame_margin(floor); const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; if (z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index d1ea31d33c..d11e2fccdc 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -16,50 +16,6 @@ namespace Slic3r { // ---- Matrix builders ------------------------------------------------------ -Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config) -{ - Transform3d pre_remap = Transform3d::Identity(); - if (!has_preslice_remap(config)) - return pre_remap; - - int pre_rx = int(config.preslice_remap_x.value); - int pre_ry = int(config.preslice_remap_y.value); - int pre_rz = int(config.preslice_remap_z.value); - - // Each remap value selects a source axis and sign. - 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); - pre_remap.linear() = remap_lin; - - // Translation for Rev modes (needs build volume extents). - 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); - Vec3d remap_trans = Vec3d::Zero(); - 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; - } - - return pre_remap; -} - Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out) { BeltRotationAxis axis = config.belt_slice_rotation.value; @@ -81,58 +37,50 @@ Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) { - // Mesh-side belt transform: rotation applied after the pre-slice axis remap. - // (Shear & scale are a g-code-side stage, not part of the mesh transform.) - Transform3d pre_remap = build_preslice_remap(config); - Matrix3d rot = build_rotation_matrix(config); - + // Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side + // stage, not part of the mesh transform.) Transform3d combined = Transform3d::Identity(); - combined.linear() = rot; - combined = combined * pre_remap; + combined.linear() = build_rotation_matrix(config); return combined; } -// ---- Bounding box remap --------------------------------------------------- - -BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) -{ - if (!has_preslice_remap(config)) - return bb; // Identity remap, or belt mode off. - - int pre_rx = int(config.preslice_remap_x.value); - int pre_ry = int(config.preslice_remap_y.value); - int pre_rz = int(config.preslice_remap_z.value); - - 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; -} - -BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config) -{ - return remap_bbox(model_object.raw_bounding_box(), config); -} - // ---- Belt floor parameters ------------------------------------------------ // Shared implementation for both PrintConfig and DynamicPrintConfig. // Template avoids duplicating the math for the two config types. namespace { +// Belt floor in the rotated slicer frame: 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 +void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out) +{ + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (rot_axis) { + case BeltRotationAxis::X: + out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.; + out.from_axis = 1; // Y + break; + case BeltRotationAxis::Y: + out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.; + out.from_axis = 0; // X + break; + case BeltRotationAxis::Z: + default: + out.shear_factor = 0.0; + out.from_axis = 1; + break; + } +} + +// Z of the belt floor directly under a point of the rotated (unshifted) frame. +inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt) +{ + return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y()); +} + + template BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( const Config &config, const BoundingBoxf3 &bb, double original_height) @@ -176,38 +124,28 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( default: unit_axis = Vec3d::UnitX(); break; } Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); + belt_floor_shear(rot_axis, angle_rad, result.floor_params); + // The slicing frame starts at the lowest point of the support region: the + // lowest belt-floor point under the footprint, not the lowest vertex. The + // belt under the leading end of an overhang lies below every vertex of the + // part, and supports have to be able to reach it (see + // BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan + // counterpart of this bbox estimate). 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(); + Vec3d rc = R * c; + double z = rc.z(); min_rz = std::min(min_rz, z); max_rz = std::max(max_rz, z); + min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc)); } + min_rz -= BeltTransformPipeline::frame_margin(result.floor_params); result.object_height = max_rz - min_rz; - // 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.); return result; @@ -216,15 +154,26 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( } // anonymous namespace BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( - const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) + const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height) { - return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); + return compute_belt_height_and_floor_impl(config, bbox, original_height); } BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) + const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, double original_height) { - return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); + return compute_belt_height_and_floor_impl(config, bbox, original_height); +} + +bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out) +{ + out = BeltFloorParams{}; + const BeltRotationAxis rot_axis = config.belt_slice_rotation.value; + const double rot_angle = config.belt_slice_rotation_angle.value; + if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON) + return false; + belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out); + return std::abs(out.shear_factor) > EPSILON; } } // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 59669fe36a..7409fb2c7f 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -16,7 +16,7 @@ class ModelObject; // Shared belt-printer transform math. // // The pre-slice pipeline applied in PrintObjectSlice.cpp is: -// trafo_out = z_shift * rotation * pre_remap * trafo_in +// trafo_out = z_shift * rotation * trafo_in // // Rotation is the sole mesh-side belt transform; shear & scale are applied // to the g-code instead (see MachineFrameTransform). This class provides the @@ -47,39 +47,11 @@ class BeltTransformPipeline public: // ---- Identity checks -------------------------------------------------- - // Whether the axis remaps (preslice_remap_* and gcode_remap_*) apply at all. - // The remap fields are only offered in the belt printer group, so a value - // left in a profile must not change a non-belt print: with belt mode off every - // belt-only key is a no-op. This is the one place to widen if a non-belt use - // ever needs them. + // Whether the G-code axis remap applies at all. The remap fields are only + // offered in the belt printer group, so a value left in a profile must not + // change a non-belt print: with belt mode off every belt-only key is a no-op. + // This is the one place to widen if a non-belt use ever needs them. static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; } - static bool axis_remap_enabled(const DynamicPrintConfig &config) - { - auto *opt = config.option("belt_printer"); - return opt != nullptr && opt->value; - } - - static bool has_preslice_remap(const PrintConfig &config) - { - return axis_remap_enabled(config) && - (int(config.preslice_remap_x.value) != int(RemapAxis::PosX) || - int(config.preslice_remap_y.value) != int(RemapAxis::PosY) || - int(config.preslice_remap_z.value) != int(RemapAxis::PosZ)); - } - - // Overload accepting DynamicPrintConfig (used in static slicing_parameters). - static bool has_preslice_remap(const DynamicPrintConfig &config) - { - if (! axis_remap_enabled(config)) - return false; - auto get_int = [&](const char *key) -> int { - auto *opt = config.option>(key); - return opt ? int(opt->value) : 0; - }; - return get_int("preslice_remap_x") != int(RemapAxis::PosX) || - get_int("preslice_remap_y") != int(RemapAxis::PosY) || - get_int("preslice_remap_z") != int(RemapAxis::PosZ); - } static bool has_rotation(const PrintConfig &config) { @@ -118,26 +90,16 @@ public: // ---- Matrix builders -------------------------------------------------- - // Build the pre-slice axis remap transform (includes Rev-mode translation). - static Transform3d build_preslice_remap(const PrintConfig &config); - // 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 (rotation * pre_remap) — the mesh-side belt - // transform that BeltSliceStrategy applies and BeltBackTransform inverts. + // Forward transform (the rotation) — the mesh-side belt transform that + // BeltSliceStrategy applies and BeltBackTransform inverts. // Does NOT include the per-object Z-shift. static Transform3d build_forward_transform(const PrintConfig &config); - // ---- Bounding box remap ----------------------------------------------- - - // Remap a bounding box through the pre-slice axis remap. - // Returns the original bbox if remap is identity. - static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config); - static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config); - // ---- Belt floor parameters -------------------------------------------- struct BeltFloorParams { @@ -146,6 +108,19 @@ public: double z_shift = 0.0; }; + // Shear factor and from-axis of the belt floor in the rotated slicer frame + // (z_floor = shear_factor * u, u = the from-axis coordinate), for the + // rotation the config selects. z_shift is left at 0. Returns false (and + // zero shear) when the config has no tilt. + static bool floor_shear(const PrintConfig &config, BeltFloorParams &out); + + // How far below the lowest belt-floor point under the footprint the slicing + // frame starts, in slicing Z. A support column meeting the belt is wider at + // its base than at its tip, so under a leading overhang the base reaches ahead + // of the part along the belt, and the layers that trim it to the belt plane + // lie below that lowest point: 10 mm along the belt. + static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); } + // Result of computing belt height + floor params. struct BeltHeightResult { double object_height; // Effective object height after shear/scale @@ -153,15 +128,15 @@ public: }; // Compute effective object height and belt floor parameters from config - // and pre-remapped bounding box. original_height is the input height + // and the object's bounding box. original_height is the input height // (bb.size().z() or model_object.max_z()). static BeltHeightResult compute_belt_height_and_floor( - const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, + const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height); // Overload for DynamicPrintConfig (used by static slicing_parameters). static BeltHeightResult compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, + const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, double original_height); }; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 68a1434699..a108588f84 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -94,8 +94,6 @@ set(lisbslic3r_sources BeltSliceStrategy.hpp BeltTransform.cpp BeltTransform.hpp - FirstLayerPlane.cpp - FirstLayerPlane.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp deleted file mode 100644 index 7642d42267..0000000000 --- a/src/libslic3r/FirstLayerPlane.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "FirstLayerPlane.hpp" -#include "BeltTransform.hpp" -#include "BoundingBox.hpp" -#include "Point.hpp" -#include "PrintConfig.hpp" -#include "libslic3r.h" - -#include -#include -#include - -namespace Slic3r { - -namespace { - -// Build the row of the gcode-axis-remap matrix R that produces machine_Z, -// AS A FUNCTION OF a slicing-frame point in the GCode generator's coordinate -// space. Without back-transform this is just R.row(2). With back-transform -// the writer applies F^-1 before R, so the effective row is (R * F^-1).row(2). -// -// Returns a pair (gradient, constant) such that: -// machine_Z(p_slicing) = gradient.dot(p_slicing) + constant -struct MachineZAffine { - Vec3d gradient = Vec3d::UnitZ(); - double constant = 0.0; -}; - -MachineZAffine compute_machine_z_affine(const PrintConfig &config) -{ - MachineZAffine out; - - // R is the matrix form of GCodeWriter::apply_axis_remap. Each output axis - // i picks one slicing-frame component (with sign + optional Rev mode - // translation) based on m_remap_{x,y,z}. We only need row 2 (the z output) - // since machine_Z is what defines the first-layer plane. - int rz = int(config.gcode_remap_z.value); - int axis = rz % 3; - double sign; - double trans; - if (rz < int(RemapAxis::NegX)) { // 0..2 = PosX/Y/Z - sign = 1.0; - trans = 0.0; - } else if (rz < int(RemapAxis::RevX)) { // 3..5 = NegX/Y/Z - sign = -1.0; - trans = 0.0; - } else { // 6..8 = RevX/Y/Z - sign = -1.0; - BoundingBoxf bbox_bed(config.printable_area.values); - Vec3d vol_max(bbox_bed.max.x(), - bbox_bed.max.y(), - config.printable_height.value); - trans = vol_max[axis]; - } - - Vec3d r_row = Vec3d::Zero(); - r_row[axis] = sign; - - // Without back-transform, machine_Z(slicing) = r_row · slicing + trans. - out.gradient = r_row; - out.constant = trans; - - if (config.gcode_back_transform.value && config.belt_printer.value) { - // BeltKinematics applies F^-1 before R when back-transform is on. - // So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans - // = (r_row^T · F^-1) · slicing + trans - // We need to compose r_row with F^-1 from the LEFT (treating r_row as - // a row vector). Eigen makes this easy: it's just F^-1.transpose() * r_row. - Transform3d forward = BeltTransformPipeline::build_forward_transform(config); - Transform3d inverse = forward.inverse(); - // Note: forward.translation() is normally zero (per-print transforms - // don't add a translation; the per-object z_shift is added separately - // in PrintObjectSlice). We still incorporate inverse.translation() in - // case a Rev-mode preslice_remap puts a translation in F. - Vec3d composed_grad = inverse.linear().transpose() * r_row; - double composed_trans = - r_row.dot(inverse.translation()) + trans; - out.gradient = composed_grad; - out.constant = composed_trans; - } - - return out; -} - -} // namespace - -FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) -{ - // -------- Resolve Auto ------------------------------------------------- - FirstLayerPlaneMode mode = config.first_layer_plane.value; - if (mode == FirstLayerPlaneMode::Auto) { - bool belt_affine_active = config.belt_printer.value && - 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; - - // -------- Band thickness ---------------------------------------------- - // Note: layer_height lives in PrintObjectConfig, not PrintConfig, so we - // can't fall back to it from here. initial_layer_print_height is in - // PrintConfig and is the right default anyway (the legacy first-layer - // semantics used initial_layer_print_height, not the regular one). - double thickness = config.first_layer_plane_thickness.value; - if (thickness <= 0.0) - thickness = config.initial_layer_print_height.value; - if (thickness <= 0.0) - thickness = 0.2; - m_thickness_mm = thickness; - - const double user_offset = config.first_layer_plane_offset.value; - - // -------- Build the plane --------------------------------------------- - auto set_axis_aligned = [&](const Vec3d &n_unit, double offset_along_n) { - m_normal = n_unit; - m_offset = offset_along_n; - }; - - switch (mode) { - case FirstLayerPlaneMode::XY: - // Legacy XY plane. Inactive: short-circuit to layer-index path. - set_axis_aligned(Vec3d::UnitZ(), user_offset); - m_active = false; - return; - - case FirstLayerPlaneMode::YZ: - set_axis_aligned(Vec3d::UnitX(), user_offset); - m_active = true; - return; - - case FirstLayerPlaneMode::XZ: - set_axis_aligned(Vec3d::UnitY(), user_offset); - m_active = true; - return; - - 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); - double cmag = mz.gradient.norm(); - if (cmag < EPSILON) { - // Degenerate: slicing point doesn't affect machine_Z. Fall back. - set_axis_aligned(Vec3d::UnitZ(), user_offset); - m_active = false; - return; - } - // Plane equation: gradient · slicing = user_offset - constant - const double K = user_offset - mz.constant; - m_normal = mz.gradient / cmag; - m_offset = K / cmag; - m_active = true; - return; - } - - case FirstLayerPlaneMode::Auto: - // Should have been resolved above. - m_active = false; - return; - } - - m_active = false; -} - -double FirstLayerPlane::distance_from_plane(const Vec3d &point_slicing_mm) const -{ - return m_normal.dot(point_slicing_mm) - m_offset; -} - -bool FirstLayerPlane::is_first_layer(const Vec3d &point_slicing_mm, - double first_layer_height_mm) const -{ - if (!m_active) - return false; - return distance_from_plane(point_slicing_mm) < first_layer_height_mm; -} - -int FirstLayerPlane::effective_layer_index(const Vec3d &point_slicing_mm) const -{ - if (!m_active) - return INT_MAX / 2; // Effectively "way past first layer". - double d = distance_from_plane(point_slicing_mm); - if (d <= 0.0) - return 0; - return int(std::floor(d / m_thickness_mm)); -} - -int FirstLayerPlane::min_effective_index_for_xy_bbox( - const BoundingBoxf &xy_bbox_mm, double slicing_z_mm) const -{ - if (!m_active) - return INT_MAX / 2; - // For the rectangular bbox in (x, y) at fixed z, the smallest value of - // (n.x*x + n.y*y + n.z*z - offset) is achieved at one of the four - // corners, with the smaller component picked when the corresponding - // normal coefficient is positive. - const double x_for_min = (m_normal.x() >= 0.0) - ? xy_bbox_mm.min.x() : xy_bbox_mm.max.x(); - const double y_for_min = (m_normal.y() >= 0.0) - ? xy_bbox_mm.min.y() : xy_bbox_mm.max.y(); - const double dmin = m_normal.x() * x_for_min - + m_normal.y() * y_for_min - + m_normal.z() * slicing_z_mm - - m_offset; - if (dmin <= 0.0) - return 0; - return int(std::floor(dmin / m_thickness_mm)); -} - -int FirstLayerPlane::min_effective_index_for_bbox3( - const BoundingBoxf3 &bbox_mm) const -{ - if (!m_active) - return INT_MAX / 2; - const double x_for_min = (m_normal.x() >= 0.0) - ? bbox_mm.min.x() : bbox_mm.max.x(); - const double y_for_min = (m_normal.y() >= 0.0) - ? bbox_mm.min.y() : bbox_mm.max.y(); - const double z_for_min = (m_normal.z() >= 0.0) - ? bbox_mm.min.z() : bbox_mm.max.z(); - const double dmin = m_normal.x() * x_for_min - + m_normal.y() * y_for_min - + m_normal.z() * z_for_min - - m_offset; - if (dmin <= 0.0) - return 0; - return int(std::floor(dmin / m_thickness_mm)); -} - -} // namespace Slic3r diff --git a/src/libslic3r/FirstLayerPlane.hpp b/src/libslic3r/FirstLayerPlane.hpp deleted file mode 100644 index 35b8346c82..0000000000 --- a/src/libslic3r/FirstLayerPlane.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef slic3r_FirstLayerPlane_hpp_ -#define slic3r_FirstLayerPlane_hpp_ - -#include "libslic3r.h" -#include "Point.hpp" -#include "BoundingBox.hpp" -#include "PrintConfig.hpp" - -namespace Slic3r { - -// Decides which extrusions get "first layer" treatment (no fan, slow speed, -// initial-layer accel/jerk, deferred temperature drop) by reference to a -// configurable plane in slicing-frame coordinates rather than the slicing -// layer index. -// -// On a normal flat-bed printer the plane is XY at slicing_Z = 0 and the -// evaluator is INACTIVE — every call site short-circuits back to the legacy -// `Layer::id() == 0` test. On a belt printer with a Z-from-Y shear the -// belt surface (machine_Z = 0) maps to a plane in slicing-frame coordinates -// derived from the gcode axis remap, so layer-index-based detection no -// longer matches the physical first printed surface. -// -// Plane representation: unit normal `n` (slicing frame) and offset along -// the normal such that the plane equation is `n · p == offset`. Signed -// perpendicular distance is `d(p) = n · p - offset`. Positive distance -// means "away from the belt surface", negative means "below the plane". -class FirstLayerPlane -{ -public: - explicit FirstLayerPlane(const PrintConfig &config); - - // Inactive when the legacy XY layer-index path should be used. This - // covers all non-belt printers and any belt printer where the user - // explicitly picked XY mode. - bool is_active() const { return m_active; } - FirstLayerPlaneMode effective_mode() const{ return m_mode; } - double band_thickness_mm() const { return m_thickness_mm; } - const Vec3d & normal() const { return m_normal; } - double plane_offset() const { return m_offset; } - - // Signed perpendicular distance from a slicing-frame point to the plane. - double distance_from_plane(const Vec3d &point_slicing_mm) const; - - // True if perpendicular distance < first_layer_height_mm. When the - // evaluator is inactive this returns false (call sites should fall back - // to the legacy per-layer path before reaching this function). - bool is_first_layer(const Vec3d &point_slicing_mm, - double first_layer_height_mm) const; - - // floor((distance - 0) / band_thickness), clamped to [0, +inf). Used - // for "first N layers" thresholds (fan, slow_down_layers). Returns 0 - // for points within the band. Returns INT_MAX/2 when inactive. - int effective_layer_index(const Vec3d &point_slicing_mm) const; - - // Min effective index over a 2D bbox at a fixed slicing_Z. Used for - // layer-level decisions (e.g. temperature transition gate) where we - // don't want to walk every extrusion in the layer. For axis-aligned - // planes this is exact; for tilted planes it's a tight lower bound - // (the plane projection of the bbox's extreme corner). - int min_effective_index_for_xy_bbox(const BoundingBoxf &xy_bbox_mm, - double slicing_z_mm) const; - - // Same as above but the bbox spans a Z range too. - int min_effective_index_for_bbox3(const BoundingBoxf3 &bbox_mm) const; - -private: - bool m_active = false; - FirstLayerPlaneMode m_mode = FirstLayerPlaneMode::XY; - Vec3d m_normal = Vec3d::UnitZ(); // unit, slicing frame - double m_offset = 0.0; // n·p == m_offset - double m_thickness_mm = 0.0; -}; - -} // namespace Slic3r - -#endif // slic3r_FirstLayerPlane_hpp_ diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 46c1458c8b..8ad30a611a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -107,7 +107,6 @@ #include "calib.hpp" #include "libslic3r_version.h" #include "GCode/BeltKinematics.hpp" -#include "FirstLayerPlane.hpp" // Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332. // We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB. #if ! defined(TBB_VERSION_MAJOR) @@ -3153,19 +3152,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato print.config().printable_height.value)); } - // Build the FirstLayerPlane evaluator. When inactive (non-belt printers - // and belt printers without Z shear), all per-path call sites short- - // circuit to the legacy Layer::id() == 0 path so g-code stays bit- - // identical to the pre-feature behavior. - m_first_layer_plane = std::make_unique(print.config()); // Belt writers only: travel-speed selection becomes per-point (see // GCodeWriter::uses_pointwise_travel_speed()), which must not change for // non-belt printers. The writer gets the same test the extrusions use, so a // travel is judged against the belt surface (belt_height_above_floor) exactly - // like the path it leads to, and not against the FirstLayerPlane, which - // misreports the height under a non-identity gcode_remap_*. Writer points - // carry the G-code origin and extruder offset that point_to_gcode() added; - // the belt surface is described in the object's own frame. + // like the path it leads to. Writer points carry the G-code origin and + // extruder offset that point_to_gcode() added; the belt surface is described + // in the object's own frame. if (print.config().belt_printer.value) { m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) { const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero(); @@ -5540,7 +5533,7 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o // apron band has no Layer, and giving it a synthetic one would feed a fabricated // Layer::id() into initial-layer temperature selection, the spiral vase probe, // gradual interpolation and cooling. Correct first-layer treatment comes from -// FirstLayerPlane in BeltAffine mode, which is evaluated per point. +// the height above the belt, which is evaluated per point. LayerResult GCode::process_belt_brim_layer( const Print &print, const std::vector &layers, @@ -6302,40 +6295,13 @@ LayerResult GCode::process_layer( } } - // First-layer plane: defer the temperature/PLR transition until the - // entire layer is past the first-layer band. When the evaluator is - // inactive (non-belt printers and belt printers without Z shear) we - // fall back to the legacy `!first_layer` predicate so behavior is - // bit-identical to the pre-feature path. + // Belt printers: defer the temperature/PLR transition until the entire layer + // is past the first-layer band above the belt. Elsewhere (non-belt printers, + // support-only layers) the legacy `!first_layer` predicate applies, so + // behavior is bit-identical to the pre-feature path. bool past_first_layer_band = !first_layer; - if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0) { - // Belt surface known for this object: measured from the belt itself, as the - // extrusions and travels are, rather than through FirstLayerPlane. + if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0) past_first_layer_band = past > 0; - } else if (m_first_layer_plane && m_first_layer_plane->is_active()) { - past_first_layer_band = false; - if (object_layer != nullptr) { - // Conservatively walk the layer's lslice bboxes; if every bbox's - // most-belt-side corner is outside the first-layer band, the - // layer is fully past it. - const Layer *ol = object_layer; - int min_eff = INT_MAX; - for (const BoundingBox &bb : ol->lslices_bboxes) { - BoundingBoxf bbf( - Vec2d(unscale(bb.min.x()), unscale(bb.min.y())), - Vec2d(unscale(bb.max.x()), unscale(bb.max.y()))); - int eff = m_first_layer_plane->min_effective_index_for_xy_bbox( - bbf, ol->print_z); - if (eff < min_eff) min_eff = eff; - if (min_eff <= 0) break; - } - past_first_layer_band = (min_eff > 0 && min_eff != INT_MAX); - } else if (support_layer != nullptr) { - // Support-only layers: gate on the support layer's bottom_z - // proximity to the plane. Conservative. - past_first_layer_band = !first_layer; - } - } if (past_first_layer_band && !m_second_layer_things_done) { // Orca: set power loss recovery @@ -8812,7 +8778,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d if (speed == 0) speed = filament_max_volumetric_speed / _mm3_per_mm; - // Use the FirstLayerPlane-aware effective layer index when active so + // Use the belt-aware effective layer index when on a belt printer so // the speed fade tracks perpendicular distance from the plane on // belt printers; otherwise this falls back to the slicing layer id. const int _layer = this->effective_layer_index_for_point(path_point_mm); @@ -10637,8 +10603,8 @@ std::string GCode::set_object_info(Print *print) { // Whether an object layer lies entirely past the first-layer band above the belt: // 1 when its lowest point is at least one band thickness above the belt, 0 when // any of it is inside the band, -1 when the belt surface is not known for this -// layer (not a belt print, an explicit first-layer plane, or no object layer), in -// which case the caller falls back to FirstLayerPlane / the slicing layer index. +// layer (not a belt print, or no object layer), in which case the caller falls +// back to the slicing layer index. int GCode::belt_layer_past_first_layer_band(const Layer *object_layer) const { if (object_layer == nullptr) @@ -10674,20 +10640,6 @@ bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &heigh : (m_layer != nullptr ? m_layer->object() : nullptr); if (object == nullptr) return false; - // Respect an explicit first-layer-plane choice: only Auto and BeltAffine mean - // "use the belt". A user who selected XY, YZ or XZ has asked for the - // FirstLayerPlane evaluator and must keep it. - const FirstLayerPlaneMode mode = m_config.first_layer_plane.value; - if (mode != FirstLayerPlaneMode::Auto && mode != FirstLayerPlaneMode::BeltAffine) - return false; - // Likewise for a dialled-in plane offset. It is expressed as a machine-Z - // shift that FirstLayerPlane converts into a perpendicular distance in the - // slicing frame; this evaluator measures along slicing Z instead, so there is - // no faithful translation of it here. Honour the user's setting by deferring - // to the evaluator that implements it rather than silently dropping it. - if (std::abs(m_config.first_layer_plane_offset.value) > EPSILON) - return false; - const SlicingParameters &sp = object->slicing_parameters(); // Deliberately NOT BeltFloorContext: its init() folds in // belt_support_floor_offset, a support-generator diagnostic. Letting that diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index c5268705c3..4aa96f9a4c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -8,7 +8,6 @@ #include "libslic3r.h" #include "GCodeWriter.hpp" #include "GCode/BeltKinematics.hpp" -#include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -400,7 +399,6 @@ public: // first-layer-plane access points (on_first_layer overload, effective // index helper) are in the protected section since they're called from // GCode internals only. - const FirstLayerPlane *first_layer_plane() const { return m_first_layer_plane.get(); } protected: class GCodeOutputStream { @@ -845,7 +843,6 @@ protected: // PrintConfig. is_active() == false on non-belt printers and on belt // printers without a Z-axis shear; in that case all per-path plane // checks short-circuit to the legacy Layer::id() == 0 path. - std::unique_ptr m_first_layer_plane; // Plate origin, kept so a writer replaced during export can be given it again. std::unique_ptr m_pressure_equalizer; @@ -938,45 +935,32 @@ protected: // On the first printing layer. This flag triggers first layer speeds. //BBS bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; } - // Per-point first-layer test. When the FirstLayerPlane evaluator is - // active, the result depends on the supplied slicing-frame point; - // otherwise we delegate to the legacy per-layer test. This is the - // entry point used by per-path call sites in _extrude. + // Per-point first-layer test. On a belt printer the result depends on the + // supplied slicing-frame point (its height above the belt); otherwise we + // delegate to the legacy per-layer test. This is the entry point used by + // per-path call sites in _extrude. bool on_first_layer(const Vec3d &point_slicing_mm) const { - // Belt printers: measure height above the belt surface itself, in the - // slicing frame. See belt_height_above_floor() for why this does not go - // through FirstLayerPlane. double h; if (this->belt_height_above_floor(point_slicing_mm, h)) return h <= m_config.initial_layer_print_height.value + EPSILON; - if (m_first_layer_plane && m_first_layer_plane->is_active()) - return m_first_layer_plane->is_first_layer( - point_slicing_mm, m_config.initial_layer_print_height.value); return on_first_layer(); } // "Effective layer index" used to drive layer-count thresholds like - // slow_down_layers. When the evaluator is active this returns the - // perpendicular distance to the plane in band_thickness_mm units; - // otherwise it returns the legacy slicing layer index. + // slow_down_layers. On a belt printer this is the height above the belt in + // first_layer_band_mm() units; otherwise it is the legacy slicing layer index. int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const { double h; if (this->belt_height_above_floor(point_slicing_mm, h)) { const double lh = this->first_layer_band_mm(); return h <= 0. ? 0 : int(std::floor(h / lh)); } - if (m_first_layer_plane && m_first_layer_plane->is_active()) - return m_first_layer_plane->effective_layer_index(point_slicing_mm); return on_first_layer() ? 0 : layer_id(); } - // Band thickness for the *effective layer index* only. FirstLayerPlane keeps - // two separate thresholds and so must this path: is_first_layer() tests - // against initial_layer_print_height, while effective_layer_index() counts - // bands of first_layer_plane_thickness. Conflating them would apply - // first-layer treatment through a whole 1mm band on a 0.2mm first layer. + // Band thickness for the *effective layer index*: one first layer height, so + // "the first N layers" means the same height above the belt as on a flat bed. double first_layer_band_mm() const { - double band = m_config.first_layer_plane_thickness.value; - if (band <= 0.) band = m_config.initial_layer_print_height.value; + const double band = m_config.initial_layer_print_height.value; return band > 0. ? band : 0.2; } @@ -985,13 +969,8 @@ protected: // // The belt surface is known exactly in the slicing frame from the slicing // parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same - // description the support generator uses. FirstLayerPlane instead derives its - // plane by composing gcode_remap_* with the g-code back-transform, so its - // answer changes with the machine's *output* axis convention: on a printer - // with a non-identity remap it reported ~86mm of clearance for geometry - // sitting directly on the belt, and no extrusion was ever classified as - // first-layer. Measuring against the belt itself is independent of every - // remap and back-transform. + // description the support generator uses, independent of every remap and + // back-transform. bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const; // 1 / 0 / -1: the object layer is entirely past the first-layer band above the // belt / reaches into it / the belt surface is not known for it. diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp index 4b247323cd..3d2cabc718 100644 --- a/src/libslic3r/GCode/BeltBackTransform.cpp +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -10,19 +10,10 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config) m_active = false; m_inverse = Transform3d::Identity(); - if (!config.belt_printer.value || !config.gcode_back_transform.value) + if (!config.belt_printer.value) return false; - // Require at least one active transform to proceed. - 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_rotation && !has_preslice_global - && !BeltTransformPipeline::has_preslice_remap(config)) - return false; - - // Build the forward pipeline (rotation * pre_remap) and store its inverse. + // Build the forward pipeline (the rotation) and store its inverse. Transform3d forward = BeltTransformPipeline::build_forward_transform(config); if (forward.isApprox(Transform3d::Identity())) return false; diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 878ae12b5a..3956450a9f 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -7,25 +7,21 @@ 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. +// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt +// printer geometry, converting G-code coordinates from the sliced (rotated) +// frame back to the machine's real coordinate space. // // Initialized once from PrintConfig, then applied per-point in // BeltKinematics::to_machine() before axis remapping. // -// Active when 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. +// Active on belt printers with a non-identity pre-slice rotation. 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. + // Initialize from belt printer config. Rebuilds the same pre-slice rotation + // 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 diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 3e4fe2492f..deb776bf32 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2619,9 +2619,6 @@ void GCodeProcessorResult::reset() { machine_frame_transform_active = false; belt_tilt_angle = 0.f; belt_z_origin = 0.f; - preslice_remap_x = RemapAxis::PosX; - preslice_remap_y = RemapAxis::PosY; - preslice_remap_z = RemapAxis::PosZ; settings_ids.reset(); filaments_count = 0; backtrace_enabled = false; @@ -3201,9 +3198,8 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) const auto *belt = config.option("belt_printer"); if (belt != nullptr) { static const char *belt_keys[] = { - "belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global", - "belt_preslice_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", - "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", + "belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "belt_frame_tilt_decouple", "belt_frame_tilt_angle", }; for (const char *key : belt_keys) @@ -4328,36 +4324,6 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } - // Belt printer: parse pre-slice axis remap from header comments. - { - auto trim = [](const std::string &s) -> std::string { - size_t start = s.find_first_not_of(" \t\r\n"); - size_t end = s.find_last_not_of(" \t\r\n"); - return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); - }; - // Pre-slice axis remap - auto parse_remap_axis = [](const std::string &s) -> RemapAxis { - if (s == "pos_x") return RemapAxis::PosX; - if (s == "pos_y") return RemapAxis::PosY; - if (s == "pos_z") return RemapAxis::PosZ; - if (s == "neg_x") return RemapAxis::NegX; - if (s == "neg_y") return RemapAxis::NegY; - if (s == "neg_z") return RemapAxis::NegZ; - if (s == "rev_x") return RemapAxis::RevX; - if (s == "rev_y") return RemapAxis::RevY; - if (s == "rev_z") return RemapAxis::RevZ; - return RemapAxis::PosX; - }; - if (boost::starts_with(comment, " preslice_remap_x = ")) { - m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(20)))); return; - } - if (boost::starts_with(comment, " preslice_remap_y = ")) { - m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(20)))); return; - } - if (boost::starts_with(comment, " preslice_remap_z = ")) { - m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(20)))); return; - } - } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 72e315c927..9b3571223c 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -304,9 +304,6 @@ class Print; // machine frame and should not be compared against `printable_height` // (which lives in the build-volume frame). bool machine_frame_transform_active{ false }; - RemapAxis preslice_remap_x{ RemapAxis::PosX }; - RemapAxis preslice_remap_y{ RemapAxis::PosY }; - RemapAxis preslice_remap_z{ RemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -405,9 +402,6 @@ class Print; belt_tilt_angle = std::forward(other).belt_tilt_angle; belt_z_origin = std::forward(other).belt_z_origin; machine_frame_transform_active = std::forward(other).machine_frame_transform_active; - preslice_remap_x = std::forward(other).preslice_remap_x; - preslice_remap_y = std::forward(other).preslice_remap_y; - preslice_remap_z = std::forward(other).preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = std::forward(other).time; #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index bc25c448dd..4b2b28d4a8 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1560,13 +1560,10 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", "printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_infinite_y", - "belt_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global", - "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", - "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", + "belt_slice_rotation", "belt_slice_rotation_angle", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "belt_frame_tilt_decouple", "belt_frame_tilt_angle", - "belt_preslice_global", - "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", - "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "belt_support_floor_offset", "enable_belt_purge_tower", "gcode_flavor", "gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 9dc9ff3cd3..9f7adaf86d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -176,8 +176,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_remap_z", // Machine-frame transform (derived from belt tilt; only affects G-code output). "belt_frame_tilt_decouple", "belt_frame_tilt_angle", - "gcode_back_transform", - "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", // Only inflates the GUI bed volume, like printable_area. "belt_printer_infinite_y", //BBS @@ -388,18 +386,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Belt printer transform options change the mesh geometry before slicing. || opt_key == "belt_printer" || opt_key == "belt_slice_rotation" - || opt_key == "belt_slice_rotation_angle" - || opt_key == "belt_slice_rotation_global" - || opt_key == "belt_preslice_global" - || opt_key == "preslice_remap_global" - || opt_key == "preslice_remap_x" - || opt_key == "preslice_remap_y" - || opt_key == "preslice_remap_z") { + || opt_key == "belt_slice_rotation_angle") { osteps.emplace_back(posSlice); } else if ( - opt_key == "belt_support_floor_offset" - || opt_key == "belt_support_floor_mode" - || opt_key == "belt_support_z_offset_mode") { + opt_key == "belt_support_floor_offset") { osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" @@ -1914,14 +1904,6 @@ StringObjectException Print::validate(std::vector *warnin "which is the edge it is meant to anchor. Set the gap to 0 when using leading " "brim length."), "brim_object_gap", object->model_object()); - - // Unconditional: this suppresses the WHOLE belt brim, not just the apron, so a - // user asking for any brim at all needs to be told they are getting none. - if (! object->belt_brim_instances_compatible()) - warn(L("This object's copies are spaced along the belt, so they would each need " - "their own brim and none is generated. Print them as separate objects, or " - "arrange the copies side by side across the belt."), - "brim_type", object->model_object()); } if (this->has_belt_brim() && m_objects.size() > 1) warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does " @@ -2107,9 +2089,7 @@ StringObjectException Print::validate(std::vector *warnin bool have_height = false; if (belt_printer) { - double raw_z = print_object.model_object()->max_z(); - if (BeltTransformPipeline::has_preslice_remap(this->config())) - raw_z = BeltTransformPipeline::remap_bbox(*print_object.model_object(), this->config()).size().z(); + const double raw_z = print_object.model_object()->max_z(); effective_max_z = raw_z; have_height = raw_z > 0; } else { @@ -3069,11 +3049,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::set re_slicing_objects; // Belt global modes couple each object's bed position into its layer Z values, // so sharing layers between "identical" objects is wrong. - bool belt_no_share = m_config.belt_printer.value && - ((m_config.belt_slice_rotation_global.value - && m_config.belt_slice_rotation.value != BeltRotationAxis::None) - || m_config.preslice_remap_global.value - || m_config.belt_preslice_global.value); + bool belt_no_share = m_config.belt_printer.value; if (!use_cache) { for (int index = 0; index < object_count; index++) { @@ -3233,7 +3209,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) for (int i = range.begin(); i < range.end(); i++) { PrintObject* obj = m_objects[i]; if (need_slicing_objects.count(obj) != 0) { - obj->generate_support_material(); + // The belt brim follows sequentially below. + obj->generate_support_material(false); } else { if (obj->set_started(posSupportMaterial)) @@ -3242,6 +3219,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } } ); + // The belt brim keeps clear of every object's layers and support layers, + // so it runs once no support step is rebuilding them any more. + for (PrintObject *obj : m_objects) + obj->generate_belt_brim(); if (m_pipeline_plugin_active) for (size_t i = 0; i < m_objects.size(); ++i) diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 425912b9c2..f60f6b4e19 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -231,11 +231,10 @@ 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) +// The model's raw bounding box, in the frame the belt floor parameters refer to. +inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig & /*config*/) { - return BeltTransformPipeline::remap_bbox(model_object, config); + return model_object.raw_bounding_box(); } // Single instance of a PrintObject. @@ -440,7 +439,6 @@ public: unsigned int belt_brim_filament() const; // False when this object's instances sit at different points ALONG the belt, which // would need a separate set of bands each. Public so validate() can explain it. - bool belt_brim_instances_compatible() const; const std::vector& belt_brim_by_layer() const { return m_belt_brim_by_layer; } const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } @@ -584,7 +582,13 @@ private: void ironing(); bool need_z_contouring() const; void contour_z(); - void generate_support_material(); + // with_belt_brim = false leaves the belt brim to generate_belt_brim(), for a + // caller that runs the support step of several objects in parallel. + void generate_support_material(bool with_belt_brim = true); + // The belt brim keeps clear of every object's layers and support layers, so + // it has to run after all support steps finished. A no-op unless a support + // step left it pending. + void generate_belt_brim(); void estimate_curled_extrusions(); void simplify_extrusion_path(); @@ -713,6 +717,8 @@ private: // Belt printer: global Z offset applied to this object's layers for shear positioning. double m_belt_global_z_offset { 0.0 }; + // generate_support_material(false) finished and generate_belt_brim() has not run yet. + bool m_belt_brim_pending { false }; // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. double m_belt_min_z { 0.0 }; // Belt printer: XY correction from global pre-slice mode, applied to G-code origin. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 94725118c7..39d5716b08 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1842,14 +1842,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ for (ModelObject *model_object : m_model.objects) { ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); // Orca: Updated for XYZ filament shrink compensation - // Belt global mode: force each instance into its own PrintObject - // so each gets independent layer Z values. - bool belt_force_separate = m_config.belt_printer.value && ( - (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))); + // Belt printers: force each instance into its own PrintObject so each + // gets independent layer Z values (its bed position is folded into them). + bool belt_force_separate = m_config.belt_printer.value; model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); @@ -1938,13 +1933,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // Belt printer global mode: when any object's instances shifted, // recompute m_belt_global_z_offset for ALL objects (it depends on // min_shift across all objects, so one move affects everyone). - if (belt_instances_shifted - && m_config.belt_printer.value - && ((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)))) { + if (belt_instances_shifted && m_config.belt_printer.value) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ee23a88940..8e5b2e2b0f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -398,31 +398,6 @@ static t_config_enum_values s_keys_map_RemapAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) -static t_config_enum_values s_keys_map_BeltSupportFloorMode { - { "none", int(BeltSupportFloorMode::None) }, - { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, -}; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode) - -static t_config_enum_values s_keys_map_BeltSupportZOffsetMode { - { "none", int(BeltSupportZOffsetMode::None) }, - { "unconditional", int(BeltSupportZOffsetMode::Unconditional) }, - { "raft_only", int(BeltSupportZOffsetMode::RaftOnly) }, -}; -CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportZOffsetMode) - -static t_config_enum_values s_keys_map_FirstLayerPlaneMode { - { "auto", int(FirstLayerPlaneMode::Auto) }, - { "xy", int(FirstLayerPlaneMode::XY) }, - { "yz", int(FirstLayerPlaneMode::YZ) }, - { "xz", int(FirstLayerPlaneMode::XZ) }, - { "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) - static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -7409,7 +7384,7 @@ void PrintConfigDef::init_fff_params() 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->mode = comDevelop; def->set_default_value(new ConfigOptionEnum(BeltRotationAxis::X)); def = this->add("belt_slice_rotation_angle", coFloat); @@ -7425,16 +7400,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(45.)); - 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. " - "Defaults to on because virtually all rotation-mode printers need it."); - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(true)); - def = this->add("belt_frame_tilt_decouple", coBool); def->label = L("Decouple machine-frame tilt"); def->category = L("Printable space"); @@ -7458,7 +7423,7 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(45.)); - // G-code axis remap with sign + // G-code axis remap with sign. Each field is its own row in the settings tab. auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis, ConfigOptionMode mode = comSimple) { auto def = this->add(key, coEnum); @@ -7472,115 +7437,14 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; - add_belt_remap("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.", - RemapAxis::PosX, comDevelop); - add_belt_remap("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.", - RemapAxis::PosY, comDevelop); - add_belt_remap("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.", - RemapAxis::PosZ, comDevelop); - - def = this->add("preslice_remap_global", coBool); - def->label = L("Global"); - def->category = L("Printable space"); - def->tooltip = L("When enabled, the pre-slice axis remap accounts for each object's bed position. " - "Without this, the remap is applied locally around each object's center, so " - "objects at different positions don't get a position-dependent contribution. " - "Mirrors the 'Global' option on the belt slicing rotation, but for the remap."); - def->mode = comDevelop; - def->set_default_value(new ConfigOptionBool(false)); - - add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comDevelop); - add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comDevelop); - add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop); + add_belt_remap("gcode_remap_x", "G-code remap X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comDevelop); + add_belt_remap("gcode_remap_y", "G-code remap Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comDevelop); + add_belt_remap("gcode_remap_z", "G-code remap Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop); // The machine-frame G-code transform (shear + scale) is no longer configured // by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis // + angle, or belt_frame_tilt_angle when decoupled) in MachineFrameTransform. - def = this->add("gcode_back_transform", coBool); - def->label = L("G-code back-transform"); - def->category = L("Printable space"); - def->tooltip = L("Undo the pre-slice mesh transform before applying the G-code axis remap " - "and machine-frame shear/scale. Required for the standard belt-printer " - "rotation pipeline."); - def->mode = comExpert; - def->set_default_value(new ConfigOptionBool(true)); - - def = this->add("belt_preslice_global", coBool); - def->label = L("Global mesh transforms"); - def->category = L("Printable space"); - def->tooltip = L("When enabled, pre-slice belt transforms (remap, shear, scale) account for " - "each object's bed position, producing correct machine coordinates without " - "relying on origin snap. Each instance gets its own PrintObject."); - def->mode = comExpert; - def->set_default_value(new ConfigOptionBool(true)); - - // 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 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"); - def->tooltip = L("Selects the reference plane used to decide which extrusions get " - "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 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_affine"}; - def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt affine plane")}; - def->mode = comExpert; - // Auto, not BeltAffine: BeltAffine activates the plane evaluator unconditionally, so on a - // non-belt printer on_first_layer(point) stopped agreeing with the legacy slicing-layer-0 - // test and first-layer speeds were skipped (brim printed at the volumetric fallback rather - // than initial_layer_speed). Auto resolves to BeltAffine only when belt_printer is set with - // a non-zero slicing rotation, and to XY (evaluator inactive, legacy behaviour) otherwise -- - // which is what this option's own description promises. - def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::Auto)); - - def = this->add("first_layer_plane_offset", coFloat); - def->label = L("Belt plane offset"); - def->category = L("Printable space"); - def->tooltip = L("Shifts the first-layer plane along its normal (mm). For axis-aligned " - "planes this is just a coordinate shift. Positive values move the plane " - "away from the belt surface (deeper into the model)."); - def->sidetext = L("mm"); - def->min = -1000; - def->max = 1000; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(0.0)); - - def = this->add("first_layer_plane_thickness", coFloat); - def->label = L("Plane band thickness"); - def->category = L("Printable space"); - def->tooltip = L("Thickness of one 'band' relative to the first-layer plane, in mm. " - "Used as the unit by which 'No cooling for the first N layers' (and " - "similar layer-count thresholds) is multiplied when the first-layer " - "plane is active. -1 means use initial_layer_print_height."); - def->sidetext = L("mm"); - def->min = -1; - def->max = 100; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(-1.0)); - // Belt support floor debug controls def = this->add("belt_support_floor_offset", coFloat); def->label = L("Support Floor Z offset"); @@ -7592,32 +7456,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); - { - auto def = this->add("belt_support_floor_mode", coEnum); - def->label = L("Floor mode"); - def->category = L("Printable space"); - def->tooltip = L("Controls belt floor awareness for supports. 'None' disables belt floor logic. " - "'Generator only' stops support generation at the belt floor plane."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"none", "generator_only"}; - def->enum_labels = {L("None"), L("Generator only")}; - def->mode = comDevelop; - def->set_default_value(new ConfigOptionEnum(BeltSupportFloorMode::GeneratorOnly)); - } - - { - auto def = this->add("belt_support_z_offset_mode", coEnum); - def->label = L("Z offset mode"); - def->category = L("Printable space"); - def->tooltip = L("How global Z offset is applied to support layers for belt printers with global shear. " - "'None' = don't offset. 'Unconditional' = offset all layers. 'Raft only' = only offset raft layers."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = {"none", "unconditional", "raft_only"}; - def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; - def->mode = comExpert; - def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); - } - def = this->add("enable_belt_purge_tower", coBool); def->label = L("Enable belt purge tower"); def->category = L("Multimaterial"); @@ -9551,9 +9389,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va //BBS: handle legacy options if (opt_key == "curr_bed_type" && value == "SuperTack Plate") { value = "Supertack Plate"; - } else if (opt_key == "belt_support_floor_mode" && (value == "clip_only" || value == "both")) { - // Never implemented; both behaved like "none". - value = "none"; } else if (opt_key == "enable_wipe_tower") { opt_key = "enable_prime_tower"; } else if (opt_key == "wipe_tower_width") { @@ -9793,6 +9628,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va "smooth_coefficient", "overhang_totally_speed", "silent_mode", "overhang_speed_classic", "anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order + // Belt printer keys retired before the first release: the global-mode and + // back-transform switches are presumed on, and the pre-slice axis remap, the + // support Z offset mode, the support floor mode (always on) and the first-layer + // plane evaluator were removed. + "belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", + "belt_support_z_offset_mode", "first_layer_plane", "first_layer_plane_offset", + "belt_preslice_global", "gcode_back_transform", "belt_support_floor_mode", "first_layer_plane_thickness", }; if (ignore.find(opt_key) != ignore.end()) { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1a9e3ea075..05ee97a806 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -294,40 +294,6 @@ enum class RemapAxis RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos }; -enum class BeltSupportFloorMode -{ - None, // No belt floor awareness - GeneratorOnly, // Only in tree support drop_nodes/contact_points -}; - -enum class BeltSupportZOffsetMode -{ - None, // Don't apply global_z_offset to support layers - Unconditional, // Apply to all support layers - RaftOnly, // Only apply to raft layers -}; - -// Selects which plane the slicer treats as the "first layer plane" — the -// reference surface used to decide which extrusions get first-layer settings -// (no fan, slow speed, initial-layer accel/jerk, deferred temperature drop). -// -// Auto resolves to: -// - XY (inactive, legacy behavior) for non-belt printers and for belt -// 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. -enum class FirstLayerPlaneMode -{ - Auto = 0, - XY, - YZ, - XZ, - BeltAffine, // formerly BeltShear; renamed to reflect rotation support -}; - enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -774,9 +740,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) -CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FirstLayerPlaneMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1898,26 +1861,14 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // g-code back-transform inverts the rotation before the machine-frame stage. ((ConfigOptionEnum, belt_slice_rotation)) ((ConfigOptionFloat, belt_slice_rotation_angle)) - ((ConfigOptionBool, belt_slice_rotation_global)) // Expert override: decouple the machine-frame tilt angle from the pre-slice // rotation angle. When disabled, the machine frame uses belt_slice_rotation_angle. ((ConfigOptionBool, belt_frame_tilt_decouple)) ((ConfigOptionFloat, belt_frame_tilt_angle)) - ((ConfigOptionEnum, preslice_remap_x)) - ((ConfigOptionEnum, preslice_remap_y)) - ((ConfigOptionEnum, preslice_remap_z)) - ((ConfigOptionBool, preslice_remap_global)) ((ConfigOptionEnum, gcode_remap_x)) ((ConfigOptionEnum, gcode_remap_y)) ((ConfigOptionEnum, gcode_remap_z)) - ((ConfigOptionBool, gcode_back_transform)) - ((ConfigOptionBool, belt_preslice_global)) - ((ConfigOptionEnum, first_layer_plane)) - ((ConfigOptionFloat, first_layer_plane_offset)) - ((ConfigOptionFloat, first_layer_plane_thickness)) ((ConfigOptionFloat, belt_support_floor_offset)) - ((ConfigOptionEnum, belt_support_floor_mode)) - ((ConfigOptionEnum, belt_support_z_offset_mode)) // Width (machine X, across the belt) of the auto-generated belt purge prism. ((ConfigOptionFloat, belt_purge_tower_width)) // Belt-printer-only "type" of purge tower: enables the auto-generated belt diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 7a46f354fc..4f46921c3f 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -984,7 +984,7 @@ void PrintObject::detect_overhangs_for_lift() } } -void PrintObject::generate_support_material() +void PrintObject::generate_support_material(bool with_belt_brim) { if (this->set_started(posSupportMaterial)) { this->clear_support_layers(); @@ -1032,12 +1032,25 @@ void PrintObject::generate_support_material() // those must exist before ToolOrdering is built at psWipeTower - one step // ahead of psSkirtBrim. The brim options already invalidate // posSupportMaterial, so this needs no extra invalidation edges. - make_belt_brim(*this); - m_print->throw_if_canceled(); + m_belt_brim_pending = true; + if (with_belt_brim) + this->generate_belt_brim(); this->set_done(posSupportMaterial); } } +void PrintObject::generate_belt_brim() +{ + if (! m_belt_brim_pending) + return; + // belt_brim_obstacles() looks up the layers and support layers of every object + // on the plate by print_z. Another object's support step rebuilds those (and + // temporarily shifts its layer Z values), so this must not overlap with it. + make_belt_brim(*this); + m_print->throw_if_canceled(); + m_belt_brim_pending = false; +} + void PrintObject::estimate_curled_extrusions() { if (this->set_started(posEstimateCurledExtrusions)) { @@ -1245,8 +1258,6 @@ bool PrintObject::has_belt_brim() const // this keeps it brimless whatever its config says, so a brim on the parts never blocks purging. if (m_config.belt_purge_tower_object.value) return false; - if (! this->belt_brim_instances_compatible()) - return false; if (m_config.brim_type == btNoBrim) return false; // An inner-only brim has no leading/extra geometry: leading_brim_length and @@ -1280,30 +1291,6 @@ unsigned int PrintObject::belt_brim_filament() const return brim_filament == 0 ? 1u : brim_filament; } -bool PrintObject::belt_brim_instances_compatible() const -{ - // One set of bands is shared by every instance of this object, so they must all sit at - // the same height on the belt. Moving an instance ALONG the belt axis changes its - // physical belt-floor Z and would put its brim at the wrong height; moving it ACROSS - // the belt does not, so side-by-side copies are fine. - // - // belt_force_separate() in PrintApply.cpp already gives one instance per PrintObject - // whenever a global belt flag is set, which the shipped belt profiles do - this only - // matters for configurations that do not. - if (m_instances.size() <= 1) - return true; - // From the config, not m_slicing_params: this runs while those can be stale. A tilt - // about Y runs the belt along X, any other tilt along Y (see compute_belt_height_and_floor). - const int axis = m_print->config().belt_slice_rotation.value == BeltRotationAxis::Y ? 0 : 1; - const Point &ref = m_instances.front().shift; - for (const PrintInstance &inst : m_instances) { - const coord_t along = axis == 0 ? inst.shift.x() - ref.x() : inst.shift.y() - ref.y(); - if (std::abs(along) > SCALED_EPSILON) - return false; - } - return true; -} - void PrintObject::clear_belt_brim() { m_belt_brim_by_layer.clear(); @@ -4154,9 +4141,17 @@ void PrintObject::update_slicing_parameters() BeltTransformPipeline::BeltFloorParams belt_floor; const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { - BoundingBoxf3 bb = BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg); - if (BeltTransformPipeline::has_preslice_remap(pcfg)) - object_height = bb.size().z(); + // The box of the mesh in the frame it is sliced in: XY centred and Z as + // placed on the bed (trafo_centered()). raw_bounding_box() has the + // instance's Z offset removed, and the belt floor is not invariant to a + // Z shift (a point's z and the floor under it move in opposite + // directions under the rotation), so an offset box under-estimates the + // height by twice the shift and the layers stop part way up the object. + BoundingBoxf3 bb; + const Transform3d trafo = this->trafo_centered(); + for (const ModelVolume *v : this->model_object()->volumes) + if (v->is_model_part()) + bb.merge(v->mesh().transformed_bounding_box(trafo * v->get_matrix())); auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height); object_height = hr.object_height; belt_floor = hr.floor_params; @@ -4217,9 +4212,13 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); if (print_config.belt_printer.value) { - bb = BeltTransformPipeline::remap_bbox(model_object, print_config); - if (BeltTransformPipeline::has_preslice_remap(print_config)) - object_max_z = (float)bb.size().z(); + // Z as placed on the bed, XY around the instance origin: the belt floor + // depends on where the box sits in Z (see update_slicing_parameters()). + if (! model_object.instances.empty()) { + bb = model_object.instance_bounding_box(0, false); + const Vec3d off = model_object.instances.front()->get_offset(); + bb.translate(-off.x(), -off.y(), 0.); + } auto hr = BeltTransformPipeline::compute_belt_height_and_floor(print_config, bb, object_max_z); object_max_z = (float)hr.object_height; belt_floor = hr.floor_params; @@ -4746,11 +4745,41 @@ void PrintObject::_generate_support_material() tree_support.generate(); } else { - PrintObjectSupportMaterial support_material(this, m_slicing_params); - support_material.generate(*this); + // The normal generator anchors its layer grid at the slicing frame origin + // (SlicingParameters: first layer at first_print_layer_height, raft at + // z = 0), so it has to see the object layers in that frame. On a belt the + // object layers carry the global Z offset (PrintObject::slice()), which is + // negative for the leading half of the belt: a top contact below z = 0 + // then turns the intermediate-layer count negative and the generator + // allocates layers until memory runs out. Lift the offset off the object + // layers and the belt floor for the duration of the run and put it back + // on everything, including the new support layers, afterwards (organic + // tree support is shifted the same way below). + const double global_z = m_belt_global_z_offset; + const bool unshift = std::abs(global_z) > EPSILON; + auto shift_object_frame = [this, global_z](double sign) { + for (Layer *layer : m_layers) + layer->print_z += sign * global_z; + m_slicing_params.belt_floor_z_shift += sign * global_z; + }; + if (unshift) + shift_object_frame(-1.); + try { + PrintObjectSupportMaterial support_material(this, m_slicing_params); + support_material.generate(*this); + } catch (...) { + if (unshift) + shift_object_frame(1.); + throw; + } + if (unshift) { + shift_object_frame(1.); + for (SupportLayer *sl : m_support_layers) + sl->print_z += global_z; + } } // Global Z offset for support layers: - // - Normal support: layers already inherit global_z_offset from object layers. + // - Normal support: generated in the object frame above and shifted afterwards. // - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads // from globally-offset object layers, so support layers already have it. // - Organic tree support: generate_tree_support_3D() computes its own Z values diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index b384a28ef3..5f70887c9c 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -343,8 +343,7 @@ static std::vector> slices_to_regions( // 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_rotation(print_config) - || BeltTransformPipeline::has_preslice_remap(print_config))); + BeltTransformPipeline::has_rotation(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 @@ -926,14 +925,9 @@ void PrintObject::slice() // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { 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(); - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; + // The belt surface is at Z=0 in centered slicer space and bb.min.z() is + // already folded into m_belt_min_z. + m_slicing_params.belt_floor_z_shift = z_shift_val; } int firstLayerReplacedBy = 0; @@ -982,8 +976,6 @@ void PrintObject::slice() const auto &pcfg = this->print()->config(); BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value << " belt_slice_rotation=" << int(pcfg.belt_slice_rotation.value) - << " belt_slice_rotation_global=" << pcfg.belt_slice_rotation_global.value - << " belt_preslice_global=" << pcfg.belt_preslice_global.value << " object=" << this->model_object()->name; if (pcfg.belt_printer.value) { @@ -1003,8 +995,7 @@ void PrintObject::slice() // couples slicer_z back into both machine_y and machine_z. Compensating // layer.print_z by belt_z_shift here makes the back-transform produce // correct machine-frame coordinates whether or not a global mode is active. - double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) - ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + const double belt_surface_z = 0.; // the belt surface is Z=0 in centered slicer space // The compensation must mirror the Z-shift actually applied, which // is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE // slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or @@ -1034,9 +1025,11 @@ void PrintObject::slice() global_z_offset += centering_z_corr; } - if (pcfg.belt_preslice_global.value) { + { // Global pre-slice mode: compute full correction c = (T.linear() - I) * d - // where T is the belt forward transform and d is the bed position. + // where T is the belt forward transform and d is the bed position, so + // objects at different bed positions print at different machine Z values + // along the inclined belt. Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; @@ -1046,31 +1039,6 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" << c.x() << ", " << c.y() << ", " << c.z() << ")" << " belt_z_shift=" << belt_z_shift << " (m_belt_min_z=" << m_belt_min_z << ")"; - } else { - // 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 - // object at Y=50 prints at Z=50. - if (pcfg.preslice_remap_global.value - && BeltTransformPipeline::has_preslice_remap(pcfg)) { - Transform3d R = BeltTransformPipeline::build_preslice_remap(pcfg); - Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); - Vec3d remap_correction = R.linear() * d - d; - global_z_offset += remap_correction.z(); - } } BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index f7d173bb60..481213fe48 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -320,7 +320,13 @@ SupportGeneratorLayersPtr generate_raft_base( // How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed. const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON)); - const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine); + // On a belt the first support layer is the leading tip of the support, a sliver + // where the belt crosses the layer, not a flange on a flat bed: inflating it + // puts lines in the air ahead of the belt crossing (and into the belt behind + // it). The belt brim takes the adhesion role instead. + const bool belt_floor_active = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON; + const float inflate_factor_1st_layer = belt_floor_active ? 0.f : + std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine); SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front(); SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front(); SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front(); diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index d8b712b55e..17dc858262 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -2768,8 +2768,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ //const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25; const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow); const bool buildplate_only = ! buildplate_covered.empty(); - const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -3311,8 +3310,6 @@ static void trim_support_layers_by_belt_floor( BeltFloorContext ctx; if (!ctx.init(slicing_params, print_config)) return; - if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) - return; tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), [&](const tbb::blocked_range &range) { diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index f3e322154a..33d379c2b5 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -117,58 +117,20 @@ TreeModelVolumes::TreeModelVolumes( m_increase_until_radius = config.increase_radius_until_radius; m_radius_0 = config.getRadius(0); m_raft_layers = config.raft_layers; - // Belt printer: add virtual belt raft layers below the object, matching - // the extra layers added in generate_support_areas() so both use the - // same layer indexing. - { - const auto &sp2 = print_object.slicing_parameters(); - const auto &pcfg2 = print_object.print()->config(); - 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(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) { - std::vector belt_layers; - belt_layers.reserve(num_extra); - for (int i = num_extra; i >= 1; --i) - belt_layers.push_back(sp2.first_object_layer_height - i * sp2.layer_height); - m_raft_layers.insert(m_raft_layers.begin(), belt_layers.begin(), belt_layers.end()); - } - } - } - // Belt floor: add belt surface polygons to anti_overhang so support is - // never generated inside the belt. + // Support blockers are consumed in the same index space as m_layer_outlines + // (object layer i lives at index num_raft_layers + i), but + // slice_support_blockers() returns them in object-layer space. Shift them. // - // This MUST run after m_raft_layers is final. m_anti_overhang is consumed - // in the same index space as m_layer_outlines -- object layer i lives at - // index num_raft_layers + i -- but slice_support_blockers() returns it in - // object-layer space. Without the shift below, every entry lands - // num_raft_layers too low: with the belt raft that is tens of layers, so - // the belt suppression is applied to the wrong layers entirely and the - // topmost object layers get none at all. + // The belt surface is deliberately NOT a blocker. A blocker is a collision, + // and a branch descending onto a collision slides off it: on a belt that + // walks the branch down the tilted surface, ahead of the part, until it + // reaches the bottom layer floating in mid-air. The belt is where branches + // END: organic_draw_branches() clips their slices with m_belt_floor and the + // first clipped slice is the contact. { const size_t num_raft = m_raft_layers.size(); - const size_t num_obj = print_object.layer_count(); if (num_raft > 0 && ! m_anti_overhang.empty()) - // Shift the support blockers into the same space. m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{}); - const auto &sp = print_object.slicing_parameters(); - const auto &pcfg = print_object.print()->config(); - BeltFloorContext ctx; - ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); - if (ctx.is_active() - && std::abs(print_object.belt_global_z_offset()) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - if (m_anti_overhang.size() < num_raft + num_obj) - m_anti_overhang.resize(num_raft + num_obj, Polygons{}); - for (size_t i = 0; i < num_obj; ++i) { - const double print_z = print_object.get_layer(i)->print_z - - print_object.belt_global_z_offset(); - append(m_anti_overhang[num_raft + i], ctx.surface_polygon(print_z)); - } - } } m_current_outline_idx = 0; @@ -192,8 +154,7 @@ TreeModelVolumes::TreeModelVolumes( const auto &pcfg2 = print_object.print()->config(); BeltFloorContext ctx; ctx.init_local(slicing_params, pcfg2, print_object.belt_global_z_offset()); - if (ctx.is_active() - && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + if (ctx.is_active()) { m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { // Object layers: local print_z (subtract global offset). if (layer_idx >= num_raft_layers) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 31b93292a7..f8b1ad1f26 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -716,6 +716,25 @@ double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const return ctx.floor_print_z(pos_slicing); } +bool TreeSupport::belt_node_landed(const Point &pos_slicing, double radius, double print_z) const +{ + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return false; + return print_z <= ctx.floor_print_z(pos_slicing) - std::abs(ctx.shear_factor()) * std::max(0., radius); +} + +bool TreeSupport::belt_polygon_landed(const ExPolygon &poly, double print_z) const +{ + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return false; + double min_floor = std::numeric_limits::max(); + for (const Point &pt : poly.contour.points) + min_floor = std::min(min_floor, ctx.floor_print_z(pt)); + return print_z <= min_floor; +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -1616,8 +1635,7 @@ void TreeSupport::generate_toolpaths() // reads as a stray brim/skirt. Gate those layer_id==0 special cases off when // the belt floor is active; false on non-belt printers so behavior is unchanged. BeltFloorContext belt_ctx; - const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config) - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly; + const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config); // generate tree support tool paths tbb::parallel_for( @@ -1898,133 +1916,6 @@ 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(). - { - BeltFloorContext ctx; - if (ctx.init(m_slicing_params, *m_print_config) - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - const auto &sp = m_slicing_params; - // 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; - } - } - } - // ORCA-Belt calibration: a counter-rotated calibration object - // stands on a support wedge that lies entirely below the object's - // first layer, where the tree pipeline has no layers at all — so - // no support content can exist yet. Seed the extension directly - // from the floating portion of the first layer (anything more - // than one layer height above the belt floor). For objects whose - // first layer rests on the belt the floating region is empty and - // behavior is unchanged. - double first_z = m_object->support_layer_count() > 0 ? m_object->get_support_layer(0)->print_z : 0.; - bool seeded = false; - if (source_areas.empty() && m_object_config->enable_support.value && !m_object->layers().empty()) { - // The layer grid may start with an empty ghost layer just below - // the object (grid rounding against the belt global Z offset) — - // anchor the seed to the first layer that has geometry. Object - // layer print_z and the floor plane are both in the globally - // offset frame here (belt_floor_z_shift was adjusted alongside - // the layer Z values in PrintObject::slice()). - const Layer *first_layer = nullptr; - for (const Layer *l : m_object->layers()) - if (!l->lslices_extrudable.empty()) { first_layer = l; break; } - if (first_layer != nullptr) { - ExPolygons floating = diff_ex(first_layer->lslices_extrudable, - ctx.surface_polygon(first_layer->bottom_z() - first_layer->height)); - if (!floating.empty()) { - source_areas = std::move(floating); - first_z = first_layer->bottom_z(); - seeded = true; - } - } - } - if (!source_areas.empty()) { - BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); - double from_extent = std::abs(bb.min(ctx.from_axis())); - double bb_min_z = std::abs(bb.min.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)); - if (seeded) { - // Seeded wedge: the depth is known exactly — down to the lowest - // belt-floor point under the floating footprint. The bbox - // heuristic above under-estimates it for meshes centered - // around their origin (every object loaded through the GUI). - double min_floor = first_z; - for (const ExPolygon &ep : source_areas) - for (const Point &pt : ep.contour.points) - min_floor = std::min(min_floor, ctx.floor_print_z(pt)); - extra_depth = std::min(std::max(0., first_z), first_z - min_floor + 2.); - } - int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); - // Seeded wedge: top layers become a dense support interface so the - // object's floating first layer bridges a roof, not sparse infill. - const int interface_layers = seeded ? std::max(0, m_object_config->support_interface_top_layers.value) : 0; - 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; - Polygons belt_surface = ctx.surface_polygon(print_z); - ExPolygons clipped = diff_ex(source_areas, belt_surface); - 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. - // Note: base areas only get infill when support_base_pattern - // is explicitly set (with the default pattern tree bases are - // walls-only) — the calibration flow sets rectilinear. - const bool roof = i <= interface_layers; - for (auto &expoly : sl->base_areas) { - sl->area_groups.emplace_back(&expoly, roof ? SupportLayer::RoofType : SupportLayer::BaseType, 0); - if (roof) - sl->area_groups.back().interface_id = i & 1; - } - 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()); - for (size_t i = 0; i < sl_vec.size(); ++i) - sl_vec[i]->set_id(i); - } - } - } - } - profiler.stage_start(STAGE_GENERATE_TOOLPATHS); m_object->print()->set_status(70, _u8L("Generating support")); generate_toolpaths(); @@ -2288,8 +2179,7 @@ void TreeSupport::draw_circles() // the Z=0 belt plane around the support footprint. false on non-belt printers, // so behavior there is unchanged. BeltFloorContext belt_ctx; - const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config) - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly; + const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config); if (m_object->support_layer_count() <= m_raft_layers) return; @@ -2484,8 +2374,7 @@ void TreeSupport::draw_circles() // is 0 so init() and init_local() coincide — this is a no-op there. { BeltFloorContext ctx; - if (ctx.init(m_slicing_params, *m_print_config) - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + if (ctx.init(m_slicing_params, *m_print_config)) { Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z); base_areas = diff_ex(base_areas, belt_surface); roof_areas = diff_ex(roof_areas, belt_surface); @@ -2962,9 +2851,7 @@ void TreeSupport::drop_nodes() const size_t tip_layers = base_radius / layer_height; //The number of layers to be shrinking the circle to create a tip. This produces a 45 degree angle. const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution; const bool support_on_buildplate_only = config.support_on_build_plate_only.value; - const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; - const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON - && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON; const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config); SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor; float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm @@ -3200,10 +3087,12 @@ void TreeSupport::drop_nodes() node_parent = p_node->parent ? p_node : neighbour; // Make sure the next pass doesn't drop down either of these (since that already happened). node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node); - // Belt floor: don't drop merged node below belt surface. - // Treat as object-surface termination (not buildplate) so - // the node gets floor/interface areas instead of base pads. - if (has_belt_floor && print_z_next <= belt_floor_print_z(next_position)) { + // Belt floor: a merged node ends once its whole circle is in the belt + // (its slices are clipped to the belt plane in draw_circles(), so it + // tapers to a tip on the belt). Treat as object-surface termination + // (not buildplate) so the node gets floor/interface areas instead of + // base pads. + if (has_belt_floor && belt_node_landed(next_position, std::max(node.radius, neighbour->radius), print_z_next)) { std::scoped_lock lock(m_ts_data->m_mutex); node_parent->to_buildplate = false; neighbour->valid = false; @@ -3287,9 +3176,9 @@ void TreeSupport::drop_nodes() ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next)); for(auto& overhang:overhangs_next) { Point next_pt = overhang.contour.centroid(); - // Belt floor: don't drop polygon node below belt surface. + // Belt floor: a polygon node ends once all of it is in the belt. // Treat as object-surface termination (not buildplate). - if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) { + if (has_belt_floor && belt_polygon_landed(overhang, print_z_next)) { p_node->to_buildplate = false; continue; } @@ -3440,9 +3329,11 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } - // Belt floor: don't drop regular node below belt surface. + // Belt floor: a node ends once its whole circle is in the belt; until + // then it keeps dropping and draw_circles() clips each layer's circle + // to the belt plane, so the branch tapers to a tip on the belt. // Treat as object-surface termination (not buildplate). - if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) { + if (has_belt_floor && belt_node_landed(next_layer_vertex, node.radius, print_z_next)) { p_node->to_buildplate = false; return; // from parallel_for_each lambda } @@ -3771,9 +3662,7 @@ void TreeSupport::generate_contact_points() const coordf_t max_bridge_length = scale_(config.max_bridge_length.value); coord_t radius_scaled = scale_(base_radius); bool on_buildplate_only = m_object_config->support_on_build_plate_only.value; - const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; - const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON - && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON; //First generate grid points to cover the entire area of the print. BoundingBox bounding_box = m_object->bounding_box(); @@ -4044,8 +3933,7 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance BeltFloorContext ctx; double local_print_z = layer->print_z - object.belt_global_z_offset(); if (ctx.init_local(object.slicing_parameters(), object.print()->config(), - object.belt_global_z_offset()) - && object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + object.belt_global_z_offset())) { Polygons belt_surface = ctx.surface_polygon(local_print_z); for (auto &p : belt_surface) outline.emplace_back(ExPolygon(p)); diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index d0855e6cb6..c6acd806e0 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -463,6 +463,13 @@ private: // Belt printer: compute the belt floor print_z at a given XY position (in slicing coords). // Returns -infinity if belt floor is not active. double belt_floor_print_z(const Point &pos_slicing) const; + // Whether a node's whole circle (radius in mm) sits at or below the belt at + // print_z. The belt is a tilted plane, so the circle's leading edge crosses it + // |shear| * radius lower than its centre; stopping a node when its centre crosses + // would leave that edge floating a radius above the belt. + bool belt_node_landed(const Point &pos_slicing, double radius, double print_z) const; + // The same for a polygon: every point of it is at or below the belt. + bool belt_polygon_landed(const ExPolygon &poly, double print_z) const; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 189e855ed9..dc284ba94f 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3462,37 +3462,6 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed. //FIXME this is a copy // Contains config settings to avoid loading them in every function. This was done to improve readability of the code. - // Belt printer: add virtual "belt raft" layers below the object so - // organic branches can extend below the model's first layer and - // terminate at the belt surface instead of creating a flat base at Z=0. - { - PrintObject &po = *print.get_object(processing.second.front()); - const auto &sp = po.slicing_parameters(); - const auto &pcfg = po.print()->config(); - BeltFloorContext ctx; - ctx.init_local(sp, pcfg, po.belt_global_z_offset()); - if (ctx.is_active() && std::abs(po.belt_global_z_offset()) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - // z_shift_local is the belt surface height at Y=0 in local coords. - // Extend below the belt so the base expansion and build-plate - // 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(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) { - // Insert belt raft layers at the front, from lowest Z to highest. - std::vector belt_layers; - belt_layers.reserve(num_extra); - for (int i = num_extra; i >= 1; --i) - belt_layers.push_back(sp.first_object_layer_height - i * sp.layer_height); - // Prepend to existing raft_layers (if any). - auto &rl = processing.first.raft_layers; - rl.insert(rl.begin(), belt_layers.begin(), belt_layers.end()); - } - } - } const TreeSupportSettings &config = processing.first; BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes."; auto t_start = std::chrono::high_resolution_clock::now(); @@ -3687,8 +3656,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons const auto &pcfg = print_object.print()->config(); BeltFloorContext ctx; ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); - if (ctx.is_active() - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + if (ctx.is_active()) { tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { if (!layer || layer->polygons.empty()) return; @@ -3984,13 +3952,6 @@ void organic_draw_branches( const double tiny_area = tiny_area_threshold(); //FIXME parallelize? for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { - // ORCA: safety offset when trimming collision/bed to improve robustness. - slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); - slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes); - // Belt floor: clip branch slices against the belt surface plane. - LayerIndex belt_idx = layer_begin + i; - if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty()) - slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]); remove_small(slices[i], tiny_area); } diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index b28f9c5d1c..d7fd871f27 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -36,6 +36,7 @@ #include #include "libslic3r/Preset.hpp" #include "libslic3r/Config.hpp" +#include #if BOOST_VERSION >= 107800 #include @@ -766,25 +767,25 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform m_gravity_arrow.reset(); return; } - const Vec3d gravity_dir = -up_dir; - // Build the arrow model (same dimensions as the axis arrows) - if (!m_gravity_arrow.is_initialized()) { - const float stem_length = Axes::DefaultStemLength; - const float tip_radius = Axes::DefaultTipRadius; - const float tip_length = Axes::DefaultTipLength; - const float stem_radius = stem_length / 75.f; // same ratio as axis cylinders - m_gravity_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + // A plain line along the tilted "up" direction -- the way the layers lean, i.e. + // the gantry -- drawn like the bed axes (no tip: the other direction is not + // possible) and shorter than them, so it reads as a hint inside the YZ corner. + const float length = 0.6f * m_axes.get_total_length(); + if (!m_gravity_arrow.is_initialized() || m_gravity_arrow_length != length) { + m_gravity_arrow.reset(); + m_gravity_arrow.init_from(smooth_cylinder(16, /*Radius*/ length / 75.f, length)); + m_gravity_arrow_length = length; } - // The arrow model points along +Z by default. Compute rotation to align with gravity_dir. - // Rotation axis = cross(+Z, gravity_dir), angle = acos(dot(+Z, gravity_dir)) + // The cylinder model points along +Z. Compute the rotation that aligns it with + // up_dir: rotation axis = cross(+Z, up_dir), angle = acos(dot(+Z, up_dir)). Vec3d from = Vec3d::UnitZ(); - Vec3d to = gravity_dir; + Vec3d to = up_dir; double dot = from.dot(to); Transform3d rot = Transform3d::Identity(); if (dot < -0.9999) { - // Nearly opposite — rotate 180° around X + // Nearly opposite -- rotate 180 degrees around X rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; } else if (dot < 0.9999) { Vec3d axis = from.cross(to).normalized(); @@ -800,7 +801,7 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d model_matrix = rot; + Transform3d model_matrix = Eigen::Translation3d(m_axes.get_origin()) * rot; shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 497617a2e3..eeabe7da82 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -116,6 +116,7 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; + float m_gravity_arrow_length{ 0.f }; Axes m_axes; float m_scale_factor{ 1.0f }; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b03e6b5f03..0af44aebf2 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1318,13 +1318,11 @@ std::vector GCodeViewer::get_plater_extruder() // Belt printers: compute the full machine->model back-transform from the print // config, so the "designed" (upright) G-code preview maps each toolpath vertex // back to Cartesian space. The G-code forward pipeline is (BeltKinematics:: -// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if -// gcode_back_transform (write already un-rotated to Cartesian) else BeltForward( -// model). So the inverse is: -// model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1 -// All parts are config-driven affines -> handles any rotation/shear/scale/axis- -// remap combination. (origin-snap is a per-instance translation that only shifts -// position, not orientation, so it is intentionally omitted.) +// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X the model +// already un-rotated to Cartesian by the back-transform. So the inverse is: +// model = AxisRemap^-1 . MachineFrame^-1 +// (origin-snap is a per-instance translation that only shifts position, not +// orientation, so it is intentionally omitted.) static Transform3d compute_belt_back_transform(const PrintConfig& cfg) { if (!cfg.belt_printer.value) @@ -1339,10 +1337,9 @@ static Transform3d compute_belt_back_transform(const PrintConfig& cfg) // build-volume offset for Rev axes). This is the matrix form of the per-point // GCodeWriter::apply_axis_remap (row convention: each OUTPUT axis selects an input // axis + sign) and MUST stay in sync with it. The build-volume max matches what the - // writer is fed in GCode.cpp (printable_area max + printable_height). NB: this is the - // transpose of the column convention used by BeltTransformPipeline::build_preslice_remap - // — the two remaps are not interchangeable. (Follow-up: precompute this matrix once in - // GCodeWriter and share it with apply_axis_remap to remove the parallel encoding.) + // writer is fed in GCode.cpp (printable_area max + printable_height). (Follow-up: + // precompute this matrix once in GCodeWriter and share it with apply_axis_remap to + // remove the parallel encoding.) Transform3d ar = Transform3d::Identity(); const int rr[3] = { int(cfg.gcode_remap_x.value), int(cfg.gcode_remap_y.value), int(cfg.gcode_remap_z.value) }; if (rr[0] != 0 || rr[1] != 1 || rr[2] != 2) { @@ -1361,11 +1358,7 @@ static Transform3d compute_belt_back_transform(const PrintConfig& cfg) } const Transform3d ar_inv = ar.inverse(); - Transform3d bf_inv = Transform3d::Identity(); - if (!cfg.gcode_back_transform.value) - bf_inv = BeltTransformPipeline::build_forward_transform(cfg).inverse(); - - return bf_inv * ar_inv * mf_inv; + return ar_inv * mf_inv; } //BBS: always load shell at preview @@ -1394,11 +1387,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness"))); // avoid processing if called with the same gcode_result. - // On a belt printer the toolpath geometry fed to libvgcode also depends on the - // designed/raw view state (the back-transform is applied in convert), so the - // same result is converted again only when that view has been toggled. - const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed; - if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) { + if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1440,22 +1429,20 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const } // convert data from PrusaSlicer format to libvgcode format. - // Belt printers: when the "designed (upright) view" is active, back-transform - // the toolpath geometry into model/Cartesian space using the general belt - // inverse (handles any mesh rotation + shear + axis remap). When off, the raw - // machine-frame G-code is shown (useful for debugging the transform itself). + // Belt printers: back-transform the toolpath geometry into model/Cartesian + // space using the general belt inverse (handles the mesh rotation, shear and + // axis remap), so the part is shown upright, the way it was designed. const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value; - Transform3d belt_inv = (is_belt && m_belt_show_designed) - ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); + Transform3d belt_inv = is_belt ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); // Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's // purge-blob advance baked into the machine-Z origin by its G92 Z0 resets). Subtract // that constant before the linear back-transform so every toolpath maps to the model's // belt coordinate. Without it the back-transform mixes the offset with the gantry-Y // term, leaving a per-move designed-Y error that min-corner anchoring cannot remove // when a bridge/keel move happens to cancel it at the bbox minimum. - if (is_belt && m_belt_show_designed && gcode_result.belt_z_origin != 0.0f) + if (is_belt && gcode_result.belt_z_origin != 0.0f) belt_inv = belt_inv * Transform3d(Eigen::Translation3d(Vec3d(0.0, 0.0, -double(gcode_result.belt_z_origin)))); - const bool apply_belt = is_belt && m_belt_show_designed + const bool apply_belt = is_belt && !belt_inv.matrix().isApprox(Transform3d::Identity().matrix()); if (apply_belt) { // The linear belt back-transform recovers the print's shape and orientation but not @@ -1692,7 +1679,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const //BBS: move the id to the end of reset m_last_result_id = gcode_result.id; - m_last_belt_show_designed = m_belt_show_designed; m_gcode_result = &gcode_result; m_move_type_counts.fill(0); for (auto& move_type_times : m_move_type_times) @@ -5147,33 +5133,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab render_legend_color_arr_recommen(window_padding); - // Belt printer: toggle for viewing designed (upright) vs. machine-frame G-code. - // Rendered with a separator and hint text so users can find it easily. - if (m_belt_view_enabled) { - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Dummy({ window_padding, 0 }); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt printer").c_str()); - ImGui::Dummy({ window_padding, 0 }); - ImGui::SameLine(); - // Checked = show the raw machine-frame G-code (designed/upright view off). Worded to - // match the canvas-toolbar menu item "Show raw G-code (belt only)". m_belt_show_designed - // is the inverse of this checkbox, so bind a temporary and flip it on change. - bool show_raw = !m_belt_show_designed; - const std::string key = wxGetApp().shortcuts().display(Shortcut::ToggleBeltRawGcode); - const std::string label = _u8L("Show raw G-code (belt only)") + (key.empty() ? std::string() : " [" + key + "]"); - if (ImGui::Checkbox(label.c_str(), &show_raw)) { - m_belt_show_designed = !show_raw; - // The designed-view back-transform is baked into the toolpath geometry at load - // time, so the toggle only takes effect once the preview is re-converted. Defer - // the refresh to the next event-loop tick (CallAfter) to avoid re-entering the - // preview load from inside legend rendering. - if (Plater* plater = wxGetApp().plater()) - plater->CallAfter([plater]() { plater->refresh_belt_view(); }); - } - } legend_height = ImGui::GetCurrentWindow()->Size.y; imgui.end(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 06d275a187..9cd669372f 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -199,8 +199,6 @@ private: std::vector m_plater_extruder; bool m_gl_data_initialized{ false }; unsigned int m_last_result_id{ 0 }; - // Belt printers: the view the loaded result was converted for (see load_as_gcode). - bool m_last_belt_show_designed{ true }; //BBS: save m_gcode_result as well const GCodeProcessorResult* m_gcode_result; std::array(EMoveType::Count)> m_move_type_counts{}; @@ -265,8 +263,6 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; - bool m_belt_show_designed = true; // Toggle: designed (upright, back-transformed) view by default; - // turn off (hotkey B) to inspect the raw machine-frame G-code. libvgcode::Viewer m_viewer; // ORCA: section view, as the viewer has it. What it cuts away casts no shadow. @@ -406,8 +402,6 @@ public: void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; } - void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; } - bool is_belt_show_designed() const { return m_belt_show_designed; } size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d6d7371c20..1b4deced82 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3800,16 +3800,6 @@ bool GLCanvas3D::handle_shortcut(const KeyChord& chord) m_dirty = true; request_extra_frame(); break; - case Shortcut::ToggleBeltRawGcode: - // Same state as the legend checkbox and the canvas-toolbar menu item. The designed-view - // back-transform is baked into the toolpaths at load time, so the preview is re-converted. - if (m_gcode_viewer.is_belt_view()) { - m_gcode_viewer.toggle_belt_show_designed(); - if (Plater* plater = wxGetApp().plater()) - plater->refresh_belt_view(); - m_dirty = true; - } - break; case Shortcut::ToggleOneLayerMode: get_gcode_viewer().get_layers_slider()->switch_one_layer_mode(); m_dirty = true; @@ -10256,20 +10246,6 @@ void GLCanvas3D::_render_canvas_toolbar() ImGui::TextColored(enable ? ImVec4(1,1,1,1) : ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), "%s", into_u8(condition ? ImGui::VisibleIcon : ImGui::HiddenIcon).c_str()); }; - // Belt printers, G-code preview only: toggle the designed (upright) view vs the raw - // machine-frame G-code. Same state as the shortcut and the legend checkbox; the reload is - // deferred (CallAfter) so the preview is not rebuilt mid-render. - if (m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.is_belt_view()) { - create_menu_item( _utf8(L("Show raw G-code (belt only)")), - true, - !m_gcode_viewer.is_belt_show_designed(), // eye lit = raw machine-frame G-code (designed view off) - [this, p]{ - m_gcode_viewer.toggle_belt_show_designed(); - p->CallAfter([p]{ p->refresh_belt_view(); }); - } - ); - ImGui::Separator(); - } create_menu_item( _utf8(L("3D Navigator")), m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index f52bd20cc2..1968df996f 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -369,17 +369,6 @@ void Preview::reload_print(bool only_gcode) m_only_gcode = only_gcode; } -void Preview::refresh_belt_view() -{ - // Re-run the G-code preview conversion so the belt "designed view" toggle takes effect - // (the back-transform is baked into the toolpath geometry in GCodeViewer::load_as_gcode, - // whose same-result cache also keys on the view state, so the re-convert runs). - // Reset m_loaded_print to bypass the "already loaded" guard the way reload_print does, but - // keep the current layer (Z) range and only-gcode mode so the view doesn't jump on toggle. - m_loaded_print = nullptr; - load_print(true /*keep_z_range*/, m_only_gcode); -} - //BBS: always load shell at preview void Preview::load_shells(const Print& print, bool force_previewing) { diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index acb3211fd9..2ad1f8788c 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -149,7 +149,6 @@ public: void load_print(bool keep_z_range = false, bool only_gcode = false); void reload_print(bool only_gcode = false); // Belt printers: re-convert the G-code preview so the "designed view" toggle takes effect. - void refresh_belt_view(); //BBS: always load shell at preview void load_shells(const Print& print, bool force_previewing = false); void reset_shells(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3684020f8a..09f3cd2b8f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17885,11 +17885,6 @@ void Plater::reload_print() p->preview->reload_print(); } -void Plater::refresh_belt_view() -{ - p->preview->refresh_belt_view(); -} - // BBS wxString Plater::get_project_name() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 83d028549b..e7d94bc606 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -388,7 +388,6 @@ public: // Belt printers: re-run the G-code preview conversion so the "designed view" toggle // (hotkey B / legend checkbox) takes effect; the back-transform is applied to the // toolpath geometry at load time. Keeps the current layer range and only-gcode mode. - void refresh_belt_view(); // SoftFever void calib_pa(const Calib_Params& params); diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index 2395d31778..8773c6f364 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -149,7 +149,6 @@ constexpr std::array shortcut_table = {{ SHORTCUT(ShowWireframe, "show_wireframe", L("Show/Hide wireframe"), CANVAS, { WXK_RETURN, CTRL_SHIFT }), SHORTCUT(ToggleGcodeWindow, "toggle_gcode_window", L("On/Off G-code window"), PREVIEW, { 'C' }), SHORTCUT(ToggleOneLayerMode, "toggle_one_layer_mode", L("On/Off one layer mode of the vertical slider"), PREVIEW, { 'L' }), - SHORTCUT(ToggleBeltRawGcode, "toggle_belt_raw_gcode", L("Show raw G-code (belt only)"), PREVIEW, { 'B' }), // Application SHORTCUT(Preferences, "preferences", L("Preferences"), GLOBAL, PREFERENCES_CHORD), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index 6a58d64971..0e61554e2f 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -46,7 +46,7 @@ enum class Shortcut : uint8_t { // Camera ViewDefault, ViewTop, ViewBottom, ViewFront, ViewRear, ViewLeft, ViewRight, ViewPlate, ZoomIn, ZoomOut, Mouse3DSettings, // Display - ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ToggleBeltRawGcode, + ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, // Application Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts, // Speed Dial diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 28a9fb0456..e77a09c940 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5250,77 +5250,24 @@ void TabPrinter::build_fff() // Belt tilt: the sole mesh-side transform and the single source of truth for // the physical tilt (drives bed rendering and support gravity tilt too). // Isometric rotation, no distortion; the back-transform inverts it before the - // machine-frame remap. - { - Line line = { L("Belt tilt"), - L("Belt tilt axis and angle, applied as a mesh rotation before " - "slicing. Also drives bed rendering and support gravity tilt. " - "Isometric (no distortion); the back-transform inverts it before " - "the machine-frame remap.") }; - line.label_path = "printer_basic_information_belt_printer#belt-tilt"; - 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); - } - { - 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.label_path = "printer_basic_information_belt_printer#pre-slice-axis-remap"; - line.append_option(belt_og->get_option("preslice_remap_x")); - line.append_option(belt_og->get_option("preslice_remap_y")); - line.append_option(belt_og->get_option("preslice_remap_z")); - line.append_option(belt_og->get_option("preslice_remap_global")); - belt_og->append_line(line); - } - belt_og->append_single_option_line("belt_preslice_global", "printer_basic_information_belt_printer#global-mesh-transforms"); - belt_og->append_single_option_line("gcode_back_transform", "printer_basic_information_belt_printer#g-code-back-transform"); - { - Line line = { L("First layer plane"), - L("Reference plane used to decide which extrusions get first-layer " - "settings (no fan, slow speed, deferred temperature drop). On belt " - "printers, Auto resolves to the tilted belt-shear plane so that " - "first-layer treatment follows perpendicular distance from the belt " - "surface, not slicing layer index.") }; - line.label_path = "printer_basic_information_belt_printer#first-layer-plane"; - line.append_option(belt_og->get_option("first_layer_plane")); - line.append_option(belt_og->get_option("first_layer_plane_offset")); - line.append_option(belt_og->get_option("first_layer_plane_thickness")); - belt_og->append_line(line); - } - // Support floor: split across lines so each setting's own mode controls - // its visibility (floor_mode = Develop, floor_offset = Advanced, z_offset_mode = Expert). + // machine-frame remap. The angle is what a user checks against the machine; + // the axis is a profile-level kinematics choice, so it is Develop-only. They + // are separate rows because a shared line is shown by its first option's mode. + belt_og->append_single_option_line("belt_slice_rotation_angle", "printer_basic_information_belt_printer#tilt-angle"); + belt_og->append_single_option_line("belt_slice_rotation", "printer_basic_information_belt_printer#tilt-axis"); belt_og->append_single_option_line("belt_support_floor_offset", "printer_basic_information_belt_printer#support-floor-z-offset"); - belt_og->append_single_option_line("belt_support_z_offset_mode", "printer_basic_information_belt_printer#z-offset-mode"); - belt_og->append_single_option_line("belt_support_floor_mode", "printer_basic_information_belt_printer#floor-mode"); - // Machine-frame transform: the shear (tan) + scale (1/cos) that map + // Machine-frame transform: the shear (cot) + scale (1/sin) that map // Cartesian G-code into the printer's physical machine frame are derived // from the belt tilt angle. Only the post-slice axis remap and the expert - // decouple override are exposed here. + // decouple override are exposed here, one option per row. { auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); - { - 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.label_path = "printer_basic_information_machine_frame_transforms#g-code-axis-remap"; - line.append_option(mf->get_option("gcode_remap_x")); - line.append_option(mf->get_option("gcode_remap_y")); - line.append_option(mf->get_option("gcode_remap_z")); - mf->append_line(line); - } - { - Line line = { L("Machine-frame tilt"), - L("The machine-frame shear (tan) and scale (1/cos) are derived from " - "the belt tilt angle. Enable 'Decouple' to set an independent " - "machine-frame angle when the physical gantry tilt differs from " - "the slicing rotation.") }; - line.label_path = "printer_basic_information_machine_frame_transforms#machine-frame-tilt"; - line.append_option(mf->get_option("belt_frame_tilt_decouple")); - line.append_option(mf->get_option("belt_frame_tilt_angle")); - mf->append_line(line); - } + mf->append_single_option_line("gcode_remap_x", "printer_basic_information_machine_frame_transforms#g-code-axis-remap"); + mf->append_single_option_line("gcode_remap_y", "printer_basic_information_machine_frame_transforms#g-code-axis-remap"); + mf->append_single_option_line("gcode_remap_z", "printer_basic_information_machine_frame_transforms#g-code-axis-remap"); + mf->append_single_option_line("belt_frame_tilt_decouple", "printer_basic_information_machine_frame_transforms#machine-frame-tilt"); + mf->append_single_option_line("belt_frame_tilt_angle", "printer_basic_information_machine_frame_transforms#machine-frame-tilt"); } option = optgroup->get_option("thumbnails"); @@ -6366,41 +6313,30 @@ void TabPrinter::toggle_options() bool expert_or_above = (m_mode >= comExpert); toggle_line("belt_printer_infinite_y", is_belt); // Belt tilt: the sole mesh-side belt transform (visible by default in belt mode). + toggle_line("belt_slice_rotation_angle", is_belt); toggle_line("belt_slice_rotation", is_belt); // Remap, back-transform, and global mesh-transforms toggles are gated by belt // mode here; finer mode-based visibility is handled by each option's - // ConfigOptionMode in PrintConfig.cpp. Both axis remaps are Develop-only: a - // printer profile sets them once for its kinematics, and a wrong value sends + // ConfigOptionMode in PrintConfig.cpp. The axis remap is Develop-only: a + // printer profile sets it once for its kinematics, and a wrong value sends // the gantry outside the machine. - for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) + for (auto el : {"gcode_remap_x", "gcode_remap_y", "gcode_remap_z"}) toggle_line(el, is_belt); - toggle_line("belt_preslice_global", is_belt); - bool belt_global = is_belt && m_config->opt_bool("belt_preslice_global"); - - // preslice_remap_global: superseded by belt_preslice_global - toggle_option("preslice_remap_global", is_belt && !belt_global); - - // Rotation is the only mesh-side belt transform. Gray out its angle/global - // sub-options when no rotation axis is selected. + // Rotation is the only mesh-side belt transform. Gray out its angle when no + // rotation axis is selected. auto rot_axis = m_config->option>("belt_slice_rotation")->value; toggle_option("belt_slice_rotation_angle", is_belt && rot_axis != BeltRotationAxis::None); - toggle_option("belt_slice_rotation_global", is_belt && rot_axis != BeltRotationAxis::None); // Machine-frame transform: derived from the belt tilt. Only the expert - // decouple override is exposed; its angle is enabled only when decoupled. + // decouple override is exposed; its angle is shown only when decoupled. toggle_line("belt_frame_tilt_decouple", is_belt && expert_or_above); - toggle_option("belt_frame_tilt_angle", - is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple")); + toggle_line("belt_frame_tilt_angle", + is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple")); - // First-layer plane: visible alongside the rest of belt-printer settings. - toggle_line("first_layer_plane", is_belt); - toggle_option("first_layer_plane_offset", is_belt); - toggle_option("first_layer_plane_thickness", is_belt); - for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset", "belt_support_z_offset_mode"}) - toggle_line(el, is_belt); + toggle_line("belt_support_floor_offset", is_belt); const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads"); toggle_line("parallel_printheads_count", support_parallel_printheads); diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index b0f36c33d9..3dbb3a986b 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -56,8 +56,6 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo config.belt_printer.value = true; config.belt_slice_rotation.value = BeltRotationAxis::X; config.belt_slice_rotation_angle.value = 30.; - config.belt_slice_rotation_global.value = true; - config.gcode_back_transform.value = true; config.gcode_remap_x.value = RemapAxis::PosX; config.gcode_remap_y.value = RemapAxis::PosZ; config.gcode_remap_z.value = RemapAxis::PosY; @@ -1027,11 +1025,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" // Machine-frame + slicer->world back-transform config (X tilt, 45 deg). PrintConfig belt_config; belt_config.belt_printer.value = true; - belt_config.gcode_back_transform.value = true; belt_config.belt_slice_rotation.value = BeltRotationAxis::X; belt_config.belt_slice_rotation_angle.value = 45.0; - belt_config.belt_slice_rotation_global.value = true; - belt_config.belt_preslice_global.value = true; belt_config.belt_frame_tilt_decouple.value = false; belt_config.belt_frame_tilt_angle.value = 45.0; diff --git a/tests/fff_print/test_precise_seam.cpp b/tests/fff_print/test_precise_seam.cpp index d5e2709cdd..b5cb913904 100644 --- a/tests/fff_print/test_precise_seam.cpp +++ b/tests/fff_print/test_precise_seam.cpp @@ -1438,7 +1438,6 @@ TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object wa { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "layer_height", 0.2 }, { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index c0e5572b97..be5311fd7b 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -30,6 +30,10 @@ #include "libslic3r/BuildVolume.hpp" #include "libslic3r/Support/TreeModelVolumes.hpp" #include "libslic3r/Support/TreeSupportCommon.hpp" +#include "libslic3r/Support/BeltFloorContext.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/Polyline.hpp" +#include #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" @@ -765,7 +769,6 @@ TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][bel { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -815,7 +818,6 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -851,7 +853,6 @@ TEST_CASE("Belt printers keep the part fan off within the band above the belt", { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -930,7 +931,6 @@ TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Pri { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -1003,7 +1003,6 @@ static DynamicPrintConfig belt_test_config() { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -1039,30 +1038,19 @@ TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged" // Every belt key a profile can carry, at a value that would change a belt print. // belt_printer stays off, so none of them may reach the G-code: the axis remaps are // gated on belt mode, the rest is only read on belt printers. build_plate_tilt_x/y - // and an explicit first_layer_plane are features of their own on a flat bed and are - // left alone here; "leading_edge_only" prints as an outer brim by design. + // is a feature of its own on a flat bed and is left alone here; "leading_edge_only" + // prints as an outer brim by design. config.set_deserialize_strict({ { "belt_printer", 0 }, { "belt_printer_infinite_y", 0 }, { "belt_slice_rotation", "y" }, { "belt_slice_rotation_angle", 30 }, - { "belt_slice_rotation_global", 0 }, - { "belt_preslice_global", 0 }, - { "preslice_remap_x", "pos_x" }, - { "preslice_remap_y", "pos_z" }, - { "preslice_remap_z", "neg_y" }, - { "preslice_remap_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, - { "gcode_back_transform", 0 }, { "belt_frame_tilt_decouple", 1 }, { "belt_frame_tilt_angle", 30 }, - { "first_layer_plane_offset", 1 }, - { "first_layer_plane_thickness", 1 }, { "belt_support_floor_offset", -5 }, - { "belt_support_floor_mode", "none" }, - { "belt_support_z_offset_mode", "raft_only" }, { "enable_belt_purge_tower", 1 }, { "belt_purge_tower_width", 10 }, { "leading_brim_length", 10 }, @@ -1136,7 +1124,6 @@ TEST_CASE("A support-only change on a belt purge print matches a fresh slice", " { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -1255,3 +1242,113 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above CHECK(collides(last + 1)); CHECK(collides(last + num_raft)); } + +// A part with an overhang on its LEADING side (the end that prints first) needs +// supports below the object's own lowest slicing layer: the belt under that overhang +// is reached before the object's first contact with it, so the support layers sit at +// a lower slicing Z than any object layer. A generator that stops at the object's +// first layer, or at global Z = 0, leaves those supports floating above the belt. +TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt][Support][Regression]") +{ + // default resolves to organic for tree support; tree_hybrid is the classic tree. + const char *support_type = GENERATE("normal(auto)", "tree(auto)"); + const char *support_style = GENERATE("default", "organic", "tree_hybrid"); + if (std::string(support_type) == "normal(auto)" && std::string(support_style) != "default") + return; // organic and tree_hybrid are tree styles + DYNAMIC_SECTION(support_type << " / " << support_style) { + // A 20 mm cube with a 2 mm thick fin that leaves its top edge and reaches + // 20 mm toward -Y, the end of the part that prints first, climbing at 45 deg + // as it goes (from z = 18 at the cube to z = 38 at the tip). With the layers + // leaning toward -Y at 45 deg the fin's underside is parallel to the layers: + // a ceiling 20 x 28 mm in one layer, with nothing but air between it and the + // belt, which lies up to 41 mm (of slicing Z) below the object's own lowest + // point. Support has to span all of it. + indexed_triangle_set its = its_make_cube(20., 20., 20.); + indexed_triangle_set fin = its_make_cube(20., 20., 2.); + Transform3d shear = Transform3d::Identity(); + shear.matrix() << 1., 0., 0., 0., + 0., 1., 0., -20., + 0., -1., 1., 38., + 0., 0., 0., 1.; + its_transform(fin, shear); + its_merge(its, fin); + TriangleMesh mesh(std::move(its)); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", support_type }, + { "support_style", support_style }, + { "support_threshold_angle", 30 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + Print print; + Model model; + init_print({ mesh }, print, model, config); + // On the bed, not at its corner: organic tree support clips its branches to + // the bed outline, and the fixture leaves the object at the origin. + model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &object = *print.objects().front(); + REQUIRE(! object.layers().empty()); + // The whole part is sliced: the layers lean at 45 deg, so the part spans + // (y + z) / sqrt(2) of slicing Z, and every layer in that span has geometry. + { + double lo = std::numeric_limits::max(), hi = std::numeric_limits::lowest(); + for (const stl_vertex &v : mesh.its.vertices) { + lo = std::min(lo, v.y() + v.z()); + hi = std::max(hi, v.y() + v.z()); + } + const double span = (hi - lo) / std::sqrt(2.); + size_t nonempty = 0; + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) + ++ nonempty; + INFO("non-empty object layers " << nonempty << ", slicing span " << span << " mm"); + CHECK(double(nonempty) * 0.2 > span - 0.6); + } + BeltFloorContext floor; + REQUIRE(floor.init(object.slicing_parameters(), print.config())); + + // The lowest support layer that prints anything, and the belt floor beneath it. + const SupportLayer *lowest = nullptr; + for (const SupportLayer *layer : object.support_layers()) + if (! layer->support_fills.empty() && (lowest == nullptr || layer->print_z < lowest->print_z)) + lowest = layer; + REQUIRE(lowest != nullptr); + double floor_under_lowest = std::numeric_limits::max(); + for (const ExtrusionEntity *entity : lowest->support_fills.flatten().entities) + for (const Polyline &pl : entity->as_polylines()) + for (const Point &pt : pl.points) + floor_under_lowest = std::min(floor_under_lowest, floor.floor_print_z(pt)); + // The object's lowest geometry. The slicing frame starts at the lowest + // belt-floor point under the footprint, so the layers below the leading + // tip of the overhang are empty. + double first_object_z = std::numeric_limits::max(); + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) { first_object_z = layer->print_z; break; } + REQUIRE(first_object_z < std::numeric_limits::max()); + INFO("lowest support z " << lowest->print_z << ", floor under it " << floor_under_lowest + << ", first object layer " << first_object_z); + // Well below the object's own lowest layer (the belt under the tip of the fin + // is ~41 mm of slicing Z below the cube's leading edge, which rests on it)... + CHECK(lowest->print_z < first_object_z - 5.); + // ...and resting on the belt: within a few layers of the floor beneath its own lines. + CHECK(lowest->print_z - floor_under_lowest < 4. * 0.2 + EPSILON); + CHECK(lowest->print_z - floor_under_lowest > -0.2 - EPSILON); + } +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 45086b9d2a..34f71a71a7 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -641,7 +641,6 @@ static DynamicPrintConfig belt_brim_config() { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -667,7 +666,6 @@ static DynamicPrintConfig belt_brim_multifilament_config(unsigned int filaments, { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, - { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, @@ -717,9 +715,10 @@ static double first_role_z(const std::string &gcode, const std::string &role) return z; } -// Number of object layers that carry a belt brim band. Each such band is emitted as one -// contiguous brim pass, so for a single object whose first-contact layer carries a band -// (the apron prologue folds into that layer's pass) this equals role_passes(gcode, "brim"). +// Number of object layers that carry a belt brim band. Every band prints at its own +// layer Z, so this equals role_layers(gcode, "brim") (plus any apron bands below the +// first object layer). It is not a pass count: the bands on the empty lead-in layers +// ahead of the object's first contact print back to back, so they fold into one pass. static int nonempty_belt_brim_layers(const PrintObject &object) { int n = 0; @@ -729,6 +728,30 @@ static int nonempty_belt_brim_layers(const PrintObject &object) return n; } +// Number of distinct Z heights at which `role` extrudes: one per layer that prints it. +static int role_layers(const std::string &gcode, const std::string &role) +{ + std::set zs; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (! line.extruding(self) || line.dist_XY(self) <= EPSILON) + return; + if (line.comment().find(role) != std::string_view::npos) + zs.insert(std::lround(self.z() * 1000.)); + }); + return int(zs.size()); +} + +// Apron bands below the object's first layer that print something. +static int belt_brim_apron_bands(const PrintObject &object) +{ + int n = 0; + for (const BeltBrimBand &band : object.belt_brim_prologue()) + if (! band.fills.empty()) + ++ n; + return n; +} + // For each active tool, the ordinal (1-based, over extruding moves) of the FIRST move whose // role comment contains `role`. Lets a per-object ordering check key off the object's // unique wall filament. @@ -803,12 +826,15 @@ TEST_CASE("Belt brim on an object layer precedes its perimeters, once", "[SkirtB CHECK(seq[0] == "brim"); CHECK(seq[1] == "perimeter"); - // Exactly once: every band is one contiguous pass (the apron prologue folds into the - // first layer's), so the pass count equals the number of layers carrying a band - not - // twice it, which double-emission would give, nor fewer, which a dropped band would. - const int bands = nonempty_belt_brim_layers(*print.objects().front()); + // Exactly once: every band prints at its own layer Z, so the number of Z heights with + // brim equals the number of bands - not fewer, which a dropped band would give. (A + // double emission would print twice at one Z: the pass count below catches that for + // the bands that sit on layers with perimeters.) + const PrintObject &object = *print.objects().front(); + const int bands = nonempty_belt_brim_layers(object); REQUIRE(bands > 0); - CHECK(role_passes(gc, "brim") == bands); + CHECK(role_layers(gc, "brim") == bands + belt_brim_apron_bands(object)); + CHECK(role_passes(gc, "brim") <= bands); } // B - single extruder (filament id 1). Every band must survive the 1-based -> 0-based @@ -828,9 +854,10 @@ TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][b init_print({ cube(20) }, print, model, config); const std::string gc = gcode(print); - const int expected = nonempty_belt_brim_layers(*print.objects().front()); + const PrintObject &object = *print.objects().front(); + const int expected = nonempty_belt_brim_layers(object) + belt_brim_apron_bands(object); REQUIRE(expected > 0); - CHECK(role_passes(gc, "brim") == expected); + CHECK(role_layers(gc, "brim") == expected); CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0 }); // filament 1 -> tool 0 } @@ -1233,51 +1260,6 @@ TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBr CHECK(two >= 1.8 * one); } -TEST_CASE("Belt brim allows instances placed across the belt", "[SkirtBrim][belt]") -{ - // Only movement ALONG the belt changes an instance's belt-floor Z, so copies placed - // side by side ACROSS it share one set of bands and must still get a brim. The first - // version of this guard refused every multi-instance object outright, silently - // dropping the brim. - // - // The global belt flags are off here so the instances stay in one PrintObject; with - // them on, PrintApply splits each instance into its own object and the case cannot - // arise at all. - auto multi_instance_has_brim = [](double dx, double dy) { - DynamicPrintConfig config = belt_brim_config(); - config.set_deserialize_strict({ - { "belt_slice_rotation_global", 0 }, - { "belt_preslice_global", 0 }, - { "preslice_remap_global", 0 }, - { "brim_type", "outer_only" }, - { "brim_width", 4 }, - { "brim_object_gap", 0 }, - }); - Print print; - Model model; - ModelObject *object = model.add_object(); - object->name += "object.stl"; - object->add_volume(cube(20)); - object->add_instance()->set_offset(Vec3d(80., 80., 0.)); - object->add_instance()->set_offset(Vec3d(80. + dx, 80. + dy, 0.)); - object->ensure_on_bed(); - print.auto_assign_extruders(object); - print.apply(model, config); - print.validate(); - print.set_status_silent(); - print.process(); - REQUIRE(print.objects().size() == 1); - REQUIRE(print.objects().front()->instances().size() == 2); - return print.objects().front()->has_belt_brim(); - }; - - // X is across the belt when the tilt is about X, since the shear then runs along Y. - CHECK(multi_instance_has_brim(40., 0.)); - // Y is along the belt: the copies sit at different belt heights and would each need - // their own bands, so the brim is refused (and validate() warns). - CHECK_FALSE(multi_instance_has_brim(0., 40.)); -} - TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") { // Supports put extra layers into the same z stream as the apron bands, which is what