mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-16 07:22:10 +00:00
Add first layer detection and fan control - prototype
This commit is contained in:
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"597ab29b-cd70-40f4-95d9-fee98b3968ad","pid":1159657,"acquiredAt":1775936463783}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -87,6 +87,8 @@ set(lisbslic3r_sources
|
||||
BeltSliceStrategy.hpp
|
||||
BeltTransform.cpp
|
||||
BeltTransform.hpp
|
||||
FirstLayerPlane.cpp
|
||||
FirstLayerPlane.hpp
|
||||
Brim.cpp
|
||||
BrimEarsPoint.hpp
|
||||
Brim.hpp
|
||||
|
||||
226
src/libslic3r/FirstLayerPlane.cpp
Normal file
226
src/libslic3r/FirstLayerPlane.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
#include "FirstLayerPlane.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
|
||||
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
|
||||
76
src/libslic3r/FirstLayerPlane.hpp
Normal file
76
src/libslic3r/FirstLayerPlane.hpp
Normal file
@@ -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_
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "Time.hpp"
|
||||
#include "GCode/ExtrusionProcessor.hpp"
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
@@ -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<FirstLayerPlane>(print.config());
|
||||
if (auto *belt_writer = dynamic_cast<BeltGCodeWriter*>(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<double>(bb.min.x()), unscale<double>(bb.min.y())),
|
||||
Vec2d(unscale<double>(bb.max.x()), unscale<double>(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<double>(path.first_point().x()),
|
||||
unscale<double>(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<const ExtrusionPathSloped*>(&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<ProcessedPoint> 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");
|
||||
|
||||
@@ -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<CoolingBuffer> m_cooling_buffer;
|
||||
std::unique_ptr<SpiralVase> 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<FirstLayerPlane> m_first_layer_plane;
|
||||
|
||||
std::unique_ptr<PressureEqualizer> 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;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#include "../GCode.hpp"
|
||||
#include "../FirstLayerPlane.hpp"
|
||||
#include "CoolingBuffer.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <float.h>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -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<PerExtruderAdjustments> 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<value>
|
||||
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<long>(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
|
||||
|
||||
@@ -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<PerExtruderAdjustments> &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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1017,6 +1017,7 @@ static std::vector<std::string> 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",
|
||||
|
||||
@@ -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<FirstLayerPlaneMode>::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>(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);
|
||||
|
||||
@@ -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<RemapAxis>, gcode_remap_z))
|
||||
((ConfigOptionBool, gcode_back_transform))
|
||||
((ConfigOptionBool, belt_preslice_global))
|
||||
((ConfigOptionEnum<FirstLayerPlaneMode>, 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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user