mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
With best_object_pos away from the bed centre the placer packs the pile inside the bin and then translates it so its centre lands on that point, without checking that it still fits there. A belt printer aims at the leading end of the belt (BabyBelt Pro: 0.5, 0.05), so any pile longer than the 50 mm around that point was pushed past the edge: four 90 mm parts on the 95 x 500 mm belt ended with one across the edge and one outside while 290 mm of belt stayed free. The final alignment now stops the pile at the edge of the bin; the items' inflated boxes leave the object spacing as the margin. A pile that does not fit along an axis is centred on it, as before.
345 lines
12 KiB
C++
345 lines
12 KiB
C++
#include <limits>
|
|
#include <catch2/catch_all.hpp>
|
|
|
|
#include "libslic3r/Arrange.hpp"
|
|
#include "libslic3r/BoundingBox.hpp"
|
|
#include "libslic3r/ClipperUtils.hpp"
|
|
#include "libslic3r/ExPolygon.hpp"
|
|
#include "libslic3r/Print.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
|
|
using namespace Slic3r;
|
|
using namespace Slic3r::arrangement;
|
|
|
|
namespace {
|
|
|
|
using Catch::Matchers::WithinRel;
|
|
|
|
// Square of the given (scaled) side, lower-left at the origin. bed_idx starts at
|
|
// 0 because arrange() seeds the nester's bin from it (see ModelArrange.cpp).
|
|
ArrangePolygon make_square(coord_t side)
|
|
{
|
|
ArrangePolygon ap;
|
|
Polygon p;
|
|
p.points = {Point(0, 0), Point(side, 0), Point(side, side), Point(0, side)};
|
|
ap.poly = ExPolygon(p);
|
|
ap.bed_idx = 0;
|
|
return ap;
|
|
}
|
|
|
|
ArrangePolygons squares(int n, double side_mm, double height_mm = 0.)
|
|
{
|
|
ArrangePolygons items;
|
|
for (int i = 0; i < n; ++i) {
|
|
items.emplace_back(make_square(scaled(side_mm)));
|
|
items.back().height = height_mm;
|
|
}
|
|
return items;
|
|
}
|
|
|
|
// Bed [0,0]..[w,h] in scaled coordinates.
|
|
BoundingBox bed(double w_mm, double h_mm)
|
|
{
|
|
return BoundingBox(Point(0, 0), Point(scaled(w_mm), scaled(h_mm)));
|
|
}
|
|
|
|
// The default progress callback prints to stdout; silence it.
|
|
ArrangeParams quiet_params(coord_t min_dist = 0)
|
|
{
|
|
ArrangeParams p{min_dist};
|
|
p.progressind = [](unsigned, std::string) {};
|
|
return p;
|
|
}
|
|
|
|
ExPolygons placed_shapes(const ArrangePolygons &items)
|
|
{
|
|
ExPolygons out;
|
|
out.reserve(items.size());
|
|
for (const ArrangePolygon &ap : items)
|
|
out.emplace_back(ap.transformed_poly());
|
|
return out;
|
|
}
|
|
|
|
// Area double-counted across the shapes: the sum counts overlaps twice, the
|
|
// union once, so the difference is the overlapping area (0 when disjoint).
|
|
double overlap_area(const ExPolygons &shapes)
|
|
{
|
|
double sum = 0;
|
|
for (const ExPolygon &e : shapes)
|
|
sum += e.area();
|
|
double uni = 0;
|
|
for (const ExPolygon &e : union_ex(shapes))
|
|
uni += e.area();
|
|
return sum - uni;
|
|
}
|
|
|
|
// Relative tolerance absorbs the area-unit rounding the clipper union introduces.
|
|
bool disjoint(const ExPolygons &shapes)
|
|
{
|
|
double total = 0;
|
|
for (const ExPolygon &e : shapes)
|
|
total += e.area();
|
|
return overlap_area(shapes) <= total * 1e-9;
|
|
}
|
|
|
|
void require_no_overlap(const ArrangePolygons &items)
|
|
{
|
|
REQUIRE(disjoint(placed_shapes(items)));
|
|
}
|
|
|
|
// The sequential-print floor is chosen by comparing object height against the nozzle,
|
|
// so the two are defined together and every expectation is derived from them.
|
|
constexpr double NOZZLE_HEIGHT_MM = 2.5;
|
|
constexpr double CLEARANCE_MM = 30.;
|
|
constexpr double NOZZLE_FLOOR_MM = MAX_OUTER_NOZZLE_DIAMETER / 2.;
|
|
|
|
ArrangeParams seq_print_params(coord_t min_dist)
|
|
{
|
|
ArrangeParams p = quiet_params(min_dist);
|
|
p.is_seq_print = true;
|
|
p.clearance_radius = float(CLEARANCE_MM);
|
|
p.nozzle_height = float(NOZZLE_HEIGHT_MM);
|
|
p.object_skirt_offset = 0.f;
|
|
return p;
|
|
}
|
|
|
|
// update_selected_items_inflation reads the bed out of the config to cap inflation.
|
|
DynamicPrintConfig bed_config()
|
|
{
|
|
DynamicPrintConfig c;
|
|
c.set_key_value("printable_area", new ConfigOptionPoints{{0, 0}, {200, 0}, {200, 200}, {0, 200}});
|
|
return c;
|
|
}
|
|
|
|
ArrangePolygons squares_of_heights(const std::vector<double> &heights_mm)
|
|
{
|
|
ArrangePolygons items;
|
|
for (double height_mm : heights_mm)
|
|
items.push_back(squares(1, 20., height_mm).front());
|
|
return items;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Prove the overlap check the other tests rely on actually detects overlap.
|
|
TEST_CASE("overlap_area detects overlap and ignores touching edges", "[Arrange]")
|
|
{
|
|
auto square_at = [](double x_mm) {
|
|
ArrangePolygon ap = make_square(scaled(20.));
|
|
ap.translation = Vec2crd(scaled(x_mm), 0);
|
|
return ap.transformed_poly();
|
|
};
|
|
ExPolygon a = square_at(0.);
|
|
|
|
SECTION("disjoint shapes are reported disjoint") {
|
|
REQUIRE(disjoint({a, square_at(30.)}));
|
|
}
|
|
SECTION("edge-touching shapes are reported disjoint") {
|
|
REQUIRE(disjoint({a, square_at(20.)}));
|
|
}
|
|
SECTION("overlapping shapes are not, and the area is measured") {
|
|
REQUIRE_FALSE(disjoint({a, square_at(10.)}));
|
|
REQUIRE_THAT(overlap_area({a, square_at(10.)}),
|
|
WithinRel(double(scaled(10.)) * scaled(20.), 1e-9)); // 10x20 mm
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Arrange places every item on the physical bed", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(5, 20.);
|
|
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
|
|
|
for (const ArrangePolygon &ap : items)
|
|
REQUIRE(ap.bed_idx == 0);
|
|
}
|
|
|
|
TEST_CASE("Arranged items stay within the bed", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(6, 30.);
|
|
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
|
|
|
for (const ArrangePolygon &ap : items) {
|
|
REQUIRE(ap.bed_idx == 0);
|
|
REQUIRE(bed(200, 200).contains(ap.transformed_poly().contour.bounding_box()));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Arranged items do not overlap", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(6, 40.);
|
|
arrange(items, bed(250, 250), quiet_params(scaled(2.)));
|
|
|
|
require_no_overlap(items);
|
|
}
|
|
|
|
TEST_CASE("Arrange spaces items by their inflation", "[Arrange]")
|
|
{
|
|
// Per-item inflation is how the arranger enforces clearance (the GUI fills it
|
|
// from min_obj_distance). Two items inflated 4mm each end up >= 8mm apart.
|
|
ArrangePolygons items = squares(4, 20.);
|
|
for (ArrangePolygon &ap : items)
|
|
ap.inflation = scaled(4.);
|
|
arrange(items, bed(200, 200), quiet_params());
|
|
|
|
// Axis-aligned squares are their own bounding boxes, so the clearance between
|
|
// a pair is the distance between their boxes (1mm slack for nester rounding).
|
|
std::vector<BoundingBox> boxes;
|
|
for (const ExPolygon &e : placed_shapes(items))
|
|
boxes.push_back(e.contour.bounding_box());
|
|
|
|
double min_gap = std::numeric_limits<double>::max();
|
|
for (size_t i = 0; i < boxes.size(); ++i)
|
|
for (size_t j = i + 1; j < boxes.size(); ++j) {
|
|
coord_t sx = std::max<coord_t>(0, std::max(boxes[j].min.x() - boxes[i].max.x(),
|
|
boxes[i].min.x() - boxes[j].max.x()));
|
|
coord_t sy = std::max<coord_t>(0, std::max(boxes[j].min.y() - boxes[i].max.y(),
|
|
boxes[i].min.y() - boxes[j].max.y()));
|
|
min_gap = std::min(min_gap, std::sqrt(double(sx) * sx + double(sy) * sy));
|
|
}
|
|
|
|
REQUIRE(min_gap >= double(scaled(8.)) - double(scaled(0.5)));
|
|
}
|
|
|
|
TEST_CASE("An item larger than the bed cannot be placed", "[Arrange]")
|
|
{
|
|
ArrangePolygons items;
|
|
items.emplace_back(make_square(scaled(20.)));
|
|
items.emplace_back(make_square(scaled(400.))); // far bigger than the bed
|
|
|
|
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
|
|
|
REQUIRE(items[0].bed_idx == 0);
|
|
REQUIRE(items[1].bed_idx == UNARRANGED);
|
|
}
|
|
|
|
TEST_CASE("Items overflowing one bed spill onto virtual beds", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(8, 90.); // eight 90mm squares cannot share a 200x200 bed
|
|
arrange(items, bed(200, 200), quiet_params(scaled(2.)));
|
|
|
|
int max_bed = 0;
|
|
for (const ArrangePolygon &ap : items) {
|
|
REQUIRE(ap.bed_idx >= 0); // placed somewhere
|
|
max_bed = std::max(max_bed, ap.bed_idx);
|
|
}
|
|
REQUIRE(max_bed >= 1); // at least one on a virtual bed
|
|
}
|
|
|
|
TEST_CASE("Arrange handles an empty input", "[Arrange]")
|
|
{
|
|
ArrangePolygons items;
|
|
REQUIRE_NOTHROW(arrange(items, bed(200, 200), quiet_params()));
|
|
REQUIRE(items.empty());
|
|
}
|
|
|
|
TEST_CASE("Arrange without final alignment keeps items disjoint", "[Arrange]")
|
|
{
|
|
// do_final_align = false selects Alignment::DONT_ALIGN (skips recentering).
|
|
ArrangePolygons items = squares(6, 40.);
|
|
ArrangeParams params = quiet_params(scaled(2.));
|
|
params.do_final_align = false;
|
|
|
|
arrange(items, bed(250, 250), params);
|
|
|
|
for (const ArrangePolygon &ap : items)
|
|
REQUIRE(ap.bed_idx == 0);
|
|
require_no_overlap(items);
|
|
}
|
|
|
|
TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]")
|
|
{
|
|
// align_center != (0.5, 0.5) selects Alignment::USER_DEFINED.
|
|
ArrangePolygons items = squares(5, 30.);
|
|
ArrangeParams params = quiet_params(scaled(2.));
|
|
params.align_center = Vec2d(0.3, 0.7);
|
|
|
|
arrange(items, bed(250, 250), params);
|
|
|
|
for (const ArrangePolygon &ap : items)
|
|
REQUIRE(ap.bed_idx == 0);
|
|
require_no_overlap(items);
|
|
}
|
|
|
|
// A belt printer starts its parts at the leading end of the belt (best_object_pos 0.5, 0.05).
|
|
// Centring a pile on a point that close to the edge pushed everything longer than the room
|
|
// around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the
|
|
// edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge.
|
|
TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]")
|
|
{
|
|
const BoundingBox belt = bed(95, 500);
|
|
ArrangePolygons items = squares(4, 90.);
|
|
ArrangeParams params = quiet_params(scaled(2.));
|
|
params.align_center = Vec2d(0.5, 0.05);
|
|
|
|
arrange(items, belt, params);
|
|
|
|
coord_t lowest = std::numeric_limits<coord_t>::max();
|
|
for (const ArrangePolygon &ap : items) {
|
|
REQUIRE(ap.bed_idx == 0);
|
|
const BoundingBox bb = ap.transformed_poly().contour.bounding_box();
|
|
CHECK(belt.contains(bb));
|
|
lowest = std::min(lowest, bb.min.y());
|
|
}
|
|
// Snapped to the edge it was aimed at, less the spacing margin, not re-centred.
|
|
CHECK(lowest < scaled(10.));
|
|
require_no_overlap(items);
|
|
}
|
|
|
|
TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]")
|
|
{
|
|
// The only place sequential-print clearance is enforced. The arrange menu offers
|
|
// no floor of its own, so a stored 0 has to be raised here or not at all.
|
|
struct Case
|
|
{
|
|
std::string description;
|
|
std::vector<double> heights;
|
|
double skirt_offset_mm;
|
|
double expected_floor_mm;
|
|
};
|
|
|
|
auto c = GENERATE(values<Case>({
|
|
{"objects taller than the nozzle need the full clearance", {NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2}, 0., CLEARANCE_MM},
|
|
{"an object exactly at the nozzle height counts as tall", {NOZZLE_HEIGHT_MM, NOZZLE_HEIGHT_MM}, 0., CLEARANCE_MM},
|
|
{"one tall object among short ones is enough", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM * 2}, 0., CLEARANCE_MM},
|
|
{"objects the nozzle clears keep only the nozzle-width floor", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM / 2}, 0., NOZZLE_FLOOR_MM},
|
|
{"a wide skirt raises the floor for short objects", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM / 2}, 3., 6.},
|
|
}));
|
|
|
|
DYNAMIC_SECTION(c.description)
|
|
{
|
|
ArrangePolygons items = squares_of_heights(c.heights);
|
|
DynamicPrintConfig cfg = bed_config();
|
|
ArrangeParams p = seq_print_params(0);
|
|
p.object_skirt_offset = float(c.skirt_offset_mm);
|
|
|
|
update_selected_items_inflation(items, &cfg, p);
|
|
|
|
CHECK(p.min_obj_distance >= scaled(c.expected_floor_mm));
|
|
CHECK(p.min_obj_distance <= scaled(c.expected_floor_mm + 0.01));
|
|
// Half each, so a pair ends up a full min_obj_distance apart.
|
|
CHECK(items.front().inflation == p.min_obj_distance / 2);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Sequential print keeps an object distance already above the floor", "[Arrange]")
|
|
{
|
|
const coord_t stored = scaled(CLEARANCE_MM * 2);
|
|
ArrangePolygons items = squares_of_heights({NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2});
|
|
DynamicPrintConfig cfg = bed_config();
|
|
ArrangeParams p = seq_print_params(stored);
|
|
|
|
update_selected_items_inflation(items, &cfg, p);
|
|
CHECK(p.min_obj_distance == stored);
|
|
}
|
|
|
|
TEST_CASE("Layered printing does not floor the object distance", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares_of_heights({NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2});
|
|
DynamicPrintConfig cfg = bed_config();
|
|
ArrangeParams p = seq_print_params(0);
|
|
p.is_seq_print = false;
|
|
|
|
update_selected_items_inflation(items, &cfg, p);
|
|
CHECK(p.min_obj_distance == 0);
|
|
}
|