12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190 |
- #include <catch_main.hpp>
- #include <fstream>
- #include <libnest2d/libnest2d.hpp>
- #include "printer_parts.hpp"
- //#include <libnest2d/geometry_traits_nfp.hpp>
- #include "../tools/svgtools.hpp"
- #include <libnest2d/utils/rotcalipers.hpp>
- #if defined(_MSC_VER) && defined(__clang__)
- #define BOOST_NO_CXX17_HDR_STRING_VIEW
- #endif
- #include "boost/multiprecision/integer.hpp"
- #include "boost/rational.hpp"
- //#include "../tools/libnfpglue.hpp"
- //#include "../tools/nfp_svgnest_glue.hpp"
- namespace libnest2d {
- #if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__)
- using LargeInt = __int128;
- #else
- using LargeInt = boost::multiprecision::int128_t;
- template<> struct _NumTag<LargeInt> { using Type = ScalarTag; };
- #endif
- template<class T> struct _NumTag<boost::rational<T>> { using Type = RationalTag; };
- using RectangleItem = libnest2d::Rectangle;
- namespace nfp {
- template<class S>
- struct NfpImpl<S, NfpLevel::CONVEX_ONLY>
- {
- NfpResult<S> operator()(const S &sh, const S &other)
- {
- return nfpConvexOnly<S, boost::rational<LargeInt>>(sh, other);
- }
- };
- }
- }
- static std::vector<libnest2d::Item>& prusaParts() {
- static std::vector<libnest2d::Item> ret;
- if(ret.empty()) {
- ret.reserve(PRINTER_PART_POLYGONS.size());
- for(auto& inp : PRINTER_PART_POLYGONS) ret.emplace_back(inp);
- }
- return ret;
- }
- TEST_CASE("Angles", "[Geometry]")
- {
- using namespace libnest2d;
- Degrees deg(180);
- Radians rad(deg);
- Degrees deg2(rad);
- REQUIRE(Approx(rad) == Pi);
- REQUIRE(Approx(deg) == 180);
- REQUIRE(Approx(deg2) == 180);
- REQUIRE(Approx(rad) == Radians(deg));
- REQUIRE(Approx(Degrees(rad)) == deg);
- REQUIRE(rad == deg);
- Segment seg = {{0, 0}, {12, -10}};
- REQUIRE(Degrees(seg.angleToXaxis()) > 270);
- REQUIRE(Degrees(seg.angleToXaxis()) < 360);
- seg = {{0, 0}, {12, 10}};
- REQUIRE(Degrees(seg.angleToXaxis()) > 0);
- REQUIRE(Degrees(seg.angleToXaxis()) < 90);
- seg = {{0, 0}, {-12, 10}};
- REQUIRE(Degrees(seg.angleToXaxis()) > 90);
- REQUIRE(Degrees(seg.angleToXaxis()) < 180);
- seg = {{0, 0}, {-12, -10}};
- REQUIRE(Degrees(seg.angleToXaxis()) > 180);
- REQUIRE(Degrees(seg.angleToXaxis()) < 270);
- seg = {{0, 0}, {1, 0}};
- REQUIRE(Degrees(seg.angleToXaxis()) == Approx(0.));
- seg = {{0, 0}, {0, 1}};
- REQUIRE(Degrees(seg.angleToXaxis()) == Approx(90.));
- seg = {{0, 0}, {-1, 0}};
- REQUIRE(Degrees(seg.angleToXaxis()) == Approx(180.));
- seg = {{0, 0}, {0, -1}};
- REQUIRE(Degrees(seg.angleToXaxis()) == Approx(270.));
- }
- // Simple TEST_CASE, does not use gmock
- TEST_CASE("ItemCreationAndDestruction", "[Nesting]")
- {
- using namespace libnest2d;
- Item sh = { {0, 0}, {1, 0}, {1, 1}, {0, 1} };
- REQUIRE(sh.vertexCount() == 4u);
- Item sh2 ({ {0, 0}, {1, 0}, {1, 1}, {0, 1} });
- REQUIRE(sh2.vertexCount() == 4u);
- // copy
- Item sh3 = sh2;
- REQUIRE(sh3.vertexCount() == 4u);
- sh2 = {};
- REQUIRE(sh2.vertexCount() == 0u);
- REQUIRE(sh3.vertexCount() == 4u);
- }
- TEST_CASE("boundingCircle", "[Geometry]") {
- using namespace libnest2d;
- using placers::boundingCircle;
- PolygonImpl p = {{{0, 10}, {10, 0}, {0, -10}, {0, 10}}, {}};
- Circle c = boundingCircle(p);
- REQUIRE(c.center().X == 0);
- REQUIRE(c.center().Y == 0);
- REQUIRE(c.radius() == Approx(10));
- shapelike::translate(p, PointImpl{10, 10});
- c = boundingCircle(p);
- REQUIRE(c.center().X == 10);
- REQUIRE(c.center().Y == 10);
- REQUIRE(c.radius() == Approx(10));
- auto parts = prusaParts();
- int i = 0;
- for(auto& part : parts) {
- c = boundingCircle(part.transformedShape());
- if(std::isnan(c.radius())) std::cout << "fail: radius is nan" << std::endl;
- else for(auto v : shapelike::contour(part.transformedShape()) ) {
- auto d = pointlike::distance(v, c.center());
- if(d > c.radius() ) {
- auto e = std::abs( 1.0 - d/c.radius());
- REQUIRE(e <= 1e-3);
- }
- }
- i++;
- }
- }
- TEST_CASE("Distance", "[Geometry]") {
- using namespace libnest2d;
- Point p1 = {0, 0};
- Point p2 = {10, 0};
- Point p3 = {10, 10};
- REQUIRE(pointlike::distance(p1, p2) == Approx(10));
- REQUIRE(pointlike::distance(p1, p3) == Approx(sqrt(200)));
- Segment seg(p1, p3);
- // REQUIRE(pointlike::distance(p2, seg) == Approx(7.0710678118654755));
- auto result = pointlike::horizontalDistance(p2, seg);
- auto check = [](TCompute<Coord> val, TCompute<Coord> expected) {
- if(std::is_floating_point<TCompute<Coord>>::value)
- REQUIRE(static_cast<double>(val) ==
- Approx(static_cast<double>(expected)));
- else
- REQUIRE(val == expected);
- };
- REQUIRE(result.second);
- check(result.first, 10);
- result = pointlike::verticalDistance(p2, seg);
- REQUIRE(result.second);
- check(result.first, -10);
- result = pointlike::verticalDistance(Point{10, 20}, seg);
- REQUIRE(result.second);
- check(result.first, 10);
- Point p4 = {80, 0};
- Segment seg2 = { {0, 0}, {0, 40} };
- result = pointlike::horizontalDistance(p4, seg2);
- REQUIRE(result.second);
- check(result.first, 80);
- result = pointlike::verticalDistance(p4, seg2);
- // Point should not be related to the segment
- REQUIRE_FALSE(result.second);
- }
- TEST_CASE("Area", "[Geometry]") {
- using namespace libnest2d;
- RectangleItem rect(10, 10);
- REQUIRE(rect.area() == Approx(100));
- RectangleItem rect2 = {100, 100};
- REQUIRE(rect2.area() == Approx(10000));
- Item item = {
- {61, 97},
- {70, 151},
- {176, 151},
- {189, 138},
- {189, 59},
- {70, 59},
- {61, 77},
- {61, 97}
- };
- REQUIRE(shapelike::area(item.transformedShape()) > 0 );
- }
- TEST_CASE("IsPointInsidePolygon", "[Geometry]") {
- using namespace libnest2d;
- RectangleItem rect(10, 10);
- Point p = {1, 1};
- REQUIRE(rect.isInside(p));
- p = {11, 11};
- REQUIRE_FALSE(rect.isInside(p));
- p = {11, 12};
- REQUIRE_FALSE(rect.isInside(p));
- p = {3, 3};
- REQUIRE(rect.isInside(p));
- }
- //TEST_CASE(GeometryAlgorithms, Intersections) {
- // using namespace binpack2d;
- // RectangleItem rect(70, 30);
- // rect.translate({80, 60});
- // RectangleItem rect2(80, 60);
- // rect2.translate({80, 0});
- //// REQUIRE_FALSE(Item::intersects(rect, rect2));
- // Segment s1({0, 0}, {10, 10});
- // Segment s2({1, 1}, {11, 11});
- // REQUIRE_FALSE(ShapeLike::intersects(s1, s1));
- // REQUIRE_FALSE(ShapeLike::intersects(s1, s2));
- //}
- TEST_CASE("LeftAndDownPolygon", "[Geometry]")
- {
- using namespace libnest2d;
- Box bin(100, 100);
- BottomLeftPlacer placer(bin);
- Item item = {{70, 75}, {88, 60}, {65, 50}, {60, 30}, {80, 20}, {42, 20},
- {35, 35}, {35, 55}, {40, 75}, {70, 75}};
- Item leftControl = { {40, 75},
- {35, 55},
- {35, 35},
- {42, 20},
- {0, 20},
- {0, 75},
- {40, 75}};
- Item downControl = {{88, 60},
- {88, 0},
- {35, 0},
- {35, 35},
- {42, 20},
- {80, 20},
- {60, 30},
- {65, 50},
- {88, 60}};
- Item leftp(placer.leftPoly(item));
- REQUIRE(shapelike::isValid(leftp.rawShape()).first);
- REQUIRE(leftp.vertexCount() == leftControl.vertexCount());
- for(unsigned long i = 0; i < leftControl.vertexCount(); i++) {
- REQUIRE(getX(leftp.vertex(i)) == getX(leftControl.vertex(i)));
- REQUIRE(getY(leftp.vertex(i)) == getY(leftControl.vertex(i)));
- }
- Item downp(placer.downPoly(item));
- REQUIRE(shapelike::isValid(downp.rawShape()).first);
- REQUIRE(downp.vertexCount() == downControl.vertexCount());
- for(unsigned long i = 0; i < downControl.vertexCount(); i++) {
- REQUIRE(getX(downp.vertex(i)) == getX(downControl.vertex(i)));
- REQUIRE(getY(downp.vertex(i)) == getY(downControl.vertex(i)));
- }
- }
- TEST_CASE("ArrangeRectanglesTight", "[Nesting]")
- {
- using namespace libnest2d;
- std::vector<RectangleItem> rects = {
- {80, 80},
- {60, 90},
- {70, 30},
- {80, 60},
- {60, 60},
- {60, 40},
- {40, 40},
- {10, 10},
- {10, 10},
- {10, 10},
- {10, 10},
- {10, 10},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {20, 20} };
- Box bin(210, 250, {105, 125});
- REQUIRE(bin.width() == 210);
- REQUIRE(bin.height() == 250);
- REQUIRE(getX(bin.center()) == 105);
- REQUIRE(getY(bin.center()) == 125);
- _Nester<BottomLeftPlacer, FirstFitSelection> arrange(bin);
- arrange.execute(rects.begin(), rects.end());
- auto max_group = std::max_element(rects.begin(), rects.end(),
- [](const Item &i1, const Item &i2) {
- return i1.binId() < i2.binId();
- });
- int groups = max_group == rects.end() ? 0 : max_group->binId() + 1;
- REQUIRE(groups == 1u);
- REQUIRE(
- std::all_of(rects.begin(), rects.end(), [](const RectangleItem &itm) {
- return itm.binId() != BIN_ID_UNSET;
- }));
- // check for no intersections, no containment:
- bool valid = true;
- for(Item& r1 : rects) {
- for(Item& r2 : rects) {
- if(&r1 != &r2 ) {
- valid = !Item::intersects(r1, r2) || Item::touches(r1, r2);
- REQUIRE(valid);
- valid = (valid && !r1.isInside(r2) && !r2.isInside(r1));
- REQUIRE(valid);
- }
- }
- }
- }
- TEST_CASE("ArrangeRectanglesLoose", "[Nesting]")
- {
- using namespace libnest2d;
- // std::vector<Rectangle> rects = { {40, 40}, {10, 10}, {20, 20} };
- std::vector<RectangleItem> rects = {
- {80, 80},
- {60, 90},
- {70, 30},
- {80, 60},
- {60, 60},
- {60, 40},
- {40, 40},
- {10, 10},
- {10, 10},
- {10, 10},
- {10, 10},
- {10, 10},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {5, 5},
- {20, 20} };
- Box bin(210, 250, {105, 125});
- REQUIRE(bin.width() == 210);
- REQUIRE(bin.height() == 250);
- REQUIRE(getX(bin.center()) == 105);
- REQUIRE(getY(bin.center()) == 125);
- Coord min_obj_distance = 5;
- _Nester<BottomLeftPlacer, FirstFitSelection> arrange(bin, min_obj_distance);
- arrange.execute(rects.begin(), rects.end());
- auto max_group = std::max_element(rects.begin(), rects.end(),
- [](const Item &i1, const Item &i2) {
- return i1.binId() < i2.binId();
- });
- auto groups = size_t(max_group == rects.end() ? 0 : max_group->binId() + 1);
- REQUIRE(groups == 1u);
- REQUIRE(
- std::all_of(rects.begin(), rects.end(), [](const RectangleItem &itm) {
- return itm.binId() != BIN_ID_UNSET;
- }));
- // check for no intersections, no containment:
- bool valid = true;
- for(Item& r1 : rects) {
- for(Item& r2 : rects) {
- if(&r1 != &r2 ) {
- valid = !Item::intersects(r1, r2);
- valid = (valid && !r1.isInside(r2) && !r2.isInside(r1));
- REQUIRE(valid);
- }
- }
- }
- }
- namespace {
- using namespace libnest2d;
- template<long long SCALE = 1, class It>
- void exportSVG(const char *loc, It from, It to) {
- static const char* svg_header =
- R"raw(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
- <svg height="500" width="500" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- )raw";
- // for(auto r : result) {
- std::fstream out(loc, std::fstream::out);
- if(out.is_open()) {
- out << svg_header;
- // Item rbin( RectangleItem(bin.width(), bin.height()) );
- // for(unsigned j = 0; j < rbin.vertexCount(); j++) {
- // auto v = rbin.vertex(j);
- // setY(v, -getY(v)/SCALE + 500 );
- // setX(v, getX(v)/SCALE);
- // rbin.setVertex(j, v);
- // }
- // out << shapelike::serialize<Formats::SVG>(rbin.rawShape()) << std::endl;
- for(auto it = from; it != to; ++it) {
- const Item &itm = *it;
- Item tsh(itm.transformedShape());
- for(unsigned j = 0; j < tsh.vertexCount(); j++) {
- auto v = tsh.vertex(j);
- setY(v, -getY(v)/SCALE + 500);
- setX(v, getX(v)/SCALE);
- tsh.setVertex(j, v);
- }
- out << shapelike::serialize<Formats::SVG>(tsh.rawShape()) << std::endl;
- }
- out << "\n</svg>" << std::endl;
- }
- out.close();
-
- // i++;
- // }
- }
- template<long long SCALE = 1>
- void exportSVG(std::vector<std::reference_wrapper<Item>>& result, int idx = 0) {
- exportSVG((std::string("out") + std::to_string(idx) + ".svg").c_str(),
- result.begin(), result.end());
- }
- }
- TEST_CASE("BottomLeftStressTest", "[Geometry]") {
- using namespace libnest2d;
- const Coord SCALE = 1000000;
- auto& input = prusaParts();
- Box bin(210*SCALE, 250*SCALE);
- BottomLeftPlacer placer(bin);
- auto it = input.begin();
- auto next = it;
- int i = 0;
- while(it != input.end() && ++next != input.end()) {
- placer.pack(*it);
- placer.pack(*next);
- auto result = placer.getItems();
- bool valid = true;
- if(result.size() == 2) {
- Item& r1 = result[0];
- Item& r2 = result[1];
- valid = !Item::intersects(r1, r2) || Item::touches(r1, r2);
- valid = (valid && !r1.isInside(r2) && !r2.isInside(r1));
- if(!valid) {
- std::cout << "error index: " << i << std::endl;
- exportSVG<SCALE>(result, i);
- }
- REQUIRE(valid);
- } else {
- std::cout << "something went terribly wrong!" << std::endl;
- FAIL();
- }
- placer.clearItems();
- it++;
- i++;
- }
- }
- TEST_CASE("convexHull", "[Geometry]") {
- using namespace libnest2d;
- ClipperLib::Path poly = PRINTER_PART_POLYGONS[0];
- auto chull = sl::convexHull(poly);
- REQUIRE(chull.size() == poly.size());
- }
- TEST_CASE("PrusaPartsShouldFitIntoTwoBins", "[Nesting]") {
- // Get the input items and define the bin.
- std::vector<Item> input = prusaParts();
- auto bin = Box(250000000, 210000000);
- // Do the nesting. Check in each step if the remaining items are less than
- // in the previous step. (Some algorithms can place more items in one step)
- size_t pcount = input.size();
- size_t bins = libnest2d::nest(input, bin, 0, {},
- ProgressFunction{[&pcount](unsigned cnt) {
- REQUIRE(cnt < pcount);
- pcount = cnt;
- }});
- // For prusa parts, 2 bins should be enough...
- REQUIRE(bins > 0u);
- REQUIRE(bins <= 2u);
- // All parts should be processed by the algorithm
- REQUIRE(
- std::all_of(input.begin(), input.end(), [](const Item &itm) {
- return itm.binId() != BIN_ID_UNSET;
- }));
- // Gather the items into piles of arranged polygons...
- using Pile = TMultiShape<ClipperLib::Polygon>;
- std::vector<Pile> piles(bins);
- for (auto &itm : input)
- piles[size_t(itm.binId())].emplace_back(itm.transformedShape());
- // Now check all the piles, the bounding box of each pile should be inside
- // the defined bin.
- for (auto &pile : piles) {
- auto bb = sl::boundingBox(pile);
- REQUIRE(sl::isInside(bb, bin));
- }
- }
- TEST_CASE("EmptyItemShouldBeUntouched", "[Nesting]") {
- auto bin = Box(250000000, 210000000); // dummy bin
- std::vector<Item> items;
- items.emplace_back(Item{}); // Emplace empty item
- items.emplace_back(Item{0, 200, 0}); // Emplace zero area item
- size_t bins = libnest2d::nest(items, bin);
- REQUIRE(bins == 0u);
- for (auto &itm : items) REQUIRE(itm.binId() == BIN_ID_UNSET);
- }
- TEST_CASE("LargeItemShouldBeUntouched", "[Nesting]") {
- auto bin = Box(250000000, 210000000); // dummy bin
- std::vector<Item> items;
- items.emplace_back(RectangleItem{250000001, 210000001}); // Emplace large item
- size_t bins = libnest2d::nest(items, bin);
- REQUIRE(bins == 0u);
- REQUIRE(items.front().binId() == BIN_ID_UNSET);
- }
- TEST_CASE("Items can be preloaded", "[Nesting]") {
- auto bin = Box({0, 0}, {250000000, 210000000}); // dummy bin
- std::vector<Item> items;
- items.reserve(2);
- NestConfig<> cfg;
- cfg.placer_config.alignment = NestConfig<>::Placement::Alignment::DONT_ALIGN;
- items.emplace_back(RectangleItem{10000000, 10000000});
- Item &fixed_rect = items.back();
- fixed_rect.translate(bin.center());
- items.emplace_back(RectangleItem{20000000, 20000000});
- Item &movable_rect = items.back();
- movable_rect.translate(bin.center());
- SECTION("Preloaded Item should be untouched") {
- fixed_rect.markAsFixedInBin(0);
- size_t bins = libnest2d::nest(items, bin, 0, cfg);
- REQUIRE(bins == 1);
- REQUIRE(fixed_rect.binId() == 0);
- REQUIRE(fixed_rect.translation().X == bin.center().X);
- REQUIRE(fixed_rect.translation().Y == bin.center().Y);
- REQUIRE(movable_rect.binId() == 0);
- REQUIRE(movable_rect.translation().X != bin.center().X);
- REQUIRE(movable_rect.translation().Y != bin.center().Y);
- }
- SECTION("Preloaded Item should not affect free bins") {
- fixed_rect.markAsFixedInBin(1);
- size_t bins = libnest2d::nest(items, bin, 0, cfg);
- REQUIRE(bins == 2);
- REQUIRE(fixed_rect.binId() == 1);
- REQUIRE(fixed_rect.translation().X == bin.center().X);
- REQUIRE(fixed_rect.translation().Y == bin.center().Y);
- REQUIRE(movable_rect.binId() == 0);
- auto bb = movable_rect.boundingBox();
- REQUIRE(bb.center().X == bin.center().X);
- REQUIRE(bb.center().Y == bin.center().Y);
- }
- }
- namespace {
- struct ItemPair {
- Item orbiter;
- Item stationary;
- };
- std::vector<ItemPair> nfp_testdata = {
- {
- {
- {80, 50},
- {100, 70},
- {120, 50},
- {80, 50}
- },
- {
- {10, 10},
- {10, 40},
- {40, 40},
- {40, 10},
- {10, 10}
- }
- },
- {
- {
- {80, 50},
- {60, 70},
- {80, 90},
- {120, 90},
- {140, 70},
- {120, 50},
- {80, 50}
- },
- {
- {10, 10},
- {10, 40},
- {40, 40},
- {40, 10},
- {10, 10}
- }
- },
- {
- {
- {40, 10},
- {30, 10},
- {20, 20},
- {20, 30},
- {30, 40},
- {40, 40},
- {50, 30},
- {50, 20},
- {40, 10}
- },
- {
- {80, 0},
- {80, 30},
- {110, 30},
- {110, 0},
- {80, 0}
- }
- },
- {
- {
- {117, 107},
- {118, 109},
- {120, 112},
- {122, 113},
- {128, 113},
- {130, 112},
- {132, 109},
- {133, 107},
- {133, 103},
- {132, 101},
- {130, 98},
- {128, 97},
- {122, 97},
- {120, 98},
- {118, 101},
- {117, 103},
- {117, 107}
- },
- {
- {102, 116},
- {111, 126},
- {114, 126},
- {144, 106},
- {148, 100},
- {148, 85},
- {147, 84},
- {102, 84},
- {102, 116},
- }
- },
- {
- {
- {99, 122},
- {108, 140},
- {110, 142},
- {139, 142},
- {151, 122},
- {151, 102},
- {142, 70},
- {139, 68},
- {111, 68},
- {108, 70},
- {99, 102},
- {99, 122},
- },
- {
- {107, 124},
- {128, 125},
- {133, 125},
- {136, 124},
- {140, 121},
- {142, 119},
- {143, 116},
- {143, 109},
- {141, 93},
- {139, 89},
- {136, 86},
- {134, 85},
- {108, 85},
- {107, 86},
- {107, 124},
- }
- },
- {
- {
- {91, 100},
- {94, 144},
- {117, 153},
- {118, 153},
- {159, 112},
- {159, 110},
- {156, 66},
- {133, 57},
- {132, 57},
- {91, 98},
- {91, 100},
- },
- {
- {101, 90},
- {103, 98},
- {107, 113},
- {114, 125},
- {115, 126},
- {135, 126},
- {136, 125},
- {144, 114},
- {149, 90},
- {149, 89},
- {148, 87},
- {145, 84},
- {105, 84},
- {102, 87},
- {101, 89},
- {101, 90},
- }
- }
- };
- std::vector<ItemPair> nfp_concave_testdata = {
- { // ItemPair
- {
- {
- {533726, 142141},
- {532359, 143386},
- {530141, 142155},
- {528649, 160091},
- {533659, 157607},
- {538669, 160091},
- {537178, 142155},
- {534959, 143386},
- {533726, 142141},
- }
- },
- {
- {
- {118305, 11603},
- {118311, 26616},
- {113311, 26611},
- {109311, 29604},
- {109300, 44608},
- {109311, 49631},
- {113300, 52636},
- {118311, 52636},
- {118308, 103636},
- {223830, 103636},
- {236845, 90642},
- {236832, 11630},
- {232825, 11616},
- {210149, 11616},
- {211308, 13625},
- {209315, 17080},
- {205326, 17080},
- {203334, 13629},
- {204493, 11616},
- {118305, 11603},
- }
- },
- }
- };
- template<nfp::NfpLevel lvl, Coord SCALE>
- void testNfp(const std::vector<ItemPair>& testdata) {
- using namespace libnest2d;
- Box bin(210*SCALE, 250*SCALE);
- int TEST_CASEcase = 0;
- auto& exportfun = exportSVG<SCALE>;
- auto onetest = [&](Item& orbiter, Item& stationary, unsigned /*testidx*/){
- TEST_CASEcase++;
- orbiter.translate({210*SCALE, 0});
- auto&& nfp = nfp::noFitPolygon<lvl>(stationary.rawShape(),
- orbiter.transformedShape());
- placers::correctNfpPosition(nfp, stationary, orbiter);
- auto valid = shapelike::isValid(nfp.first);
- /*Item infp(nfp.first);
- if(!valid.first) {
- std::cout << "TEST_CASE instance: " << TEST_CASEidx << " "
- << valid.second << std::endl;
- std::vector<std::reference_wrapper<Item>> inp = {std::ref(infp)};
- exportfun(inp, bin, TEST_CASEidx);
- }*/
- REQUIRE(valid.first);
- Item infp(nfp.first);
- int i = 0;
- auto rorbiter = orbiter.transformedShape();
- auto vo = nfp::referenceVertex(rorbiter);
- REQUIRE(stationary.isInside(infp));
- for(auto v : infp) {
- auto dx = getX(v) - getX(vo);
- auto dy = getY(v) - getY(vo);
- Item tmp = orbiter;
- tmp.translate({dx, dy});
- bool touching = Item::touches(tmp, stationary);
- if(!touching || !valid.first) {
- std::vector<std::reference_wrapper<Item>> inp = {
- std::ref(stationary), std::ref(tmp), std::ref(infp)
- };
- exportfun(inp, TEST_CASEcase*i++);
- }
- REQUIRE(touching);
- }
- };
- unsigned tidx = 0;
- for(auto& td : testdata) {
- auto orbiter = td.orbiter;
- auto stationary = td.stationary;
- onetest(orbiter, stationary, tidx++);
- }
- tidx = 0;
- for(auto& td : testdata) {
- auto orbiter = td.stationary;
- auto stationary = td.orbiter;
- onetest(orbiter, stationary, tidx++);
- }
- }
- }
- TEST_CASE("nfpConvexConvex", "[Geometry]") {
- testNfp<nfp::NfpLevel::CONVEX_ONLY, 1>(nfp_testdata);
- }
- //TEST_CASE(GeometryAlgorithms, nfpConcaveConcave) {
- // TEST_CASENfp<NfpLevel::BOTH_CONCAVE, 1000>(nfp_concave_TEST_CASEdata);
- //}
- TEST_CASE("pointOnPolygonContour", "[Geometry]") {
- using namespace libnest2d;
- RectangleItem input(10, 10);
- placers::EdgeCache<PolygonImpl> ecache(input);
- auto first = *input.begin();
- REQUIRE(getX(first) == getX(ecache.coords(0)));
- REQUIRE(getY(first) == getY(ecache.coords(0)));
- auto last = *std::prev(input.end());
- REQUIRE(getX(last) == getX(ecache.coords(1.0)));
- REQUIRE(getY(last) == getY(ecache.coords(1.0)));
- for(int i = 0; i <= 100; i++) {
- auto v = ecache.coords(i*(0.01));
- REQUIRE(shapelike::touches(v, input.transformedShape()));
- }
- }
- TEST_CASE("mergePileWithPolygon", "[Geometry]") {
- using namespace libnest2d;
- RectangleItem rect1(10, 15);
- RectangleItem rect2(15, 15);
- RectangleItem rect3(20, 15);
- rect2.translate({10, 0});
- rect3.translate({25, 0});
- TMultiShape<PolygonImpl> pile;
- pile.push_back(rect1.transformedShape());
- pile.push_back(rect2.transformedShape());
- auto result = nfp::merge(pile, rect3.transformedShape());
- REQUIRE(result.size() == 1);
- RectangleItem ref(45, 15);
- REQUIRE(shapelike::area(result.front()) == Approx(ref.area()));
- }
- namespace {
- long double refMinAreaBox(const PolygonImpl& p) {
- auto it = sl::cbegin(p), itx = std::next(it);
- long double min_area = std::numeric_limits<long double>::max();
- auto update_min = [&min_area, &it, &itx, &p]() {
- Segment s(*it, *itx);
- PolygonImpl rotated = p;
- sl::rotate(rotated, -s.angleToXaxis());
- auto bb = sl::boundingBox(rotated);
- auto area = cast<long double>(sl::area(bb));
- if(min_area > area) min_area = area;
- };
- while(itx != sl::cend(p)) {
- update_min();
- ++it; ++itx;
- }
- it = std::prev(sl::cend(p)); itx = sl::cbegin(p);
- update_min();
- return min_area;
- }
- template<class T> struct BoostGCD {
- T operator()(const T &a, const T &b) { return boost::gcd(a, b); }
- };
- using Unit = int64_t;
- using Ratio = boost::rational<boost::multiprecision::int128_t>;
- }
- //TEST_CASE(GeometryAlgorithms, MinAreaBBCClk) {
- // auto u = [](ClipperLib::cInt n) { return n*1000000; };
- // PolygonImpl poly({ {u(0), u(0)}, {u(4), u(1)}, {u(2), u(4)}});
- // long double arearef = refMinAreaBox(poly);
- // long double area = minAreaBoundingBox<PolygonImpl, Unit, Ratio>(poly).area();
- // REQUIRE(std::abs(area - arearef) <= 500e6 );
- //}
- TEST_CASE("MinAreaBBWithRotatingCalipers", "[Geometry]") {
- long double err_epsilon = 500e6l;
- for(ClipperLib::Path rinput : PRINTER_PART_POLYGONS) {
- PolygonImpl poly(rinput);
- long double arearef = refMinAreaBox(poly);
- auto bb = minAreaBoundingBox<PathImpl, Unit, Ratio>(rinput);
- long double area = cast<long double>(bb.area());
- bool succ = std::abs(arearef - area) < err_epsilon;
- REQUIRE(succ);
- }
- for(ClipperLib::Path rinput : STEGOSAUR_POLYGONS) {
- rinput.pop_back();
- std::reverse(rinput.begin(), rinput.end());
- PolygonImpl poly(removeCollinearPoints<PathImpl, PointImpl, Unit>(rinput, 1000000));
- long double arearef = refMinAreaBox(poly);
- auto bb = minAreaBoundingBox<PolygonImpl, Unit, Ratio>(poly);
- long double area = cast<long double>(bb.area());
- bool succ = std::abs(arearef - area) < err_epsilon;
- REQUIRE(succ);
- }
- }
- template<class It> MultiPolygon merged_pile(It from, It to, int bin_id)
- {
- MultiPolygon pile;
- pile.reserve(size_t(to - from));
-
- for (auto it = from; it != to; ++it) {
- if (it->binId() == bin_id) pile.emplace_back(it->transformedShape());
- }
-
- return nfp::merge(pile);
- }
- TEST_CASE("Test for bed center distance optimization", "[Nesting], [NestKernels]")
- {
- static const constexpr ClipperLib::cInt W = 10000000;
-
- // Get the input items and define the bin.
- std::vector<RectangleItem> input(9, {W, W});
-
- auto bin = Box::infinite();
-
- NfpPlacer::Config pconfig;
-
- pconfig.object_function = [](const Item &item) -> double {
- return pl::magnsq<PointImpl, double>(item.boundingBox().center());
- };
-
- size_t bins = nest(input, bin, 0, NestConfig{pconfig});
-
- REQUIRE(bins == 1);
-
- // Gather the items into piles of arranged polygons...
- MultiPolygon pile;
- pile.reserve(input.size());
-
- for (auto &itm : input) {
- REQUIRE(itm.binId() == 0);
- pile.emplace_back(itm.transformedShape());
- }
-
- MultiPolygon m = merged_pile(input.begin(), input.end(), 0);
-
- REQUIRE(m.size() == 1);
-
- REQUIRE(sl::area(m) == Approx(9. * W * W));
- }
- TEST_CASE("Test for biggest bounding box area", "[Nesting], [NestKernels]")
- {
- static const constexpr ClipperLib::cInt W = 10000000;
- static const constexpr size_t N = 100;
-
- // Get the input items and define the bin.
- std::vector<RectangleItem> input(N, {W, W});
-
- auto bin = Box::infinite();
-
- NfpPlacer::Config pconfig;
- pconfig.rotations = {0.};
- Box pile_box;
- pconfig.before_packing =
- [&pile_box](const MultiPolygon &pile,
- const _ItemGroup<PolygonImpl> &/*packed_items*/,
- const _ItemGroup<PolygonImpl> &/*remaining_items*/) {
- pile_box = sl::boundingBox(pile);
- };
- pconfig.object_function = [&pile_box](const Item &item) -> double {
- Box b = sl::boundingBox(item.boundingBox(), pile_box);
- double area = b.area<double>() / (W * W);
- return -area;
- };
-
- size_t bins = nest(input, bin, 0, NestConfig{pconfig});
-
- // To debug:
- exportSVG<1000000>("out", input.begin(), input.end());
-
- REQUIRE(bins == 1);
-
- MultiPolygon pile = merged_pile(input.begin(), input.end(), 0);
- Box bb = sl::boundingBox(pile);
-
- // Here the result shall be a stairway of boxes
- REQUIRE(pile.size() == N);
- REQUIRE(bb.area() == N * N * W * W);
- }
|