Fix belt brim emission: dropped first-contact bands, tool selection, inner-only predicate (A,B,C,D)

- Emit coincident belt_brim_by_layer bands even when the leading object layer
  has no InstanceVisit (zero-extrusion lead-in / no coinciding support), so the
  brim at first belt contact is no longer dropped.
- Register each coincident band's brim filament in ToolOrdering and emit each
  band exactly once, in its brim-filament pass; emit ordinary-layer aprons in
  the brim pass before object extrusion (correct tool, brim-first) instead of
  with whatever tool was active.
- has_belt_brim(): inner-only brims need brim_width>0 (leading/extra produce no
  inner geometry), fixing spurious prime-tower/spiral rejection; mirror in
  wants_brim. Single-extruder/single-object output is unchanged except
  previously-dropped bands now print.
This commit is contained in:
harrierpigeon
2026-08-06 15:29:40 -05:00
parent c1a90fc451
commit 1bd3404c03
5 changed files with 175 additions and 24 deletions

View File

@@ -5891,16 +5891,12 @@ LayerResult GCode::process_layer(
//BBS: set layer time fan speed after layer change gcode
gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n";
// Belt printers: an apron band prints below its own object's first layer, but with
// several objects on the belt another one can already be printing at this print_z.
// The layer then has an object layer and takes this ordinary path instead of the
// brim-only branch, so the band has to be emitted here or it would be dropped.
// Before any object extrusion at this Z, as the brim must go down first.
if (print.has_belt_brim()) {
const Vec2d saved_origin = m_origin;
gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx);
this->set_origin(saved_origin);
}
// Belt printers: ordinary-layer apron bands (a band whose print_z coincides with an
// object/support layer, so it takes this path rather than the brim-only branch) are
// NOT emitted here anymore. They used to be laid down with whatever tool happened to
// be active; instead they are now emitted inside the extruder loop below, in their
// own brim-filament pass and before that pass's object extrusion, so the brim goes
// down first with the correct tool. See the emit_belt_brim_for_extruder call.
//Calibration Layer-specific GCode
// ORCA-Belt: on belt printers the calibration object is counter-rotated to
@@ -6584,6 +6580,49 @@ LayerResult GCode::process_layer(
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
m_skirt_group_done.resize(print.skirt_brim_groups().size());
// Belt brim bookkeeping. A coincident belt_brim_by_layer band must be emitted
// exactly once, in its object's brim-filament pass; this records which have gone
// down so the in-visit emit and the end-of-layer orphan sweep never double it.
// Key = (LayerToPrint index, instance_id).
std::set<std::pair<size_t, size_t>> belt_brim_emitted;
// Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an
// object/support layer) whose brim filament is this pass's extruder. Mirrors
// emit_belt_brim_bands() per band, but filtered to one brim filament so each band
// prints in the correct tool's pass (Finding B). extruder_id is 0-based (the
// reindexed tool domain); belt_brim_filament() is 1-based, so subtract one.
auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string {
std::string gc;
for (const LayerToPrint &ltp : layers) {
const BeltBrimBand *band = ltp.belt_brim_band;
if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr)
continue;
const PrintObject &object = *ltp.original_object;
if (! object.has_belt_brim() || (unsigned int)(object.belt_brim_filament() - 1) != extruder_id)
continue;
// Speeds, flow and retraction all read m_config.
m_config.apply(print.default_region_config());
m_config.apply(object.config(), true);
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;
for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) {
// Band geometry is object-local, like the object's own extrusions.
const Point &offset = object.instances()[i].shift;
this->set_origin(unscale(offset));
this->on_set_origin(&object, offset);
m_avoid_crossing_perimeters.use_external_mp();
for (const ExtrusionEntity *ee : band->fills.entities)
if (ee != nullptr)
gc += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed));
m_avoid_crossing_perimeters.use_external_mp(false);
m_avoid_crossing_perimeters.disable_once();
}
}
return gc;
};
for (unsigned int extruder_id : layer_tools.extruders)
{
if (print.config().skirt_type == stCombined && !print.skirt_brim_groups().empty()) {
@@ -6681,6 +6720,16 @@ LayerResult GCode::process_layer(
if (layer_tools.has_wipe_tower && m_wipe_tower)
m_last_processor_extrusion_role = erWipeTower;
// Belt printers: now that this pass's tool is selected, lay down any ordinary-layer
// apron band whose brim filament is this extruder, before the object extrusion at
// this Z (brim goes down first, with the correct tool). Restore the origin so the
// object-setup code below is unaffected.
if (print.has_belt_brim()) {
const Vec2d saved_origin = m_origin;
gcode += emit_belt_brim_for_extruder(extruder_id);
this->set_origin(saved_origin);
}
auto &filament_plan = filament_to_print_instances[extruder_id];
std::vector<InstanceToPrint> &instances_to_print = filament_plan.first;
const std::vector<InstanceVisit> &instance_visits = filament_plan.second;
@@ -6697,8 +6746,20 @@ LayerResult GCode::process_layer(
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer,
layer_to_print.object_layer);
const PrintObject &vobj = instance_to_print.print_object;
if (vobj.has_belt_brim()) {
// Coincident belt brim: emit once, only in this object's brim-filament
// pass (extruder_id and belt_brim_filament()-1 are both 0-based here),
// and dedup on the LayerToPrint index (not Layer::id()) so the orphan
// sweep below never re-emits it.
if (extruder_id == (unsigned int)(vobj.belt_brim_filament() - 1) &&
belt_brim_emitted.insert({ instance_to_print.layer_id, instance_to_print.instance_id }).second)
gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer,
layer_to_print.object_layer);
} else {
gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer,
layer_to_print.object_layer);
}
}
// To control print speed of the 1st object layer printed over raft interface.
@@ -6886,6 +6947,37 @@ LayerResult GCode::process_layer(
}
}
}
// Belt brim orphan sweep (Finding C). A coincident belt_brim_by_layer band lives on
// an object layer, but that layer can yield no InstanceVisit above - a zero-extrusion
// lead-in slice with no coinciding support - so the in-visit emit never fired and the
// band would be dropped. Emit any such band exactly once here, keyed the same way as
// the in-visit emit so already-printed bands are skipped. These orphan layers carry
// no object material, so ending on the brim's position is harmless; we still save and
// restore m_origin, and only toolchange when the brim filament differs from the active
// one - a no-op on single-extruder prints, keeping their output unchanged.
if (print.has_belt_brim()) {
const Vec2d saved_origin = m_origin;
for (const LayerToPrint &ltp : layers) {
const PrintObject *obj = ltp.original_object;
if (obj == nullptr || ! obj->has_belt_brim() || ltp.object_layer == nullptr)
continue;
const size_t ltp_idx = size_t(&ltp - layers.data());
const unsigned int brim0 = (unsigned int)(obj->belt_brim_filament() - 1);
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) ? obj->instances().size()
: single_object_instance_idx + 1;
for (size_t instance_id = i_begin; instance_id < i_end && instance_id < obj->instances().size(); ++ instance_id) {
if (! belt_brim_emitted.insert({ ltp_idx, instance_id }).second)
continue;
if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim0)
gcode += this->set_extruder(brim0, print_z);
gcode += generate_object_brim(print, *obj, instance_id, first_layer, ltp.object_layer);
}
}
this->set_origin(saved_origin);
}
if (first_layer) {
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
if (!iter->second.empty())

View File

@@ -876,14 +876,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
// push. Deliberately not layer_tools.has_object, which drives skirt marking
// and wiping overrides.
if (! object.belt_brim_prologue().empty()) {
unsigned int brim_filament = 0;
for (size_t i = 0; i < object.num_printing_regions(); ++ i) {
const unsigned int f = object.printing_region(i).config().outer_wall_filament_id.value;
if (f > 0 && (brim_filament == 0 || f < brim_filament))
brim_filament = f;
}
if (brim_filament == 0)
brim_filament = 1;
// 1-based, same domain the object/support pushes above use; reindexed to 0-based
// with the rest of the list later.
const unsigned int brim_filament = object.belt_brim_filament();
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
if (band.fills.empty())
continue;
@@ -893,6 +888,25 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
}
// Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than
// below it, but that layer can produce no InstanceVisit in process_layer - a
// zero-extrusion lead-in slice with no coinciding support - and the band would then
// be silently dropped. Register the brim filament on every layer that carries a
// coincident band, in the same 1-based domain as the prologue push above, so a brim
// pass always exists there.
if (object.has_belt_brim()) {
const unsigned int brim_filament = object.belt_brim_filament();
const auto &by_layer = object.belt_brim_by_layer();
const size_t n = std::min(by_layer.size(), object.layers().size());
for (size_t i = 0; i < n; ++ i) {
if (by_layer[i].empty())
continue;
LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z);
layer_tools.extruders.push_back(brim_filament);
layer_tools.has_belt_brim = true;
}
}
for (auto& layer : m_layer_tools) {
// Sort and remove duplicates
sort_remove_duplicates(layer.extruders);
@@ -940,6 +954,14 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
// below the object's first layer, and treating those layers as raft would put a
// wipe tower at negative Z. Belt brim and the prime tower are mutually
// exclusive (rejected in Print::validate()), so simply drop the clause there.
//
// Gate on config.belt_printer, NOT on has_belt_brim: every layer below the
// object bottom on a belt printer is legitimately a sub-object stream - brim
// apron, belt support printed below Z0, or the object's own lead-in - and none of
// them is ever raft, because Print::validate() rejects raft_layers>0 on a belt
// printer outright. Narrowing this to has_belt_brim would reclassify
// belt-support-below-floor layers as raft on brim-less belt prints and reintroduce
// the negative-Z wipe tower, so the broad belt_printer gate is correct.
const bool belt_no_raft_gap = config.belt_printer.value;
for (LayerTools &lt : m_layer_tools)
lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))

