Belt printers: brim laid onto the tilted belt, with a leading apron

A belt printer slices in a rotated frame, so the belt surface is a tilted
plane rather than the Z=0 bed plane.  Each slicing layer touches the belt
only along a narrow strip at its leading edge - about 0.2mm at 45 degrees -
so a part's first layer is really a first line, with almost no contact patch
to hold it down while the belt drags it forward.  Brim was hard-disabled on
belt printers, leaving no remedy at all.

Generate the brim on the belt plane instead.  The object's belt footprint is
the union over layers of each slice clipped to that layer's contact band; the
brim is offset from it in a "flattened" frame where the shear axis is
stretched by 1/cos(tilt), so ordinary Clipper offsets measure true on-belt
distance.  It is emitted as cross-belt lines, one per layer band, anchored to
a fixed fraction of the band so every line shares a nozzle-to-belt clearance
and therefore comes out the same width; flow is matched to the resulting band
pitch, keeping the sheet uniform and gap-free.

Three new controls, all belt-only:

  * Leading brim length - extends the brim ahead of the part along the belt,
    on every downhill-facing edge of its contact area.  This apron necessarily
    prints BELOW the object's first layer, since layer 0 is the part's leading
    contact, so it needs brim-only bands of its own.
  * Extra brim width - widens the brim sideways across the belt only.
  * Brim type "Leading edge only" - brim at the part's first belt contact and
    nothing after it.  Appended last in BrimType so no existing value shifts;
    degrades to an outer brim off belt printers, with a warning.

The apron bands are lightweight records rather than a Layer subclass, so no
fabricated Layer::id() can leak into initial-layer temperature selection, the
spiral vase probe, cooling or gradual interpolation.  They are generated in
posSupportMaterial because their print_z values must exist before ToolOrdering
is built at psWipeTower, and they are emitted from a short dedicated branch in
process_layer that runs before any layer pointer is dereferenced.

The footprint is closed before offsetting outwards: a belt contact patch is
often a broken-up strip, and the merged offset rings of two islands closer
than 2 x brim_width would otherwise fill the space between them - space that
lies under the part.

Also fixes a pre-existing bug where PrintObject::get_first_layer_bbox()
overwrote a valid bbox with an unassigned one on any belt printer with a brim
configured, because has_brim() was true while make_brim() returned early.

Belt brim is refused alongside the prime tower and spiral vase, and requires
one instance per PrintObject - translating an instance along the belt axis
changes its physical belt-floor Z.  Untilted belt printers are unchanged: they
still get no brim, since the plate brim is emitted out of skirt_brim_groups(),
which _make_skirt() never builds for a belt printer.
This commit is contained in:
harrierpigeon
2026-08-06 01:08:44 -05:00
parent 386364f84b
commit 55b4dca9bc
24 changed files with 1744 additions and 31 deletions
+36 -9
View File
@@ -630,11 +630,25 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
const bool gcf_is_klipper = gcflavor == GCodeFlavor::gcfKlipper;
// Belt printer: detect early since it affects multiple toggle decisions below.
// `is_belt_tilted` is the stricter test that mirrors PrintObject::has_belt_brim():
// only a tilted belt gets the belt-plane brim, while a belt printer with no
// rotation is geometrically a flat bed and keeps the ordinary plate brim.
bool is_belt_printer = false;
bool is_belt_tilted = false;
{
const auto *belt_opt = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
const auto &printer_cfg = preset_bundle->printers.get_edited_preset().config;
const auto *belt_opt = printer_cfg.option<ConfigOptionBool>("belt_printer");
if (belt_opt)
is_belt_printer = belt_opt->value;
const auto *axis = printer_cfg.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto *angle = printer_cfg.option<ConfigOptionFloat>("belt_slice_rotation_angle");
if (is_belt_printer && axis != nullptr && angle != nullptr) {
// Same window as Print::has_tilted_belt(); shared constants so the GUI and
// the backend cannot drift apart.
const double tilt = std::abs(angle->value);
is_belt_tilted = (axis->value == BeltRotationAxis::X || axis->value == BeltRotationAxis::Y)
&& tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG;
}
}
bool have_volumetric_extrusion_rate_slope = config->option<ConfigOptionFloat>("max_volumetric_extrusion_rate_slope")->value > 0;
@@ -808,21 +822,34 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_field("skirt_height", false);
}
bool have_brim = (config->opt_enum<BrimType>("brim_type") != btNoBrim) && !is_belt_printer;
if (is_belt_printer)
toggle_field("brim_type", false);
// Belt printers now get a brim too, laid onto the tilted belt by BeltBrim.cpp,
// so brim type / width / object gap all apply. A belt printer with no tilt is
// geometrically a flat bed and uses the ordinary plate brim, hence the separate
// is_belt_tilted test.
bool have_brim = config->opt_enum<BrimType>("brim_type") != btNoBrim;
toggle_field("brim_object_gap", have_brim);
toggle_field("brim_use_efc_outline", have_brim);
toggle_field("combine_brims", have_brim);
bool have_brim_width = (config->opt_enum<BrimType>("brim_type") != btNoBrim) && config->opt_enum<BrimType>("brim_type") != btAutoBrim &&
// Both are first-layer-only concepts that the belt path cannot honour.
toggle_field("brim_use_efc_outline", have_brim && !is_belt_tilted);
toggle_field("combine_brims", have_brim && !is_belt_tilted);
bool have_brim_width = have_brim && config->opt_enum<BrimType>("brim_type") != btAutoBrim &&
config->opt_enum<BrimType>("brim_type") != btPainted;
toggle_field("brim_width", have_brim_width);
// On a tilted belt Auto / Mouse ear / Painted all collapse to outer-only at the
// configured width, so the width field has to stay live for them too.
toggle_field("brim_width", have_brim_width || (have_brim && is_belt_tilted));
toggle_field("brim_flow_ratio", have_brim);
// Paired toggle_line + toggle_field: cb_toggle_line is null in the per-object
// override panel, so the row cannot be hidden there and greying out is the
// fallback. Both extras are belt-only: one extends the brim ahead along the belt,
// the other widens it across the belt.
for (auto el : { "leading_brim_length", "extra_brim_width" }) {
toggle_line(el, is_belt_tilted);
toggle_field(el, is_belt_tilted && have_brim);
}
// Wall filament selectors use the same logic as in Print::extruders().
toggle_field("outer_wall_filament_id", have_perimeters || have_brim);
toggle_field("inner_wall_filament_id", have_perimeters || have_brim);
bool have_brim_ear = (config->opt_enum<BrimType>("brim_type") == btEar);
bool have_brim_ear = (config->opt_enum<BrimType>("brim_type") == btEar) && !is_belt_tilted;
const auto brim_width = config->opt_float("brim_width");
// disable brim_ears_max_angle and brim_ears_detection_length if brim_width is 0
toggle_field("brim_ears_max_angle", brim_width > 0.0f);