From 54e079ae25bd2949bb9dc77e91cbd4b42463e59a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:08:28 -0500 Subject: [PATCH 01/15] Organic supports: restore the per-slice collision, bed and belt trim dda58b07cd stripped debug instrumentation from TreeSupport3D.cpp with a script, and that script also deleted the loop in organic_draw_branches() that trims every branch slice against the collision volume, the bed and, on a belt, the belt plane. This is the generator every printer uses, not a belt code path, and it is the one place where raistlin7447's export fixtures differed from main with belt printing off. Restore the loop as it was on main, with the belt-floor clip. The new test prints a cube carrying a 60 mm plate with organic supports on a flat-bed printer and checks on every support layer that no support extrusion comes within 0.2 mm of the part's slice. It guards that invariant; on this fixture the loop's own effect is a sub-millimetre reshaping of one branch (verified by slicing the fixture with and without the loop), below the asserted gap, so the test does not by itself fail without the loop. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/Support/TreeSupport3D.cpp | 7 ++ tests/fff_print/test_print.cpp | 94 +++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index dc284ba94f..ef267fba68 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3952,6 +3952,13 @@ void organic_draw_branches( const double tiny_area = tiny_area_threshold(); //FIXME parallelize? for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { + // ORCA: safety offset when trimming collision/bed to improve robustness. + slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); + slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes); + // Belt floor: clip branch slices against the belt surface plane. + LayerIndex belt_idx = layer_begin + i; + if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty()) + slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]); remove_small(slices[i], tiny_area); } diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 32c37882f5..546f3667d5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -31,10 +31,12 @@ #include "libslic3r/Support/TreeModelVolumes.hpp" #include "libslic3r/Support/TreeSupportCommon.hpp" #include "libslic3r/Support/BeltFloorContext.hpp" +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Polyline.hpp" #include #include +#include #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" @@ -1244,6 +1246,98 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above CHECK(collides(last + num_raft)); } +// organic_draw_branches() trims every branch slice against the collision volume (the +// part grown by the support XY distance), the bed and, on a belt, the belt plane before +// it becomes support, so a branch never runs into the part it supports. Not a belt +// feature: this is the generator every printer uses. +TEST_CASE("Organic tree supports keep their distance from the part", "[Print][Support]") +{ + // A 20 mm cube carrying a 60 x 60 mm plate: a 20 mm wide ceiling all around the + // cube, 16 mm above the bed, with the cube's four corners in the way of the branches + // that drop from it. The plate reaches into the cube so the two shells overlap + // instead of sharing a face. + indexed_triangle_set its = its_make_cube(20., 20., 20.); + indexed_triangle_set plate = its_make_cube(60., 60., 4.); + its_translate(its, Vec3f(20.f, 20.f, 0.f)); + its_translate(plate, Vec3f(0.f, 0.f, 16.f)); + its_merge(its, plate); + TriangleMesh mesh(std::move(its)); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "support_threshold_angle", 30 }, + }); + Print print; + Model model; + init_print({ mesh }, print, model, config); + // On the bed, not at its corner (the fixture leaves the object at the origin). + model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &object = *print.objects().front(); + INFO("object layers " << object.layers().size() << ", support layers " << object.support_layers().size()); + REQUIRE(! object.support_layers().empty()); + // Support exists under the plate at all. + size_t support_layers_with_fills = 0; + for (const SupportLayer *layer : object.support_layers()) + if (! layer->support_fills.empty()) + ++ support_layers_with_fills; + INFO("support layers with extrusions " << support_layers_with_fills); + CHECK(support_layers_with_fills > 20); + + // Object layers by print_z, to look up the part's slice at a support layer's height. + std::map object_layers; + for (const Layer *layer : object.layers()) + object_layers[scaled(layer->print_z)] = layer; + auto contains = [](const ExPolygons &expolys, const Point &pt) { + for (const ExPolygon &ex : expolys) + if (ex.contains(pt)) + return true; + return false; + }; + // No support extrusion may run closer to the part's slice than half a line width: + // the generator keeps the support XY distance (0.35 mm by default) plus the line's + // own half width away from it. + const float min_gap = scaled(0.2); + size_t too_close = 0, points = 0, layers_checked = 0, layers_unmatched = 0; + for (const SupportLayer *layer : object.support_layers()) { + if (layer->support_fills.empty()) + continue; + // The object layer whose slab spans this support layer's height. + auto it = object_layers.lower_bound(scaled(layer->print_z - EPSILON)); + if (it == object_layers.end()) { + ++ layers_unmatched; + continue; + } + ++ layers_checked; + const ExPolygons grown = offset_ex(it->second->lslices, min_gap); + for (const ExtrusionEntity *entity : layer->support_fills.flatten().entities) + for (const Slic3r::Polyline &pl : entity->as_polylines()) + for (size_t i = 0; i < pl.points.size(); ++ i) { + // The vertices and the midpoints of the segments between them. + ++ points; + if (contains(grown, pl.points[i])) + ++ too_close; + if (i + 1 < pl.points.size() && contains(grown, (pl.points[i] + pl.points[i + 1]) / 2)) + ++ too_close; + } + } + INFO("support layers checked " << layers_checked << " (unmatched " << layers_unmatched << "), support points " << points + << ", within 0.2 mm of the part " << too_close); + CHECK(layers_checked > 20); + CHECK(layers_unmatched == 0); + REQUIRE(points > 0); + CHECK(too_close == 0); +} + // Two parts along the belt: the second part's slicing frame starts at the belt // below its leading end, so its first layers are empty and interleave with the // first part's printing layers. Those must not reach the G-code as layer changes From 4eaaf9a992974108b37471591344e4ee9ae69291 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 02/15] Belt G-code: first-layer speed test subtracts the instance offset only The writer hands set_first_layer_point_test() a point with the plate origin (its own XY offset) already removed, but the test subtracted the whole of m_origin, which carries the plate origin as well as the instance shift. On a plate other than the first the point was moved by the plate origin a second time and the band test looked at the wrong spot. Subtract only the part of m_origin that is not the writer's offset. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c96d299211..6f4c15561f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3192,8 +3192,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (print.config().belt_printer.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(), + // The writer hands over the point with the plate origin (its XY offset) already + // taken off, while m_origin still carries it: take off the instance part only. + const Vec2d plate_offset = m_writer.get_xy_offset().cast(); + return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(), + point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(), point_logical.z())); }); } From 05e37d838c8a23d9fa18a47f2b86a9969e15e3b1 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 03/15] Belt G-code: the mixed-filament sub-layer pass rotates its origin too process_layer()'s sub-layer pass (several filaments in one layer without a purge tower) calls set_origin() per instance like the main instance loop, but not on_set_origin(), which on a belt printer runs the origin through the belt transform. Add the call so both passes place the instance the same way. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 6f4c15561f..a2d2ea80af 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7321,6 +7321,9 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + // Same as the main instance loop: a belt printer rotates the origin through + // the belt transform (BeltGCode::on_set_origin). + this->on_set_origin(&instance_to_print.print_object, offset); // --- Build emission plan --- // Each entry represents one travel_to_z + extrude pass. Per-object mode produces From fba8b8b029275035707198a8f9a9eb6ecb57ebec Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 04/15] Belt G-code: reset the first-layer band state per object In by-object printing the cooling buffer is reset for every object, but m_belt_in_band, which tracks whether the extrusion is inside the band along the belt where the part fan stays off, kept the previous object's value. If the previous object ended inside the band the next one never emitted its band start marker. Reset it with the cooling buffer. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index a2d2ea80af..2b38fa0a51 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4176,6 +4176,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Reset the cooling buffer internal state (the current position, feed rate, accelerations). m_cooling_buffer->set_current_extruder(initial_extruder_id, get_extruder_id(initial_extruder_id)); m_cooling_buffer->reset(this->writer().get_position()); + // The belt first-layer band is tracked per object as well: if the previous + // object ended inside the band, this one has to open its own. + m_belt_in_band = false; // Process all layers of a single object instance (sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. From febd82f6dfd35b3269348dd202df1f70f08123e5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 05/15] Belt G-code: count only the layers that are written collect_layers_to_print() drops the belt layers that print nothing (an object's empty lead-in), but m_layer_count still counted every object and support layer, so "total layers count", the total_layer_count placeholder and the M73 progress disagreed with the layer changes in the file. Count with the same predicate, shared through belt_object_layer_prints_something(). The by-object overload of collect_layers_to_print() now drops those layers as well, so both print sequences write the same layer changes. Dropping the empty entries per object has one more effect on multi- filament belt prints: a layer no longer selects a filament that it then prints nothing with. On belt_project.3mf (two filaments, belt purge tower) the T commands go from 472 to 106 while the extruded length per filament is unchanged; each of the removed tool changes was followed by no extrusion. The empty-layers test now runs for both print sequences and also checks "total layers count" against the layer changes. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 52 +++++++++++++++++++++++++++------- tests/fff_print/test_print.cpp | 14 +++++++-- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 2b38fa0a51..6797f34821 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2222,6 +2222,22 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. +// Belt printers: whether an object layer writes anything, its own extrusions or a belt +// brim band riding on it. Shared by collect_layers_to_print() (which drops the layers +// that do not) and the layer count. +static bool belt_object_layer_prints_something(const PrintObject &object, const Layer &layer) +{ + if (layer.has_extrusions()) + return true; + if (object.has_belt_brim()) { + const auto &by_layer = object.belt_brim_by_layer(); + const size_t id = layer.id(); + if (id < by_layer.size() && ! by_layer[id].empty()) + return true; + } + return false; +} + std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer) { std::vector layers_to_print; @@ -2378,6 +2394,17 @@ std::vector GCode::collect_layers_to_print(const PrintObjec PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers); } + // Belt printers: drop the layers that print nothing (see the by-layer overload), so + // the by-object export writes the same layer changes as the by-layer one. + if (object.print()->config().belt_printer.value) + layers_to_print.erase( + std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) { + return ! ((ltp.object_layer != nullptr && belt_object_layer_prints_something(object, *ltp.object_layer)) || + (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) || + (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty())); + }), + layers_to_print.end()); + return layers_to_print; } @@ -2448,18 +2475,13 @@ std::vector>> GCode::collec // moves it sees, so a gap folds every later layer into the one before it. if (print.config().belt_printer.value) { auto prints_something = [](const LayerToPrint <p) { - if (ltp.object_layer != nullptr && ltp.object_layer->has_extrusions()) + if (ltp.object_layer != nullptr && ltp.original_object != nullptr && + belt_object_layer_prints_something(*ltp.original_object, *ltp.object_layer)) return true; if (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) return true; if (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()) return true; - if (ltp.object_layer != nullptr && ltp.original_object != nullptr && ltp.original_object->has_belt_brim()) { - const auto &by_layer = ltp.original_object->belt_brim_by_layer(); - const size_t id = ltp.object_layer->id(); - if (id < by_layer.size() && ! by_layer[id].empty()) - return true; - } return false; }; layers_to_print.erase( @@ -3204,15 +3226,21 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; + // On a belt, collect_layers_to_print() drops the layers that print nothing (an + // object's empty lead-in), so they must not be counted here either or the layer + // count in the file disagrees with its layer changes. + const bool belt = print.config().belt_printer.value; if (print.config().print_sequence == PrintSequence::ByObject) { // Add each of the object's layers separately. for (auto object : print.objects()) { std::vector zs; zs.reserve(object->layers().size() + object->support_layers().size()); for (auto layer : object->layers()) - zs.push_back(layer->print_z); + if (! belt || belt_object_layer_prints_something(*object, *layer)) + zs.push_back(layer->print_z); for (auto layer : object->support_layers()) - zs.push_back(layer->print_z); + if (! belt || layer->has_extrusions()) + zs.push_back(layer->print_z); // Belt brim apron bands each get their own change_layer() call. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); @@ -3232,9 +3260,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato for (auto object : print.objects()) { zs.reserve(zs.size() + object->layers().size() + object->support_layers().size()); for (auto layer : object->layers()) - zs.push_back(layer->print_z); + if (! belt || belt_object_layer_prints_something(*object, *layer)) + zs.push_back(layer->print_z); for (auto layer : object->support_layers()) - zs.push_back(layer->print_z); + if (! belt || layer->has_extrusions()) + zs.push_back(layer->print_z); // See the ByObject branch: apron bands are real printed layers. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 546f3667d5..a84bc3b451 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1362,6 +1362,9 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ { "machine_start_gcode", "T[initial_tool]\n" }, { "layer_change_gcode", "G92 E0\n" }, }); + // Both export paths drop the empty layers and count the layers the same way. + SECTION("by layer") { config.set_deserialize_strict({{ "print_sequence", "by layer" }}); } + SECTION("by object") { config.set_deserialize_strict({{ "print_sequence", "by object" }}); } Print print; Model model; TriangleMesh cube_a(its_make_cube(20., 20., 20.)); @@ -1376,7 +1379,7 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ const std::string gc = gcode(print); REQUIRE(! gc.empty()); - size_t layers = 0, empty = 0, total_header = 0; + size_t layers = 0, empty = 0, total_header = 0, total_count = 0; bool extruded = true; // before the first layer change std::istringstream in(gc); std::string line; @@ -1387,17 +1390,24 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ ++ layers; extruded = false; } else if (line.rfind("; total layer number: ", 0) == 0) { + // Counted by the G-code processor from the layer changes it saw. total_header = size_t(std::atoi(line.c_str() + 22)); + } else if (line.rfind("; total layers count = ", 0) == 0) { + // GCode::m_layer_count, counted up front from the objects' layers; it also + // drives the M73 progress and the total_layer_count placeholder. + total_count = size_t(std::atoi(line.c_str() + 23)); } else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos && (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) { extruded = true; } } close_layer(); - INFO("layers " << layers << ", header " << total_header << ", layers without extrusion " << empty); + INFO("layers " << layers << ", header " << total_header << ", count " << total_count + << ", layers without extrusion " << empty); CHECK(layers > 150); // both cubes, 141 layers each, overlapping along the belt CHECK(empty == 0); CHECK(total_header == layers); + CHECK(total_count == layers); } // A part with an overhang on its LEADING side (the end that prints first) needs From b2fb552e325694ab6c27bef383fc2ce6157edc05 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:25 -0500 Subject: [PATCH 06/15] PrintObject: a re-slice invalidates posDetectOverhangsForLift posSlice's invalidation list did not include posDetectOverhangsForLift. A re-slice starts the layers over with empty overhang regions while the step stayed done, so GCode::needs_retraction() had no overhangs to test against until something else invalidated it. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PrintObject.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 4f46921c3f..8b5e115be3 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1767,7 +1767,11 @@ bool PrintObject::invalidate_step(PrintObjectStep step) // posSimplifySupportPath is listed with posSupportMaterial: invalidate_steps() does not // propagate, so without it a re-slice regenerated the supports but kept the step done, // and the new support paths were exported unsimplified, unlike a fresh slice. - invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath }); + // posDetectOverhangsForLift reads the layers' overhang regions, which a re-slice + // starts over empty: without it here the step stayed done and the lift logic in + // GCode::needs_retraction() had no overhangs to test against until something else + // invalidated it. + invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath, posDetectOverhangsForLift }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; // The exact belt_floor_z_shift is recomputed when slice() runs again. From 9289fc4dd33572b6ca4cf16c5ff7dad15c7ba261 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 07/15] Leading-edge brim takes part in the outer-wall-first rule The first-layer rule that prints the outer wall first when a brim is attached to it, and the brim_type change rule that regenerates the perimeters for it, only knew btOuterOnly. btLeadingEdgeOnly, the belt brim at the part's first contact, is an outer brim too. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PerimeterGenerator.cpp | 4 +++- src/libslic3r/PrintObject.cpp | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 41db0ed9db..b1c6ed21fa 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -1839,8 +1839,10 @@ void PerimeterGenerator::process_classic() bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; if (is_outer_wall_first || //BBS: always print outer wall first when there indeed has brim. + // btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). (this->layer_id == 0 && - this->object_config->brim_type == BrimType::btOuterOnly && + (this->object_config->brim_type == BrimType::btOuterOnly || + this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && this->object_config->brim_width.value > 0)) entities.reverse(); // Orca: sandwich mode. Apply after 1st layer. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 8b5e115be3..c2abb5466a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1373,7 +1373,10 @@ bool PrintObject::invalidate_state_by_config_options( const auto* new_brim_type = new_config.option>(opt_key); //BBS: When switch to manual brim, the object must have brim, then re-generate perimeter //to make the wall order of first layer to be outer-first - if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly) + // btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it + // takes part in the same first-layer wall order rule. + if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly || + old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly) steps.emplace_back(posPerimeters); } } else if ( From d5cfaae5dd33ba0f708d55b43b5d6be383a2bdee Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 08/15] Belt brim: adding or removing an object rebuilds the other objects' brims A belt brim is clipped against the other objects on the plate and is built with its object's support step. When an object was added or deleted only the print-level skirt/brim and export steps were invalidated, so the remaining objects kept brims clipped against objects that were no longer there, or overlapping ones that had arrived. Invalidate posSupportMaterial on every object in that case on a belt printer. Also reword the comments that still described the global Z offset as a minimum across all objects. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/PrintApply.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 39d5716b08..53f93f5b69 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1916,6 +1916,12 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } if (new_objects || deleted_objects) update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); + // A belt brim is clipped against the other objects on the plate (BeltBrim.cpp, + // belt_brim_obstacles), and it is rebuilt with its object's support step: an + // object that arrived or left changes every other object's brim. + if ((new_objects || deleted_objects) && m_config.belt_printer.value) + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSupportMaterial)); if (new_objects) update_apply_status(false); print_regions_reshuffled = true; @@ -1930,9 +1936,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } } - // Belt printer global mode: when any object's instances shifted, - // recompute m_belt_global_z_offset for ALL objects (it depends on - // min_shift across all objects, so one move affects everyone). + // Belt printer: when any object's instances shifted, re-slice every object. + // The global Z offset follows each object's position along the belt, and the + // belt brims are clipped against the other objects. if (belt_instances_shifted && m_config.belt_printer.value) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); From 798ca8272e318afa1c063526413e8b1106b2cb1d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 09/15] Printer settings: build plate tilt is read-only on a belt printer update_fff() derives build_plate_tilt_x/y from the belt tilt on a belt printer, so a value typed into those fields was silently overwritten. Disable the two fields while belt_printer is on. Co-Authored-By: Claude Fable 5.1 --- src/slic3r/GUI/Tab.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bd19a62024..f5a0e62458 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6349,6 +6349,10 @@ void TabPrinter::toggle_options() // Belt printer: show belt-specific settings only when belt_printer is enabled. bool is_belt = m_config->opt_bool("belt_printer"); + // update_fff() derives build_plate_tilt_{x,y} from the belt tilt on a belt + // printer, so an edit here would be overwritten; keep them read-only there. + toggle_option("build_plate_tilt_x", !is_belt); + toggle_option("build_plate_tilt_y", !is_belt); bool expert_or_above = (m_mode >= comExpert); toggle_line("belt_printer_infinite_y", is_belt); // Belt tilt: the sole mesh-side belt transform (visible by default in belt mode). From e78f437ce4ec97742ea5d49ab3252e7dec5a1027 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 10/15] Belt temperature tower: refuse a range without a model The calibration fell back to the 230-190 tower when no embossed model existed for the requested range, so the printed numbers did not match the temperatures. Show an error naming the range and stop instead. Co-Authored-By: Claude Fable 5.1 --- src/slic3r/GUI/Plater.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 86ed871ed6..6542abb8f6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17080,9 +17080,15 @@ void Plater::calib_temp(const Calib_Params& params) { const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; if (!boost::filesystem::exists(asset)) { - BOOST_LOG_TRIVIAL(warning) << "[belt_temp] no embossed provini for " << t_start << "->" << t_end - << ", falling back to 230_190 (embossed numbers will not match)"; - asset = calib_dir + "belt_temp_tower_230_190.stl"; + // The embossed numbers are part of the model, so another model's tower would + // print numbers that do not match its temperatures. + MessageDialog dlg(static_cast(wxGetApp().mainframe), + format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " + "Use a range the tower models cover, for example 230 to 190."), + t_start, t_end), + _L("Temperature tower"), wxICON_ERROR | wxOK); + dlg.ShowModal(); + return; } if (!add_model(false, asset) || model().objects.empty()) return; From 1fb585dc5c268fa0a4b1386490b8980dc5a41a56 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 11/15] Belt config: fix the tilt tooltip math and extract the remap strings The machine-frame transform is a shear of cot(tilt) and a scale of 1/sin(tilt), not tan and 1/cos; fix the tooltip and the matching comment in BeltGCode.cpp. The gcode_remap_* labels and tooltips were passed through L() as variables inside a lambda, which the string extraction does not see; pass L("literal") at the call sites. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltGCode.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 1726fc6eac..54d9abe30f 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,7 +25,7 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) // for the physical tilt the G-code viewer uses to enable belt view. file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str()); file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value); - // Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt + // Machine-frame transform: shear (cot) + scale (1/sin) derived from the belt // tilt angle (or belt_frame_tilt_angle when decoupled). file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0); file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 503e7c464b..6723b903fd 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7414,8 +7414,8 @@ void PrintConfigDef::init_fff_params() def = this->add("belt_frame_tilt_angle", coFloat); def->label = L("Machine-frame tilt angle"); def->category = L("Printable space"); - def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (tan) and " - "scale (1/cos) applied to G-code. Only used when 'Decouple machine-frame " + def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and " + "scale (1/sin) applied to G-code. Only used when 'Decouple machine-frame " "tilt' is enabled; otherwise the belt tilt angle is used."); def->sidetext = L("°"); def->min = -89.9; @@ -7423,23 +7423,29 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(45.)); - // G-code axis remap with sign. Each field is its own row in the settings tab. - auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, - RemapAxis default_axis, ConfigOptionMode mode = comSimple) { + // G-code axis remap with sign. Each field is its own row in the settings tab. The + // labels and tooltips are literals in L() so they are extracted for translation. + auto add_belt_remap = [this](const char *key, const std::string &label, const std::string &tooltip, + RemapAxis default_axis, ConfigOptionMode mode) { auto def = this->add(key, coEnum); - def->label = L(label); + def->label = label; def->category = L("Printable space"); - def->tooltip = L(tooltip); + def->tooltip = tooltip; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; def->mode = mode; // Visibility may also be gated by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionEnum(default_axis)); }; - - add_belt_remap("gcode_remap_x", "G-code remap X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comDevelop); - add_belt_remap("gcode_remap_y", "G-code remap Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comDevelop); - add_belt_remap("gcode_remap_z", "G-code remap Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop); + add_belt_remap("gcode_remap_x", L("G-code remap X"), + L("Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation."), + RemapAxis::PosX, comDevelop); + add_belt_remap("gcode_remap_y", L("G-code remap Y"), + L("Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation."), + RemapAxis::PosY, comDevelop); + add_belt_remap("gcode_remap_z", L("G-code remap Z"), + L("Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation."), + RemapAxis::PosZ, comDevelop); // The machine-frame G-code transform (shear + scale) is no longer configured // by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis From 441113cf3b99f829c814ff910762ce84a3ab105b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 12/15] Belt: remove dead code belt_remapped_bbox() had no callers; belt_min_z() and m_belt_global_xy_correction were written but never read; LayerTools::has_belt_brim was set but never read; belt_surface_z was a named zero. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode/ToolOrdering.cpp | 2 -- src/libslic3r/GCode/ToolOrdering.hpp | 4 ---- src/libslic3r/Print.hpp | 10 ---------- src/libslic3r/PrintObjectSlice.cpp | 7 ++----- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 979371e304..f61587d562 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1023,7 +1023,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto continue; LayerTools &layer_tools = this->tools_for_layer(band.print_z); layer_tools.extruders.push_back(brim_filament); - layer_tools.has_belt_brim = true; } } @@ -1042,7 +1041,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto 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; } } diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 3d42457e1a..5c11f482cd 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -179,10 +179,6 @@ public: // Should a skirt be printed at this layer? // Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed. bool has_skirt = false; - // Belt printers: is this one of the brim-only apron layers below the object's - // first layer? Kept separate from has_object so skirt marking and wiping - // overrides are unaffected. - bool has_belt_brim = false; // Will there be anything extruded on this layer for the wipe tower? // Due to the support layers possibly interleaving the object layers, // wipe tower will be disabled for some support only layers. diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f60f6b4e19..c726f7b79f 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -231,12 +231,6 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(data) {} }; -// The model's raw bounding box, in the frame the belt floor parameters refer to. -inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig & /*config*/) -{ - return model_object.raw_bounding_box(); -} - // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -721,8 +715,6 @@ private: bool m_belt_brim_pending { false }; // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. double m_belt_min_z { 0.0 }; - // Belt printer: XY correction from global pre-slice mode, applied to G-code origin. - Vec2d m_belt_global_xy_correction { Vec2d::Zero() }; // Belt printer: exact belt_floor_z_shift computed during posSlice from a // vertex-level scan of the post-transform mesh. Cached separately from // m_slicing_params so that rebuilding m_slicing_params on a non-belt-affecting @@ -733,8 +725,6 @@ private: bool m_belt_floor_z_shift_cache_valid { false }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } - double belt_min_z() const { return m_belt_min_z; } - Vec2d belt_global_xy_correction() const { return m_belt_global_xy_correction; } private: diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 5f70887c9c..06e71a60b3 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -902,7 +902,6 @@ void PrintObject::slice() // them. m_belt_min_z = 0.; m_belt_global_z_offset = 0.; - m_belt_global_xy_correction = Vec2d::Zero(); this->clear_layers(); m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); @@ -995,7 +994,6 @@ void PrintObject::slice() // couples slicer_z back into both machine_y and machine_z. Compensating // layer.print_z by belt_z_shift here makes the back-transform produce // correct machine-frame coordinates whether or not a global mode is active. - const double belt_surface_z = 0.; // the belt surface is Z=0 in centered slicer space // The compensation must mirror the Z-shift actually applied, which // is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE // slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or @@ -1003,7 +1001,7 @@ void PrintObject::slice() // no lift was applied, and an unclamped m_belt_min_z here would // leak straight into the layer Z values, floating the whole object // off the belt by exactly that amount. - double belt_z_shift = std::min(m_belt_min_z, 0.) - belt_surface_z; + double belt_z_shift = std::min(m_belt_min_z, 0.); // the belt surface is Z=0 in centered slicer space double global_z_offset = belt_z_shift; // Centering correction: trafo_centered pretranslates by @@ -1034,7 +1032,6 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; global_z_offset += c.z(); - m_belt_global_xy_correction = Vec2d(c.x(), c.y()); BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" << c.x() << ", " << c.y() << ", " << c.z() << ")" @@ -1042,7 +1039,7 @@ void PrintObject::slice() } BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset - << " (relative to min across " << this->print()->objects().size() << " objects)"; + << " (" << this->print()->objects().size() << " objects on the plate)"; m_belt_global_z_offset = global_z_offset; if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) From a564ec23fe2a001cb9bfb63ecd196d44542c39a6 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 16:09:26 -0500 Subject: [PATCH 13/15] Belt: refresh comments that described earlier code BeltBrim.cpp still described the brim as running inside the parallel support step; it runs sequentially after it (generate_belt_brim). The GCodeWriter, calib.cpp and calib.hpp comments referred to an inheritance layout and a dynamic_cast that no longer exist. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltBrim.cpp | 15 ++++++--------- src/libslic3r/GCodeWriter.hpp | 4 ++-- src/libslic3r/calib.cpp | 3 +-- src/libslic3r/calib.hpp | 6 +++--- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 3f2507f24e..66a271384f 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -376,15 +376,12 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // 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. +// SEQUENCING: this reads the layers and support layers of every object on the plate, so +// it must not overlap with another object's support step, which rebuilds them. +// Print::process() therefore generates the belt brims one object after the other once the +// parallel support step is over (PrintObject::generate_belt_brim()), and an object that +// arrives on or leaves the plate invalidates every other object's support step +// (PrintApply.cpp) so the brims are clipped against what is there now. // `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 diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 88be72a6f1..12a56dacdc 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -236,8 +236,8 @@ protected: Vec3d apply_axis_remap(const Vec3d &pos) const; // Motion uses the global/base process variant until a filament becomes active. - // Protected so subclasses index the per-extruder speed options (travel_speed, - // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. + // Indexes the per-extruder speed options (travel_speed, travel_speed_z, + // initial_layer_travel_speed). size_t m_cached_extruder_idx; private: diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index cdf105fa69..3217edf1da 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -896,8 +896,7 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod } m_writer = std::move(belt_writer); } else if (m_writer && dynamic_cast(&m_writer->kinematics()) != nullptr) { - // Previously configured for a belt printer; drop back to a plain writer, - // exactly as the old dynamic_cast check did. + // Previously configured for a belt printer; drop back to a plain writer. m_writer = std::make_shared(); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index 1f1c7844dc..4ef676cdb1 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -370,9 +370,9 @@ private: const Calib_Params &m_params; - // Polymorphic so belt printers get belt kinematics in world-coordinates - // mode (_refresh_writer); shared_ptr keeps the class copyable — the writer - // is rebuilt by refresh_setup() before every use anyway. + // Belt printers get belt kinematics installed on it (_refresh_writer); + // shared_ptr keeps the class copyable — the writer is rebuilt by + // refresh_setup() before every use anyway. std::shared_ptr m_writer{std::make_shared()}; Vec3d m_starting_point; bool m_is_start_point_fixed = false; From cfad587c2d6662666a72a1ab52de0aedeb701b5c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 17:31:48 -0500 Subject: [PATCH 14/15] Belt: follow-ups from a second review pass Guard the layer count and the per-object layer collection against an object that is left without a layer to print on a belt (the counting loop stepped before begin() and front() was taken of an empty vector). Check the belt temperature tower's embossed model before the current project is replaced, not after. Only invalidate the support step of objects that own a belt brim when an object is added or removed. The empty-layers test now counts an extrusion only where material is laid down along a move. The BeltBrim.cpp SEQUENCING note says exactly which layers are read, and the machine-frame scale is 1/|sin|. Remove more code that nothing calls: the kinematics inverse (to_logical, apply_axis_remap_inverse, to_build_volume and the state kept for them), the world_coordinates(), is_active() and belt_brim_areas_by_layer() accessors, the PrintConfig overload of physical_tilt() and the DynamicPrintConfig overload of compute_belt_height_and_floor(). Comments in GCode.hpp, BeltSliceStrategy.hpp/.cpp and PrintObjectSlice.cpp that described the retired pre-slice remap and plane-evaluator still did; the purge-tower width tooltip named the wrong switch. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltBrim.cpp | 14 ++++---- src/libslic3r/BeltGCode.cpp | 2 +- src/libslic3r/BeltSliceStrategy.cpp | 3 +- src/libslic3r/BeltSliceStrategy.hpp | 20 +++++------ src/libslic3r/BeltTransform.cpp | 34 +++---------------- src/libslic3r/BeltTransform.hpp | 11 ------ src/libslic3r/GCode.cpp | 5 +++ src/libslic3r/GCode.hpp | 13 ------- src/libslic3r/GCode/BeltBackTransform.hpp | 3 -- src/libslic3r/GCode/BeltKinematics.cpp | 15 +-------- src/libslic3r/GCode/BeltKinematics.hpp | 11 ------ src/libslic3r/GCode/MachineKinematics.cpp | 23 ------------- src/libslic3r/GCode/MachineKinematics.hpp | 14 -------- src/libslic3r/Print.hpp | 3 -- src/libslic3r/PrintApply.cpp | 5 +-- src/libslic3r/PrintConfig.cpp | 4 +-- src/libslic3r/PrintObjectSlice.cpp | 25 +++++--------- src/slic3r/GUI/Plater.cpp | 41 ++++++++++++++++------- tests/fff_print/test_print.cpp | 23 +++++++------ 19 files changed, 83 insertions(+), 186 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 66a271384f..a25659b30e 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -376,12 +376,14 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // overhang outside the belt footprint and land in the brim ring, which the flattened // brim_object_gap - a belt-plane separation - does not cover. // -// SEQUENCING: this reads the layers and support layers of every object on the plate, so -// it must not overlap with another object's support step, which rebuilds them. -// Print::process() therefore generates the belt brims one object after the other once the -// parallel support step is over (PrintObject::generate_belt_brim()), and an object that -// arrives on or leaves the plate invalidates every other object's support step -// (PrintApply.cpp) so the brims are clipped against what is there now. +// SEQUENCING: this reads every object's layers and this object's own support layers. +// Another object's support step shifts that object's layer Z into the object frame for +// the duration of the run (PrintObject::_generate_support_material()), so the brims must +// not overlap with the parallel support step: Print::process() generates them one object +// after the other once that step is over (PrintObject::generate_belt_brim()), and an +// object that arrives on or leaves the plate invalidates the other brim owners' support +// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this +// object's supports are dodged; another object's support at the same Z is not. // `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 diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 54d9abe30f..e76e76b84f 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,7 +25,7 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) // for the physical tilt the G-code viewer uses to enable belt view. file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str()); file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value); - // Machine-frame transform: shear (cot) + scale (1/sin) derived from the belt + // Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt // tilt angle (or belt_frame_tilt_angle when decoupled). file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0); file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 8aac729f1b..7b25ac53ec 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -73,8 +73,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, z_shift.matrix()(2, 3) = z_shift_val; trafo = z_shift * trafo; } - // out_belt_min_z is only meaningful in belt mode; the standalone-remap path - // never reported it. + // out_belt_min_z is only meaningful in belt mode. if (out_belt_min_z && config.belt_printer.value) { *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index 33a7ed4979..1385818b5c 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -8,25 +8,23 @@ namespace Slic3r { -// Belt printer / pre-slice transform strategy. +// Belt printer pre-slice transform strategy. // -// Composes, in order, the pre-slice mesh transforms applied before slicing: -// 1. Pre-slice axis remap (standalone — works without belt mode) -// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a +// Composes, in order, the mesh transforms applied before slicing on a belt printer: +// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a // g-code-side stage, see MachineFrameTransform) -// 3. Per-object Z-shift that lifts the mesh above the build plate +// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the +// belt below its footprint // -// Isolates this belt/remap-specific logic from the generic slicing pipeline in +// Isolates this belt-specific logic from the generic slicing pipeline in // PrintObjectSlice.cpp. class BeltSliceStrategy { public: - // Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place. - // No-op when neither a remap nor a belt rotation is configured. + // Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt + // rotation is configured. // - // out_belt_min_z (if non-null) receives the minimum mesh Z after the - // transforms, but only in belt-printer mode — the standalone-remap path - // never reported it. + // out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms. static void apply_preslice_transforms(Transform3d &trafo, const PrintConfig &config, const ModelVolumePtrs &model_volumes, diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index d11e2fccdc..9863e80831 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -46,8 +46,6 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co // ---- Belt floor parameters ------------------------------------------------ -// Shared implementation for both PrintConfig and DynamicPrintConfig. -// Template avoids duplicating the math for the two config types. namespace { // Belt floor in the rotated slicer frame: the image of z_machine = 0 under R. @@ -81,33 +79,15 @@ inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, con } -template BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( - const Config &config, const BoundingBoxf3 &bb, double original_height) + const PrintConfig &config, const BoundingBoxf3 &bb, double original_height) { BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract the mesh rotation from config (the sole mesh-side belt transform). - BeltRotationAxis rot_axis; - double rot_angle; - - if constexpr (std::is_same_v) { - rot_axis = config.belt_slice_rotation.value; - rot_angle = config.belt_slice_rotation_angle.value; - } else { - // DynamicPrintConfig path - auto get_float = [&](const char *key) { - auto *opt = config.template option(key); - return opt ? opt->value : 0.0; - }; - auto get_rot_axis = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? opt->value : BeltRotationAxis::None; - }; - rot_axis = get_rot_axis("belt_slice_rotation"); - rot_angle = get_float("belt_slice_rotation_angle"); - } + // The mesh rotation (the sole mesh-side belt transform). + const BeltRotationAxis rot_axis = config.belt_slice_rotation.value; + const double rot_angle = config.belt_slice_rotation_angle.value; bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; if (!has_rotation) @@ -159,12 +139,6 @@ BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_heig return compute_belt_height_and_floor_impl(config, bbox, original_height); } -BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, double original_height) -{ - return compute_belt_height_and_floor_impl(config, bbox, original_height); -} - bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out) { out = BeltFloorParams{}; diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 7409fb2c7f..1aae8ba39b 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -82,12 +82,6 @@ public: return t; } - static PhysicalTilt physical_tilt(const PrintConfig &config) - { - return physical_tilt(config.belt_slice_rotation.value, - config.belt_slice_rotation_angle.value); - } - // ---- Matrix builders -------------------------------------------------- // Build the 3x3 rotation matrix from belt_slice_rotation* config. @@ -133,11 +127,6 @@ public: static BeltHeightResult compute_belt_height_and_floor( const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height); - - // Overload for DynamicPrintConfig (used by static slicing_parameters). - static BeltHeightResult compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, - double original_height); }; } // namespace Slic3r diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 6797f34821..c92ebc8e1c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2431,6 +2431,9 @@ std::vector>> GCode::collec errors.push_back(e); continue; } + // On a belt an object may be left without a layer to print at all. + if (per_object[i].empty()) + continue; OrderingItem ordering_item; ordering_item.object_idx = i; ordering.reserve(ordering.size() + per_object[i].size()); @@ -3244,6 +3247,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Belt brim apron bands each get their own change_layer() call. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); + if (zs.empty()) + continue; std::sort(zs.begin(), zs.end()); //BBS: merge numerically very close Z values. auto end_it = std::unique(zs.begin(), zs.end()); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 4aa96f9a4c..da78aa0a35 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -393,13 +393,6 @@ public: } }; - // Public accessor for the first-layer plane evaluator. Used by - // CoolingBuffer (which is constructed with a GCode reference and needs - // to read the plane for per-segment fan re-evaluation). All other - // first-layer-plane access points (on_first_layer overload, effective - // index helper) are in the protected section since they're called from - // GCode internals only. - protected: class GCodeOutputStream { public: @@ -839,12 +832,6 @@ protected: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; - // First-layer plane evaluator. Constructed once per print from the - // PrintConfig. is_active() == false on non-belt printers and on belt - // printers without a Z-axis shear; in that case all per-path plane - // checks short-circuit to the legacy Layer::id() == 0 path. - // Plate origin, kept so a writer replaced during export can be given it again. - std::unique_ptr m_pressure_equalizer; std::unique_ptr m_pa_processor; diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 3956450a9f..236ad84f51 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -28,9 +28,6 @@ public: // no back-transform is active. Vec3d apply(const Vec3d &pos) const; - // True if a non-identity back-transform is active. - bool is_active() const { return m_active; } - private: bool m_active = false; Transform3d m_inverse = Transform3d::Identity(); diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp index c36c088658..27d5dad105 100644 --- a/src/libslic3r/GCode/BeltKinematics.cpp +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -10,12 +10,8 @@ namespace Slic3r { BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates) : m_world_coordinates(world_coordinates) { - m_back_active = m_back_transform.init_from_config(config); + m_back_transform.init_from_config(config); m_machine_frame.init_from_config(config); - if (m_back_active) - // BeltBackTransform stores the inverse of this; keep the forward so - // to_logical() can reverse the whole chain. - m_back_forward = BeltTransformPipeline::build_forward_transform(config); } Vec3d BeltKinematics::to_machine(const Vec3d &p) const @@ -25,15 +21,6 @@ Vec3d BeltKinematics::to_machine(const Vec3d &p) const return m_machine_frame.apply(after_remap); } -Vec3d BeltKinematics::to_logical(const Vec3d &machine) const -{ - const Vec3d before_frame = m_machine_frame.apply_inverse(machine); - const Vec3d before_remap = this->apply_axis_remap_inverse(before_frame); - if (m_world_coordinates || ! m_back_active) - return before_remap; - return m_back_forward * before_remap; -} - void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates) { writer.set_kinematics(std::make_unique(config, world_coordinates)); diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp index 8f10bd1074..7dc08635ab 100644 --- a/src/libslic3r/GCode/BeltKinematics.hpp +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -31,12 +31,6 @@ public: explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false); Vec3d to_machine(const Vec3d &p) const override; - Vec3d to_logical(const Vec3d &machine) const override; - // Machine -> build-volume frame. Only the machine-frame shear/scale is undone, - // matching what GCodeProcessor's bounds validation wants. This is deliberately - // NOT to_logical(). - Vec3d to_build_volume(const Vec3d &machine) const override - { return m_machine_frame.apply_inverse(machine); } // A belt writer has always emitted full XYZ on every move, whether or not any // individual stage reports itself active. Making this conditional would change @@ -47,14 +41,9 @@ public: // coordinates and G2/G3 cannot describe it. bool supports_arc_moves() const override { return false; } - bool world_coordinates() const { return m_world_coordinates; } - private: BeltBackTransform m_back_transform; MachineFrameTransform m_machine_frame; - // Forward of what m_back_transform inverts, kept so to_logical() can undo it. - Transform3d m_back_forward { Transform3d::Identity() }; - bool m_back_active { false }; bool m_world_coordinates { false }; }; diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp index 063020fe8a..d9b4b65513 100644 --- a/src/libslic3r/GCode/MachineKinematics.cpp +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -17,32 +17,9 @@ Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; } -// Inverse of the above. Output axis i is fed by source axis (r_i % 3); walking -// the three outputs therefore fills every source component exactly once, so long -// as the remap is a permutation (which set_axis_remap callers guarantee). -Vec3d CartesianKinematics::apply_axis_remap_inverse(const Vec3d &machine) const -{ - if (!has_axis_remap()) - return machine; - Vec3d out = Vec3d::Zero(); - const int r[3] = { m_remap_x, m_remap_y, m_remap_z }; - for (int i = 0; i < 3; ++i) { - const int axis = r[i] % 3; - if (r[i] < 3) out[axis] = machine[i]; - else if (r[i] < 6) out[axis] = -machine[i]; - else out[axis] = m_build_vol_max[axis] - machine[i]; - } - return out; -} - Vec3d CartesianKinematics::to_machine(const Vec3d &p) const { return this->apply_axis_remap(p); } -Vec3d CartesianKinematics::to_logical(const Vec3d &machine) const -{ - return this->apply_axis_remap_inverse(machine); -} - } // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp index 5c53bd9f58..fc13282998 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -24,17 +24,6 @@ public: // Logical placed point -> emitted machine point. virtual Vec3d to_machine(const Vec3d &p) const = 0; - // Inverse of to_machine(), back to the logical placed frame. Intended for - // consumers that must reconstruct model coordinates from emitted G-code - // (the G-code viewer's upright preview). - virtual Vec3d to_logical(const Vec3d &machine) const = 0; - - // Machine point -> build-volume frame, for bounds validation only. This is - // deliberately NOT to_logical(): the build-volume check wants the physical - // frame the printable area is expressed in, not the model frame. Keeping - // them separate stops the two contracts from being confused. - virtual Vec3d to_build_volume(const Vec3d &machine) const = 0; - // True when a move must emit X, Y and Z because omitting a word would be // wrong under this mapping. Deliberately not called "couples_axes": a pure // axis permutation forces full emission without physically coupling axes. @@ -72,8 +61,6 @@ class CartesianKinematics : public MachineKinematics { public: Vec3d to_machine(const Vec3d &p) const override; - Vec3d to_logical(const Vec3d &machine) const override; - Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } bool must_emit_all_axes() const override { return this->has_axis_remap(); } bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); } @@ -93,7 +80,6 @@ public: protected: Vec3d apply_axis_remap(const Vec3d &pos) const; - Vec3d apply_axis_remap_inverse(const Vec3d &pos) const; int m_remap_x { 0 }; int m_remap_y { 1 }; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c726f7b79f..fae1fcdb0b 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -431,10 +431,7 @@ public: // 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. 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; } void clear_belt_brim(); void set_belt_brim(std::vector &&by_layer, diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 53f93f5b69..096f505542 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1918,10 +1918,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); // A belt brim is clipped against the other objects on the plate (BeltBrim.cpp, // belt_brim_obstacles), and it is rebuilt with its object's support step: an - // object that arrived or left changes every other object's brim. + // object that arrived or left changes every other brim owner's brim. if ((new_objects || deleted_objects) && m_config.belt_printer.value) for (PrintObject *object : m_objects) - update_apply_status(object->invalidate_step(posSupportMaterial)); + if (object->has_belt_brim()) + update_apply_status(object->invalidate_step(posSupportMaterial)); if (new_objects) update_apply_status(false); print_regions_reshuffled = true; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 6723b903fd..c95ee6f5ce 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7415,7 +7415,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Machine-frame tilt angle"); def->category = L("Printable space"); def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and " - "scale (1/sin) applied to G-code. Only used when 'Decouple machine-frame " + "scale (1/|sin|) applied to G-code. Only used when 'Decouple machine-frame " "tilt' is enabled; otherwise the belt tilt angle is used."); def->sidetext = L("°"); def->min = -89.9; @@ -7477,7 +7477,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Belt purge tower width"); def->category = L("Printable space"); def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically " - "generated on belt printers when the prime tower is enabled and multiple " + "generated on belt printers when the belt purge tower is enabled and multiple " "filaments are used. Filament-change purging is routed into this prism's " "extrusions instead of a classic wipe tower. Its height is computed " "automatically from the worst-case purge volume per layer: a wider prism " diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 06e71a60b3..facce01787 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -909,19 +909,10 @@ void PrintObject::slice() // Belt floor Z-shift: where is the belt surface in final slicer space? // - // The belt surface is at model_Y=0 (XZ belt plane). After the full - // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt - // surface equation in slicer space is: - // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val - // - // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of - // the belt surface in centered-pre-shear slicer space, which is 0 - // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt - // surface away from Z=0 by the centering offset). - // - // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). - // - // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val + // The belt surface is the model's Z=0 plane. After the belt rotation and the + // Z-shift it is the plane Z_belt = shear_factor * from_axis + z_shift_val in + // slicer space, with z_shift_val = max(0, -m_belt_min_z), the lift that starts + // the slicing frame at the belt below the footprint. if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; // The belt surface is at Z=0 in centered slicer space and bb.min.z() is @@ -967,10 +958,10 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); - // Belt printer global mode: offset all layer Z values so objects at - // different bed positions print at different heights on the tilted belt. - // This is a post-slicing adjustment — the sliced geometry is identical - // regardless of global mode, only the output Z coordinates change. + // Belt printer: offset all layer Z values so objects at different positions + // along the belt print at different heights on the tilted belt. This is a + // post-slicing adjustment: the sliced geometry is the same, only the output Z + // coordinates change. { const auto &pcfg = this->print()->config(); BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6542abb8f6..fadcd11f56 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17017,11 +17017,38 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { } +// The belt provini tower (Calib_Params::test_model 1) is one embossed model per +// temperature range. +static std::string belt_temp_tower_asset(const Calib_Params ¶ms) +{ + const int t_start = (int) lround(params.start); + const int t_end = (int) lround(params.end); + return Slic3r::resources_dir() + "/calib/temperature_tower/belt_temp_tower_" + + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; +} + void Plater::calib_temp(const Calib_Params& params) { constexpr double base_temp_tower_nozzle_diameter = 0.4; constexpr double base_temp_tower_block_height = 10.0; constexpr int base_temp_tower_temp_step = 5; + // A belt provini tower exists only for the ranges it was embossed for, and another + // range's model would print numbers that do not match its temperatures. Refuse + // before the current project is replaced. + if (params.mode == CalibMode::Calib_Temp_Tower && params.test_model >= 1) { + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (printer_config.has("belt_printer") && printer_config.opt_bool("belt_printer") && + ! boost::filesystem::exists(belt_temp_tower_asset(params))) { + MessageDialog dlg(static_cast(wxGetApp().mainframe), + format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " + "Use a range the tower models cover, for example 230 to 190."), + (int) lround(params.start), (int) lround(params.end)), + _L("Temperature tower"), wxICON_ERROR | wxOK); + dlg.ShowModal(); + return; + } + } + const auto calib_temp_name = _L("Nozzle temperature test"); new_project(false, false, calib_temp_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); @@ -17077,19 +17104,9 @@ void Plater::calib_temp(const Calib_Params& params) { temps.push_back(t); if (temps.empty()) temps.push_back(t_start); - const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; - std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; - if (!boost::filesystem::exists(asset)) { - // The embossed numbers are part of the model, so another model's tower would - // print numbers that do not match its temperatures. - MessageDialog dlg(static_cast(wxGetApp().mainframe), - format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " - "Use a range the tower models cover, for example 230 to 190."), - t_start, t_end), - _L("Temperature tower"), wxICON_ERROR | wxOK); - dlg.ShowModal(); + const std::string asset = belt_temp_tower_asset(params); + if (!boost::filesystem::exists(asset)) // refused above, before new_project() return; - } if (!add_model(false, asset) || model().objects.empty()) return; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index a84bc3b451..58f8d12e39 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1381,26 +1381,27 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ size_t layers = 0, empty = 0, total_header = 0, total_count = 0; bool extruded = true; // before the first layer change - std::istringstream in(gc); - std::string line; auto close_layer = [&]() { if (! extruded) ++ empty; }; - while (std::getline(in, line)) { - if (line.rfind(";LAYER_CHANGE", 0) == 0) { + GCodeReader reader; + reader.apply_config(config); + reader.parse_buffer(gc, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string &raw = line.raw(); + if (raw.rfind(";LAYER_CHANGE", 0) == 0) { close_layer(); ++ layers; extruded = false; - } else if (line.rfind("; total layer number: ", 0) == 0) { + } else if (raw.rfind("; total layer number: ", 0) == 0) { // Counted by the G-code processor from the layer changes it saw. - total_header = size_t(std::atoi(line.c_str() + 22)); - } else if (line.rfind("; total layers count = ", 0) == 0) { + total_header = size_t(std::atoi(raw.c_str() + 22)); + } else if (raw.rfind("; total layers count = ", 0) == 0) { // GCode::m_layer_count, counted up front from the objects' layers; it also // drives the M73 progress and the total_layer_count placeholder. - total_count = size_t(std::atoi(line.c_str() + 23)); - } else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos - && (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) { + total_count = size_t(std::atoi(raw.c_str() + 23)); + } else if (! extruded && line.extruding(self) && line.dist_XY(self) > EPSILON) { + // Material laid down along a move: a wipe or an unretraction does not count. extruded = true; } - } + }); close_layer(); INFO("layers " << layers << ", header " << total_header << ", count " << total_count << ", layers without extrusion " << empty); From ed8c1f92d7c43b4f38c598b59a5da273660d48bd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 19:08:58 -0500 Subject: [PATCH 15/15] Belt brim: cut the leading-edge brim at the first contact layer The leading-edge-only brim is the outer brim cut down to the part's first contact with the belt, and the cut was taken at layers().front(). Since the slicing frame starts at the belt below the footprint (#16236) that is an empty lead-in layer whose contact lies ahead of the part, so the cut removed the whole region and the brim type produced no brim at all. Take the cut at the first layer with geometry. The all-brim-types test now includes leading_edge_only, and a new test checks that the brim starts no later than the part and covers fewer layers than the outer brim. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/BeltBrim.cpp | 18 +++++++++++---- tests/fff_print/test_skirt_brim.cpp | 36 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index a25659b30e..920790891c 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -488,11 +488,19 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) - // The cut is the uphill edge of the first layer's contact band: everything - // past it belongs to later contacts. - bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, - bc.ctx.cutoff_u(object.layers().front()->print_z)); + if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { + // The cut is the uphill edge of the first contact's band: everything past it + // belongs to later contacts. The first contact is the first layer with + // geometry, not layers().front(): the slicing frame starts at the belt below + // the footprint, so the leading layers are empty and their contact lies ahead + // of the part. + const Layer *first_contact = nullptr; + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) { first_contact = layer; break; } + if (first_contact == nullptr) + return; + bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z)); + } if (bc.region.empty()) return; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 34f71a71a7..639c36d6c7 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -1140,7 +1140,7 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") // Auto / Mouse ear / Painted collapse to outer-only rather than crashing or // silently producing nothing. const char *brim_type = GENERATE("auto_brim", "brim_ears", "painted", "outer_only", - "inner_only", "outer_and_inner", "no_brim"); + "inner_only", "outer_and_inner", "leading_edge_only", "no_brim"); DYNAMIC_SECTION("brim_type " << brim_type) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ @@ -1157,6 +1157,40 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") } } +// The leading-edge-only brim is the outer brim cut down to the part's first contact +// with the belt. The cut has to be taken at the first layer with geometry: the slicing +// frame starts at the belt below the footprint, so layers().front() is an empty lead-in +// layer whose contact lies ahead of the part, and a cut taken there left no brim at all. +TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else", "[SkirtBrim][belt][Regression]") +{ + auto brim_gcode = [](const char *brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return slice({ cube(20) }, config); + }; + const std::string leading = brim_gcode("leading_edge_only"); + const std::string outer = brim_gcode("outer_only"); + + const double brim_z = first_role_z(leading, "brim"); + const double peri_z = first_role_z(leading, "perimeter"); + REQUIRE(brim_z < std::numeric_limits::max()); + REQUIRE(peri_z < std::numeric_limits::max()); + // At the first contact: the brim starts no later than the part does... + CHECK(brim_z <= peri_z + EPSILON); + // ...and stops there, while the outer brim keeps following the footprint. + const int leading_layers = role_layers(leading, "brim"); + const int outer_layers = role_layers(outer, "brim"); + INFO("brim layers: leading-edge " << leading_layers << ", outer " << outer_layers); + CHECK(leading_layers > 0); + CHECK(leading_layers < outer_layers); +} + TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") { // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim