From 1b6fb2a81f2c14fc1ddce598d0a9dd2897d3ce9f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:00:38 -0500 Subject: [PATCH] Belt: fix first-layer speed and the slow_down_layers ramp never applying Fixes the report in #12998 (comment 5465250754): first-layer speed and the slow_down_layers ramp were ignored on a belt printer. The report reads as a per-object problem, but neither applied to *any* object -- the reporter's first part slowed down because slow_down_for_layer_cooling was on, which is CoolingBuffer's time-per-layer mechanism, not initial_layer_speed. FirstLayerPlane decides first-layer-ness by perpendicular distance to a plane it derives by composing gcode_remap_* with compute_machine_z_affine(). The plane is therefore a function of how G-code is *addressed*, not of where the belt is: change the output axis convention and the plane moves. On MCBELT-TYPE2 the first layer measured 86.2 mm from the plane and got effective index 431, far past any slow_down_layers ramp. on_first_layer(point) and effective_layer_index_for_point() now measure height above the belt surface, using the belt description already carried in SlicingParameters -- belt_floor_shear_factor / belt_floor_from_axis / belt_floor_z_shift -- the same description the support generator uses. That is a property of how the object was sliced, so no remap or back-transform can perturb it. Deliberately not via BeltFloorContext: its init() folds in belt_support_floor_offset, a support-generator diagnostic, and letting that option steer the model's first-layer speed band would be a surprising coupling (a negative value would switch the slowdown off outright). Preserving the existing first-layer-plane settings: * first_layer_plane XY/YZ/XZ keeps the FirstLayerPlane evaluator, as those are explicit opt-outs. * A non-zero first_layer_plane_offset also keeps it. The offset is a machine-Z shift that FirstLayerPlane converts into a perpendicular distance in the slicing frame; this evaluator measures along slicing Z, so there is no faithful translation. Deferring to the evaluator that implements the setting beats silently ignoring it. * The two thresholds stay separate, exactly as FirstLayerPlane keeps them: the first-layer boolean tests initial_layer_print_height, while the effective layer index counts bands of first_layer_plane_thickness. Brim and coincident apron bands are emitted before m_layer is switched to their object -- for an apron band there is no Layer at all -- so both paths publish the belt-floor owner explicitly. Without that a brim's classification would borrow whichever object was visited previously, making it depend on plate order. Note that first-layer-ness drives more than speed: extrusion acceleration, jerk, the first-layer flow ratio and eligibility for overhang speed/fan analysis all read it, so all of them are corrected on belt printers by this change. Classification still samples only each path's first point, as it did before. Non-belt is unaffected by construction: belt_height_above_floor() returns false when the belt floor is inactive and both call sites fall back to the previous path. FirstLayerPlane stays in place for its other modes and for CoolingBuffer, whose machine-coordinate probe is a separate outstanding bug. Measured, MCTEST4 on MCBELT-TYPE2 (initial_layer_speed=5, slow_down_layers=40): 15 distinct feedrates with no gradient and F300 absent, becomes 70 including the full ramp 300(5) 382(6) 465(8) 630(10) 795(13) ... Two bare cubes on a belt: 0 slow extrusions becomes 2378 across Z 32.36..95.18. The same two cubes on a Cartesian printer keep their slow extrusions confined to Z 0.20..2.00. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/GCode.cpp | 50 +++++++++++++++++++++++++++++++++++++++++ src/libslic3r/GCode.hpp | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 32976fad76..fff437731f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6,6 +6,7 @@ #include "libslic3r.h" #include "I18N.hpp" #include "GCode.hpp" +#include #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" @@ -5234,6 +5235,10 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o // geometry is already in plate coordinates. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // m_layer is not switched to this object until after brim emission, so name + // the belt-floor owner explicitly or the classification borrows whichever + // object was visited last. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const Point &offset = object.instances()[instance_id].shift; this->set_origin(unscale(offset)); this->on_set_origin(&object, offset); @@ -6642,6 +6647,9 @@ LayerResult GCode::process_layer( // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // Apron bands have no Layer at all (m_layer is null here), so the belt + // floor owner has to be named the same way the object brim names it. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() : single_object_instance_idx + 1; @@ -10307,6 +10315,48 @@ std::string GCode::set_object_info(Print *print) { return gcode.str(); } +bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const +{ + // The owning object, which is what carries the belt description. During + // object-brim and coincident-apron emission m_layer still points at whichever + // object was visited last (or at nothing at all), so those paths publish the + // owner explicitly -- otherwise a brim's speed would depend on plate order. + const PrintObject *object = m_belt_floor_object != nullptr ? m_belt_floor_object + : (m_layer != nullptr ? m_layer->object() : nullptr); + if (object == nullptr) + return false; + // Respect an explicit first-layer-plane choice: only Auto and BeltAffine mean + // "use the belt". A user who selected XY, YZ or XZ has asked for the + // FirstLayerPlane evaluator and must keep it. + const FirstLayerPlaneMode mode = m_config.first_layer_plane.value; + if (mode != FirstLayerPlaneMode::Auto && mode != FirstLayerPlaneMode::BeltAffine) + return false; + // Likewise for a dialled-in plane offset. It is expressed as a machine-Z + // shift that FirstLayerPlane converts into a perpendicular distance in the + // slicing frame; this evaluator measures along slicing Z instead, so there is + // no faithful translation of it here. Honour the user's setting by deferring + // to the evaluator that implements it rather than silently dropping it. + if (std::abs(m_config.first_layer_plane_offset.value) > EPSILON) + return false; + + const SlicingParameters &sp = object->slicing_parameters(); + // Deliberately NOT BeltFloorContext: its init() folds in + // belt_support_floor_offset, a support-generator diagnostic. Letting that + // option move the model's first-layer speed band would be a surprising + // coupling -- a negative value would switch the slowdown off entirely. + // The belt surface itself is just shear * u + z_shift. + if (std::abs(sp.belt_floor_shear_factor) < EPSILON) + return false; + const double u = sp.belt_floor_from_axis == 0 ? point_slicing_mm.x() : point_slicing_mm.y(); + const double floor_z = sp.belt_floor_shear_factor * u + sp.belt_floor_z_shift; + // Measured along the slicing Z, not perpendicular to the belt: layers are + // horizontal slabs in the sliced frame, so the slab holding the material that + // rests on the belt at this point is the one within one layer height of it. + // A perpendicular measure would shrink the band by 1/cos(tilt). + height_mm = point_slicing_mm.z() - floor_z; + return true; +} + // convert a model-space scaled point into G-code coordinates Vec2d GCode::point_to_gcode(const Point &point) const { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 26b8bc78a5..c78a9675e2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -831,6 +831,18 @@ protected: // Counter standing in for Layer::id() on apron layers, which precede layer 0. size_t m_belt_brim_layer_idx{0}; + // Belt brim only. Brim and coincident apron bands are emitted before m_layer + // is switched to their object, so belt_height_above_floor() would otherwise + // read the previously visited object's belt description -- making a brim's + // classification depend on plate visiting order. Those paths publish the + // owner here for the duration of the emission. Never left set. + const PrintObject *m_belt_floor_object{nullptr}; + struct BeltFloorObjectGuard { + const PrintObject *&slot; + BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; } + ~BeltFloorObjectGuard() { slot = nullptr; } + }; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS @@ -853,6 +865,12 @@ protected: // otherwise we delegate to the legacy per-layer test. This is the // entry point used by per-path call sites in _extrude. bool on_first_layer(const Vec3d &point_slicing_mm) const { + // Belt printers: measure height above the belt surface itself, in the + // slicing frame. See belt_height_above_floor() for why this does not go + // through FirstLayerPlane. + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) + return h <= m_config.initial_layer_print_height.value + EPSILON; if (m_first_layer_plane && m_first_layer_plane->is_active()) return m_first_layer_plane->is_first_layer( point_slicing_mm, m_config.initial_layer_print_height.value); @@ -863,10 +881,40 @@ protected: // 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 { + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) { + const double lh = this->first_layer_band_mm(); + return h <= 0. ? 0 : int(std::floor(h / lh)); + } if (m_first_layer_plane && m_first_layer_plane->is_active()) return m_first_layer_plane->effective_layer_index(point_slicing_mm); return on_first_layer() ? 0 : layer_id(); } + + // Band thickness for the *effective layer index* only. FirstLayerPlane keeps + // two separate thresholds and so must this path: is_first_layer() tests + // against initial_layer_print_height, while effective_layer_index() counts + // bands of first_layer_plane_thickness. Conflating them would apply + // first-layer treatment through a whole 1mm band on a 0.2mm first layer. + double first_layer_band_mm() const { + double band = m_config.first_layer_plane_thickness.value; + if (band <= 0.) band = m_config.initial_layer_print_height.value; + return band > 0. ? band : 0.2; + } + + // Height of a slicing-frame point above the belt surface, or false when this + // is not a belt print. + // + // The belt surface is known exactly in the slicing frame from the slicing + // parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same + // description the support generator uses. FirstLayerPlane instead derives its + // plane by composing gcode_remap_* with the g-code back-transform, so its + // answer changes with the machine's *output* axis convention: on a printer + // with a non-identity remap it reported ~86mm of clearance for geometry + // sitting directly on the belt, and no extrusion was ever classified as + // first-layer. Measuring against the belt itself is independent of every + // remap and back-transform. + bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const; int layer_id() const { if (m_layer == nullptr) return -1;