#include #include "test_utils.hpp" #include #include #include #include #include "libslic3r/Model.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" #include "libslic3r/Format/3mf.hpp" #include "libslic3r/ModelArrange.hpp" static Slic3r::Model get_example_model_with_20mm_cube() { using namespace Slic3r; Model model; ModelObject* new_object = model.add_object(); new_object->name = "20mm_cube"; new_object->add_instance(); TriangleMesh mesh = make_cube(20., 20., 20.); mesh.translate(Vec3f{-10.f, -10.f, 0.}); ModelVolume* new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; return model; } [[maybe_unused]] static Slic3r::Model get_example_model_with_random_cube_objects(size_t N = 0) { using namespace Slic3r; Model model; auto cube_count = N == 0 ? random_value(size_t(1), size_t(100)) : N; INFO("Cube count " << cube_count); ModelObject* new_object = model.add_object(); new_object->name = "20mm_cube"; TriangleMesh mesh = make_cube(20., 20., 20.); ModelVolume* new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; for (size_t i = 0; i < cube_count; ++i) { ModelInstance *inst = new_object->add_instance(); arr2::transform_instance(*inst, Vec2d{random_value(-arr2::UnscaledCoordLimit / 10., arr2::UnscaledCoordLimit / 10.), random_value(-arr2::UnscaledCoordLimit / 10., arr2::UnscaledCoordLimit / 10.)}, random_value(0., 2 * PI)); } return model; } static Slic3r::Model get_example_model_with_arranged_primitives() { using namespace Slic3r; Model model; ModelObject* new_object = model.add_object(); new_object->name = "20mm_cube"; ModelInstance *cube_inst = new_object->add_instance(); TriangleMesh mesh = make_cube(20., 20., 20.); mesh.translate(Vec3f{-10.f, -10.f, 0.}); ModelVolume* new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; ModelInstance *inst = new_object->add_instance(*cube_inst); auto tr = inst->get_matrix(); tr.translate(Vec3d{25., 0., 0.}); inst->set_transformation(Geometry::Transformation{tr}); new_object = model.add_object(); new_object->name = "20mm_cyl"; new_object->add_instance(); mesh = make_cylinder(10., 20.); mesh.translate(Vec3f{0., -25.f, 0.}); new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; new_object = model.add_object(); new_object->name = "20mm_sphere"; new_object->add_instance(); mesh = make_sphere(10.); mesh.translate(Vec3f{25., -25.f, 0.}); new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; return model; } class RandomArrangeSettings: public Slic3r::arr2::ArrangeSettingsView { Slic3r::arr2::ArrangeSettingsDb::Values m_v; std::mt19937 m_rng; public: explicit RandomArrangeSettings(int seed) : m_rng(seed) { std::uniform_real_distribution fdist(0., 100.f); std::uniform_int_distribution<> bdist(0, 1); std::uniform_int_distribution<> dist; m_v.d_obj = fdist(m_rng); m_v.d_bed = fdist(m_rng); m_v.rotations = bdist(m_rng); m_v.geom_handling = static_cast(dist(m_rng) % ghCount); m_v.arr_strategy = static_cast(dist(m_rng) % asCount); m_v.xl_align = static_cast(dist(m_rng) % xlpCount); } explicit RandomArrangeSettings() : m_rng(std::random_device{} ()) {} float get_distance_from_objects() const override { return m_v.d_obj; } float get_distance_from_bed() const override { return m_v.d_bed; } bool is_rotation_enabled() const override { return m_v.rotations; } XLPivots get_xl_alignment() const override { return m_v.xl_align; } GeometryHandling get_geometry_handling() const override { return m_v.geom_handling; } ArrangeStrategy get_arrange_strategy() const override { return m_v.arr_strategy; } }; TEST_CASE("ModelInstance should be retrievable when imbued into ArrangeItem", "[arrange2][integration]") { using namespace Slic3r; Model model = get_example_model_with_20mm_cube(); auto mi = model.objects.front()->instances.front(); arr2::ArrangeItem itm; arr2::PhysicalOnlyVBedHandler vbedh; auto vbedh_ptr = static_cast(&vbedh); auto arrbl = arr2::ArrangeableModelInstance{mi, vbedh_ptr, nullptr, {0, 0}}; arr2::imbue_id(itm, arrbl.id()); std::optional id_returned = arr2::retrieve_id(itm); REQUIRE((id_returned && *id_returned == mi->id())); } struct PhysicalBed { Slic3r::arr2::InfiniteBed bed; Slic3r::arr2::PhysicalOnlyVBedHandler vbedh; int bed_idx_min = 0, bed_idx_max = 0; }; struct XStriderBed { Slic3r::arr2::RectangleBed bed; Slic3r::arr2::XStriderVBedHandler vbedh; int bed_idx_min = 0, bed_idx_max = 100; XStriderBed() : bed{Slic3r::scaled(250.), Slic3r::scaled(210.)}, vbedh{bounding_box(bed), bounding_box(bed).size().x() / 10} {} }; TEMPLATE_TEST_CASE("Writing arrange transformations into ModelInstance should be correct", "[arrange2][integration]", PhysicalBed, XStriderBed) { auto [tx, ty, rot] = GENERATE(map( [](int i) { return std::make_tuple(-Slic3r::arr2::UnscaledCoordLimit / 2. + i * Slic3r::arr2::UnscaledCoordLimit / 100., -Slic3r::arr2::UnscaledCoordLimit / 2. + i * Slic3r::arr2::UnscaledCoordLimit / 100., -PI + i * (2 * PI / 100.)); }, range(0, 100))); using namespace Slic3r; Model model = get_example_model_with_20mm_cube(); auto transl = scaled(Vec2d(tx, ty)); INFO("Translation = : " << transl.transpose()); INFO("Rotation is: " << rot * 180 / PI); auto mi = model.objects.front()->instances.front(); BoundingBox bb_before = scaled(to_2d(arr2::instance_bounding_box(*mi))); TestType bed_case; auto bed_index = random_value(bed_case.bed_idx_min, bed_case.bed_idx_max); bed_case.vbedh.assign_bed(arr2::VBedPlaceableMI{*mi}, bed_index); INFO("bed_index = " << bed_index); auto builder = arr2::SceneBuilder{} .set_bed(bed_case.bed) .set_model(model) .set_arrange_settings(arr2::ArrangeSettings{}.set_distance_from_objects(0.)) .set_virtual_bed_handler(&bed_case.vbedh); arr2::Scene scene{std::move(builder)}; using ArrItem = arr2::ArrangeItem; auto cvt = arr2::ArrangeableToItemConverter::create(scene); ArrItem itm; scene.model().visit_arrangeable(model.objects.front()->instances.front()->id(), [&cvt, &itm](const arr2::Arrangeable &arrbl){ itm = cvt->convert(arrbl); }); BoundingBox bb_itm_before = arr2::fixed_bounding_box(itm); REQUIRE((bb_itm_before.min - bb_before.min).norm() < SCALED_EPSILON); REQUIRE((bb_itm_before.max - bb_before.max).norm() < SCALED_EPSILON); arr2::rotate(itm, rot); arr2::translate(itm, transl); arr2::set_bed_index(itm, arr2::PhysicalBedId); if (auto id = retrieve_id(itm)) { scene.model().visit_arrangeable(*id, [&itm](arr2::Arrangeable &arrbl) { arrbl.transform(unscaled(get_translation(itm)), get_rotation(itm)); }); } auto phys_tr = bed_case.vbedh.get_physical_bed_trafo(bed_index); auto outline = arr2::extract_convex_outline(*mi, phys_tr); BoundingBox bb_after = get_extents(outline); BoundingBox bb_itm_after = arr2::fixed_bounding_box(itm); REQUIRE((bb_itm_after.min - bb_after.min).norm() < 2 * SCALED_EPSILON); REQUIRE((bb_itm_after.max - bb_after.max).norm() < 2 * SCALED_EPSILON); } struct OutlineExtractorConvex { auto operator() (const Slic3r::ModelInstance *mi) { return Slic3r::arr2::extract_convex_outline(*mi); } }; struct OutlineExtractorFull { auto operator() (const Slic3r::ModelInstance *mi) { return Slic3r::arr2::extract_full_outline(*mi); } }; TEMPLATE_TEST_CASE("Outline extraction from ModelInstance", "[arrange2][integration]", OutlineExtractorConvex, OutlineExtractorFull) { using namespace Slic3r; using OutlineExtractor = TestType; Model model = get_example_model_with_20mm_cube(); ModelInstance *mi = model.objects.front()->instances.front(); auto matrix = mi->get_matrix(); matrix.scale(Vec3d{random_value(0.1, 5.), random_value(0.1, 5.), random_value(0.1, 5.)}); matrix.rotate(Eigen::AngleAxisd(random_value(-PI, PI), Vec3d::UnitZ())); matrix.translate(Vec3d{random_value(-100., 100.), random_value(-100., 100.), random_value(0., 100.)}); mi->set_transformation(Geometry::Transformation{matrix}); GIVEN("An empty ModelInstance without mesh") { const ModelInstance *mi = model.add_object()->add_instance(); WHEN("the outline is generated") { auto outline = OutlineExtractor{}(mi); THEN ("the outline is empty") { REQUIRE(outline.empty()); } } } GIVEN("A simple cube as outline") { const ModelInstance *mi = model.objects.front()->instances.front(); WHEN("the outline is generated") { auto outline = OutlineExtractor{}(mi); THEN("the 2D ortho projection of the model bounding box is the " "same as the outline's bb") { auto bb = unscaled(get_extents(outline)); auto modelbb = to_2d(model.bounding_box_exact()); REQUIRE((bb.min - modelbb.min).norm() < EPSILON); REQUIRE((bb.max - modelbb.max).norm() < EPSILON); } } } } template auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap) { return VBH{}; } template<> auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap) { return Slic3r::arr2::PhysicalOnlyVBedHandler{}; } template<> auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap) { return Slic3r::arr2::XStriderVBedHandler{bedbb, gap}; } template<> auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap) { return Slic3r::arr2::YStriderVBedHandler{bedbb, gap}; } template<> auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap) { return Slic3r::arr2::GridStriderVBedHandler{bedbb, gap}; } TEMPLATE_TEST_CASE("Common virtual bed handlers", "[arrange2][integration][vbeds]", Slic3r::arr2::PhysicalOnlyVBedHandler, Slic3r::arr2::XStriderVBedHandler, Slic3r::arr2::YStriderVBedHandler, Slic3r::arr2::GridStriderVBedHandler) { using namespace Slic3r; using VBP = arr2::VBedPlaceableMI; Model model = get_example_model_with_20mm_cube(); const auto bedsize = Vec2d{random_value(21., 500.), random_value(21., 500.)}; const Vec2crd bed_displace = {random_value(scaled(-100.), scaled(100.)), random_value(scaled(-100.), scaled(100.))}; const BoundingBox bedbb{bed_displace, scaled(bedsize) + bed_displace}; INFO("Bed boundaries bedbb = { {" << unscaled(bedbb.min).transpose() << "}, {" << unscaled(bedbb.max).transpose() << "} }" ); auto modelbb = model.bounding_box_exact(); // Center the single instance within the model arr2::transform_instance(*model.objects.front()->instances.front(), unscaled(bedbb.center()) - to_2d(modelbb.center()), 0.); const auto vbed_gap = GENERATE(0, random_value(1, scaled(100.))); INFO("vbed_gap = " << unscaled(vbed_gap)); std::unique_ptr vbedh = std::make_unique( create_vbed_handler(bedbb, vbed_gap)); GIVEN("A ModelInstance on the physical bed") { ModelInstance *mi = model.objects.front()->instances.front(); WHEN ("trying to move the item to an invalid bed index") { auto &mi_to_move = *model.objects.front()->add_instance(*mi); Transform3d mi_trafo_before = mi_to_move.get_matrix(); bool was_accepted = vbedh->assign_bed(VBP{mi_to_move}, arr2::Unarranged); Transform3d mi_trafo_after = mi_to_move.get_matrix(); THEN("the model instance should be unchanged") { REQUIRE(!was_accepted); REQUIRE(mi_trafo_before.isApprox(mi_trafo_after)); } } } GIVEN("A ModelInstance being assigned to a virtual bed") { ModelInstance *mi = model.objects.front()->instances.front(); auto bedidx_to = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000)); INFO("bed index = " << bedidx_to); auto &mi_to_move = *model.objects.front()->add_instance(*mi); // Move model instance to the given virtual bed bool was_accepted = vbedh->assign_bed(VBP{mi_to_move}, bedidx_to); WHEN ("querying the virtual bed index of this item") { int bedidx_on = vbedh->get_bed_index(VBP{mi_to_move}); THEN("should actually be on that bed, or the assign should be discarded") { REQUIRE(((!was_accepted) || (bedidx_to == bedidx_on))); } THEN("assigning the same bed index again should produce the same result") { auto &mi_to_move_cpy = *model.objects.front()->add_instance(mi_to_move); bool was_accepted_rep = vbedh->assign_bed(VBP{mi_to_move_cpy}, bedidx_to); int bedidx_on_rep = vbedh->get_bed_index(VBP{mi_to_move_cpy}); REQUIRE(was_accepted_rep == was_accepted); REQUIRE(((!was_accepted_rep) || (bedidx_to == bedidx_on_rep))); } } WHEN ("moving back to the physical bed") { auto &mi_back_to_phys = *model.objects.front()->add_instance(mi_to_move); bool moved_back_to_physical = vbedh->assign_bed(VBP{mi_back_to_phys}, arr2::PhysicalBedId); THEN("model instance should actually move back to the physical bed") { REQUIRE(moved_back_to_physical); int bedidx_mi2 = vbedh->get_bed_index(VBP{mi_back_to_phys}); REQUIRE(bedidx_mi2 == 0); } THEN("the bounding box should be inside bed") { auto bbf = arr2::instance_bounding_box(mi_back_to_phys); auto bb = BoundingBox{scaled(to_2d(bbf))}; INFO("bb = { {" << unscaled(bb.min).transpose() << "}, {" << unscaled(bb.max).transpose() << "} }" ); REQUIRE(bedbb.contains(bb)); } } WHEN("extracting transformed model instance bounding box using the " "physical bed trafo") { int from_bed_idx = vbedh->get_bed_index(VBP{mi_to_move}); auto physical_bed_trafo = vbedh->get_physical_bed_trafo(from_bed_idx); auto &mi_back_to_phys = *model.objects.front()->add_instance(mi_to_move); mi_back_to_phys.set_transformation(Geometry::Transformation{ physical_bed_trafo * mi_back_to_phys.get_matrix()}); auto bbf = arr2::instance_bounding_box(mi_back_to_phys); auto bb = BoundingBox{scaled(to_2d(bbf))}; THEN("the bounding box should be inside bed") { INFO("bb = { {" << unscaled(bb.min).transpose() << "}, {" << unscaled(bb.max).transpose() << "} }" ); REQUIRE(bedbb.contains(bb)); } THEN("the outline should be inside the physical bed") { auto outline = arr2::extract_convex_outline(mi_to_move, physical_bed_trafo); auto bb = get_extents(outline); INFO("bb = { {" << bb.min.transpose() << "}, {" << bb.max.transpose() << "} }" ); REQUIRE(bedbb.contains(bb)); } } } } TEST_CASE("Virtual bed handlers - StriderVBedHandler", "[arrange2][integration][vbeds]") { using namespace Slic3r; using VBP = arr2::VBedPlaceableMI; Model model = get_example_model_with_20mm_cube(); static const Vec2d bedsize{250., 210.}; static const BoundingBox bedbb{{0, 0}, scaled(bedsize)}; static const auto modelbb = model.bounding_box_exact(); GIVEN("An instance of StriderVBedHandler with a stride of the bed width" " and random non-negative gap") { auto [instance_pos, instance_displace] = GENERATE(table({ {"start", unscaled(bedbb.min) - to_2d(modelbb.min) + Vec2d::Ones() * EPSILON}, // at the min edge of vbed {"middle", unscaled(bedbb.center()) - to_2d(modelbb.center())}, // at the center {"end", unscaled(bedbb.max) - to_2d(modelbb.max) - Vec2d::Ones() * EPSILON} // at the max edge of vbed })); // Center the single instance within the model arr2::transform_instance(*model.objects.front()->instances.front(), instance_displace, 0.); INFO("Instance pos at " << instance_pos << " of bed"); coord_t gap = GENERATE(0, random_value(1, scaled(100.))); INFO("Gap is " << unscaled(gap)); arr2::XStriderVBedHandler vbh{bedbb, gap}; WHEN("a model instance is on the Nth virtual bed (spatially)") { ModelInstance *mi = model.objects.front()->instances.front(); auto &mi_to_move = *model.objects.front()->add_instance(*mi); auto bed_index = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000)); INFO("N is " << bed_index); double bed_disp = bed_index * unscaled(vbh.stride_scaled()); arr2::transform_instance(mi_to_move, Vec2d{bed_disp, 0.}, 0.); THEN("the bed index of this model instance should be max(0, N)") { REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == bed_index); } THEN("the physical trafo should move the instance back to bed 0") { auto tr = vbh.get_physical_bed_trafo(bed_index); mi_to_move.set_transformation(Geometry::Transformation{tr * mi_to_move.get_matrix()}); REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == 0); auto instbb = BoundingBox{scaled(to_2d(arr2::instance_bounding_box(mi_to_move)))}; INFO("bedbb = { {" << bedbb.min.transpose() << "}, {" << bedbb.max.transpose() << "} }" ); INFO("instbb = { {" << instbb.min.transpose() << "}, {" << instbb.max.transpose() << "} }" ); REQUIRE(bedbb.contains(instbb)); } } WHEN("a model instance is on the physical bed") { ModelInstance *mi = model.objects.front()->instances.front(); auto &mi_to_move = *model.objects.front()->add_instance(*mi); THEN("assigning the model instance to the Nth bed will move it N*stride in the X axis") { auto bed_index = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000)); INFO("N is " << bed_index); if (vbh.assign_bed(VBP{mi_to_move}, bed_index)) REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == bed_index); else REQUIRE(bed_index < 0); auto tr = vbh.get_physical_bed_trafo(bed_index); auto ref_pos = tr * Vec3d::Zero(); auto displace = bed_index * (unscaled(vbh.stride_scaled())); REQUIRE(ref_pos.x() == Approx(-displace)); auto ref_pos_mi = mi_to_move.get_matrix() * Vec3d::Zero(); REQUIRE(ref_pos_mi.x() == Approx(instance_displace.x() + (bed_index >= 0) * displace)); } } } GIVEN("An instance of StriderVBedHandler with a stride of the bed width" " and a 100mm gap") { coord_t gap = scaled(100.); arr2::XStriderVBedHandler vbh{bedbb, gap}; WHEN("a model instance is within the gap on the Nth virtual bed") { ModelInstance *mi = model.objects.front()->instances.front(); auto &mi_to_move = *model.objects.front()->add_instance(*mi); auto bed_index = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000)); INFO("N is " << bed_index); auto bed_disp = Vec2d{bed_index * unscaled(vbh.stride_scaled()), 0.}; auto instbb_before = to_2d(arr2::instance_bounding_box(mi_to_move)); auto transl_to_bed_end = Vec2d{bed_disp + unscaled(bedbb.max) - instbb_before.min + Vec2d::Ones() * EPSILON}; arr2::transform_instance(mi_to_move, transl_to_bed_end + Vec2d{unscaled(gap / 2), 0.}, 0.); THEN("the model instance should reside on the Nth logical bed but " "outside of the bed boundaries") { REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == bed_index); auto instbb = BoundingBox{scaled(to_2d(arr2::instance_bounding_box(mi_to_move)))}; INFO("bedbb = { {" << bedbb.min.transpose() << "}, {" << bedbb.max.transpose() << "} }" ); INFO("instbb = { {" << instbb.min.transpose() << "}, {" << instbb.max.transpose() << "} }" ); REQUIRE(! bedbb.contains(instbb)); } } } } TEMPLATE_TEST_CASE("Bed needs to be completely filled with 1cm cubes", "[arrange2][integration][bedfilling]", Slic3r::arr2::ArrangeItem) { using namespace Slic3r; using ArrItem = TestType; std::string basepath = TEST_DATA_DIR PATH_SEPARATOR; DynamicPrintConfig cfg; cfg.load_from_ini(basepath + "default_fff.ini", ForwardCompatibilitySubstitutionRule::Enable); cfg.set_key_value("bed_shape", new ConfigOptionPoints( {{0., 0.}, {100., 0.}, {100., 100.}, {0, 100.}})); Model m; ModelObject* new_object = m.add_object(); new_object->name = "10mm_box"; new_object->add_instance(); TriangleMesh mesh = make_cube(10., 10., 10.); ModelVolume* new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; store_3mf("fillbed_10mm.3mf", &m, &cfg, false); arr2::ArrangeSettings settings; settings.values().d_obj = 0.; settings.values().d_bed = 0.; arr2::FixedSelection sel({{true}}); arr2::Scene scene{arr2::SceneBuilder{} .set_model(m) .set_arrange_settings(settings) .set_selection(&sel) .set_bed(cfg)}; auto task = arr2::FillBedTask::create(scene); auto result = task->process_native(arr2::DummyCtl{}); result->apply_on(scene.model()); store_3mf("fillbed_10mm_result.3mf", &m, &cfg, false); Points bedpts = get_bed_shape(cfg); arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts); REQUIRE(bed.which() == 1); // Rectangle bed auto bedbb = unscaled(bounding_box(bed)); auto bedbbsz = bedbb.size(); REQUIRE(m.objects.size() == 1); REQUIRE(m.objects.front()->instances.size() == bedbbsz.x() * bedbbsz.y() / 100); REQUIRE(task->unselected.empty()); REQUIRE(result->to_add.size() + result->arranged_items.size() == arr2::model_instance_count(m)); // All the existing items should be on the physical bed REQUIRE(std::all_of(result->arranged_items.begin(), result->arranged_items.end(), [](auto &itm) { return arr2::get_bed_index(itm) == 0; })); REQUIRE( std::all_of(result->to_add.begin(), result->to_add.end(), [](auto &itm) { return arr2::get_bed_index(itm) == 0; })); } template static void foreach_combo(const Slic3r::Range &range, const Fn &fn) { std::vector pairs(range.size(), false); assert(range.size() >= 2); pairs[range.size() - 1] = true; pairs[range.size() - 2] = true; do { std::vector::value_type> items; for (size_t i = 0; i < pairs.size(); i++) { if (pairs[i]) { auto it = range.begin(); std::advance(it, i); items.emplace_back(*it); } } fn (items[0], items[1]); } while (std::next_permutation(pairs.begin(), pairs.end())); } TEST_CASE("Testing minimum area bounding box rotation on simple cubes", "[arrange2][integration]") { using namespace Slic3r; BoundingBox bb{Point::Zero(), scaled(Vec2d(10., 10.))}; Polygon sh = arr2::to_rectangle(bb); auto prot = random_value(0., 2 * PI); sh.translate(Vec2crd{random_value(-scaled(10.), scaled(10.)), random_value(-scaled(10.), scaled(10.))}); sh.rotate(prot); INFO("box item is rotated by: " << prot << " rads"); arr2::ArrangeItem itm{sh}; arr2::rotate(itm, random_value(0., 2 * PI)); double rot = arr2::get_min_area_bounding_box_rotation(itm); arr2::translate(itm, Vec2crd{random_value(-scaled(10.), scaled(10.)), random_value(-scaled(10.), scaled(10.))}); arr2::rotate(itm, rot); auto itmbb = arr2::fixed_bounding_box(itm); REQUIRE(std::abs(itmbb.size().norm() - bb.size().norm()) < SCALED_EPSILON * SCALED_EPSILON); } template bool is_collision_free(const Slic3r::Range &item_range) { using namespace Slic3r; bool collision_free = true; foreach_combo(item_range, [&collision_free](auto &itm1, auto &itm2) { auto outline1 = offset(arr2::fixed_outline(itm1), -scaled(EPSILON)); auto outline2 = offset(arr2::fixed_outline(itm2), -scaled(EPSILON)); auto inters = intersection(outline1, outline2); collision_free = collision_free && inters.empty(); }); return collision_free; } TEST_CASE("Testing a simple arrange on cubes", "[arrange2][integration]") { using namespace Slic3r; Model model = get_example_model_with_random_cube_objects(size_t{10}); arr2::ArrangeSettings settings; settings.set_rotation_enabled(true); auto bed = arr2::RectangleBed{scaled(250.), scaled(210.)}; arr2::Scene scene{arr2::SceneBuilder{} .set_model(model) .set_arrange_settings(settings) .set_bed(bed)}; auto task = arr2::ArrangeTask::create(scene); REQUIRE(task->printable.selected.size() == arr2::model_instance_count(model)); auto result = task->process_native(arr2::DummyCtl{}); REQUIRE(result); REQUIRE(result->items.size() == task->printable.selected.size()); bool applied = result->apply_on(scene.model()); REQUIRE(applied); REQUIRE(std::all_of(result->items.begin(), result->items.end(), [](auto &item) { return arr2::is_arranged(item); })); REQUIRE(std::all_of(task->printable.selected.begin(), task->printable.selected.end(), [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); })); REQUIRE(std::all_of(task->unprintable.selected.begin(), task->unprintable.selected.end(), [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); })); REQUIRE(is_collision_free(range(task->printable.selected))); } TEST_CASE("Testing arrangement involving virtual beds", "[arrange2][integration]") { using namespace Slic3r; Model model = get_example_model_with_arranged_primitives(); DynamicPrintConfig cfg; cfg.load_from_ini(std::string(TEST_DATA_DIR PATH_SEPARATOR) + "default_fff.ini", ForwardCompatibilitySubstitutionRule::Enable); auto bed = arr2::to_arrange_bed(get_bed_shape(cfg)); auto bedbb = bounding_box(bed); auto bedsz = unscaled(bedbb.size()); auto strategy = GENERATE(arr2::ArrangeSettingsView::asAuto, arr2::ArrangeSettingsView::asPullToCenter); INFO ("Strategy = " << strategy); auto settings = arr2::ArrangeSettings{} .set_distance_from_objects(0.) .set_arrange_strategy(strategy); arr2::Scene scene{arr2::SceneBuilder{} .set_model(model) .set_arrange_settings(settings) .set_bed(cfg)}; auto itm_conv = arr2::ArrangeableToItemConverter::create(scene); auto task = arr2::ArrangeTask::create(scene, *itm_conv); ModelObject* new_object = model.add_object(); new_object->name = "big_cube"; ModelInstance *bigcube_inst = new_object->add_instance(); TriangleMesh mesh = make_cube(bedsz.x() - 5., bedsz.y() - 5., 20.); ModelVolume* new_volume = new_object->add_volume(mesh); new_volume->name = new_object->name; { arr2::ArrangeItem bigitm; scene.model().visit_arrangeable(bigcube_inst->id(), [&bigitm, &itm_conv]( const arr2::Arrangeable &arrbl) { bigitm = itm_conv->convert(arrbl); }); task->printable.selected.emplace_back(std::move(bigitm)); } REQUIRE(task->printable.selected.size() == arr2::model_instance_count(model)); auto result = task->process_native(arr2::DummyCtl{}); REQUIRE(result); REQUIRE(result->items.size() == task->printable.selected.size()); REQUIRE(std::all_of(result->items.begin(), std::prev(result->items.end()), [](auto &item) { return arr2::get_bed_index(item) == 1; })); REQUIRE(arr2::get_bed_index(result->items.back()) == arr2::PhysicalBedId); bool applied = result->apply_on(scene.model()); REQUIRE(applied); store_3mf("vbed_test_result.3mf", &model, &cfg, false); REQUIRE(std::all_of(task->printable.selected.begin(), task->printable.selected.end(), [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); })); REQUIRE(is_collision_free(Range{task->printable.selected.begin(), std::prev(task->printable.selected.end())})); } bool settings_eq(const Slic3r::arr2::ArrangeSettingsView &v1, const Slic3r::arr2::ArrangeSettingsView &v2) { return v1.is_rotation_enabled() == v2.is_rotation_enabled() && v1.get_arrange_strategy() == v2.get_arrange_strategy() && v1.get_distance_from_bed() == Approx(v2.get_distance_from_bed()) && v1.get_distance_from_objects() == Approx(v2.get_distance_from_objects()) && v1.get_geometry_handling() == v2.get_geometry_handling() && v1.get_xl_alignment() == v2.get_xl_alignment(); ; } namespace Slic3r { namespace arr2 { class MocWT: public ArrangeableWipeTowerBase { public: using ArrangeableWipeTowerBase::ArrangeableWipeTowerBase; }; class MocWTH : public WipeTowerHandler { std::function m_sel_pred; ObjectID m_id; public: MocWTH(const ObjectID &id) : m_id{id} {} void visit(std::function fn) override { MocWT wt{m_id, Polygon{}, m_sel_pred}; fn(wt); } void visit(std::function fn) const override { MocWT wt{m_id, Polygon{}, m_sel_pred}; fn(wt); } void set_selection_predicate(std::function pred) override { m_sel_pred = std::move(pred); } }; }} // namespace Slic3r::arr2 TEST_CASE("Test SceneBuilder", "[arrange2][integration]") { using namespace Slic3r; GIVEN("An empty SceneBuilder") { arr2::SceneBuilder bld; WHEN("building an ArrangeScene from it") { arr2::Scene scene{std::move(bld)}; THEN("The scene should still be initialized consistently with empty model") { // This would segfault if model_wt isn't initialized properly REQUIRE(scene.model().arrangeable_count() == 0); REQUIRE(settings_eq(scene.settings(), arr2::ArrangeSettings{})); REQUIRE(scene.selected_ids().empty()); } THEN("The associated bed should be an instance of InfiniteBed") { scene.visit_bed([](auto &bed){ REQUIRE(std::is_convertible_v); }); } } WHEN("pushing random settings into the builder") { RandomArrangeSettings settings; auto bld2 = arr2::SceneBuilder{}.set_arrange_settings(&settings); arr2::Scene scene{std::move(bld)}; THEN("settings of the resulting scene should be equal") { REQUIRE(settings_eq(scene.settings(), settings)); } } } GIVEN("An existing instance of the class Model") { auto N = random_value(1, 20); Model model = get_example_model_with_random_cube_objects(N); INFO("model object count " << N); WHEN("a scene is built from a builder that holds a reference to an existing model") { arr2::Scene scene{arr2::SceneBuilder{}.set_model(&model)}; THEN("the model of the constructed scene should have the same number of arrangeables") { REQUIRE(scene.model().arrangeable_count() == arr2::model_instance_count(model)); } } } GIVEN("An instance of DynamicPrintConfig with rectangular bed") { std::string basepath = TEST_DATA_DIR PATH_SEPARATOR; DynamicPrintConfig cfg; cfg.load_from_ini(basepath + "default_fff.ini", ForwardCompatibilitySubstitutionRule::Enable); WHEN("a scene is built with a bed initialized from this DynamicPrintConfig") { arr2::Scene scene(arr2::SceneBuilder{}.set_bed(cfg)); auto bedbb = bounding_box(get_bed_shape(cfg)); THEN("the bed should be a rectangular bed with the same dimensions as the bed points") { scene.visit_bed([&bedbb, &scene](auto &bed) { constexpr bool is_rect = std::is_convertible_v< decltype(bed), arr2::RectangleBed>; REQUIRE(is_rect); if constexpr (is_rect) { bedbb.offset(scaled(scene.settings().get_distance_from_objects() / 2.)); REQUIRE(bedbb.size().x() == bed.width()); REQUIRE(bedbb.size().y() == bed.height()); } }); } } } GIVEN("A wipe tower handler that uses the builder's selection mask") { arr2::SceneBuilder bld; Model mdl; bld.set_model(mdl); bld.set_wipe_tower_handler(std::make_unique(mdl.wipe_tower.id())); WHEN("the selection mask is initialized as a fallback default in the created scene") { arr2::Scene scene{std::move(bld)}; THEN("the wipe tower should use the fallback selmask (created after set_wipe_tower)") { // Should be the wipe tower REQUIRE(scene.model().arrangeable_count() == 1); bool wt_selected = false; scene.model() .visit_arrangeable(mdl.wipe_tower.id(), [&wt_selected]( const arr2::Arrangeable &arrbl) { wt_selected = arrbl.is_selected(); }); REQUIRE(wt_selected); } } } } TEST_CASE("Testing duplicate function to really duplicate the whole Model", "[arrange2][integration]") { using namespace Slic3r; Model model = get_example_model_with_arranged_primitives(); store_3mf("dupl_example.3mf", &model, nullptr, false); size_t instcnt = arr2::model_instance_count(model); size_t copies_num = random_value(1, 10); INFO("Copies: " << copies_num); auto bed = arr2::InfiniteBed{}; arr2::ArrangeSettings settings; settings.set_arrange_strategy(arr2::ArrangeSettings::asPullToCenter); arr2::DuplicableModel dup_model{&model, arr2::VirtualBedHandler::create(bed), bounding_box(bed)}; arr2::Scene scene{arr2::BasicSceneBuilder{} .set_arrangeable_model(&dup_model) .set_arrange_settings(&settings) .set_bed(bed)}; auto task = arr2::MultiplySelectionTask::create(scene, copies_num); auto result = task->process_native(arr2::DummyCtl{}); bool applied = result->apply_on(scene.model()); if (applied) { dup_model.apply_duplicates(); store_3mf("dupl_example_result.3mf", &model, nullptr, false); REQUIRE(applied); } size_t new_instcnt = arr2::model_instance_count(model); REQUIRE(new_instcnt == (copies_num + 1) * instcnt); REQUIRE(std::all_of(result->arranged_items.begin(), result->arranged_items.end(), [](auto &item) { return arr2::is_arranged(item); })); REQUIRE(std::all_of(result->to_add.begin(), result->to_add.end(), [](auto &item) { return arr2::is_arranged(item); })); REQUIRE(std::all_of(task->selected.begin(), task->selected.end(), [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); })); REQUIRE(is_collision_free(range(task->selected))); } // TODO: //TEST_CASE("Testing fit-into-bed rotation search", "[arrange2][integration]") //{ // using namespace Slic3r; // Model model; // ModelObject* new_object = model.add_object(); // new_object->name = "big_cube"; // new_object->add_instance(); // TriangleMesh mesh = make_cube(205., 220., 10.); // mesh.rotate_z(15 * PI / 180); // ModelVolume* new_volume = new_object->add_volume(mesh); // new_volume->name = new_object->name; // store_3mf("rotfail.3mf", &model, nullptr, false); // arr2::RectangleBed bed{scaled(250.), scaled(210.)}; // arr2::Scene scene{ // arr2::SceneBuilder{} // .set_bed(bed) // .set_model(model) // .set_arrange_settings(arr2::ArrangeSettings{} // .set_distance_from_objects(0.) // .set_rotation_enabled(true) // ) // }; // auto task = arr2::ArrangeTask::create(scene); // auto result = task->process_native(arr2::DummyCtl{}); // REQUIRE(result->items.size() == 1); // REQUIRE(arr2::get_rotation(result->items.front()) > 0.); // REQUIRE(arr2::is_arranged(result->items.front())); //}