From 59e40a2c2eab87543ca55be4c035a91be29d00d9 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Thu, 17 Sep 2026 09:14:20 -0300 Subject: [PATCH] Print unsupported walls last (#15411) --- src/libslic3r/ExtrusionEntity.hpp | 4 + src/libslic3r/GCode.cpp | 22 ++- src/libslic3r/GCode.hpp | 2 +- src/libslic3r/Layer.cpp | 1 + src/libslic3r/PerimeterGenerator.cpp | 71 ++++++++ src/libslic3r/Preset.cpp | 1 + src/libslic3r/PrintConfig.cpp | 10 ++ src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 1 + src/slic3r/GUI/ConfigManipulation.cpp | 1 + src/slic3r/GUI/Tab.cpp | 1 + tests/fff_print/test_perimeters.cpp | 240 ++++++++++++++++++++++++++ 12 files changed, 353 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index e8348b3bd5..de80247bce 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity { public: ExtrusionPaths paths; + // ORCA: Set on a loop extruded entirely in mid air and out of reach of the layer below: it has + // nothing to lean on until this layer is bridged, so the G-code writer holds it back until the + // infill is down. See defer_unsupported_loops() in PerimeterGenerator.cpp. + bool print_after_infill = false; ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {} ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {} diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 01457d9344..81fc81b488 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6603,6 +6603,8 @@ LayerResult GCode::process_layer( } // Then print infill gcode += this->extrude_infill(print, by_region_specific, false); + // Then the walls left hanging in mid air, now that the infill can anchor them + gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true); // Then print perimeters of regions that has is_infill_first == true gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true); } @@ -6898,6 +6900,7 @@ LayerResult GCode::process_layer( has_insert_timelapse_gcode = true; } gcode += this->extrude_infill(print, by_region_specific, false); + gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true); gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true); // ironing gcode += this->extrude_infill(print, by_region_specific, true); @@ -7638,7 +7641,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de } // Extrude perimeters: Decide where to put seams (hide or align seams). -std::string GCode::extrude_perimeters(const Print &print, const std::vector &by_region, bool is_first_layer, bool is_infill_first) +std::string GCode::extrude_perimeters(const Print &print, const std::vector &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only) { std::string gcode; for (const ObjectByExtruder::Island::Region ®ion : by_region) @@ -7657,7 +7660,24 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector 0. && scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) wipe_support.emplace(); + + // ORCA: loops flagged as extruded in mid air, out of reach of the layer below, are held back + // for a second pass after the infill that anchors them. Infill already precedes infill first walls. + const bool defer_unsupported = !is_infill_first; + auto waits_for_infill = [](const ExtrusionEntity *ee) { + return ee->is_loop() && static_cast(ee)->print_after_infill; + }; + + // The deferred pass runs after the infill, so the loops the first pass emitted are + // already down and belong in the prefix an inward wipe may land on. + if (wipe_support && defer_unsupported && unsupported_loops_only) + for (const ExtrusionEntity* ee : region.perimeters) + if (!waits_for_infill(ee)) + wipe_support->append(*ee); + for (const ExtrusionEntity* ee : region.perimeters) { + if (defer_unsupported && waits_for_infill(ee) != unsupported_loops_only) + continue; gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters, wipe_support ? &*wipe_support : nullptr); if (wipe_support) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 3933fd4e56..8cf4aa03fb 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -534,7 +534,7 @@ private: // For sequential print, the instance of the object to be printing has to be defined. const size_t single_object_instance_idx); - std::string extrude_perimeters(const Print& print, const std::vector& by_region, bool is_first_layer, bool is_infill_first); + std::string extrude_perimeters(const Print& print, const std::vector& by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only = false); std::string extrude_infill(const Print& print, const std::vector& by_region, bool ironing); std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role); diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index b7ec08f856..7bdef5a4ff 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -153,6 +153,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co && config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) && config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value && config.detect_overhang_wall == other_config.detect_overhang_wall + && config.unsupported_wall_last == other_config.unsupported_wall_last && config.overhang_reverse == other_config.overhang_reverse && config.overhang_reverse_threshold == other_config.overhang_reverse_threshold && config.wall_direction == other_config.wall_direction diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index a6b38889a5..9f6ec856ba 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p if (!paths.empty()) { if (extrusion->is_closed) { ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); + extrusion_loop.inset_idx = extrusion->inset_idx; if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (pg_extrusion.is_contour || pg_extrusions.size() == 2)) extrusion_loop.make_counter_clockwise(); @@ -1318,6 +1319,73 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_ } } +// A loop made of nothing but overhang paths lies entirely off the lower layer. +static bool is_unsupported_loop(const ExtrusionEntity *entity) +{ + if (!entity->is_loop()) + return false; + const ExtrusionPaths &paths = static_cast(entity)->paths; + return !paths.empty() && std::all_of(paths.begin(), paths.end(), + [](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; }); +} + +// ORCA: A wall loop with nothing under it has nothing to lean on, so whatever the configured wall +// sequence it is extruded after the loops that anchor it, innermost first. A loop that runs alongside +// an anchored one belongs to the same wall stack and keeps its place ahead of the infill, which needs +// it as an anchor; one that touches nothing has only that infill to rest on, so it is flagged for the +// G-code writer to hold it back until the infill is down. +static void defer_unsupported_loops(const PerimeterGenerator &perimeter_generator, ExtrusionEntityCollection &entities) +{ + if (!perimeter_generator.config->unsupported_wall_last) + return; + + ExtrusionEntitiesPtr &src = entities.entities; + auto first_deferred = std::stable_partition(src.begin(), src.end(), + [](const ExtrusionEntity *entity) { return !is_unsupported_loop(entity); }); + if (first_deferred == src.end()) + return; + + std::stable_sort(first_deferred, src.end(), + [](const ExtrusionEntity *lhs, const ExtrusionEntity *rhs) { return lhs->inset_idx > rhs->inset_idx; }); + + auto collect_lines = [](const ExtrusionEntity *entity, Lines &out) { + Polylines polylines; + entity->collect_polylines(polylines); + append(out, to_lines(polylines)); + }; + + Lines anchored; + for (auto it = src.begin(); it != first_deferred; ++it) + collect_lines(*it, anchored); + + std::vector unattached; + for (auto it = first_deferred; it != src.end(); ++it) + unattached.emplace_back(static_cast(*it)); + + // A loop leaning on a loop that is itself anchored is anchored as well, so spread outwards from + // the anchored loops until no unsupported loop is left touching what was reached. + const double touch_distance = 1.5 * std::max(perimeter_generator.ext_perimeter_flow.scaled_spacing(), + perimeter_generator.perimeter_flow.scaled_spacing()); + while (!anchored.empty()) { + AABBTreeLines::LinesDistancer distancer{std::move(anchored)}; + anchored.clear(); + for (ExtrusionLoop *&loop : unattached) { + if (loop == nullptr) + continue; + const Points points = loop->as_polyline().points; + if (std::any_of(points.begin(), points.end(), + [&distancer, touch_distance](const Point &point) { return distancer.distance_from_lines(point) < touch_distance; })) { + collect_lines(loop, anchored); + loop = nullptr; + } + } + } + + for (ExtrusionLoop *loop : unattached) + if (loop != nullptr) + loop->print_after_infill = true; +} + void PerimeterGenerator::process_classic() { group_region_by_fuzzify(*this); @@ -1804,6 +1872,8 @@ void PerimeterGenerator::process_classic() } } + defer_unsupported_loops(*this, entities); + // append perimeters for this slice as a collection if (! entities.empty()) this->loops->append(entities); @@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne() reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, this->config->overhang_reverse_internal_only); } + defer_unsupported_loops(*this, extrusion_coll); this->loops->append(extrusion_coll); } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b2b5d9277b..1002f5be89 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1058,6 +1058,7 @@ static std::vector s_Preset_print_options{ "reduce_crossing_wall", "detect_thin_wall", "detect_overhang_wall", + "unsupported_wall_last", "overhang_reverse", "overhang_reverse_threshold", "overhang_reverse_internal_only", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f187e6ab82..ea39cbeb5b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5547,6 +5547,16 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + def = this->add("unsupported_wall_last", coBool); + def->label = L("Print unsupported walls last"); + def->category = L("Quality"); + def->tooltip = L("Wall loops that lie entirely in mid air are printed once something can hold them:\n" + "they are extruded after the other walls of their island, innermost first, whatever the wall order is.\n" + "A loop that only the bridges of this layer can anchor waits until those bridges are printed, while a loop running " + "alongside a supported wall keeps its place before the infill, which needs it as an anchor."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("outer_wall_filament_id", coInt); def->gui_type = ConfigOptionDef::GUIType::i_enum_open; def->label = L("Outer walls"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index d161863a9c..727246ef73 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1353,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloatsNullable, filament_ironing_speed)) // Detect bridging perimeters ((ConfigOptionBool, detect_overhang_wall)) + ((ConfigOptionBool, unsupported_wall_last)) ((ConfigOptionInt, outer_wall_filament_id)) ((ConfigOptionInt, inner_wall_filament_id)) ((ConfigOptionFloatOrPercent, inner_wall_line_width)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index bb2a355daa..3b7e889472 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1501,6 +1501,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "fuzzy_skin_octaves" || opt_key == "fuzzy_skin_persistence" || opt_key == "detect_overhang_wall" + || opt_key == "unsupported_wall_last" || opt_key == "overhang_reverse" || opt_key == "overhang_reverse_internal_only" || opt_key == "overhang_reverse_threshold" diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 0244710298..f40c71ec4a 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1134,6 +1134,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall"); bool has_overhang_reverse = config->opt_bool("overhang_reverse"); bool allow_overhang_reverse = !has_spiral_vase; + toggle_line("unsupported_wall_last", has_detect_overhang_wall); toggle_line("overhang_reverse", allow_overhang_reverse); toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse); bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only"); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 28d3478bf2..021724ad96 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2794,6 +2794,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang"); optgroup->append_single_option_line("detect_overhang_wall", "quality_settings_overhangs#detect-overhang-wall"); + optgroup->append_single_option_line("unsupported_wall_last", "quality_settings_overhangs#unsupported-wall-last"); optgroup->append_single_option_line("make_overhang_printable", "quality_settings_overhangs#make-overhang-printable"); optgroup->append_single_option_line("make_overhang_printable_angle", "quality_settings_overhangs#maximum-angle"); optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area"); diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index a98877ad2f..7067c60c61 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -4,9 +4,14 @@ #include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleMesh.hpp" #include #include +#include +#include #include #include "test_helpers.hpp" @@ -255,3 +260,238 @@ TEST_CASE("Only one wall on the first layer needs a bottom shell", "[Perimeters] // No bottom shell: the option is inert, down to the same walls an unchecked box gives. CHECK_THAT(one_wall_no_shell, Catch::Matchers::WithinAbs(plain_no_shell, 1.0)); } + +namespace { + +// The layer that closes the cavity of box_over_cavity(), the first one printed over air. +const double cavity_ceiling_z = 6.2; + +// A cone standing on its tip, flaring by 5mm of radius per mm of height: at a layer height of 0.2 every +// wall of a layer lands a full millimetre outside the one below, entirely off the layer below but right +// alongside the walls printed with it. +TriangleMesh flared_cone() +{ + TriangleMesh cone = make_cone(20., 4.); + cone.mirror(Z); + cone.translate(0., 0., 4.); + return cone; +} + +// A 30mm box holding a 20mm cavity from z=2 to z=6, with a 4mm hole punched down through the ceiling +// of that cavity. The layer at cavity_ceiling_z bridges the cavity, and the walls of the hole sit in +// the middle of that bridge, 15mm clear of anything the layer below supports. +Print &box_over_cavity(Print &print, Model &model, const DynamicPrintConfig &config) +{ + ModelObject *object = model.add_object(); + object->name = "box_over_cavity.stl"; + object->add_volume(make_cube(30., 30., 8.), ModelVolumeType::MODEL_PART, false); + TriangleMesh cavity = make_cube(20., 20., 4.); + cavity.translate(5.f, 5.f, 2.f); + object->add_volume(std::move(cavity), ModelVolumeType::NEGATIVE_VOLUME, false); + TriangleMesh hole = make_cube(4., 4., 6.); + hole.translate(13.f, 13.f, 5.f); + object->add_volume(std::move(hole), ModelVolumeType::NEGATIVE_VOLUME, false); + object->add_instance(); + object->ensure_on_bed(); + + print.auto_assign_extruders(object); + print.apply(model, config); + print.validate(); + print.set_status_silent(); + return print; +} + +// Every setting the assertions below depend on, so none of them rests on a default. +DynamicPrintConfig unsupported_walls_config(const char *wall_generator, bool unsupported_wall_last) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "wall_generator", wall_generator }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "wall_loops", 3 }, + { "detect_overhang_wall", true }, + // Outer wall first, so an unsupported loop only ends up last if the feature puts it there. + { "wall_sequence", "outer wall/inner wall" }, + { "is_infill_first", false }, + { "sparse_infill_density", "15%" }, + { "unsupported_wall_last", unsupported_wall_last }, + { "gcode_comments", true }, + }); + return config; +} + +// A loop extruded entirely in mid air: every one of its paths is an overhang. +bool unsupported_loop(const ExtrusionEntity *entity) +{ + if (! entity->is_loop()) + return false; + const ExtrusionPaths &paths = static_cast(entity)->paths; + return ! paths.empty() && std::all_of(paths.begin(), paths.end(), + [](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; }); +} + +// The loops of every wall island of the print, island by island, in extrusion order. +std::vector> wall_islands(const Print &print) +{ + std::vector> islands; + for (const Layer *layer : print.objects().front()->layers()) + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *island : region->perimeters.entities) { + std::vector loops; + for (const ExtrusionEntity *entity : static_cast(island)->entities) + if (entity->is_loop()) + loops.push_back(static_cast(entity)); + islands.push_back(std::move(loops)); + } + return islands; +} + +// Islands where a loop that is anchored is extruded after one that is not. +int islands_with_a_supported_loop_last(const Print &print) +{ + int count = 0; + for (const std::vector &loops : wall_islands(print)) { + bool seen_unsupported = false; + for (const ExtrusionLoop *loop : loops) { + if (unsupported_loop(loop)) + seen_unsupported = true; + else if (seen_unsupported) { + ++ count; + break; + } + } + } + return count; +} + +// The unsupported loops of the print, and those of them held back for the infill. +std::vector unsupported_loops(const Print &print, double print_z = -1.) +{ + std::vector loops; + for (const Layer *layer : print.objects().front()->layers()) { + if (print_z >= 0. && std::abs(layer->print_z - print_z) > EPSILON) + continue; + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *island : region->perimeters.entities) + for (const ExtrusionEntity *entity : static_cast(island)->entities) + if (unsupported_loop(entity)) + loops.push_back(static_cast(entity)); + } + return loops; +} + +int loops_held_back_for_infill(const std::vector &loops) +{ + return int(std::count_if(loops.begin(), loops.end(), [](const ExtrusionLoop *loop) { return loop->print_after_infill; })); +} + +// The G-code emitted at `print_z`, so the order of one layer can be read on its own. +std::string layer_gcode(const std::string &gcode, double print_z) +{ + std::string out; + GCodeReader reader; + reader.parse_buffer(gcode, [&out, print_z](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (std::abs(self.z() - print_z) < EPSILON) + out += line.raw() + "\n"; + }); + return out; +} + +} // namespace + +// Whatever the wall order asks for, a loop with nothing under it cannot be extruded before the loops it +// leans on. The flared cone gives every layer an outer wall that lands completely off the one below, and +// the outer wall first sequence would otherwise put it down before any of them. +TEST_CASE("Unsupported wall loops are extruded after the walls that anchor them", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + auto slice_cone = [wall_generator](bool unsupported_wall_last, Print &print) { + init_and_process_print({ flared_cone() }, print, unsupported_walls_config(wall_generator, unsupported_wall_last)); + REQUIRE_FALSE(print.objects().empty()); + }; + + Print on; + slice_cone(true, on); + // Without unsupported loops to reorder the rest of the test would pass on an empty print. + REQUIRE(unsupported_loops(on).size() > 0); + CHECK(islands_with_a_supported_loop_last(on) == 0); + + SECTION("the held back loops run innermost first") { + for (const std::vector &loops : wall_islands(on)) { + int previous_inset = std::numeric_limits::max(); + for (const ExtrusionLoop *loop : loops) + if (unsupported_loop(loop)) { + CHECK(loop->inset_idx <= previous_inset); + previous_inset = loop->inset_idx; + } + } + } + + SECTION("switched off, the configured wall order is left alone") { + Print off; + slice_cone(false, off); + REQUIRE(unsupported_loops(off).size() == unsupported_loops(on).size()); + // Outer wall first puts the unsupported outer wall ahead of the walls behind it. + CHECK(islands_with_a_supported_loop_last(off) > 0); + } +} + +// A loop the walls cannot reach is a different case: only the bridges of its own layer will ever hold it, +// so it has to wait for them - while a loop that runs alongside a wall keeps its place, because the +// bridges anchor on it instead. +TEST_CASE("A wall loop out of reach of the layer below waits for the infill", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + Print print; + Model model; + box_over_cavity(print, model, unsupported_walls_config(wall_generator, true)); + print.process(); + + const std::vector hole_loops = unsupported_loops(print, cavity_ceiling_z); + REQUIRE(hole_loops.size() > 0); + CHECK(loops_held_back_for_infill(hole_loops) == int(hole_loops.size())); + + SECTION("a loop alongside a supported wall is not held back") { + Print cone; + init_and_process_print({ flared_cone() }, cone, unsupported_walls_config(wall_generator, true)); + const std::vector loops = unsupported_loops(cone); + REQUIRE(loops.size() > 0); + CHECK(loops_held_back_for_infill(loops) == 0); + } + + SECTION("switched off, no loop is held back") { + Print off; + Model off_model; + box_over_cavity(off, off_model, unsupported_walls_config(wall_generator, false)); + off.process(); + const std::vector loops = unsupported_loops(off, cavity_ceiling_z); + REQUIRE(loops.size() == hole_loops.size()); + CHECK(loops_held_back_for_infill(loops) == 0); + } +} + +// The held back loops reach the G-code in a second pass, after the infill of their layer: on the layer +// that closes the cavity the walls of the hole are extruded once the bridge is down, so the layer emits +// perimeters, then infill, then the perimeters that were waiting for it. +TEST_CASE("Loops waiting for the infill are extruded after it", "[Perimeters]") +{ + const char *wall_generator = GENERATE("classic", "arachne"); + CAPTURE(wall_generator); + + auto ceiling_roles = [wall_generator](bool unsupported_wall_last) { + Print print; + Model model; + box_over_cavity(print, model, unsupported_walls_config(wall_generator, unsupported_wall_last)); + const std::string layer = layer_gcode(gcode(print), cavity_ceiling_z); + REQUIRE_FALSE(layer.empty()); + return role_sequence(layer, { "perimeter", "infill" }); + }; + + CHECK(ceiling_roles(true) == std::vector{ "perimeter", "infill", "perimeter" }); + CHECK(ceiling_roles(false) == std::vector{ "perimeter", "infill" }); +}