Add outer-only mouse ears and align ear radius controls (#15015)

Improve mouse ear brim controls
This commit is contained in:
maddavo
2026-08-02 12:54:16 +10:00
committed by GitHub
parent fb36d5e73b
commit 13ae3a1c90
14 changed files with 252 additions and 53 deletions

View File

@@ -4,6 +4,7 @@
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Layer.hpp"
#include <boost/algorithm/string.hpp>
@@ -32,6 +33,30 @@ static size_t brim_loop_count(Print &print)
return n;
}
static bool brim_enters_first_layer_hole(Print &print)
{
const PrintObject *object = print.get_object(0);
Polygons holes;
for (const ExPolygon &slice : object->layers().front()->lslices)
holes.insert(holes.end(), slice.holes.begin(), slice.holes.end());
const Vec3d plate_origin = print.get_plate_origin();
Point shift = object->instances().front().shift_without_plate_offset();
shift += Point(scaled(plate_origin.x()), scaled(plate_origin.y()));
for (Polygon &hole : holes)
hole.translate(shift);
for (const auto &kv : print.get_brimMap()) {
Polylines brim_paths;
kv.second.collect_polylines(brim_paths);
for (const Polyline &path : brim_paths)
for (const Point &point : path.points)
if (contains(holes, point, false))
return true;
}
return false;
}
// 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]")
@@ -225,6 +250,131 @@ TEST_CASE("Brim ears appear only at corners within the max angle", "[SkirtBrim]"
}
}
TEST_CASE("Outer-only brim ears stay out of model holes", "[SkirtBrim]")
{
const bool outer_only = GENERATE(false, true);
DYNAMIC_SECTION("brim_ears_outer_only=" << outer_only) {
Print print;
init_and_process_print({ TestMesh::cube_with_concave_hole }, print, {
{ "skirt_loops", 0 },
{ "brim_type", "brim_ears" },
{ "brim_width", 2 },
{ "brim_ears_max_angle", 125 },
{ "brim_ears_detection_length", 0 },
{ "brim_ears_outer_only", outer_only },
{ "initial_layer_line_width", 0.5 },
});
REQUIRE(brim_loop_count(print) > 0);
CHECK(brim_enters_first_layer_hole(print) != outer_only);
}
}
TEST_CASE("Painted brim ear radius controls sliced size", "[SkirtBrim]")
{
constexpr double ear_radius = 10.0;
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirt_loops", 0 },
{ "brim_type", "painted" },
{ "brim_width", 15 },
{ "brim_object_gap", 0.1 },
{ "brim_ears_outer_only", true },
{ "initial_layer_line_width", 0.5 },
});
Print print;
Model model;
init_print({ cube(20) }, print, model, config);
print.process();
const PrintObject *object = print.get_object(0);
REQUIRE(!object->layers().front()->lslices.empty());
const Point ear_center = object->layers().front()->lslices.front().contour.points.front();
Transform3d model_transform = model.objects.front()->instances.front()->get_transformation().get_matrix_no_offset();
const Point &center_offset = object->center_offset();
model_transform = model_transform.pretranslate(
Vec3d(-unscale<double>(center_offset.x()), -unscale<double>(center_offset.y()), 0));
Vec3d model_pos = model_transform.inverse() *
Vec3d(unscale<double>(ear_center.x()), unscale<double>(ear_center.y()), 0);
model_pos.z() = model.objects.front()->raw_mesh_bounding_box().min.z() - 0.0001;
model.objects.front()->brim_points = {
BrimPoint(model_pos.cast<float>(), float(ear_radius)),
};
print.apply(model, config);
print.process();
const Vec3d plate_origin = print.get_plate_origin();
Point path_center = ear_center + object->instances().front().shift_without_plate_offset();
path_center += Point(scaled(plate_origin.x()), scaled(plate_origin.y()));
double max_path_radius = 0.0;
for (const auto &kv : print.get_brimMap()) {
Polylines brim_paths;
kv.second.collect_polylines(brim_paths);
for (const Polyline &path : brim_paths)
for (const Point &point : path.points)
max_path_radius = std::max(max_path_radius, unscale<double>((point - path_center).cast<double>().norm()));
}
REQUIRE(max_path_radius > 0.0);
INFO("Outermost painted-ear path radius: " << max_path_radius << " mm");
CHECK(max_path_radius > ear_radius - 0.5);
CHECK(max_path_radius < ear_radius);
}
TEST_CASE("Outer-only painted brim ears stay out of model holes", "[SkirtBrim]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirt_loops", 0 },
{ "brim_type", "painted" },
{ "brim_ears_outer_only", true },
{ "initial_layer_line_width", 0.5 },
});
Print print;
Model model;
init_print({ TestMesh::cube_with_concave_hole }, print, model, config);
// Slice once to obtain exact outer and inner contour points in print
// coordinates, then express them in the model coordinates painted ears store.
print.process();
const PrintObject *object = print.get_object(0);
REQUIRE(!object->layers().front()->lslices.empty());
REQUIRE(!object->layers().front()->lslices.front().holes.empty());
Transform3d model_transform = model.objects.front()->instances.front()->get_transformation().get_matrix_no_offset();
const Point &center_offset = object->center_offset();
model_transform = model_transform.pretranslate(
Vec3d(-unscale<double>(center_offset.x()), -unscale<double>(center_offset.y()), 0));
const double bottom_z = model.objects.front()->raw_mesh_bounding_box().min.z() - 0.0001;
auto painted_point = [&model_transform, bottom_z](const Point &point) {
Vec3d model_pos = model_transform.inverse() *
Vec3d(unscale<double>(point.x()), unscale<double>(point.y()), 0);
model_pos.z() = bottom_z;
return BrimPoint(model_pos.cast<float>(), 3.f);
};
const ExPolygon &first_slice = object->layers().front()->lslices.front();
Polygon inner_contour = first_slice.holes.front();
inner_contour.reverse();
const Points inner_ear_points = inner_contour.concave_points(55. * PI / 180.);
REQUIRE(!inner_ear_points.empty());
model.objects.front()->brim_points = {
painted_point(first_slice.contour.points.front()),
painted_point(inner_ear_points.front()),
};
print.apply(model, config);
print.process();
REQUIRE(brim_loop_count(print) > 0);
CHECK_FALSE(brim_enters_first_layer_hole(print));
}
SCENARIO("Skirt has the configured number of loops", "[SkirtBrim]") {
GIVEN("20mm cube and default config") {
WHEN("skirt_loops is set to 2") {