View File

@@ -1374,9 +1374,15 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
for (const PrintObject *object : m_objects) {
const PrintObjectConfig &ocfg = object->config();
// Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive
// brim_width (leading/extra widen only the outer ring), so keep this
// predicate in step or the belt-brim warnings below would fire for a brim
// that has_belt_brim() rejects.
const bool wants_brim = ocfg.brim_type != btNoBrim
&& (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|| ocfg.extra_brim_width.value > 0.);
&& (ocfg.brim_type == btInnerOnly
? ocfg.brim_width.value > 0.
: (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|| ocfg.extra_brim_width.value > 0.));
if (! wants_brim)
continue;

View File

@@ -392,6 +392,12 @@ public:
// trimming and the spiral vase probe, and widening it would perturb belt
// support output.
bool has_belt_brim() const;
// Brim filament for this object's belt brim, returned in the 1-based domain of
// PrintRegion::outer_wall_filament_id and the raw values pushed into
// LayerTools::extruders in ToolOrdering::collect_extruders (ToolOrdering reindexes
// the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id
// over the printing regions, 1 if none is explicitly set.
unsigned int belt_brim_filament() const;
// False when this object's instances sit at different points ALONG the belt, which
// would need a separate set of bands each. Public so validate() can explain it.
bool belt_brim_instances_compatible() const;

View File

@@ -1171,12 +1171,37 @@ bool PrintObject::has_belt_brim() const
return false;
if (m_config.brim_type == btNoBrim)
return false;
if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0.
&& m_config.extra_brim_width.value <= 0.)
// An inner-only brim has no leading/extra geometry: leading_brim_length and
// extra_brim_width both widen the OUTER ring, which btInnerOnly never emits, so it
// produces nothing unless brim_width itself is positive. Every other brim type is
// satisfied by any one of the three widths. Requiring the width here (instead of
// "any width") stops has_belt_brim() - and therefore Print::validate() - from
// rejecting the prime tower / spiral vase for a brim that would never be drawn.
if (m_config.brim_type == btInnerOnly) {
if (m_config.brim_width.value <= 0.)
return false;
} else if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0.
&& m_config.extra_brim_width.value <= 0.) {
return false;
}
return ! this->has_raft();
}
unsigned int PrintObject::belt_brim_filament() const
{
// 1-based, matching PrintRegion::outer_wall_filament_id and the raw values pushed
// into LayerTools::extruders in ToolOrdering::collect_extruders (the whole list is
// reindexed to 0-based later). Lowest positive outer-wall filament over the
// printing regions; 1 when none is explicitly set.
unsigned int brim_filament = 0;
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
const unsigned int f = this->printing_region(i).config().outer_wall_filament_id.value;
if (f > 0 && (brim_filament == 0 || f < brim_filament))
brim_filament = f;
}
return brim_filament == 0 ? 1u : brim_filament;
}
bool PrintObject::belt_brim_instances_compatible() const
{
// One set of bands is shared by every instance of this object, so they must all sit at