Belt printer: address the review on #14394

Code review items (raistlin7447):

1. PrintObject::slice() zeroes m_belt_min_z, m_belt_global_z_offset and
   m_belt_global_xy_correction before slicing. They were only written in belt
   mode, so a project switched to a normal printer, or whose tilt axis was set
   to None, kept the old offsets and shifted the adaptive infill octree and the
   organic support layers by them.
2. TreeModelVolumes shifts the support blockers into the raft-offset index
   space; a test now pins the index the blocker lands on.
3. The final-alignment clamp in libnest2d is opt-in (NfpPConfig::clamp_to_bin)
   and arrange sets it for belt printers only. Printers with an off-centre
   best_object_pos keep their alignment; a flat-bed test pins that.
4. The preview's belt view follows the loaded G-code, not the selected printer:
   GCodeProcessor carries the file's belt keys (and, for a belt file, its bed)
   into export_config_for_render(), and GCodeViewer enables the belt view from
   the header tilt.
5. belt_shift_layer_grid() also shifts the cached belt floor and the global Z
   offset, so a support-only or brim-only change after the purge-prism snap
   matches a fresh slice.
6. update_print_fff_config() resets raft_layers and draft_shield on a belt
   printer instead of only greying out the fields Print::validate() rejects.
7. GCodeWriter takes a first-layer point test instead of the FirstLayerPlane;
   GCode installs one that measures from the belt surface, like its
   extrusions, so the first-layer travel speed and the second-layer
   temperature change no longer depend on the gcode_remap_* convention.
8. belt_brim_clip_leading_edge() is exported and called by both the generator
   and the test.
9. Both phong.vs shaders use slope.up_direction for the overhang highlight.

The pre-slice and G-code axis remaps are gated on belt_printer through
BeltTransformPipeline::axis_remap_enabled(), so belt keys left in a profile
cannot change a non-belt print.

Tests requested in the review: belt-only keys at non-default values leave
non-belt G-code unchanged; switching a sliced project from belt to non-belt
(and tilt axis None) matches a fresh slice; a support-only change on a belt
purge print matches a fresh slice; non-belt start G-code moves keep the
first-layer Z in the processor; the belt brim's segment count catches a band
emitted twice.

The belt-to-non-belt test exposed an unrelated gap: invalidate_step(posSlice)
re-invalidated posSupportMaterial but not posSimplifySupportPath, so after
any re-slice the regenerated support paths were exported unsimplified.
posSimplifySupportPath is now in that list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
harrierpigeon
2026-10-05 23:06:01 -05:00
co-authored by Claude Fable 5.1
parent a3dea04ade
commit 61a0db4a87
24 changed files with 680 additions and 95 deletions
+61 -15
View File
@@ -107,6 +107,7 @@
#include "calib.hpp"
#include "libslic3r_version.h"
#include "GCode/BeltKinematics.hpp"
#include "FirstLayerPlane.hpp"
// Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332.
// We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB.
#if ! defined(TBB_VERSION_MAJOR)
@@ -3133,15 +3134,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->init_belt_writer(print);
m_writer.set_is_bbl_machine(is_bbl_printers);
// Standalone axis remap (works with or without belt mode).
// Sync the writer's remap state to the current export UNCONDITIONALLY — even at
// the identity mapping (0,1,2) — so a reused writer never retains a stale
// non-identity mapping from a prior export. has_axis_remap() returns false at
// identity, so identity/default output stays unchanged.
// G-code axis remap. Only belt printers get one (see
// BeltTransformPipeline::axis_remap_enabled): a remap left in a profile must
// not change a non-belt print. Sync the writer's remap state to the current
// export UNCONDITIONALLY — even at the identity mapping (0,1,2) — so a reused
// writer never retains a stale non-identity mapping from a prior export.
// has_axis_remap() returns false at identity, so identity/default output stays
// unchanged.
{
int rx = int(print.config().gcode_remap_x.value);
int ry = int(print.config().gcode_remap_y.value);
int rz = int(print.config().gcode_remap_z.value);
const bool remap = BeltTransformPipeline::axis_remap_enabled(print.config());
int rx = remap ? int(print.config().gcode_remap_x.value) : int(RemapAxis::PosX);
int ry = remap ? int(print.config().gcode_remap_y.value) : int(RemapAxis::PosY);
int rz = remap ? int(print.config().gcode_remap_z.value) : int(RemapAxis::PosZ);
m_writer.set_axis_remap(rx, ry, rz);
BoundingBoxf bbox_bed(print.config().printable_area.values);
m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
@@ -3153,13 +3157,21 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// 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());
// Belt writers only: the plane also switches travel-speed selection to be
// per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not
// change for non-belt printers.
// Belt writers only: travel-speed selection becomes per-point (see
// GCodeWriter::uses_pointwise_travel_speed()), which must not change for
// non-belt printers. The writer gets the same test the extrusions use, so a
// travel is judged against the belt surface (belt_height_above_floor) exactly
// like the path it leads to, and not against the FirstLayerPlane, which
// misreports the height under a non-identity gcode_remap_*. Writer points
// carry the G-code origin and extruder offset that point_to_gcode() added;
// the belt surface is described in the object's own frame.
if (print.config().belt_printer.value) {
m_writer.set_first_layer_plane(
m_first_layer_plane.get(),
print.config().initial_layer_print_height.value);
m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) {
const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero();
return this->on_first_layer(Vec3d(point_logical.x() - m_origin.x() + extruder_offset.x(),
point_logical.y() - m_origin.y() + extruder_offset.y(),
point_logical.z()));
});
}
// How many times will be change_layer() called?
@@ -6295,7 +6307,11 @@ LayerResult GCode::process_layer(
// 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()) {
if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0) {
// Belt surface known for this object: measured from the belt itself, as the
// extrusions and travels are, rather than through FirstLayerPlane.
past_first_layer_band = past > 0;
} else if (m_first_layer_plane && m_first_layer_plane->is_active()) {
past_first_layer_band = false;
if (object_layer != nullptr) {
// Conservatively walk the layer's lslice bboxes; if every bbox's
@@ -10617,6 +10633,36 @@ std::string GCode::set_object_info(Print *print) {
return gcode.str();
}
// Whether an object layer lies entirely past the first-layer band above the belt:
// 1 when its lowest point is at least one band thickness above the belt, 0 when
// any of it is inside the band, -1 when the belt surface is not known for this
// layer (not a belt print, an explicit first-layer plane, or no object layer), in
// which case the caller falls back to FirstLayerPlane / the slicing layer index.
int GCode::belt_layer_past_first_layer_band(const Layer *object_layer) const
{
if (object_layer == nullptr)
return -1;
// The belt surface is linear in the sliced XY, so a bbox's lowest point above
// it is at one of its corners.
double min_height = std::numeric_limits<double>::max();
bool known = false;
for (const BoundingBox &bb : object_layer->lslices_bboxes) {
const double xs[2] = { unscale<double>(bb.min.x()), unscale<double>(bb.max.x()) };
const double ys[2] = { unscale<double>(bb.min.y()), unscale<double>(bb.max.y()) };
for (double x : xs)
for (double y : ys) {
double h;
if (! this->belt_height_above_floor(Vec3d(x, y, object_layer->print_z), h))
return -1;
known = true;
min_height = std::min(min_height, h);
}
}
if (! known)
return -1;
return min_height >= this->first_layer_band_mm() - EPSILON ? 1 : 0;
}
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