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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
This commit is contained in:
harrierpigeon
2026-09-08 23:59:02 -05:00
co-authored by Claude Opus 5
parent 767db71500
commit 1b6fb2a81f
2 changed files with 98 additions and 0 deletions
+50
View File
@@ -6,6 +6,7 @@
#include "libslic3r.h"
#include "I18N.hpp"
#include "GCode.hpp"
#include <cstdio>
#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
{