diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000000..cce3208834 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"597ab29b-cd70-40f4-95d9-fee98b3968ad","pid":1159657,"acquiredAt":1775936463783} \ No newline at end of file diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index db2bc46fe1..ea90f2588e 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -1,8 +1,27 @@ #include "BeltGCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "Geometry.hpp" namespace Slic3r { +namespace { + +// Decide whether a particular destination point gets first-layer treatment. +// When the plane evaluator is active, distance from the plane wins; otherwise +// fall back to the layer-coarse m_is_first_layer flag set by the caller. +inline bool belt_point_on_first_layer( + const FirstLayerPlane *plane, + double first_layer_thickness_mm, + bool layer_first_flag, + const Vec3d &point_slicing_mm) +{ + if (plane && plane->is_active()) + return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm); + return layer_first_flag; +} + +} // namespace + // ---- Belt configuration --------------------------------------------------- void BeltGCodeWriter::set_belt_angle(double angle_deg) @@ -52,7 +71,10 @@ std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string GCodeG1Formatter w; w.emit_xyz(machine); - auto speed = m_is_first_layer + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(point.x(), point.y(), m_pos.z())); + auto speed = first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -78,8 +100,11 @@ std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) double speed = this->config.travel_speed_z.value; if (speed == 0.) { - speed = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") - : this->config.travel_speed.value; + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(m_pos.x(), m_pos.y(), z)); + speed = first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; } // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. @@ -142,8 +167,11 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) Vec3d dest_point = point; + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); auto travel_speed = - m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; // Handle pending z_hop if (std::abs(m_to_lift) > EPSILON) { diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index a41f890503..828dd43bb3 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -5,6 +5,8 @@ namespace Slic3r { +class FirstLayerPlane; + // Belt-printer-specific GCode writer. // // Inherits from GCodeWriter and overrides movement methods to apply @@ -22,6 +24,16 @@ public: void set_origin_snap(int axis, bool enable, double offset, double bbox_min); Vec3d to_machine_coords(const Vec3d &pos) const; + // First-layer plane: when set to a non-null active evaluator, travel + // speed selection consults the plane per-move and uses + // initial_layer_travel_speed for points within first_layer_height_mm + // of the plane (regardless of slicing layer index). + void set_first_layer_plane(const FirstLayerPlane *plane, + double first_layer_height_mm) { + m_first_layer_plane = plane; + m_first_layer_thickness_mm = first_layer_height_mm; + } + // Overridden movement methods std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; @@ -39,6 +51,9 @@ private: bool m_origin_snap[3] = {false, false, false}; double m_origin_offset[3] = {0., 0., 0.}; double m_origin_bbox_min[3] = {0., 0., 0.}; + // Borrowed pointer; lifetime owned by GCode. null = inactive. + const FirstLayerPlane *m_first_layer_plane = nullptr; + double m_first_layer_thickness_mm = 0.; }; } // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index a3e813b1da..689dee8c83 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -87,6 +87,8 @@ 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 new file mode 100644 index 0000000000..23327aea7a --- /dev/null +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -0,0 +1,226 @@ +#include "FirstLayerPlane.hpp" +#include "BeltTransform.hpp" + +#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) { + // BeltGCodeWriter 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) { + if (config.belt_printer.value && + config.belt_shear_z.value != BeltShearMode::None) { + mode = FirstLayerPlaneMode::BeltShear; + } else { + mode = 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::BeltShear: { + // 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 new file mode 100644 index 0000000000..35b8346c82 --- /dev/null +++ b/src/libslic3r/FirstLayerPlane.hpp @@ -0,0 +1,76 @@ +#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 93b87cbba8..a9122d9bfd 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -24,6 +24,7 @@ #include "Time.hpp" #include "GCode/ExtrusionProcessor.hpp" #include +#include #include #include #include @@ -2438,6 +2439,17 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } } + // 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()); + if (auto *belt_writer = dynamic_cast(m_writer.get())) { + belt_writer->set_first_layer_plane( + m_first_layer_plane.get(), + print.config().initial_layer_print_height.value); + } + // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; @@ -4588,7 +4600,38 @@ LayerResult GCode::process_layer( } } - if (!first_layer && !m_second_layer_things_done) { + // 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. + bool past_first_layer_band = !first_layer; + 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 const auto plr_mode = print.config().enable_power_loss_recovery.value; gcode += m_writer->enable_power_loss_recovery(plr_mode); @@ -6204,6 +6247,18 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (is_bridge(path.role())) description += " (bridge)"; + // First-layer plane evaluation: compute the path's slicing-frame point + // once and reuse for every per-path call site below. When the plane + // evaluator is inactive (non-belt printers, or belt printers without + // a Z-axis shear) `path_on_first_layer` falls back to the legacy + // layer-id check, so behavior is bit-identical to the pre-feature path. + const Vec3d path_point_mm{ + unscale(path.first_point().x()), + unscale(path.first_point().y()), + m_layer ? m_layer->print_z : 0.0 + }; + const bool path_on_first_layer = this->on_first_layer(path_point_mm); + const ExtrusionPathSloped* sloped = dynamic_cast(&path); const auto get_sloped_z = [&sloped, this](double z_ratio) { @@ -6249,7 +6304,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // adjust acceleration if (m_config.default_acceleration.value > 0) { double acceleration; - if (this->on_first_layer() && m_config.initial_layer_acceleration.value > 0) { + if (path_on_first_layer && m_config.initial_layer_acceleration.value > 0) { acceleration = m_config.initial_layer_acceleration.value; #if 0 } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { @@ -6275,7 +6330,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // adjust X Y jerk if (m_config.default_jerk.value > 0) { - if (this->on_first_layer() && m_config.initial_layer_jerk.value > 0) { + if (path_on_first_layer && m_config.initial_layer_jerk.value > 0) { jerk = m_config.initial_layer_jerk.value; } else if (m_config.outer_wall_jerk.value > 0 && is_external_perimeter(path.role())) { jerk = m_config.outer_wall_jerk.value; @@ -6335,7 +6390,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // Additionally, adjust the value if we are on the first layer (except for brims and skirts) - if (this->on_first_layer() && (path.role() != erBrim && path.role() != erSkirt)) { + if (path_on_first_layer && (path.role() != erBrim && path.role() != erSkirt)) { _mm3_per_mm *= m_config.first_layer_flow_ratio; } } @@ -6393,14 +6448,17 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (speed == 0) speed = filament_max_volumetric_speed / _mm3_per_mm; - if (this->on_first_layer()) { + if (path_on_first_layer) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached if (path.role() != erBottomSurface) speed = m_config.get_abs_value("initial_layer_speed"); } else if(m_config.slow_down_layers > 1){ - const auto _layer = layer_id(); + // Use the FirstLayerPlane-aware effective layer index when active 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); if (_layer > 0 && _layer < m_config.slow_down_layers) { const auto first_layer_speed = is_perimeter(path.role()) @@ -6473,7 +6531,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool variable_speed = false; std::vector new_points {}; - if (m_config.enable_overhang_speed && !this->on_first_layer() && + if (m_config.enable_overhang_speed && !path_on_first_layer && (is_bridge(path.role()) || is_perimeter(path.role()))) { bool is_external = is_external_perimeter(path.role()); double ref_speed = is_external ? m_config.get_abs_value("outer_wall_speed") : m_config.get_abs_value("inner_wall_speed"); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 9004f30fd3..99d95eeb61 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -5,6 +5,7 @@ #include "ExPolygon.hpp" #include "GCodeWriter.hpp" #include "BeltGCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -303,6 +304,14 @@ public: } }; + // Public accessor for the first-layer plane evaluator. Used by + // CoolingBuffer (which is constructed with a GCode reference and needs + // to read the plane for per-segment fan re-evaluation). All other + // 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 { public: @@ -598,6 +607,11 @@ protected: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; + // First-layer plane evaluator. Constructed once per print from the + // 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; std::unique_ptr m_pressure_equalizer; @@ -657,6 +671,25 @@ 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. + bool on_first_layer(const Vec3d &point_slicing_mm) const { + 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. + int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const { + 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(); + } int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 41e612fdab..407163b3dd 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,10 +1,14 @@ #include "../GCode.hpp" +#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" #include #include #include +#include +#include #include #include +#include #include #include @@ -28,6 +32,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_num_extruders = std::max(ex.id() + 1, m_num_extruders); m_extruder_ids.emplace_back(ex.id()); } + + // Borrow the first-layer plane from the GCode generator. When inactive + // (non-belt printers and belt printers without Z shear), per-line fan + // re-evaluation is skipped and behavior is bit-identical to the legacy + // per-layer path. + m_first_layer_plane = gcodegen.first_layer_plane(); } void CoolingBuffer::reset(const Vec3d &position) @@ -328,6 +338,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); + // First-layer plane: per-segment fan re-evaluation post-pass. Walks + // the cooled-down gcode and inserts inline M106 commands at band + // crossings (where the path's perpendicular distance to the plane + // crosses close_fan_the_first_x_layers thresholds). No-op when + // the evaluator is inactive. + if (m_first_layer_plane && m_first_layer_plane->is_active()) + out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); m_gcode.clear(); } return out; @@ -1011,4 +1028,210 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } +// Pure helper: compute the main fan speed for a given effective layer index. +// Mirrors the inline logic in change_extruder_set_fan but is callable from +// per-line code in apply_first_layer_plane_fan_eval. +int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, + unsigned int extruder_id) const +{ +#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) + float fan_min_speed = EXTRUDER_CFG(fan_min_speed); + float fan_max_speed = EXTRUDER_CFG(fan_max_speed); + bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); + int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); + int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); + float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); + float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); +#undef EXTRUDER_CFG + + if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) + close_fan_the_first_x_layers = 1; + + float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; + if (effective_layer_id >= close_fan_the_first_x_layers) { + if (layer_time < slow_down_layer_time) { + fan_speed_new = fan_max_speed; + } else if (layer_time < fan_cooling_layer_time) { + double t = (layer_time - slow_down_layer_time) / + (fan_cooling_layer_time - slow_down_layer_time); + fan_speed_new = float(int(floor(t * fan_min_speed + + (1. - t) * fan_max_speed) + 0.5)); + } + if (effective_layer_id + 1 < full_fan_speed_layer) { + float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) + / float(full_fan_speed_layer - close_fan_the_first_x_layers); + fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); + } + } else { + fan_speed_new = 0.f; + } + return int(fan_speed_new); +} + +// Post-pass: walk the cooled-down gcode line by line, track XYZ position, +// and insert M106 commands at first-layer-plane band crossings so the fan +// follows perpendicular distance to the plane rather than the slicing-layer +// index. Only invoked when the FirstLayerPlane evaluator is active. +// +// This implementation is intentionally minimal: it overrides only the MAIN +// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc +// special fans remain at their layer-level values from apply_layer_cooldown. +// That keeps the per-line logic small while still giving the user precise +// fan control near the belt surface, which is the main quality concern. +std::string CoolingBuffer::apply_first_layer_plane_fan_eval( + std::string &&gcode_in, size_t /*layer_id*/, float layer_time) +{ + if (!m_first_layer_plane || !m_first_layer_plane->is_active()) + return std::move(gcode_in); + + const std::string &gcode = gcode_in; + std::string out; + out.reserve(gcode.size() + 256); + + // Track position in slicing-frame mm. Seed from m_current_pos which the + // CoolingBuffer keeps up-to-date across layers. + Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); + + // Track current main fan speed by parsing M106 commands as we walk so + // we can restore it after a band exit. + int current_main_fan = m_fan_speed; + int pre_band_main_fan = current_main_fan; + // Implicit initial state: assume the layer started "out of the band" + // (i.e., the layer-level fan setting from apply_layer_cooldown is in + // effect). The first movement we encounter will reconcile this. + bool in_first_layer_band = false; + unsigned int active_extruder = m_current_extruder; + + auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { + if (line_sv.size() < 3) return false; + if (line_sv[0] != 'G') return false; + if (line_sv[1] != '0' && line_sv[1] != '1') return false; + if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; + const char *c = line_sv.data() + 3; + const char *end = line_sv.data() + line_sv.size(); + bool any = false; + while (c < end && *c != ';') { + while (c < end && (*c == ' ' || *c == '\t')) ++c; + if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; + char axis = *c; + ++c; + if (axis == 'X' || axis == 'Y' || axis == 'Z') { + char *next; + double v = std::strtod(c, &next); + if (next != c) { + if (axis == 'X') p.x() = v; + else if (axis == 'Y') p.y() = v; + else p.z() = v; + c = next; + any = true; + continue; + } + } + // Skip unrecognized word. + while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') + ++c; + } + return any; + }; + + auto parse_m106 = [](const std::string_view &line_sv) -> int { + // Returns -1 if not an M106, otherwise the S value (0..255). + if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; + if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) + return -1; + // Find S + size_t s_pos = line_sv.find('S'); + if (s_pos == std::string_view::npos) return -1; + const char *c = line_sv.data() + s_pos + 1; + char *next; + long v = std::strtol(c, &next, 10); + if (next == c) return -1; + return int(std::clamp(v, 0, 255)); + }; + + auto parse_m107 = [](const std::string_view &line_sv) -> bool { + return line_sv.size() >= 4 && line_sv[0] == 'M' && + line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; + }; + + auto parse_tool_change = [this](const std::string_view &line_sv) -> int { + // Returns the new extruder id, or -1 if not a toolchange. + if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; + if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) + return -1; + const char *c = line_sv.data() + m_toolchange_prefix.size(); + char *next; + long v = std::strtol(c, &next, 10); + if (next == c) return -1; + return int(v); + }; + + const char *p = gcode.c_str(); + const char *end = gcode.c_str() + gcode.size(); + while (p < end) { + const char *line_end = p; + while (line_end < end && *line_end != '\n') ++line_end; + const char *next_line = line_end; + if (next_line < end) ++next_line; // include the '\n' + + std::string_view line_sv(p, line_end - p); + + // Track tool changes so the per-line fan eval uses the right extruder. + int new_tool = parse_tool_change(line_sv); + if (new_tool >= 0) + active_extruder = unsigned(new_tool); + + // Track existing fan commands so we can restore the right value when + // exiting a band. + int m106_speed = parse_m106(line_sv); + if (m106_speed >= 0) { + current_main_fan = m106_speed; + if (!in_first_layer_band) + pre_band_main_fan = m106_speed; + } else if (parse_m107(line_sv)) { + current_main_fan = 0; + if (!in_first_layer_band) + pre_band_main_fan = 0; + } + + // Movement line: parse XYZ, evaluate plane, possibly emit a fan + // change BEFORE this line. + bool moved = parse_xyz_into(line_sv, cur_pos_mm); + if (moved) { + const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); + const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); + const bool now_in_band = eff_idx < std::max(close_n, 1); + if (now_in_band != in_first_layer_band) { + // Band crossing: emit a M106 with the appropriate speed. + int target_fan; + if (now_in_band) { + // Entering the first-layer band: fan off. + pre_band_main_fan = current_main_fan; + target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); + } else { + // Exiting the band: restore the layer's normal fan speed. + // Use compute_main_fan_speed with the effective index so + // the linear ramp factor (close_fan→full_fan_speed_layer) + // also follows distance from the plane. + target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); + if (target_fan == 0) + target_fan = pre_band_main_fan; + } + if (target_fan != current_main_fan) { + out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan); + current_main_fan = target_fan; + m_fan_speed = target_fan; + m_current_fan_speed = target_fan; + } + in_first_layer_band = now_in_band; + } + } + + out.append(p, next_line - p); + p = next_line; + } + + return out; +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index fba27b289b..4bb8f26476 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,6 +10,7 @@ namespace Slic3r { class GCode; class Layer; +class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -18,7 +19,7 @@ struct PerExtruderAdjustments; // // The simple it sounds, the actual implementation is significantly more complex. // Namely, for a multi-extruder print, each material may require a different cooling logic. -// For example, some materials may not like to print too slowly, while with some materials +// For example, some materials may not like to print too slowly, while with some materials // we may slow down significantly. // class CoolingBuffer { @@ -36,6 +37,21 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); + // First-layer plane: per-line fan re-evaluation post-pass. Walks the + // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at + // band-crossing transitions in slicing-frame coordinates. Only runs + // when m_first_layer_plane is active. + std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, + size_t layer_id, + float layer_time); + + // Pure helper: compute the main fan speed for a given effective layer + // index (layer-id units, mapped through the plane evaluator) and the + // current extruder. Mirrors the inline logic in the change_extruder_set_fan + // lambda but is callable from per-line code. + int compute_main_fan_speed(int effective_layer_id, float layer_time, + unsigned int extruder_id) const; + // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -57,6 +73,9 @@ private: unsigned int m_current_extruder; //BBS: current fan speed int m_current_fan_speed; + // First-layer plane evaluator, borrowed from GCode. Null = inactive + // (legacy per-layer fan control). + const FirstLayerPlane *m_first_layer_plane = nullptr; }; } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 4d01757826..f76ea93e55 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1017,6 +1017,7 @@ static std::vector s_Preset_printer_options { "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_preslice_global", + "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", "belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_z", "belt_origin_offset_z", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 47d0b9c549..4ec0547ddb 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -341,6 +341,15 @@ static t_config_enum_values s_keys_map_BeltSupportZOffsetMode { }; 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_shear", int(FirstLayerPlaneMode::BeltShear) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FirstLayerPlaneMode) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6188,6 +6197,51 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + // First-layer plane: which surface defines "first layer" for fan / speed / + // accel decisions. On belt printers the slicing-frame layer 0 is a tilted + // slab that no longer corresponds to the physical first printed layer. + // Auto picks BeltShear when belt_shear_z != None, otherwise XY (legacy). + 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 shear plane when belt_shear_z is non-None, " + "otherwise XY (legacy). Pick XY explicitly to opt out and force the " + "legacy slicing-layer-0 detection."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"auto", "xy", "yz", "xz", "belt_shear"}; + def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt shear plane")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::Auto)); + + def = this->add("first_layer_plane_offset", coFloat); + def->label = L("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)); + auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, const char *axis_label) { auto def = this->add(key_snap, coBool); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index d17c46f0aa..afe5ec58ac 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -203,6 +203,26 @@ enum class BeltSupportZOffsetMode 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 Z-axis shear. +// - BeltShear for belt printers with belt_shear_z != None. +// +// 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, + BeltShear, +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -552,6 +572,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) 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) @@ -1496,6 +1517,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((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)) ((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionBool, belt_origin_snap_y)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 986ff614f6..4eaa6c5788 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4451,6 +4451,18 @@ void TabPrinter::build_fff() } optgroup->append_single_option_line("belt_preslice_global"); optgroup->append_single_option_line("gcode_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.append_option(optgroup->get_option("first_layer_plane")); + line.append_option(optgroup->get_option("first_layer_plane_offset")); + line.append_option(optgroup->get_option("first_layer_plane_thickness")); + optgroup->append_line(line); + } { Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; line.append_option(optgroup->get_option("belt_origin_snap_x")); @@ -5368,6 +5380,14 @@ void TabPrinter::toggle_options() toggle_option("belt_origin_offset_y", is_belt && m_config->opt_bool("belt_origin_snap_y") && !belt_global); toggle_option("belt_origin_offset_z", is_belt && m_config->opt_bool("belt_origin_snap_z") && !belt_global); + // First-layer plane: visible alongside the rest of belt-printer settings. + // The Auto default keeps legacy XY behavior on non-belt printers, so it's + // safe to also show it in the developer mode for non-belt printers. + bool show_first_layer_plane = is_belt || (m_mode >= comDevelop); + toggle_line("first_layer_plane", show_first_layer_plane); + toggle_option("first_layer_plane_offset", show_first_layer_plane); + toggle_option("first_layer_plane_thickness", show_first_layer_plane); + toggle_line("belt_support_floor_mode", is_belt); }