mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 07:11:10 +00:00
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes. clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer. * Remove Unused Project Includes From Files With Platform-Specific Code A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block. * Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass The platform-file pass treated pchheader.hpp as an ordinary header and emptied it, and left GUI_Preview.hpp and 14 other files relying on headers they no longer reached directly. * Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call * Include Headers That Files Reached Through Ones the Cleanup Removed * Drop Includes Duplicated by the Cleanup or by Main's Own Additions * Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
334 lines
11 KiB
C++
334 lines
11 KiB
C++
#include <algorithm>
|
|
#include <catch2/catch_all.hpp>
|
|
#include "libslic3r/libslic3r.h"
|
|
#include "libslic3r/Polygon.hpp"
|
|
#include "libslic3r/Point.hpp"
|
|
#include <string>
|
|
#include "libslic3r/Config.hpp"
|
|
#include <vector>
|
|
#include <limits>
|
|
#include <cstddef>
|
|
#include <cmath>
|
|
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include <catch2/generators/catch_generators.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"
|
|
#include "libslic3r/MultiMaterialSegmentation.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);
|
|
}
|
|
|
|
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);
|
|
}
|