diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 40ecc60f58..fa119c6609 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -324,24 +324,46 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, } } -// Union of everything actually extruded at `print_z` by any object, expressed in -// `self`'s local slicing frame. Includes `self` itself: its slice at this Z can -// overhang outside the belt footprint and land in the brim ring, which the -// flattened brim_object_gap - a belt-plane separation - does not cover. -static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, coordf_t print_z, coordf_t tol) +// Union of everything extruded at `print_z` that the brim must keep clear of, expressed +// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can +// overhang outside the belt footprint and land in the brim ring, which the flattened +// brim_object_gap - a belt-plane separation - does not cover. +// +// THREADING: this runs inside posSupportMaterial, which Print::process() executes for all +// objects in a tbb::parallel_for (Print.cpp). Object slices are finished by then and safe +// to read across objects, but SUPPORT layers are not: another object's thread may be +// inside clear_support_layers() - which deletes the SupportLayer pointers - right now, so +// touching a foreign object's support_layers() here is a use-after-free. Only this +// object's own supports are consulted; they are complete, because make_belt_brim() runs at +// the tail of this object's own generate_support_material(). The cost is that the brim +// does not dodge a *different* object's support at the same Z, which needs the objects to +// overlap in the belt direction in the first place. +// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole +// objects are skipped without materialising their polygons. On a typical plate the +// objects do not overlap and every foreign object drops out here, which matters because +// this runs once per band - hundreds of times per object. +static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, + const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol) { const Point shift_self = self.instances().empty() ? Point(0, 0) : self.instances().front().shift_without_plate_offset(); Polygons out; - for (const PrintObject *o : print.objects()) + for (const PrintObject *o : print.objects()) { + const bool is_self = (o == &self); for (const PrintInstance &inst : o->instances()) { const Point delta = inst.shift_without_plate_offset() - shift_self; if (const Layer *l = o->get_layer_at_printz(print_z, tol)) { - Polygons ps = to_polygons(l->lslices); - for (Polygon &p : ps) - p.translate(delta); - polygons_append(out, std::move(ps)); + BoundingBox lb = get_extents(l->lslices); + lb.translate(delta.x(), delta.y()); + if (lb.overlap(region_bbox)) { + Polygons ps = to_polygons(l->lslices); + for (Polygon &p : ps) + p.translate(delta); + polygons_append(out, std::move(ps)); + } } + if (! is_self) + continue; if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) { Polygons ps = sl->support_fills.polygons_covered_by_spacing(); for (Polygon &p : ps) @@ -349,6 +371,9 @@ static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self, polygons_append(out, std::move(ps)); } } + } + if (out.size() < 2) + return out; // union_() of 0 or 1 polygons is pure overhead return union_(out); } @@ -453,7 +478,7 @@ void make_belt_brim(PrintObject &object) std::vector areas_by_layer(nlayers); for (size_t i = 0; i < nlayers; ++ i) { const Layer &layer = *object.layers()[i]; - const Polygons obstacles = belt_brim_obstacles(print, object, layer.print_z, 0.5 * layer.height); + const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height); belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]); } @@ -473,7 +498,7 @@ void make_belt_brim(PrintObject &object) + bc.ctx.floor_offset() + bc.ctx.z_shift(); if (h > EPSILON) for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) { - const Polygons obstacles = belt_brim_obstacles(print, object, z, 0.5 * h); + const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h); BeltBrimBand band; band.print_z = z; band.height = h; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f2caca404f..5b76f53ab5 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2125,22 +2125,20 @@ std::vector GCode::collect_layers_to_print(const PrintObjec std::vector> warning_ranges; - // Belt printers: the brim apron is stuck to the belt AHEAD of the part, which - // on a tilted belt means below the object's first layer. Those bands carry no - // object or support layer, so they are emitted first and handled by - // process_layer()'s brim-only branch. Already ordered lowest print_z first. - for (const BeltBrimBand &band : object.belt_brim_prologue()) { - LayerToPrint prologue_layer; - prologue_layer.belt_brim_band = &band; - prologue_layer.original_object = &object; - layers_to_print.push_back(prologue_layer); - } - // Pair the object layers with the support layers by z. + // + // Belt printers add a third stream: brim apron bands, which sit on the belt AHEAD of + // the part and so print below the object's first layer. They are merged here rather + // than pushed as standalone records, because a band's print_z can coincide with a + // support layer of this same object - and the print-wide merge downstream keeps only + // one record per object per z, so a standalone band would be silently overwritten. size_t idx_object_layer = 0; size_t idx_support_layer = 0; + size_t idx_brim_band = 0; + const auto &brim_bands = object.belt_brim_prologue(); // ordered by ascending print_z const LayerToPrint* last_extrusion_layer = nullptr; - while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()) { + while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size() + || idx_brim_band < brim_bands.size()) { LayerToPrint layer_to_print; double print_z_min = std::numeric_limits::max(); if (idx_object_layer < object.layers().size()) { @@ -2153,6 +2151,11 @@ std::vector GCode::collect_layers_to_print(const PrintObjec print_z_min = std::min(print_z_min, layer_to_print.support_layer->print_z); } + if (idx_brim_band < brim_bands.size()) { + layer_to_print.belt_brim_band = &brim_bands[idx_brim_band++]; + print_z_min = std::min(print_z_min, layer_to_print.belt_brim_band->print_z); + } + if (layer_to_print.object_layer && layer_to_print.object_layer->print_z > print_z_min + EPSILON) { layer_to_print.object_layer = nullptr; --idx_object_layer; @@ -2163,11 +2166,17 @@ std::vector GCode::collect_layers_to_print(const PrintObjec --idx_support_layer; } + if (layer_to_print.belt_brim_band && layer_to_print.belt_brim_band->print_z > print_z_min + EPSILON) { + layer_to_print.belt_brim_band = nullptr; + --idx_brim_band; + } + layer_to_print.original_object = &object; layers_to_print.push_back(layer_to_print); bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions()) - || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()); + || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()) + || (layer_to_print.belt_brim_band && ! layer_to_print.belt_brim_band->fills.empty()); // Check that there are extrusions on the very first layer. The case with empty // first layer may result in skirt/brim in the air and maybe other issues. @@ -5266,20 +5275,26 @@ LayerResult GCode::process_belt_brim_layer( const bool last_layer, const size_t single_object_instance_idx) { - // layer_id 0: the apron precedes every object layer and nothing downstream - // indexes by it. spiral_vase_enable false: spiral vase is refused alongside - // belt brim in Print::validate(). cooling_buffer_flush true: an apron layer is - // a complete layer, and the default (object_layer || raft_layer || last_layer) - // would be false here, so fan and slowdown would never be applied to it. + // layer_id 0 is deliberate, not a placeholder. CoolingBuffer reads it for the + // initial_layer_fan_speed override and the close_fan_the_first_x_layers gate + // (CoolingBuffer.cpp), and every apron band is first-layer material by the only + // definition that means anything on a belt: it lies on the belt plane itself. Numbering + // the bands 1, 2, 3... would ramp the fan up while still printing on the belt. + // spiral_vase_enable false: spiral vase is refused alongside belt brim in + // Print::validate(). cooling_buffer_flush true: an apron layer is a complete layer, and + // the default (object_layer || raft_layer || last_layer) is false here, so fan and + // slowdown would otherwise never be applied to it. LayerResult result { {}, 0, false, true }; if (layer_tools.extruders.empty()) // Nothing to extrude. return result; coordf_t print_z = 0.; + coordf_t height = 0.; for (const LayerToPrint <p : layers) if (ltp.belt_brim_band != nullptr) { print_z = ltp.belt_brim_band->print_z; + height = ltp.belt_brim_band->height; break; } @@ -5298,8 +5313,63 @@ LayerResult GCode::process_belt_brim_layer( const unsigned int extruder_id = layer_tools.extruders.front(); if (m_writer->filament() == nullptr || m_writer->filament()->id() != extruder_id) gcode += this->set_extruder(extruder_id, print_z); + + // An apron band is a real printed layer: it is counted in m_layer_count, it advances + // m_layer_index through change_layer(), and the G-code viewer needs its Z/height tags. + // Keep the same caches and hooks the ordinary path maintains, or the first object layer + // would compute its height against a stale pre-apron Z and layer-change templates would + // skip these layers entirely. + { + char buf[64]; + sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z); + gcode += buf; + sprintf(buf, ";Z:%g\n", print_z); + gcode += buf; + const float band_height = float(height); + sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), band_height); + gcode += buf; + m_last_layer_z = float(print_z); + m_max_layer_z = std::max(m_max_layer_z, m_last_layer_z); + m_last_height = band_height; + } + + if (! m_config.before_layer_change_gcode.value.empty()) { + DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1)); + config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + gcode += this->placeholder_parser_process("before_layer_change_gcode", + print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; + } + gcode += this->change_layer(print_z); + if (! m_config.layer_change_gcode.value.empty()) { + DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + gcode += this->placeholder_parser_process("layer_change_gcode", + print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; + } + + gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); + + result.gcode = std::move(gcode); + return result; +} + +// Emit every apron band carried by this set of layers. +// +// Shared by the brim-only branch above and the ordinary process_layer() path. Both need +// it: an apron band prints below its OWN object's first layer, but on a multi-object belt +// another object can already be printing at that print_z, in which case the layer has an +// object layer, takes the ordinary path, and the band would be silently dropped. +std::string GCode::emit_belt_brim_bands(const Print &print, + const std::vector &layers, + const size_t single_object_instance_idx) +{ + std::string gcode; for (const LayerToPrint <p : layers) { const BeltBrimBand *band = ltp.belt_brim_band; if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) @@ -5324,9 +5394,7 @@ LayerResult GCode::process_belt_brim_layer( m_avoid_crossing_perimeters.disable_once(); } } - - result.gcode = std::move(gcode); - return result; + return gcode; } // Bedslinger model. The heavier the bed load, the lower the achievable Y acceleration for a given @@ -5821,6 +5889,17 @@ 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); + } + //Calibration Layer-specific GCode // ORCA-Belt: on belt printers the calibration object is counter-rotated to // stand upright in slicing space on top of a support wedge, so its first diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d4c7bf9bc3..54b7e8abd4 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -418,6 +418,14 @@ protected: const bool last_layer, const size_t single_object_instance_idx); + // Emit the apron bands carried by these layers. Called from both the brim-only + // branch and the ordinary path, since a band's print_z can coincide with another + // object's layer on a multi-object belt. + std::string emit_belt_brim_bands( + const Print &print, + const std::vector &layers, + const size_t single_object_instance_idx); + LayerResult process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 189f9e178e..2df4cde79b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1401,10 +1401,13 @@ StringObjectException Print::validate(std::vector *warnin "brim length."), "brim_object_gap", object->model_object()); - if (ocfg.leading_brim_length.value > 0. && object->instances().size() > 1) - warn(L("This object has several instances sharing one belt position, so no brim is " - "generated for it. Arrange the copies along the belt instead."), - "leading_brim_length", object->model_object()); + // Unconditional: this suppresses the WHOLE belt brim, not just the apron, so a + // user asking for any brim at all needs to be told they are getting none. + if (! object->belt_brim_instances_compatible()) + warn(L("This object's copies are spaced along the belt, so they would each need " + "their own brim and none is generated. Print them as separate objects, or " + "arrange the copies side by side across the belt."), + "brim_type", object->model_object()); } if (this->has_belt_brim() && m_objects.size() > 1) warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does " diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 72a12918c7..4b1f4891f2 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -392,6 +392,9 @@ public: // trimming and the spiral vase probe, and widening it would perturb belt // support output. bool has_belt_brim() 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; const std::vector& belt_brim_by_layer() const { return m_belt_brim_by_layer; } const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 1b8ddf3a9e..c5d5409d73 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1167,11 +1167,7 @@ bool PrintObject::has_belt_brim() const { if (! m_print->has_tilted_belt()) return false; - // Translating an instance along the belt axis changes its physical belt-floor - // Z, so one set of bands cannot serve several instances sharing a PrintObject. - // belt_force_separate() in PrintApply.cpp gives one instance per object - // whenever a global belt flag is set, which the shipped belt profiles do. - if (m_instances.size() > 1) + if (! this->belt_brim_instances_compatible()) return false; if (m_config.brim_type == btNoBrim) return false; @@ -1181,6 +1177,28 @@ bool PrintObject::has_belt_brim() const return ! this->has_raft(); } +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 + // the same height on the belt. Moving an instance ALONG the belt axis changes its + // physical belt-floor Z and would put its brim at the wrong height; moving it ACROSS + // the belt does not, so side-by-side copies are fine. + // + // belt_force_separate() in PrintApply.cpp already gives one instance per PrintObject + // whenever a global belt flag is set, which the shipped belt profiles do - this only + // matters for configurations that do not. + if (m_instances.size() <= 1) + return true; + const int axis = m_slicing_params.belt_floor_from_axis; + const Point &ref = m_instances.front().shift; + for (const PrintInstance &inst : m_instances) { + const coord_t along = axis == 0 ? inst.shift.x() - ref.x() : inst.shift.y() - ref.y(); + if (std::abs(along) > SCALED_EPSILON) + return false; + } + return true; +} + void PrintObject::clear_belt_brim() { m_belt_brim_by_layer.clear(); diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 755dec06e0..b5d315db2a 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -633,3 +633,107 @@ TEST_CASE("Belt brim lines all have the same width", "[SkirtBrim][belt]") const float hi = *std::max_element(widths.begin(), widths.end()); CHECK_THAT(hi, Catch::Matchers::WithinRel(lo, 1e-4)); } + +TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBrim][belt]") +{ + // An apron band prints below its OWN object's first layer, but with two objects on the + // belt the second one is already printing at that print_z. The layer then has an + // object layer and takes the ordinary process_layer() path rather than the brim-only + // branch, so the band must be emitted from both or it is silently dropped. A + // single-object print cannot exercise this. + auto brim_passes = [](int object_count) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 3 }, + { "leading_brim_length", 8 }, + { "brim_object_gap", 0 }, + }); + std::vector meshes; + for (int i = 0; i < object_count; ++ i) { + TriangleMesh m = cube(20); + // Offset along the belt so the second object starts well after the first. + m.translate(0.f, float(40 * i), 0.f); + meshes.emplace_back(std::move(m)); + } + Print print; + Model model; + init_print(std::move(meshes), print, model, config); + print.process(); + return role_passes(gcode(print), "brim"); + }; + + const int one = brim_passes(1); + const int two = brim_passes(2); + REQUIRE(one > 0); + // Two identical objects should carry twice the brim. Merely asserting `two > one` + // would not be decisive: the FIRST object's apron survives the bug, because nothing + // else is printing that early, so only the second object's apron goes missing. + // Requiring close to 2x is what actually detects the dropped bands. + CHECK(two >= 1.8 * one); +} + +TEST_CASE("Belt brim allows instances placed across the belt", "[SkirtBrim][belt]") +{ + // Only movement ALONG the belt changes an instance's belt-floor Z, so copies placed + // side by side ACROSS it share one set of bands and must still get a brim. The first + // version of this guard refused every multi-instance object outright, silently + // dropping the brim. + // + // The global belt flags are off here so the instances stay in one PrintObject; with + // them on, PrintApply splits each instance into its own object and the case cannot + // arise at all. + auto multi_instance_has_brim = [](double dx, double dy) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "belt_slice_rotation_global", 0 }, + { "belt_preslice_global", 0 }, + { "preslice_remap_global", 0 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + Print print; + Model model; + ModelObject *object = model.add_object(); + object->name += "object.stl"; + object->add_volume(cube(20)); + object->add_instance()->set_offset(Vec3d(80., 80., 0.)); + object->add_instance()->set_offset(Vec3d(80. + dx, 80. + dy, 0.)); + object->ensure_on_bed(); + print.auto_assign_extruders(object); + print.apply(model, config); + print.validate(); + print.set_status_silent(); + print.process(); + REQUIRE(print.objects().size() == 1); + REQUIRE(print.objects().front()->instances().size() == 2); + return print.objects().front()->has_belt_brim(); + }; + + // X is across the belt when the tilt is about X, since the shear then runs along Y. + CHECK(multi_instance_has_brim(40., 0.)); + // Y is along the belt: the copies sit at different belt heights and would each need + // their own bands, so the brim is refused (and validate() warns). + CHECK_FALSE(multi_instance_has_brim(0., 40.)); +} + +TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") +{ + // Supports put extra layers into the same z stream as the apron bands, which is what + // the three-way merge in collect_layers_to_print() exists to handle: a band sharing a + // print_z with a support layer of the SAME object used to overwrite it in the + // print-wide merge. A smoke test - it cannot prove the collision occurred - but it + // does exercise the merge with all three streams populated. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + { "enable_support", 1 }, + }); + const std::string gc = slice({ TestMesh::overhang }, config); + REQUIRE(! gc.empty()); + CHECK(role_passes(gc, "brim") > 0); +}