mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
Print unsupported walls last (#15411)
This commit is contained in:
@@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ExtrusionPaths paths;
|
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(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
|
||||||
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
|
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
|
||||||
|
|||||||
+21
-1
@@ -6603,6 +6603,8 @@ LayerResult GCode::process_layer(
|
|||||||
}
|
}
|
||||||
// Then print infill
|
// Then print infill
|
||||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
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
|
// Then print perimeters of regions that has is_infill_first == true
|
||||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, 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;
|
has_insert_timelapse_gcode = true;
|
||||||
}
|
}
|
||||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
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);
|
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
|
||||||
// ironing
|
// ironing
|
||||||
gcode += this->extrude_infill(print, by_region_specific, true);
|
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).
|
// 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;
|
std::string gcode;
|
||||||
for (const ObjectByExtruder::Island::Region ®ion : by_region)
|
for (const ObjectByExtruder::Island::Region ®ion : 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. &&
|
m_config.wipe_inward_distance.value > 0. &&
|
||||||
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
|
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
|
||||||
wipe_support.emplace();
|
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) {
|
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,
|
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
|
||||||
wipe_support ? &*wipe_support : nullptr);
|
wipe_support ? &*wipe_support : nullptr);
|
||||||
if (wipe_support)
|
if (wipe_support)
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ private:
|
|||||||
// For sequential print, the instance of the object to be printing has to be defined.
|
// For sequential print, the instance of the object to be printing has to be defined.
|
||||||
const size_t single_object_instance_idx);
|
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_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);
|
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
|
||||||
|
|
||||||
|
|||||||
@@ -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.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.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
|
||||||
&& config.detect_overhang_wall == other_config.detect_overhang_wall
|
&& 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 == other_config.overhang_reverse
|
||||||
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
|
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
|
||||||
&& config.wall_direction == other_config.wall_direction
|
&& config.wall_direction == other_config.wall_direction
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
|||||||
if (!paths.empty()) {
|
if (!paths.empty()) {
|
||||||
if (extrusion->is_closed) {
|
if (extrusion->is_closed) {
|
||||||
ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole);
|
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) ==
|
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) ==
|
||||||
(pg_extrusion.is_contour || pg_extrusions.size() == 2))
|
(pg_extrusion.is_contour || pg_extrusions.size() == 2))
|
||||||
extrusion_loop.make_counter_clockwise();
|
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()
|
void PerimeterGenerator::process_classic()
|
||||||
{
|
{
|
||||||
group_region_by_fuzzify(*this);
|
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
|
// append perimeters for this slice as a collection
|
||||||
if (! entities.empty())
|
if (! entities.empty())
|
||||||
this->loops->append(entities);
|
this->loops->append(entities);
|
||||||
@@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne()
|
|||||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||||
this->config->overhang_reverse_internal_only);
|
this->config->overhang_reverse_internal_only);
|
||||||
}
|
}
|
||||||
|
defer_unsupported_loops(*this, extrusion_coll);
|
||||||
this->loops->append(extrusion_coll);
|
this->loops->append(extrusion_coll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1058,6 +1058,7 @@ static std::vector<std::string> s_Preset_print_options{
|
|||||||
"reduce_crossing_wall",
|
"reduce_crossing_wall",
|
||||||
"detect_thin_wall",
|
"detect_thin_wall",
|
||||||
"detect_overhang_wall",
|
"detect_overhang_wall",
|
||||||
|
"unsupported_wall_last",
|
||||||
"overhang_reverse",
|
"overhang_reverse",
|
||||||
"overhang_reverse_threshold",
|
"overhang_reverse_threshold",
|
||||||
"overhang_reverse_internal_only",
|
"overhang_reverse_internal_only",
|
||||||
|
|||||||
@@ -5547,6 +5547,16 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comAdvanced;
|
def->mode = comAdvanced;
|
||||||
def->set_default_value(new ConfigOptionBool(true));
|
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 = this->add("outer_wall_filament_id", coInt);
|
||||||
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
|
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
|
||||||
def->label = L("Outer walls");
|
def->label = L("Outer walls");
|
||||||
|
|||||||
@@ -1353,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionFloatsNullable, filament_ironing_speed))
|
((ConfigOptionFloatsNullable, filament_ironing_speed))
|
||||||
// Detect bridging perimeters
|
// Detect bridging perimeters
|
||||||
((ConfigOptionBool, detect_overhang_wall))
|
((ConfigOptionBool, detect_overhang_wall))
|
||||||
|
((ConfigOptionBool, unsupported_wall_last))
|
||||||
((ConfigOptionInt, outer_wall_filament_id))
|
((ConfigOptionInt, outer_wall_filament_id))
|
||||||
((ConfigOptionInt, inner_wall_filament_id))
|
((ConfigOptionInt, inner_wall_filament_id))
|
||||||
((ConfigOptionFloatOrPercent, inner_wall_line_width))
|
((ConfigOptionFloatOrPercent, inner_wall_line_width))
|
||||||
|
|||||||
@@ -1501,6 +1501,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
|||||||
|| opt_key == "fuzzy_skin_octaves"
|
|| opt_key == "fuzzy_skin_octaves"
|
||||||
|| opt_key == "fuzzy_skin_persistence"
|
|| opt_key == "fuzzy_skin_persistence"
|
||||||
|| opt_key == "detect_overhang_wall"
|
|| opt_key == "detect_overhang_wall"
|
||||||
|
|| opt_key == "unsupported_wall_last"
|
||||||
|| opt_key == "overhang_reverse"
|
|| opt_key == "overhang_reverse"
|
||||||
|| opt_key == "overhang_reverse_internal_only"
|
|| opt_key == "overhang_reverse_internal_only"
|
||||||
|| opt_key == "overhang_reverse_threshold"
|
|| opt_key == "overhang_reverse_threshold"
|
||||||
|
|||||||
@@ -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_detect_overhang_wall = config->opt_bool("detect_overhang_wall");
|
||||||
bool has_overhang_reverse = config->opt_bool("overhang_reverse");
|
bool has_overhang_reverse = config->opt_bool("overhang_reverse");
|
||||||
bool allow_overhang_reverse = !has_spiral_vase;
|
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", allow_overhang_reverse);
|
||||||
toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_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");
|
bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only");
|
||||||
|
|||||||
@@ -2794,6 +2794,7 @@ void TabPrint::build()
|
|||||||
|
|
||||||
optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang");
|
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("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", "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_angle", "quality_settings_overhangs#maximum-angle");
|
||||||
optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area");
|
optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area");
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
||||||
#include "libslic3r/Layer.hpp"
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Print.hpp"
|
#include "libslic3r/Print.hpp"
|
||||||
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
|
#include "libslic3r/Model.hpp"
|
||||||
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#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.
|
// 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));
|
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" });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user