mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-07 10:07:38 +00:00
A belt printer slices in a rotated frame, so the belt surface is a tilted
plane rather than the Z=0 bed plane. Each slicing layer touches the belt
only along a narrow strip at its leading edge - about 0.2mm at 45 degrees -
so a part's first layer is really a first line, with almost no contact patch
to hold it down while the belt drags it forward. Brim was hard-disabled on
belt printers, leaving no remedy at all.
Generate the brim on the belt plane instead. The object's belt footprint is
the union over layers of each slice clipped to that layer's contact band; the
brim is offset from it in a "flattened" frame where the shear axis is
stretched by 1/cos(tilt), so ordinary Clipper offsets measure true on-belt
distance. It is emitted as cross-belt lines, one per layer band, anchored to
a fixed fraction of the band so every line shares a nozzle-to-belt clearance
and therefore comes out the same width; flow is matched to the resulting band
pitch, keeping the sheet uniform and gap-free.
Three new controls, all belt-only:
* Leading brim length - extends the brim ahead of the part along the belt,
on every downhill-facing edge of its contact area. This apron necessarily
prints BELOW the object's first layer, since layer 0 is the part's leading
contact, so it needs brim-only bands of its own.
* Extra brim width - widens the brim sideways across the belt only.
* Brim type "Leading edge only" - brim at the part's first belt contact and
nothing after it. Appended last in BrimType so no existing value shifts;
degrades to an outer brim off belt printers, with a warning.
The apron bands are lightweight records rather than a Layer subclass, so no
fabricated Layer::id() can leak into initial-layer temperature selection, the
spiral vase probe, cooling or gradual interpolation. They are generated in
posSupportMaterial because their print_z values must exist before ToolOrdering
is built at psWipeTower, and they are emitted from a short dedicated branch in
process_layer that runs before any layer pointer is dereferenced.
The footprint is closed before offsetting outwards: a belt contact patch is
often a broken-up strip, and the merged offset rings of two islands closer
than 2 x brim_width would otherwise fill the space between them - space that
lies under the part.
Also fixes a pre-existing bug where PrintObject::get_first_layer_bbox()
overwrote a valid bbox with an unassigned one on any belt printer with a brim
configured, because has_brim() was true while make_brim() returned early.
Belt brim is refused alongside the prime tower and spiral vase, and requires
one instance per PrintObject - translating an instance along the belt axis
changes its physical belt-floor Z. Untilted belt printers are unchanged: they
still get no brim, since the plate brim is emitted out of skirt_brim_groups(),
which _make_skirt() never builds for a belt printer.
636 lines
26 KiB
C++
636 lines
26 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include "libslic3r/ClipperUtils.hpp"
|
|
#include "libslic3r/GCodeReader.hpp"
|
|
#include "libslic3r/Layer.hpp"
|
|
#include "libslic3r/Config.hpp"
|
|
#include "libslic3r/Geometry.hpp"
|
|
#include "libslic3r/Geometry/ConvexHull.hpp"
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include <cmath>
|
|
|
|
#include "test_helpers.hpp" // get access to init_print, etc
|
|
|
|
using namespace Slic3r::Test;
|
|
using namespace Slic3r;
|
|
|
|
// Distinct brim regions (combine_brims merges touching brims into one covering >1 object).
|
|
static int brim_count(const Print &print)
|
|
{
|
|
int n = 0;
|
|
for (const auto &group : print.skirt_brim_groups())
|
|
n += (int) group.brims.size();
|
|
return n;
|
|
}
|
|
|
|
// Total brim loops across all objects.
|
|
static size_t brim_loop_count(Print &print)
|
|
{
|
|
size_t n = 0;
|
|
for (const auto &kv : print.get_brimMap())
|
|
n += kv.second.items_count();
|
|
return n;
|
|
}
|
|
|
|
// The span is skirt_height layers, or every layer when a draft shield is on (forced even at
|
|
// height 0); per-object skirts are rejected in By object printing (no room between objects).
|
|
TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]")
|
|
{
|
|
const int object_layers = 100; // 20mm cube at 0.2mm layers
|
|
const char *skirt_type = GENERATE("combined", "perobject");
|
|
const char *print_seq = GENERATE("by layer", "by object");
|
|
const char *draft_shield = GENERATE("disabled", "enabled");
|
|
const int skirt_height = GENERATE(0, 1, 3);
|
|
|
|
DYNAMIC_SECTION(skirt_type << " | " << print_seq << " | draft=" << draft_shield << " | height=" << skirt_height) {
|
|
auto do_slice = [&] {
|
|
return slice_two_cubes_arranged({
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_height", skirt_height },
|
|
{ "skirt_distance", 3 },
|
|
{ "skirt_type", skirt_type },
|
|
{ "draft_shield", draft_shield },
|
|
{ "print_sequence", print_seq },
|
|
{ "layer_height", 0.2 },
|
|
});
|
|
};
|
|
const bool draft = std::string(draft_shield) == "enabled";
|
|
const bool has_skirt = draft || skirt_height > 0;
|
|
const bool unsafe_by_object = std::string(skirt_type) == "perobject"
|
|
&& std::string(print_seq) == "by object" && has_skirt;
|
|
|
|
if (unsafe_by_object) {
|
|
REQUIRE_THROWS(do_slice());
|
|
} else {
|
|
const int expected_layers = draft ? object_layers : skirt_height;
|
|
CHECK(role_passes(do_slice(), "skirt") == expected_layers);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each per-object skirt prints right before its own object, so distant objects yield two
|
|
// non-contiguous skirt passes; close objects group into a single skirt.
|
|
TEST_CASE("Per-object skirts group when objects are close", "[SkirtBrim]")
|
|
{
|
|
auto [gap, expected_skirts] = GENERATE(table<double, int>({ { 5.0, 1 }, { 60.0, 2 } }));
|
|
DYNAMIC_SECTION("gap=" << gap) {
|
|
const std::string gcode = slice_two_cubes_apart(gap, {
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_height", 1 },
|
|
{ "skirt_distance", 3 },
|
|
{ "skirt_type", "perobject" },
|
|
{ "print_sequence", "by layer" },
|
|
{ "layer_height", 0.2 },
|
|
});
|
|
CHECK(role_passes(gcode, "skirt") == expected_skirts);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Per-object skirt is generated per instance", "[SkirtBrim]")
|
|
{
|
|
Print print;
|
|
Model model;
|
|
place_two_cube_instances_apart(60, {
|
|
{ "skirt_type", "perobject" },
|
|
{ "skirt_height", 1 },
|
|
{ "skirt_distance", 2 },
|
|
{ "skirt_loops", 1 },
|
|
{ "brim_type", "no_brim" },
|
|
}, print, model);
|
|
print.process();
|
|
|
|
REQUIRE(print.skirt_brim_groups().size() == 2);
|
|
REQUIRE(print.skirt().items_count() == 2);
|
|
for (const Print::SkirtBrimGroup &group : print.skirt_brim_groups()) {
|
|
REQUIRE(group.instances.size() == 1);
|
|
REQUIRE(group.instances.front().object_id == print.get_object(0)->id());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Combine brims merges touching brims", "[SkirtBrim]")
|
|
{
|
|
auto [gap, combine, expected_brims] = GENERATE(table<double, int, int>({
|
|
{ 5.0, 1, 1 }, // touching + combine -> one merged brim
|
|
{ 5.0, 0, 2 }, // touching, no combine -> separate
|
|
{ 60.0, 1, 2 }, // far apart -> nothing to merge
|
|
}));
|
|
DYNAMIC_SECTION("gap=" << gap << " combine_brims=" << combine) {
|
|
Print print;
|
|
Model model;
|
|
place_two_cubes_apart(gap, {
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_height", 1 },
|
|
{ "skirt_distance", 3 },
|
|
{ "skirt_type", "perobject" },
|
|
{ "print_sequence", "by layer" },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "combine_brims", combine },
|
|
{ "layer_height", 0.2 },
|
|
}, print, model);
|
|
print.process();
|
|
CHECK(brim_count(print) == expected_brims);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Object brims are generated per instance", "[SkirtBrim]")
|
|
{
|
|
Print print;
|
|
Model model;
|
|
place_two_cube_instances_apart(60, {
|
|
{ "skirt_loops", 0 },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "combine_brims", 0 },
|
|
}, print, model);
|
|
print.process();
|
|
|
|
REQUIRE(print.skirt_brim_groups().size() == 1);
|
|
REQUIRE(print.skirt_brim_groups().front().brims.size() == 2);
|
|
for (const Print::SkirtBrimGroup::Brim &brim : print.skirt_brim_groups().front().brims) {
|
|
REQUIRE(brim.instances.size() == 1);
|
|
REQUIRE(brim.instances.front().object_id == print.get_object(0)->id());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Uncombined neighboring brims precede their respective objects", "[SkirtBrim]")
|
|
{
|
|
Print print;
|
|
Model model;
|
|
place_two_cubes_apart(0, {
|
|
{ "skirt_loops", 0 },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "combine_brims", 0 },
|
|
}, print, model);
|
|
print.process();
|
|
|
|
REQUIRE(print.skirt_brim_groups().size() == 1);
|
|
REQUIRE(print.skirt_brim_groups().front().brims.size() == 2);
|
|
CHECK(role_sequence(gcode(print), { "brim", "perimeter" }) ==
|
|
std::vector<std::string>{ "brim", "perimeter", "brim", "perimeter" });
|
|
}
|
|
|
|
TEST_CASE("Combine brims merges neighboring object instances", "[SkirtBrim]")
|
|
{
|
|
Print print;
|
|
Model model;
|
|
place_two_cube_instances_apart(5, {
|
|
{ "skirt_loops", 0 },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "combine_brims", 1 },
|
|
}, print, model);
|
|
print.process();
|
|
|
|
REQUIRE(print.skirt_brim_groups().size() == 1);
|
|
REQUIRE(print.skirt_brim_groups().front().brims.size() == 1);
|
|
REQUIRE(print.skirt_brim_groups().front().brims.front().instances.size() == 2);
|
|
const std::vector<std::string> expected{ "brim", "perimeter" };
|
|
CHECK(role_sequence(gcode(print), { "brim", "perimeter" }) == expected);
|
|
}
|
|
|
|
// Each object's skirt and brim come right before that object, not all skirts then all brims first.
|
|
TEST_CASE("By-layer per-object skirt and brim precede each object", "[SkirtBrim]")
|
|
{
|
|
const std::string gcode = slice_two_cubes_apart(60, { // far apart: a skirt+brim per object
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_height", 1 },
|
|
{ "skirt_distance", 3 },
|
|
{ "skirt_type", "perobject" },
|
|
{ "print_sequence", "by layer" },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "layer_height", 0.2 },
|
|
});
|
|
const std::vector<std::string> expected{ "skirt", "brim", "perimeter", "skirt", "brim", "perimeter" };
|
|
CHECK(role_sequence(gcode, { "skirt", "brim", "perimeter" }) == expected);
|
|
}
|
|
|
|
// A square's corners are 90 degrees, so they get ears only when brim_ears_max_angle is above 90.
|
|
TEST_CASE("Brim ears appear only at corners within the max angle", "[SkirtBrim]")
|
|
{
|
|
auto [max_angle, expect_ears] = GENERATE(table<int, bool>({ { 91, true }, { 90, false }, { 89, false } }));
|
|
DYNAMIC_SECTION("brim_ears_max_angle=" << max_angle) {
|
|
Print print;
|
|
init_and_process_print({ cube(20) }, print, {
|
|
{ "skirt_loops", 0 },
|
|
{ "brim_type", "brim_ears" },
|
|
{ "brim_width", 1 },
|
|
{ "brim_ears_max_angle", max_angle },
|
|
{ "initial_layer_line_width", 0.5 },
|
|
});
|
|
if (expect_ears) CHECK(brim_loop_count(print) > 0);
|
|
else CHECK(brim_loop_count(print) == 0);
|
|
}
|
|
}
|
|
|
|
SCENARIO("Skirt has the configured number of loops", "[SkirtBrim]") {
|
|
GIVEN("20mm cube and default config") {
|
|
WHEN("skirt_loops is set to 2") {
|
|
Print print;
|
|
init_and_process_print({cube(20)}, print, {
|
|
{ "skirt_height", 1 },
|
|
{ "skirt_distance", 1 },
|
|
{ "skirt_loops", 2 }
|
|
});
|
|
THEN("Skirt Extrusion collection has 2 loops in it") {
|
|
REQUIRE(print.skirt().items_count() == 2);
|
|
REQUIRE(print.skirt().flatten().entities.size() == 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("Brim has the configured number of loops", "[SkirtBrim]") {
|
|
GIVEN("20mm cube and default config, 1mm first layer width") {
|
|
WHEN("Brim is set to 6mm") {
|
|
Print print;
|
|
init_and_process_print({cube(20)}, print, {
|
|
{ "brim_type", "outer_only" },
|
|
{ "initial_layer_line_width", 1 },
|
|
{ "brim_width", 6 }
|
|
});
|
|
THEN("Brim Extrusion collection has 6 loops in it") {
|
|
REQUIRE(brim_loop_count(print) == 6);
|
|
}
|
|
}
|
|
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
|
|
Print print;
|
|
init_and_process_print({cube(20)}, print, {
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 6 },
|
|
{ "initial_layer_line_width", 0.5 }
|
|
});
|
|
THEN("Brim Extrusion collection has 12 loops in it") {
|
|
REQUIRE(brim_loop_count(print) == 12);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static double first_extrusion_feedrate_for_feature(const std::string &gcode, const std::string_view feature)
|
|
{
|
|
double feedrate = 0.0;
|
|
bool feature_active = false;
|
|
GCodeReader parser;
|
|
parser.parse_buffer(gcode, [&feedrate, &feature_active, feature] (GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
const std::string_view comment = line.comment();
|
|
if (comment.find("FEATURE:") != std::string_view::npos || comment.find("TYPE:") != std::string_view::npos)
|
|
feature_active = comment.find(feature) != std::string_view::npos;
|
|
|
|
if (feature_active && line.extruding(self) && line.dist_XY(self) > 0) {
|
|
feedrate = line.new_F(self);
|
|
self.quit_parsing();
|
|
}
|
|
});
|
|
return feedrate;
|
|
}
|
|
|
|
TEST_CASE("Skirt height is honored", "[SkirtBrim]") {
|
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
|
config.set_deserialize_strict({
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_height", 5 },
|
|
{ "wall_loops", 0 },
|
|
});
|
|
|
|
std::string gcode;
|
|
SECTION("printing a single object") {
|
|
gcode = slice({ cube(20) }, config);
|
|
}
|
|
SECTION("printing multiple objects") {
|
|
gcode = slice({ cube(20), cube(20) }, config);
|
|
}
|
|
|
|
REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t) config.opt_int("skirt_height"));
|
|
}
|
|
|
|
TEST_CASE("Brim uses first layer speed", "[SkirtBrim]") {
|
|
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "gcode_comments", true },
|
|
{ "initial_layer_speed", 10 },
|
|
{ "initial_layer_infill_speed", 20 },
|
|
{ "machine_start_gcode", "" },
|
|
{ "skirt_loops", 0 },
|
|
{ "slow_down_for_layer_cooling", false },
|
|
{ "z_hop", 0 }
|
|
});
|
|
|
|
const std::string gcode = Slic3r::Test::slice({cube(20)}, config);
|
|
|
|
const double brim_feedrate = first_extrusion_feedrate_for_feature(gcode, "Brim");
|
|
REQUIRE(brim_feedrate > 0.0);
|
|
REQUIRE_THAT(brim_feedrate, Catch::Matchers::WithinAbs(600.0, 1e-3));
|
|
|
|
const double bottom_surface_feedrate = first_extrusion_feedrate_for_feature(gcode, "Bottom surface");
|
|
REQUIRE(bottom_surface_feedrate > 0.0);
|
|
REQUIRE_THAT(bottom_surface_feedrate, Catch::Matchers::WithinAbs(1200.0, 1e-3));
|
|
}
|
|
|
|
SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
|
|
GIVEN("A default configuration") {
|
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
|
config.set_num_extruders(4);
|
|
config.set_deserialize_strict({
|
|
{ "initial_layer_print_height", 0.3 },
|
|
// avoid altering speeds unexpectedly
|
|
{ "slow_down_for_layer_cooling", false },
|
|
{ "initial_layer_speed", "100%" },
|
|
// remove noise from top/solid layers
|
|
{ "top_shell_layers", 0 },
|
|
{ "bottom_shell_layers", 1 },
|
|
{ "machine_start_gcode", "T[initial_tool]\n" },
|
|
});
|
|
|
|
WHEN("Brim width is set to 5") {
|
|
config.set_deserialize_strict({
|
|
{ "wall_loops", 0 },
|
|
{ "skirt_loops", 0 },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
});
|
|
THEN("Brim is generated") {
|
|
std::string gcode = slice({ cube(20) }, config);
|
|
REQUIRE(! layers_with_role(gcode, "brim").empty());
|
|
}
|
|
}
|
|
|
|
WHEN("brim width to 1 with layer_width of 0.5") {
|
|
config.set_deserialize_strict({
|
|
{ "skirt_loops", 0 },
|
|
{ "initial_layer_line_width", 0.5 },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 1 },
|
|
});
|
|
THEN("2 brim lines") {
|
|
Print print;
|
|
init_and_process_print({ cube(20) }, print, config);
|
|
REQUIRE(brim_loop_count(print) == 2);
|
|
}
|
|
}
|
|
|
|
WHEN("Object is plated with overhang support and a brim") {
|
|
config.set_deserialize_strict({
|
|
{ "layer_height", 0.4 },
|
|
{ "initial_layer_print_height", 0.4 },
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_distance", 0 },
|
|
{ "enable_support", 1 },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
});
|
|
THEN("Support and brim are both emitted") {
|
|
std::string gcode = slice({ TestMesh::overhang }, config);
|
|
REQUIRE(! layers_with_role(gcode, "support").empty());
|
|
REQUIRE(! layers_with_role(gcode, "brim").empty());
|
|
}
|
|
}
|
|
|
|
WHEN("an object with support is surrounded by a skirt") {
|
|
config.set_deserialize_strict({
|
|
{ "enable_support", 1 },
|
|
{ "skirt_loops", 1 },
|
|
{ "skirt_distance", 2 },
|
|
{ "brim_type", "no_brim" },
|
|
{ "z_hop", 0 },
|
|
});
|
|
THEN("the skirt is long enough to enclose the object and its support") {
|
|
std::string gcode = slice({ TestMesh::overhang }, config);
|
|
const double first_layer_z = config.opt_float("initial_layer_print_height");
|
|
|
|
// On the first layer, accumulate the skirt loop length and collect the
|
|
// object + support extrusion points; the skirt must enclose them.
|
|
double skirt_length = 0.0;
|
|
Points footprint;
|
|
GCodeReader parser;
|
|
parser.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (! line.extruding(self) || line.dist_XY(self) <= 0 || std::abs(self.z() - first_layer_z) > 0.01)
|
|
return;
|
|
if (line.comment().find("skirt") != std::string_view::npos)
|
|
skirt_length += line.dist_XY(self);
|
|
else
|
|
footprint.push_back(Point::new_scale(line.new_X(self), line.new_Y(self)));
|
|
});
|
|
|
|
const double hull_perimeter = unscale<double>(Geometry::convex_hull(footprint).split_at_first_point().length());
|
|
REQUIRE(hull_perimeter > 0.0); // guard against an empty footprint passing trivially
|
|
REQUIRE(skirt_length > hull_perimeter);
|
|
}
|
|
}
|
|
|
|
WHEN("Large minimum skirt length is used.") {
|
|
// One skirt loop around a 20mm cube is ~88mm, so 500mm forces extra loops.
|
|
config.set_deserialize_strict({
|
|
{ "skirt_loops", 1 },
|
|
{ "min_skirt_length", 500 },
|
|
});
|
|
THEN("The skirt is extended to at least the minimum length") {
|
|
std::string gcode = slice({ cube(20) }, config);
|
|
double skirt_length = 0.0;
|
|
GCodeReader parser;
|
|
parser.parse_buffer(gcode, [&skirt_length](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (line.extruding(self) && line.comment().find("skirt") != std::string_view::npos)
|
|
skirt_length += line.dist_XY(self);
|
|
});
|
|
REQUIRE(skirt_length >= 500.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Belt printers ---------------------------------------------------------------
|
|
//
|
|
// On a tilted belt the brim is laid onto the belt PLANE rather than into the Z=0
|
|
// bed plane, so it is spread across many layers instead of living on the first
|
|
// one. The discriminating measurement is the number of contiguous brim runs in
|
|
// the G-code: a flat plate brim gives a single run, a belt brim gives one per
|
|
// layer that carries a band. Distinct Z values are useless here, because the
|
|
// machine-frame transform couples Y into Z so every belt move has its own Z.
|
|
static DynamicPrintConfig belt_brim_config()
|
|
{
|
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
|
config.set_deserialize_strict({
|
|
{ "belt_printer", 1 },
|
|
{ "belt_slice_rotation", "x" },
|
|
{ "belt_slice_rotation_angle", 45 },
|
|
{ "belt_slice_rotation_global", 1 },
|
|
{ "gcode_remap_x", "rev_x" },
|
|
{ "gcode_remap_y", "pos_z" },
|
|
{ "gcode_remap_z", "pos_y" },
|
|
{ "layer_height", 0.2 },
|
|
{ "initial_layer_print_height", 0.2 },
|
|
{ "skirt_loops", 0 },
|
|
{ "top_shell_layers", 0 },
|
|
{ "bottom_shell_layers", 1 },
|
|
{ "machine_start_gcode", "T[initial_tool]\n" },
|
|
});
|
|
return config;
|
|
}
|
|
|
|
TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]")
|
|
{
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
// A plate-brim implementation would score 1 here.
|
|
CHECK(role_passes(gcode, "brim") > 10);
|
|
}
|
|
|
|
TEST_CASE("Belt brim is absent when both widths are zero", "[SkirtBrim][belt]")
|
|
{
|
|
// The "no effect when disabled" guard: brim_type Auto is the shipped default and
|
|
// reports has_brim() even at width 0, so this also pins the gate that keeps the
|
|
// flat plate brim from running on a tilted belt.
|
|
const char *brim_type = GENERATE("auto_brim", "outer_only", "no_brim");
|
|
DYNAMIC_SECTION("brim_type " << brim_type) {
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", brim_type },
|
|
{ "brim_width", 0 },
|
|
{ "leading_brim_length", 0 },
|
|
{ "extra_brim_width", 0 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
CHECK(role_passes(gcode, "brim") == 0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Leading brim length alone produces a belt brim", "[SkirtBrim][belt]")
|
|
{
|
|
// Exercises the leading_brim_length-only enablement path and the downhill sweep.
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 0 },
|
|
{ "leading_brim_length", 5 },
|
|
{ "brim_object_gap", 0 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
CHECK(role_passes(gcode, "brim") > 0);
|
|
}
|
|
|
|
TEST_CASE("Leading brim length reaches further ahead of the object", "[SkirtBrim][belt]")
|
|
{
|
|
// Compared between two runs rather than against an absolute coordinate, so the
|
|
// assertion survives any change of origin or axis remap.
|
|
auto brim_extent = [](double extra) {
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 3 },
|
|
{ "leading_brim_length", extra },
|
|
{ "brim_object_gap", 0 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
// The apron prints before the object reaches the belt, so it shows up as brim
|
|
// extrusion at the lowest machine Z of any brim move.
|
|
double min_z = std::numeric_limits<double>::max();
|
|
GCodeReader parser;
|
|
parser.parse_buffer(gcode, [&min_z](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (line.extruding(self) && line.comment().find("brim") != std::string_view::npos)
|
|
min_z = std::min(min_z, static_cast<double>(self.z()));
|
|
});
|
|
return min_z;
|
|
};
|
|
const double without = brim_extent(0.);
|
|
const double with = brim_extent(10.);
|
|
REQUIRE(without < std::numeric_limits<double>::max());
|
|
REQUIRE(with < std::numeric_limits<double>::max());
|
|
CHECK(with < without);
|
|
}
|
|
|
|
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");
|
|
DYNAMIC_SECTION("brim_type " << brim_type) {
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", brim_type },
|
|
{ "brim_width", 5 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
REQUIRE(! gcode.empty());
|
|
if (std::string(brim_type) == "no_brim")
|
|
CHECK(role_passes(gcode, "brim") == 0);
|
|
else if (std::string(brim_type) != "inner_only")
|
|
// A solid cube has no holes, so inner_only legitimately yields nothing.
|
|
CHECK(role_passes(gcode, "brim") > 0);
|
|
}
|
|
}
|
|
|
|
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
|
|
// cannot reach the G-code on any belt printer: it is emitted out of
|
|
// skirt_brim_groups(), which _make_skirt() builds, and that returns early for every
|
|
// belt printer. So an untilted belt printer gets nothing - unchanged by this
|
|
// feature. Making the flat brim work here would mean reopening the belt skirt gate,
|
|
// which is a separate change; Print::validate() warns instead.
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "belt_slice_rotation", "none" },
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
CHECK(role_passes(gcode, "brim") == 0);
|
|
}
|
|
|
|
TEST_CASE("Belt brim does not resurrect the skirt", "[SkirtBrim][belt]")
|
|
{
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "skirt_loops", 2 },
|
|
});
|
|
const std::string gcode = slice({ cube(20) }, config);
|
|
CHECK(role_passes(gcode, "skirt") == 0);
|
|
}
|
|
|
|
TEST_CASE("Belt brim lines all have the same width", "[SkirtBrim][belt]")
|
|
{
|
|
// Each brim line's extrusion volume comes from its nozzle-to-belt clearance. Anchoring
|
|
// every line to a fixed fraction of its own band gives them all the same clearance, so
|
|
// they all come out the same width. The nominal-spacing lattice this replaced let each
|
|
// line land wherever it fell inside its band, so the clearance - and the width with it -
|
|
// varied by 2x, which showed up as visibly ragged brim.
|
|
DynamicPrintConfig config = belt_brim_config();
|
|
config.set_deserialize_strict({
|
|
{ "brim_type", "outer_only" },
|
|
{ "brim_width", 5 },
|
|
{ "brim_object_gap", 0 },
|
|
});
|
|
Print print;
|
|
init_and_process_print({ cube(20) }, print, config);
|
|
const PrintObject *obj = print.objects().front();
|
|
|
|
std::vector<float> widths;
|
|
auto collect = [&widths](const ExtrusionEntityCollection &coll) {
|
|
for (const ExtrusionEntity *ee : coll.entities)
|
|
if (const auto *path = dynamic_cast<const ExtrusionPath *>(ee))
|
|
widths.push_back(path->width);
|
|
};
|
|
for (const ExtrusionEntityCollection &band : obj->belt_brim_by_layer())
|
|
collect(band);
|
|
for (const BeltBrimBand &band : obj->belt_brim_prologue())
|
|
collect(band.fills);
|
|
|
|
REQUIRE(widths.size() > 10);
|
|
const float lo = *std::min_element(widths.begin(), widths.end());
|
|
const float hi = *std::max_element(widths.begin(), widths.end());
|
|
CHECK_THAT(hi, Catch::Matchers::WithinRel(lo, 1e-4));
|
|
}
|