test_arrange_integration.cpp 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080
  1. #include <catch2/catch.hpp>
  2. #include "test_utils.hpp"
  3. #include <libslic3r/Arrange/Arrange.hpp>
  4. #include <libslic3r/Arrange/Items/ArrangeItem.hpp>
  5. #include <libslic3r/Arrange/Tasks/ArrangeTask.hpp>
  6. #include <libslic3r/Arrange/SceneBuilder.hpp>
  7. #include "libslic3r/Model.hpp"
  8. #include "libslic3r/Geometry/ConvexHull.hpp"
  9. #include "libslic3r/Format/3mf.hpp"
  10. #include "libslic3r/ModelArrange.hpp"
  11. static Slic3r::Model get_example_model_with_20mm_cube()
  12. {
  13. using namespace Slic3r;
  14. Model model;
  15. ModelObject* new_object = model.add_object();
  16. new_object->name = "20mm_cube";
  17. new_object->add_instance();
  18. TriangleMesh mesh = make_cube(20., 20., 20.);
  19. mesh.translate(Vec3f{-10.f, -10.f, 0.});
  20. ModelVolume* new_volume = new_object->add_volume(mesh);
  21. new_volume->name = new_object->name;
  22. return model;
  23. }
  24. [[maybe_unused]]
  25. static Slic3r::Model get_example_model_with_random_cube_objects(size_t N = 0)
  26. {
  27. using namespace Slic3r;
  28. Model model;
  29. auto cube_count = N == 0 ? random_value(size_t(1), size_t(100)) : N;
  30. INFO("Cube count " << cube_count);
  31. ModelObject* new_object = model.add_object();
  32. new_object->name = "20mm_cube";
  33. TriangleMesh mesh = make_cube(20., 20., 20.);
  34. ModelVolume* new_volume = new_object->add_volume(mesh);
  35. new_volume->name = new_object->name;
  36. for (size_t i = 0; i < cube_count; ++i) {
  37. ModelInstance *inst = new_object->add_instance();
  38. arr2::transform_instance(*inst,
  39. Vec2d{random_value(-arr2::UnscaledCoordLimit / 10., arr2::UnscaledCoordLimit / 10.),
  40. random_value(-arr2::UnscaledCoordLimit / 10., arr2::UnscaledCoordLimit / 10.)},
  41. random_value(0., 2 * PI));
  42. }
  43. return model;
  44. }
  45. static Slic3r::Model get_example_model_with_arranged_primitives()
  46. {
  47. using namespace Slic3r;
  48. Model model;
  49. ModelObject* new_object = model.add_object();
  50. new_object->name = "20mm_cube";
  51. ModelInstance *cube_inst = new_object->add_instance();
  52. TriangleMesh mesh = make_cube(20., 20., 20.);
  53. mesh.translate(Vec3f{-10.f, -10.f, 0.});
  54. ModelVolume* new_volume = new_object->add_volume(mesh);
  55. new_volume->name = new_object->name;
  56. ModelInstance *inst = new_object->add_instance(*cube_inst);
  57. auto tr = inst->get_matrix();
  58. tr.translate(Vec3d{25., 0., 0.});
  59. inst->set_transformation(Geometry::Transformation{tr});
  60. new_object = model.add_object();
  61. new_object->name = "20mm_cyl";
  62. new_object->add_instance();
  63. mesh = make_cylinder(10., 20.);
  64. mesh.translate(Vec3f{0., -25.f, 0.});
  65. new_volume = new_object->add_volume(mesh);
  66. new_volume->name = new_object->name;
  67. new_object = model.add_object();
  68. new_object->name = "20mm_sphere";
  69. new_object->add_instance();
  70. mesh = make_sphere(10.);
  71. mesh.translate(Vec3f{25., -25.f, 0.});
  72. new_volume = new_object->add_volume(mesh);
  73. new_volume->name = new_object->name;
  74. return model;
  75. }
  76. class RandomArrangeSettings: public Slic3r::arr2::ArrangeSettingsView {
  77. Slic3r::arr2::ArrangeSettingsDb::Values m_v;
  78. std::mt19937 m_rng;
  79. public:
  80. explicit RandomArrangeSettings(int seed) : m_rng(seed)
  81. {
  82. std::uniform_real_distribution<float> fdist(0., 100.f);
  83. std::uniform_int_distribution<> bdist(0, 1);
  84. std::uniform_int_distribution<> dist;
  85. m_v.d_obj = fdist(m_rng);
  86. m_v.d_bed = fdist(m_rng);
  87. m_v.rotations = bdist(m_rng);
  88. m_v.geom_handling = static_cast<GeometryHandling>(dist(m_rng) % ghCount);
  89. m_v.arr_strategy = static_cast<ArrangeStrategy>(dist(m_rng) % asCount);
  90. m_v.xl_align = static_cast<XLPivots>(dist(m_rng) % xlpCount);
  91. }
  92. explicit RandomArrangeSettings() : m_rng(std::random_device{} ()) {}
  93. float get_distance_from_objects() const override { return m_v.d_obj; }
  94. float get_distance_from_bed() const override { return m_v.d_bed; }
  95. bool is_rotation_enabled() const override { return m_v.rotations; }
  96. XLPivots get_xl_alignment() const override { return m_v.xl_align; }
  97. GeometryHandling get_geometry_handling() const override { return m_v.geom_handling; }
  98. ArrangeStrategy get_arrange_strategy() const override { return m_v.arr_strategy; }
  99. };
  100. TEST_CASE("ModelInstance should be retrievable when imbued into ArrangeItem",
  101. "[arrange2][integration]")
  102. {
  103. using namespace Slic3r;
  104. Model model = get_example_model_with_20mm_cube();
  105. auto mi = model.objects.front()->instances.front();
  106. arr2::ArrangeItem itm;
  107. arr2::PhysicalOnlyVBedHandler vbedh;
  108. auto vbedh_ptr = static_cast<arr2::VirtualBedHandler *>(&vbedh);
  109. auto arrbl = arr2::ArrangeableModelInstance{mi, vbedh_ptr, nullptr, {0, 0}};
  110. arr2::imbue_id(itm, arrbl.id());
  111. std::optional<ObjectID> id_returned = arr2::retrieve_id(itm);
  112. REQUIRE((id_returned && *id_returned == mi->id()));
  113. }
  114. struct PhysicalBed
  115. {
  116. Slic3r::arr2::InfiniteBed bed;
  117. Slic3r::arr2::PhysicalOnlyVBedHandler vbedh;
  118. int bed_idx_min = 0, bed_idx_max = 0;
  119. };
  120. struct XStriderBed
  121. {
  122. Slic3r::arr2::RectangleBed bed;
  123. Slic3r::arr2::XStriderVBedHandler vbedh;
  124. int bed_idx_min = 0, bed_idx_max = 100;
  125. XStriderBed() :
  126. bed{Slic3r::scaled(250.), Slic3r::scaled(210.)},
  127. vbedh{bounding_box(bed), bounding_box(bed).size().x() / 10} {}
  128. };
  129. TEMPLATE_TEST_CASE("Writing arrange transformations into ModelInstance should be correct",
  130. "[arrange2][integration]",
  131. PhysicalBed,
  132. XStriderBed)
  133. {
  134. auto [tx, ty, rot] = GENERATE(map(
  135. [](int i) {
  136. return std::make_tuple(-Slic3r::arr2::UnscaledCoordLimit / 2. + i * Slic3r::arr2::UnscaledCoordLimit / 100.,
  137. -Slic3r::arr2::UnscaledCoordLimit / 2. + i * Slic3r::arr2::UnscaledCoordLimit / 100.,
  138. -PI + i * (2 * PI / 100.));
  139. },
  140. range(0, 100)));
  141. using namespace Slic3r;
  142. Model model = get_example_model_with_20mm_cube();
  143. auto transl = scaled(Vec2d(tx, ty));
  144. INFO("Translation = : " << transl.transpose());
  145. INFO("Rotation is: " << rot * 180 / PI);
  146. auto mi = model.objects.front()->instances.front();
  147. BoundingBox bb_before = scaled(to_2d(arr2::instance_bounding_box(*mi)));
  148. TestType bed_case;
  149. auto bed_index = random_value<int>(bed_case.bed_idx_min, bed_case.bed_idx_max);
  150. bed_case.vbedh.assign_bed(arr2::VBedPlaceableMI{*mi}, bed_index);
  151. INFO("bed_index = " << bed_index);
  152. auto builder = arr2::SceneBuilder{}
  153. .set_bed(bed_case.bed)
  154. .set_model(model)
  155. .set_arrange_settings(arr2::ArrangeSettings{}.set_distance_from_objects(0.))
  156. .set_virtual_bed_handler(&bed_case.vbedh);
  157. arr2::Scene scene{std::move(builder)};
  158. using ArrItem = arr2::ArrangeItem;
  159. auto cvt = arr2::ArrangeableToItemConverter<ArrItem>::create(scene);
  160. ArrItem itm;
  161. scene.model().visit_arrangeable(model.objects.front()->instances.front()->id(),
  162. [&cvt, &itm](const arr2::Arrangeable &arrbl){
  163. itm = cvt->convert(arrbl);
  164. });
  165. BoundingBox bb_itm_before = arr2::fixed_bounding_box(itm);
  166. REQUIRE((bb_itm_before.min - bb_before.min).norm() < SCALED_EPSILON);
  167. REQUIRE((bb_itm_before.max - bb_before.max).norm() < SCALED_EPSILON);
  168. arr2::rotate(itm, rot);
  169. arr2::translate(itm, transl);
  170. arr2::set_bed_index(itm, arr2::PhysicalBedId);
  171. if (auto id = retrieve_id(itm)) {
  172. scene.model().visit_arrangeable(*id, [&itm](arr2::Arrangeable &arrbl) {
  173. arrbl.transform(unscaled(get_translation(itm)), get_rotation(itm));
  174. });
  175. }
  176. auto phys_tr = bed_case.vbedh.get_physical_bed_trafo(bed_index);
  177. auto outline = arr2::extract_convex_outline(*mi, phys_tr);
  178. BoundingBox bb_after = get_extents(outline);
  179. BoundingBox bb_itm_after = arr2::fixed_bounding_box(itm);
  180. REQUIRE((bb_itm_after.min - bb_after.min).norm() < 2 * SCALED_EPSILON);
  181. REQUIRE((bb_itm_after.max - bb_after.max).norm() < 2 * SCALED_EPSILON);
  182. }
  183. struct OutlineExtractorConvex {
  184. auto operator() (const Slic3r::ModelInstance *mi)
  185. {
  186. return Slic3r::arr2::extract_convex_outline(*mi);
  187. }
  188. };
  189. struct OutlineExtractorFull {
  190. auto operator() (const Slic3r::ModelInstance *mi)
  191. {
  192. return Slic3r::arr2::extract_full_outline(*mi);
  193. }
  194. };
  195. TEMPLATE_TEST_CASE("Outline extraction from ModelInstance",
  196. "[arrange2][integration]",
  197. OutlineExtractorConvex,
  198. OutlineExtractorFull)
  199. {
  200. using namespace Slic3r;
  201. using OutlineExtractor = TestType;
  202. Model model = get_example_model_with_20mm_cube();
  203. ModelInstance *mi = model.objects.front()->instances.front();
  204. auto matrix = mi->get_matrix();
  205. matrix.scale(Vec3d{random_value(0.1, 5.),
  206. random_value(0.1, 5.),
  207. random_value(0.1, 5.)});
  208. matrix.rotate(Eigen::AngleAxisd(random_value(-PI, PI), Vec3d::UnitZ()));
  209. matrix.translate(Vec3d{random_value(-100., 100.),
  210. random_value(-100., 100.),
  211. random_value(0., 100.)});
  212. mi->set_transformation(Geometry::Transformation{matrix});
  213. GIVEN("An empty ModelInstance without mesh")
  214. {
  215. const ModelInstance *mi = model.add_object()->add_instance();
  216. WHEN("the outline is generated") {
  217. auto outline = OutlineExtractor{}(mi);
  218. THEN ("the outline is empty") {
  219. REQUIRE(outline.empty());
  220. }
  221. }
  222. }
  223. GIVEN("A simple cube as outline") {
  224. const ModelInstance *mi = model.objects.front()->instances.front();
  225. WHEN("the outline is generated") {
  226. auto outline = OutlineExtractor{}(mi);
  227. THEN("the 2D ortho projection of the model bounding box is the "
  228. "same as the outline's bb")
  229. {
  230. auto bb = unscaled(get_extents(outline));
  231. auto modelbb = to_2d(model.bounding_box_exact());
  232. REQUIRE((bb.min - modelbb.min).norm() < EPSILON);
  233. REQUIRE((bb.max - modelbb.max).norm() < EPSILON);
  234. }
  235. }
  236. }
  237. }
  238. template<class VBH>
  239. auto create_vbed_handler(const Slic3r::BoundingBox &bedbb, coord_t gap)
  240. {
  241. return VBH{};
  242. }
  243. template<>
  244. auto create_vbed_handler<Slic3r::arr2::PhysicalOnlyVBedHandler>(const Slic3r::BoundingBox &bedbb, coord_t gap)
  245. {
  246. return Slic3r::arr2::PhysicalOnlyVBedHandler{};
  247. }
  248. template<>
  249. auto create_vbed_handler<Slic3r::arr2::XStriderVBedHandler>(const Slic3r::BoundingBox &bedbb, coord_t gap)
  250. {
  251. return Slic3r::arr2::XStriderVBedHandler{bedbb, gap};
  252. }
  253. template<>
  254. auto create_vbed_handler<Slic3r::arr2::YStriderVBedHandler>(const Slic3r::BoundingBox &bedbb, coord_t gap)
  255. {
  256. return Slic3r::arr2::YStriderVBedHandler{bedbb, gap};
  257. }
  258. template<>
  259. auto create_vbed_handler<Slic3r::arr2::GridStriderVBedHandler>(const Slic3r::BoundingBox &bedbb, coord_t gap)
  260. {
  261. return Slic3r::arr2::GridStriderVBedHandler{bedbb, gap};
  262. }
  263. TEMPLATE_TEST_CASE("Common virtual bed handlers",
  264. "[arrange2][integration][vbeds]",
  265. Slic3r::arr2::PhysicalOnlyVBedHandler,
  266. Slic3r::arr2::XStriderVBedHandler,
  267. Slic3r::arr2::YStriderVBedHandler,
  268. Slic3r::arr2::GridStriderVBedHandler)
  269. {
  270. using namespace Slic3r;
  271. using VBP = arr2::VBedPlaceableMI;
  272. Model model = get_example_model_with_20mm_cube();
  273. const auto bedsize = Vec2d{random_value(21., 500.), random_value(21., 500.)};
  274. const Vec2crd bed_displace = {random_value(scaled(-100.), scaled(100.)),
  275. random_value(scaled(-100.), scaled(100.))};
  276. const BoundingBox bedbb{bed_displace, scaled(bedsize) + bed_displace};
  277. INFO("Bed boundaries bedbb = { {" << unscaled(bedbb.min).transpose() << "}, {"
  278. << unscaled(bedbb.max).transpose() << "} }" );
  279. auto modelbb = model.bounding_box_exact();
  280. // Center the single instance within the model
  281. arr2::transform_instance(*model.objects.front()->instances.front(),
  282. unscaled(bedbb.center()) - to_2d(modelbb.center()),
  283. 0.);
  284. const auto vbed_gap = GENERATE(0, random_value(1, scaled(100.)));
  285. INFO("vbed_gap = " << unscaled(vbed_gap));
  286. std::unique_ptr<arr2::VirtualBedHandler> vbedh = std::make_unique<TestType>(
  287. create_vbed_handler<TestType>(bedbb, vbed_gap));
  288. GIVEN("A ModelInstance on the physical bed")
  289. {
  290. ModelInstance *mi = model.objects.front()->instances.front();
  291. WHEN ("trying to move the item to an invalid bed index")
  292. {
  293. auto &mi_to_move = *model.objects.front()->add_instance(*mi);
  294. Transform3d mi_trafo_before = mi_to_move.get_matrix();
  295. bool was_accepted = vbedh->assign_bed(VBP{mi_to_move}, arr2::Unarranged);
  296. Transform3d mi_trafo_after = mi_to_move.get_matrix();
  297. THEN("the model instance should be unchanged") {
  298. REQUIRE(!was_accepted);
  299. REQUIRE(mi_trafo_before.isApprox(mi_trafo_after));
  300. }
  301. }
  302. }
  303. GIVEN("A ModelInstance being assigned to a virtual bed")
  304. {
  305. ModelInstance *mi = model.objects.front()->instances.front();
  306. auto bedidx_to = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000));
  307. INFO("bed index = " << bedidx_to);
  308. auto &mi_to_move = *model.objects.front()->add_instance(*mi);
  309. // Move model instance to the given virtual bed
  310. bool was_accepted = vbedh->assign_bed(VBP{mi_to_move}, bedidx_to);
  311. WHEN ("querying the virtual bed index of this item")
  312. {
  313. int bedidx_on = vbedh->get_bed_index(VBP{mi_to_move});
  314. THEN("should actually be on that bed, or the assign should be discarded") {
  315. REQUIRE(((!was_accepted) || (bedidx_to == bedidx_on)));
  316. }
  317. THEN("assigning the same bed index again should produce the same result")
  318. {
  319. auto &mi_to_move_cpy = *model.objects.front()->add_instance(mi_to_move);
  320. bool was_accepted_rep = vbedh->assign_bed(VBP{mi_to_move_cpy}, bedidx_to);
  321. int bedidx_on_rep = vbedh->get_bed_index(VBP{mi_to_move_cpy});
  322. REQUIRE(was_accepted_rep == was_accepted);
  323. REQUIRE(((!was_accepted_rep) || (bedidx_to == bedidx_on_rep)));
  324. }
  325. }
  326. WHEN ("moving back to the physical bed")
  327. {
  328. auto &mi_back_to_phys = *model.objects.front()->add_instance(mi_to_move);
  329. bool moved_back_to_physical = vbedh->assign_bed(VBP{mi_back_to_phys}, arr2::PhysicalBedId);
  330. THEN("model instance should actually move back to the physical bed")
  331. {
  332. REQUIRE(moved_back_to_physical);
  333. int bedidx_mi2 = vbedh->get_bed_index(VBP{mi_back_to_phys});
  334. REQUIRE(bedidx_mi2 == 0);
  335. }
  336. THEN("the bounding box should be inside bed")
  337. {
  338. auto bbf = arr2::instance_bounding_box(mi_back_to_phys);
  339. auto bb = BoundingBox{scaled(to_2d(bbf))};
  340. INFO("bb = { {" << unscaled(bb.min).transpose() << "}, {"
  341. << unscaled(bb.max).transpose() << "} }" );
  342. REQUIRE(bedbb.contains(bb));
  343. }
  344. }
  345. WHEN("extracting transformed model instance bounding box using the "
  346. "physical bed trafo")
  347. {
  348. int from_bed_idx = vbedh->get_bed_index(VBP{mi_to_move});
  349. auto physical_bed_trafo = vbedh->get_physical_bed_trafo(from_bed_idx);
  350. auto &mi_back_to_phys = *model.objects.front()->add_instance(mi_to_move);
  351. mi_back_to_phys.set_transformation(Geometry::Transformation{
  352. physical_bed_trafo * mi_back_to_phys.get_matrix()});
  353. auto bbf = arr2::instance_bounding_box(mi_back_to_phys);
  354. auto bb = BoundingBox{scaled(to_2d(bbf))};
  355. THEN("the bounding box should be inside bed")
  356. {
  357. INFO("bb = { {" << unscaled(bb.min).transpose() << "}, {"
  358. << unscaled(bb.max).transpose() << "} }" );
  359. REQUIRE(bedbb.contains(bb));
  360. }
  361. THEN("the outline should be inside the physical bed")
  362. {
  363. auto outline = arr2::extract_convex_outline(mi_to_move,
  364. physical_bed_trafo);
  365. auto bb = get_extents(outline);
  366. INFO("bb = { {" << bb.min.transpose() << "}, {"
  367. << bb.max.transpose() << "} }" );
  368. REQUIRE(bedbb.contains(bb));
  369. }
  370. }
  371. }
  372. }
  373. TEST_CASE("Virtual bed handlers - StriderVBedHandler", "[arrange2][integration][vbeds]")
  374. {
  375. using namespace Slic3r;
  376. using VBP = arr2::VBedPlaceableMI;
  377. Model model = get_example_model_with_20mm_cube();
  378. static const Vec2d bedsize{250., 210.};
  379. static const BoundingBox bedbb{{0, 0}, scaled(bedsize)};
  380. static const auto modelbb = model.bounding_box_exact();
  381. GIVEN("An instance of StriderVBedHandler with a stride of the bed width"
  382. " and random non-negative gap")
  383. {
  384. auto [instance_pos, instance_displace] = GENERATE(table<std::string, Vec2d>({
  385. {"start", unscaled(bedbb.min) - to_2d(modelbb.min) + Vec2d::Ones() * EPSILON}, // at the min edge of vbed
  386. {"middle", unscaled(bedbb.center()) - to_2d(modelbb.center())}, // at the center
  387. {"end", unscaled(bedbb.max) - to_2d(modelbb.max) - Vec2d::Ones() * EPSILON} // at the max edge of vbed
  388. }));
  389. // Center the single instance within the model
  390. arr2::transform_instance(*model.objects.front()->instances.front(),
  391. instance_displace,
  392. 0.);
  393. INFO("Instance pos at " << instance_pos << " of bed");
  394. coord_t gap = GENERATE(0, random_value(1, scaled(100.)));
  395. INFO("Gap is " << unscaled(gap));
  396. arr2::XStriderVBedHandler vbh{bedbb, gap};
  397. WHEN("a model instance is on the Nth virtual bed (spatially)")
  398. {
  399. ModelInstance *mi = model.objects.front()->instances.front();
  400. auto &mi_to_move = *model.objects.front()->add_instance(*mi);
  401. auto bed_index = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000));
  402. INFO("N is " << bed_index);
  403. double bed_disp = bed_index * unscaled(vbh.stride_scaled());
  404. arr2::transform_instance(mi_to_move, Vec2d{bed_disp, 0.}, 0.);
  405. THEN("the bed index of this model instance should be max(0, N)")
  406. {
  407. REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == bed_index);
  408. }
  409. THEN("the physical trafo should move the instance back to bed 0")
  410. {
  411. auto tr = vbh.get_physical_bed_trafo(bed_index);
  412. mi_to_move.set_transformation(Geometry::Transformation{tr * mi_to_move.get_matrix()});
  413. REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == 0);
  414. auto instbb = BoundingBox{scaled(to_2d(arr2::instance_bounding_box(mi_to_move)))};
  415. INFO("bedbb = { {" << bedbb.min.transpose() << "}, {" << bedbb.max.transpose() << "} }" );
  416. INFO("instbb = { {" << instbb.min.transpose() << "}, {" << instbb.max.transpose() << "} }" );
  417. REQUIRE(bedbb.contains(instbb));
  418. }
  419. }
  420. WHEN("a model instance is on the physical bed")
  421. {
  422. ModelInstance *mi = model.objects.front()->instances.front();
  423. auto &mi_to_move = *model.objects.front()->add_instance(*mi);
  424. THEN("assigning the model instance to the Nth bed will move it N*stride in the X axis")
  425. {
  426. auto bed_index = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000));
  427. INFO("N is " << bed_index);
  428. if (vbh.assign_bed(VBP{mi_to_move}, bed_index))
  429. REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == bed_index);
  430. else
  431. REQUIRE(bed_index < 0);
  432. auto tr = vbh.get_physical_bed_trafo(bed_index);
  433. auto ref_pos = tr * Vec3d::Zero();
  434. auto displace = bed_index * (unscaled(vbh.stride_scaled()));
  435. REQUIRE(ref_pos.x() == Approx(-displace));
  436. auto ref_pos_mi = mi_to_move.get_matrix() * Vec3d::Zero();
  437. REQUIRE(ref_pos_mi.x() == Approx(instance_displace.x() + (bed_index >= 0) * displace));
  438. }
  439. }
  440. }
  441. GIVEN("An instance of StriderVBedHandler with a stride of the bed width"
  442. " and a 100mm gap")
  443. {
  444. coord_t gap = scaled(100.);
  445. arr2::XStriderVBedHandler vbh{bedbb, gap};
  446. WHEN("a model instance is within the gap on the Nth virtual bed")
  447. {
  448. ModelInstance *mi = model.objects.front()->instances.front();
  449. auto &mi_to_move = *model.objects.front()->add_instance(*mi);
  450. auto bed_index = GENERATE(random_value(-1000, -1), 0, random_value(1, 1000));
  451. INFO("N is " << bed_index);
  452. auto bed_disp = Vec2d{bed_index * unscaled(vbh.stride_scaled()), 0.};
  453. auto instbb_before = to_2d(arr2::instance_bounding_box(mi_to_move));
  454. auto transl_to_bed_end = Vec2d{bed_disp + unscaled(bedbb.max)
  455. - instbb_before.min + Vec2d::Ones() * EPSILON};
  456. arr2::transform_instance(mi_to_move,
  457. transl_to_bed_end + Vec2d{unscaled(gap / 2), 0.},
  458. 0.);
  459. THEN("the model instance should reside on the Nth logical bed but "
  460. "outside of the bed boundaries")
  461. {
  462. REQUIRE(vbh.get_bed_index(VBP{mi_to_move}) == bed_index);
  463. auto instbb = BoundingBox{scaled(to_2d(arr2::instance_bounding_box(mi_to_move)))};
  464. INFO("bedbb = { {" << bedbb.min.transpose() << "}, {" << bedbb.max.transpose() << "} }" );
  465. INFO("instbb = { {" << instbb.min.transpose() << "}, {" << instbb.max.transpose() << "} }" );
  466. REQUIRE(! bedbb.contains(instbb));
  467. }
  468. }
  469. }
  470. }
  471. TEMPLATE_TEST_CASE("Bed needs to be completely filled with 1cm cubes",
  472. "[arrange2][integration][bedfilling]",
  473. Slic3r::arr2::ArrangeItem)
  474. {
  475. using namespace Slic3r;
  476. using ArrItem = TestType;
  477. std::string basepath = TEST_DATA_DIR PATH_SEPARATOR;
  478. DynamicPrintConfig cfg;
  479. cfg.load_from_ini(basepath + "default_fff.ini",
  480. ForwardCompatibilitySubstitutionRule::Enable);
  481. cfg.set_key_value("bed_shape",
  482. new ConfigOptionPoints(
  483. {{0., 0.}, {100., 0.}, {100., 100.}, {0, 100.}}));
  484. Model m;
  485. ModelObject* new_object = m.add_object();
  486. new_object->name = "10mm_box";
  487. new_object->add_instance();
  488. TriangleMesh mesh = make_cube(10., 10., 10.);
  489. ModelVolume* new_volume = new_object->add_volume(mesh);
  490. new_volume->name = new_object->name;
  491. store_3mf("fillbed_10mm.3mf", &m, &cfg, false);
  492. arr2::ArrangeSettings settings;
  493. settings.values().d_obj = 0.;
  494. settings.values().d_bed = 0.;
  495. arr2::FixedSelection sel({{true}});
  496. arr2::Scene scene{arr2::SceneBuilder{}
  497. .set_model(m)
  498. .set_arrange_settings(settings)
  499. .set_selection(&sel)
  500. .set_bed(cfg)};
  501. auto task = arr2::FillBedTask<ArrItem>::create(scene);
  502. auto result = task->process_native(arr2::DummyCtl{});
  503. result->apply_on(scene.model());
  504. store_3mf("fillbed_10mm_result.3mf", &m, &cfg, false);
  505. Points bedpts = get_bed_shape(cfg);
  506. arr2::ArrangeBed bed = arr2::to_arrange_bed(bedpts);
  507. REQUIRE(bed.which() == 1); // Rectangle bed
  508. auto bedbb = unscaled(bounding_box(bed));
  509. auto bedbbsz = bedbb.size();
  510. REQUIRE(m.objects.size() == 1);
  511. REQUIRE(m.objects.front()->instances.size() ==
  512. bedbbsz.x() * bedbbsz.y() / 100);
  513. REQUIRE(task->unselected.empty());
  514. REQUIRE(result->to_add.size() + result->arranged_items.size() == arr2::model_instance_count(m));
  515. // All the existing items should be on the physical bed
  516. REQUIRE(std::all_of(result->arranged_items.begin(),
  517. result->arranged_items.end(), [](auto &itm) {
  518. return arr2::get_bed_index(itm) == 0;
  519. }));
  520. REQUIRE(
  521. std::all_of(result->to_add.begin(), result->to_add.end(), [](auto &itm) {
  522. return arr2::get_bed_index(itm) == 0;
  523. }));
  524. }
  525. template<class It, class Fn>
  526. static void foreach_combo(const Slic3r::Range<It> &range, const Fn &fn)
  527. {
  528. std::vector<bool> pairs(range.size(), false);
  529. assert(range.size() >= 2);
  530. pairs[range.size() - 1] = true;
  531. pairs[range.size() - 2] = true;
  532. do {
  533. std::vector<typename std::iterator_traits<It>::value_type> items;
  534. for (size_t i = 0; i < pairs.size(); i++) {
  535. if (pairs[i]) {
  536. auto it = range.begin();
  537. std::advance(it, i);
  538. items.emplace_back(*it);
  539. }
  540. }
  541. fn (items[0], items[1]);
  542. } while (std::next_permutation(pairs.begin(), pairs.end()));
  543. }
  544. TEST_CASE("Testing minimum area bounding box rotation on simple cubes", "[arrange2][integration]")
  545. {
  546. using namespace Slic3r;
  547. BoundingBox bb{Point::Zero(), scaled(Vec2d(10., 10.))};
  548. Polygon sh = arr2::to_rectangle(bb);
  549. auto prot = random_value(0., 2 * PI);
  550. sh.translate(Vec2crd{random_value<coord_t>(-scaled(10.), scaled(10.)),
  551. random_value<coord_t>(-scaled(10.), scaled(10.))});
  552. sh.rotate(prot);
  553. INFO("box item is rotated by: " << prot << " rads");
  554. arr2::ArrangeItem itm{sh};
  555. arr2::rotate(itm, random_value(0., 2 * PI));
  556. double rot = arr2::get_min_area_bounding_box_rotation(itm);
  557. arr2::translate(itm,
  558. Vec2crd{random_value<coord_t>(-scaled(10.), scaled(10.)),
  559. random_value<coord_t>(-scaled(10.), scaled(10.))});
  560. arr2::rotate(itm, rot);
  561. auto itmbb = arr2::fixed_bounding_box(itm);
  562. REQUIRE(std::abs(itmbb.size().norm() - bb.size().norm()) <
  563. SCALED_EPSILON * SCALED_EPSILON);
  564. }
  565. template<class It>
  566. bool is_collision_free(const Slic3r::Range<It> &item_range)
  567. {
  568. using namespace Slic3r;
  569. bool collision_free = true;
  570. foreach_combo(item_range, [&collision_free](auto &itm1, auto &itm2) {
  571. auto outline1 = offset(arr2::fixed_outline(itm1), -scaled<float>(EPSILON));
  572. auto outline2 = offset(arr2::fixed_outline(itm2), -scaled<float>(EPSILON));
  573. auto inters = intersection(outline1, outline2);
  574. collision_free = collision_free && inters.empty();
  575. });
  576. return collision_free;
  577. }
  578. TEST_CASE("Testing a simple arrange on cubes", "[arrange2][integration]")
  579. {
  580. using namespace Slic3r;
  581. Model model = get_example_model_with_random_cube_objects(size_t{10});
  582. arr2::ArrangeSettings settings;
  583. settings.set_rotation_enabled(true);
  584. auto bed = arr2::RectangleBed{scaled(250.), scaled(210.)};
  585. arr2::Scene scene{arr2::SceneBuilder{}
  586. .set_model(model)
  587. .set_arrange_settings(settings)
  588. .set_bed(bed)};
  589. auto task = arr2::ArrangeTask<arr2::ArrangeItem>::create(scene);
  590. REQUIRE(task->printable.selected.size() == arr2::model_instance_count(model));
  591. auto result = task->process_native(arr2::DummyCtl{});
  592. REQUIRE(result);
  593. REQUIRE(result->items.size() == task->printable.selected.size());
  594. bool applied = result->apply_on(scene.model());
  595. REQUIRE(applied);
  596. REQUIRE(std::all_of(result->items.begin(),
  597. result->items.end(),
  598. [](auto &item) { return arr2::is_arranged(item); }));
  599. REQUIRE(std::all_of(task->printable.selected.begin(), task->printable.selected.end(),
  600. [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); }));
  601. REQUIRE(std::all_of(task->unprintable.selected.begin(), task->unprintable.selected.end(),
  602. [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); }));
  603. REQUIRE(is_collision_free(range(task->printable.selected)));
  604. }
  605. TEST_CASE("Testing arrangement involving virtual beds", "[arrange2][integration]")
  606. {
  607. using namespace Slic3r;
  608. Model model = get_example_model_with_arranged_primitives();
  609. DynamicPrintConfig cfg;
  610. cfg.load_from_ini(std::string(TEST_DATA_DIR PATH_SEPARATOR) + "default_fff.ini",
  611. ForwardCompatibilitySubstitutionRule::Enable);
  612. auto bed = arr2::to_arrange_bed(get_bed_shape(cfg));
  613. auto bedbb = bounding_box(bed);
  614. auto bedsz = unscaled(bedbb.size());
  615. auto strategy = GENERATE(arr2::ArrangeSettingsView::asAuto,
  616. arr2::ArrangeSettingsView::asPullToCenter);
  617. INFO ("Strategy = " << strategy);
  618. auto settings = arr2::ArrangeSettings{}
  619. .set_distance_from_objects(0.)
  620. .set_arrange_strategy(strategy);
  621. arr2::Scene scene{arr2::SceneBuilder{}
  622. .set_model(model)
  623. .set_arrange_settings(settings)
  624. .set_bed(cfg)};
  625. auto itm_conv = arr2::ArrangeableToItemConverter<arr2::ArrangeItem>::create(scene);
  626. auto task = arr2::ArrangeTask<arr2::ArrangeItem>::create(scene, *itm_conv);
  627. ModelObject* new_object = model.add_object();
  628. new_object->name = "big_cube";
  629. ModelInstance *bigcube_inst = new_object->add_instance();
  630. TriangleMesh mesh = make_cube(bedsz.x() - 5., bedsz.y() - 5., 20.);
  631. ModelVolume* new_volume = new_object->add_volume(mesh);
  632. new_volume->name = new_object->name;
  633. {
  634. arr2::ArrangeItem bigitm;
  635. scene.model().visit_arrangeable(bigcube_inst->id(),
  636. [&bigitm, &itm_conv](
  637. const arr2::Arrangeable &arrbl) {
  638. bigitm = itm_conv->convert(arrbl);
  639. });
  640. task->printable.selected.emplace_back(std::move(bigitm));
  641. }
  642. REQUIRE(task->printable.selected.size() == arr2::model_instance_count(model));
  643. auto result = task->process_native(arr2::DummyCtl{});
  644. REQUIRE(result);
  645. REQUIRE(result->items.size() == task->printable.selected.size());
  646. REQUIRE(std::all_of(result->items.begin(),
  647. std::prev(result->items.end()),
  648. [](auto &item) { return arr2::get_bed_index(item) == 1; }));
  649. REQUIRE(arr2::get_bed_index(result->items.back()) == arr2::PhysicalBedId);
  650. bool applied = result->apply_on(scene.model());
  651. REQUIRE(applied);
  652. store_3mf("vbed_test_result.3mf", &model, &cfg, false);
  653. REQUIRE(std::all_of(task->printable.selected.begin(), task->printable.selected.end(),
  654. [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); }));
  655. REQUIRE(is_collision_free(Range{task->printable.selected.begin(), std::prev(task->printable.selected.end())}));
  656. }
  657. bool settings_eq(const Slic3r::arr2::ArrangeSettingsView &v1,
  658. const Slic3r::arr2::ArrangeSettingsView &v2)
  659. {
  660. return v1.is_rotation_enabled() == v2.is_rotation_enabled() &&
  661. v1.get_arrange_strategy() == v2.get_arrange_strategy() &&
  662. v1.get_distance_from_bed() == Approx(v2.get_distance_from_bed()) &&
  663. v1.get_distance_from_objects() == Approx(v2.get_distance_from_objects()) &&
  664. v1.get_geometry_handling() == v2.get_geometry_handling() &&
  665. v1.get_xl_alignment() == v2.get_xl_alignment();
  666. ;
  667. }
  668. namespace Slic3r { namespace arr2 {
  669. class MocWT: public ArrangeableWipeTowerBase {
  670. public:
  671. using ArrangeableWipeTowerBase::ArrangeableWipeTowerBase;
  672. };
  673. class MocWTH : public WipeTowerHandler {
  674. std::function<bool()> m_sel_pred;
  675. ObjectID m_id;
  676. public:
  677. MocWTH(const ObjectID &id) : m_id{id} {}
  678. void visit(std::function<void(Arrangeable &)> fn) override
  679. {
  680. MocWT wt{m_id, Polygon{}, m_sel_pred};
  681. fn(wt);
  682. }
  683. void visit(std::function<void(const Arrangeable &)> fn) const override
  684. {
  685. MocWT wt{m_id, Polygon{}, m_sel_pred};
  686. fn(wt);
  687. }
  688. void set_selection_predicate(std::function<bool()> pred) override
  689. {
  690. m_sel_pred = std::move(pred);
  691. }
  692. };
  693. }} // namespace Slic3r::arr2
  694. TEST_CASE("Test SceneBuilder", "[arrange2][integration]")
  695. {
  696. using namespace Slic3r;
  697. GIVEN("An empty SceneBuilder")
  698. {
  699. arr2::SceneBuilder bld;
  700. WHEN("building an ArrangeScene from it")
  701. {
  702. arr2::Scene scene{std::move(bld)};
  703. THEN("The scene should still be initialized consistently with empty model")
  704. {
  705. // This would segfault if model_wt isn't initialized properly
  706. REQUIRE(scene.model().arrangeable_count() == 0);
  707. REQUIRE(settings_eq(scene.settings(), arr2::ArrangeSettings{}));
  708. REQUIRE(scene.selected_ids().empty());
  709. }
  710. THEN("The associated bed should be an instance of InfiniteBed")
  711. {
  712. scene.visit_bed([](auto &bed){
  713. REQUIRE(std::is_convertible_v<decltype(bed), arr2::InfiniteBed>);
  714. });
  715. }
  716. }
  717. WHEN("pushing random settings into the builder")
  718. {
  719. RandomArrangeSettings settings;
  720. auto bld2 = arr2::SceneBuilder{}.set_arrange_settings(&settings);
  721. arr2::Scene scene{std::move(bld)};
  722. THEN("settings of the resulting scene should be equal")
  723. {
  724. REQUIRE(settings_eq(scene.settings(), settings));
  725. }
  726. }
  727. }
  728. GIVEN("An existing instance of the class Model")
  729. {
  730. auto N = random_value(1, 20);
  731. Model model = get_example_model_with_random_cube_objects(N);
  732. INFO("model object count " << N);
  733. WHEN("a scene is built from a builder that holds a reference to an existing model")
  734. {
  735. arr2::Scene scene{arr2::SceneBuilder{}.set_model(&model)};
  736. THEN("the model of the constructed scene should have the same number of arrangeables") {
  737. REQUIRE(scene.model().arrangeable_count() == arr2::model_instance_count(model));
  738. }
  739. }
  740. }
  741. GIVEN("An instance of DynamicPrintConfig with rectangular bed")
  742. {
  743. std::string basepath = TEST_DATA_DIR PATH_SEPARATOR;
  744. DynamicPrintConfig cfg;
  745. cfg.load_from_ini(basepath + "default_fff.ini",
  746. ForwardCompatibilitySubstitutionRule::Enable);
  747. WHEN("a scene is built with a bed initialized from this DynamicPrintConfig")
  748. {
  749. arr2::Scene scene(arr2::SceneBuilder{}.set_bed(cfg));
  750. auto bedbb = bounding_box(get_bed_shape(cfg));
  751. THEN("the bed should be a rectangular bed with the same dimensions as the bed points")
  752. {
  753. scene.visit_bed([&bedbb, &scene](auto &bed) {
  754. constexpr bool is_rect = std::is_convertible_v<
  755. decltype(bed), arr2::RectangleBed>;
  756. REQUIRE(is_rect);
  757. if constexpr (is_rect) {
  758. bedbb.offset(scaled(scene.settings().get_distance_from_objects() / 2.));
  759. REQUIRE(bedbb.size().x() == bed.width());
  760. REQUIRE(bedbb.size().y() == bed.height());
  761. }
  762. });
  763. }
  764. }
  765. }
  766. GIVEN("A wipe tower handler that uses the builder's selection mask")
  767. {
  768. arr2::SceneBuilder bld;
  769. Model mdl;
  770. bld.set_model(mdl);
  771. bld.set_wipe_tower_handler(std::make_unique<arr2::MocWTH>(mdl.wipe_tower.id()));
  772. WHEN("the selection mask is initialized as a fallback default in the created scene")
  773. {
  774. arr2::Scene scene{std::move(bld)};
  775. THEN("the wipe tower should use the fallback selmask (created after set_wipe_tower)")
  776. {
  777. // Should be the wipe tower
  778. REQUIRE(scene.model().arrangeable_count() == 1);
  779. bool wt_selected = false;
  780. scene.model()
  781. .visit_arrangeable(mdl.wipe_tower.id(),
  782. [&wt_selected](
  783. const arr2::Arrangeable &arrbl) {
  784. wt_selected = arrbl.is_selected();
  785. });
  786. REQUIRE(wt_selected);
  787. }
  788. }
  789. }
  790. }
  791. TEST_CASE("Testing duplicate function to really duplicate the whole Model",
  792. "[arrange2][integration]")
  793. {
  794. using namespace Slic3r;
  795. Model model = get_example_model_with_arranged_primitives();
  796. store_3mf("dupl_example.3mf", &model, nullptr, false);
  797. size_t instcnt = arr2::model_instance_count(model);
  798. size_t copies_num = random_value(1, 10);
  799. INFO("Copies: " << copies_num);
  800. auto bed = arr2::InfiniteBed{};
  801. arr2::ArrangeSettings settings;
  802. settings.set_arrange_strategy(arr2::ArrangeSettings::asPullToCenter);
  803. arr2::DuplicableModel dup_model{&model, arr2::VirtualBedHandler::create(bed), bounding_box(bed)};
  804. arr2::Scene scene{arr2::BasicSceneBuilder{}
  805. .set_arrangeable_model(&dup_model)
  806. .set_arrange_settings(&settings)
  807. .set_bed(bed)};
  808. auto task = arr2::MultiplySelectionTask<arr2::ArrangeItem>::create(scene, copies_num);
  809. auto result = task->process_native(arr2::DummyCtl{});
  810. bool applied = result->apply_on(scene.model());
  811. if (applied) {
  812. dup_model.apply_duplicates();
  813. store_3mf("dupl_example_result.3mf", &model, nullptr, false);
  814. REQUIRE(applied);
  815. }
  816. size_t new_instcnt = arr2::model_instance_count(model);
  817. REQUIRE(new_instcnt == (copies_num + 1) * instcnt);
  818. REQUIRE(std::all_of(result->arranged_items.begin(),
  819. result->arranged_items.end(),
  820. [](auto &item) { return arr2::is_arranged(item); }));
  821. REQUIRE(std::all_of(result->to_add.begin(),
  822. result->to_add.end(),
  823. [](auto &item) { return arr2::is_arranged(item); }));
  824. REQUIRE(std::all_of(task->selected.begin(), task->selected.end(),
  825. [&bed](auto &item) { return bounding_box(bed).contains(arr2::envelope_bounding_box(item)); }));
  826. REQUIRE(is_collision_free(range(task->selected)));
  827. }