Print unsupported walls last (#15411)

This commit is contained in:
Ian Bassi
2026-09-17 09:14:20 -03:00
committed by GitHub
parent 82e91bd472
commit 59e40a2c2e
12 changed files with 353 additions and 2 deletions
+4
View File
@@ -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) {}
+21 -1
View File
@@ -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<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first)
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only)
{
std::string gcode;
for (const ObjectByExtruder::Island::Region &region : by_region)
@@ -7657,7 +7660,24 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
m_config.wipe_inward_distance.value > 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<const ExtrusionLoop *>(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)
+1 -1
View File
@@ -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<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first);
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& 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<ObjectByExtruder::Island::Region>& by_region, bool ironing);
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
+1
View File
@@ -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
+71
View File
@@ -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<const ExtrusionLoop *>(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<ExtrusionLoop *> unattached;
for (auto it = first_deferred; it != src.end(); ++it)
unattached.emplace_back(static_cast<ExtrusionLoop *>(*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<Line> 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<false>(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);
}
+1
View File
@@ -1058,6 +1058,7 @@ static std::vector<std::string> 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",
+10
View File
@@ -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");
+1
View File
@@ -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))
+1
View File
@@ -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"
+1
View File
@@ -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");
+1
View File
@@ -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");
+240
View File
@@ -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 <algorithm>
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#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<const ExtrusionLoop *>(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<std::vector<const ExtrusionLoop*>> wall_islands(const Print &print)
{
std::vector<std::vector<const ExtrusionLoop*>> islands;
for (const Layer *layer : print.objects().front()->layers())
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *island : region->perimeters.entities) {
std::vector<const ExtrusionLoop*> loops;
for (const ExtrusionEntity *entity : static_cast<const ExtrusionEntityCollection*>(island)->entities)
if (entity->is_loop())
loops.push_back(static_cast<const ExtrusionLoop*>(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<const ExtrusionLoop*> &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<const ExtrusionLoop*> unsupported_loops(const Print &print, double print_z = -1.)
{
std::vector<const ExtrusionLoop*> 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<const ExtrusionEntityCollection*>(island)->entities)
if (unsupported_loop(entity))
loops.push_back(static_cast<const ExtrusionLoop*>(entity));
}
return loops;
}
int loops_held_back_for_infill(const std::vector<const ExtrusionLoop*> &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<const ExtrusionLoop*> &loops : wall_islands(on)) {
int previous_inset = std::numeric_limits<int>::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<const ExtrusionLoop*> 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<const ExtrusionLoop*> 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<const ExtrusionLoop*> 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<std::string>{ "perimeter", "infill", "perimeter" });
CHECK(ceiling_roles(false) == std::vector<std::string>{ "perimeter", "infill" });
}