#ifndef NFP_SVGNEST_HPP #define NFP_SVGNEST_HPP #include #include #include namespace libnest2d { namespace __svgnest { using std::sqrt; using std::min; using std::max; using std::abs; using std::isnan; //template struct _Scale { // static const BP2D_CONSTEXPR long long Value = 1000000; //}; template struct _alg { using Contour = TContour; using Point = TPoint; using iCoord = TCoord; using Coord = double; using Shapes = nfp::Shapes; static const Coord TOL; #define dNAN std::nan("") struct Vector { Coord x = 0.0, y = 0.0; bool marked = false; Vector() = default; Vector(Coord X, Coord Y): x(X), y(Y) {} Vector(const Point& p): x(Coord(getX(p))), y(Coord(getY(p))) {} operator Point() const { return {iCoord(x), iCoord(y)}; } Vector& operator=(const Point& p) { x = getX(p), y = getY(p); return *this; } bool operator!=(const Vector& v) const { return v.x != x || v.y != y; } Vector(std::initializer_list il): x(*il.begin()), y(*std::next(il.begin())) {} }; static inline Coord x(const Point& p) { return Coord(getX(p)); } static inline Coord y(const Point& p) { return Coord(getY(p)); } static inline Coord x(const Vector& p) { return p.x; } static inline Coord y(const Vector& p) { return p.y; } class Cntr { std::vector v_; public: Cntr(const Contour& c) { v_.reserve(c.size()); std::transform(c.begin(), c.end(), std::back_inserter(v_), [](const Point& p) { return Vector(double(x(p)) / 1e6, double(y(p)) / 1e6); }); std::reverse(v_.begin(), v_.end()); v_.pop_back(); } Cntr() = default; Coord offsetx = 0; Coord offsety = 0; size_t size() const { return v_.size(); } bool empty() const { return v_.empty(); } typename std::vector::const_iterator cbegin() const { return v_.cbegin(); } typename std::vector::const_iterator cend() const { return v_.cend(); } typename std::vector::iterator begin() { return v_.begin(); } typename std::vector::iterator end() { return v_.end(); } Vector& operator[](size_t idx) { return v_[idx]; } const Vector& operator[](size_t idx) const { return v_[idx]; } template void emplace_back(Args&&...args) { v_.emplace_back(std::forward(args)...); } template void push(Args&&...args) { v_.emplace_back(std::forward(args)...); } void clear() { v_.clear(); } operator Contour() const { Contour cnt; cnt.reserve(v_.size() + 1); std::transform(v_.begin(), v_.end(), std::back_inserter(cnt), [](const Vector& vertex) { return Point(iCoord(vertex.x) * 1000000, iCoord(vertex.y) * 1000000); }); if(!cnt.empty()) cnt.emplace_back(cnt.front()); S sh = shapelike::create(cnt); // std::reverse(cnt.begin(), cnt.end()); return shapelike::getContour(sh); } }; inline static bool _almostEqual(Coord a, Coord b, Coord tolerance = TOL) { return std::abs(a - b) < tolerance; } // returns true if p lies on the line segment defined by AB, // but not at any endpoints may need work! static bool _onSegment(const Vector& A, const Vector& B, const Vector& p) { // vertical line if(_almostEqual(A.x, B.x) && _almostEqual(p.x, A.x)) { if(!_almostEqual(p.y, B.y) && !_almostEqual(p.y, A.y) && p.y < max(B.y, A.y) && p.y > min(B.y, A.y)){ return true; } else{ return false; } } // horizontal line if(_almostEqual(A.y, B.y) && _almostEqual(p.y, A.y)){ if(!_almostEqual(p.x, B.x) && !_almostEqual(p.x, A.x) && p.x < max(B.x, A.x) && p.x > min(B.x, A.x)){ return true; } else{ return false; } } //range check if((p.x < A.x && p.x < B.x) || (p.x > A.x && p.x > B.x) || (p.y < A.y && p.y < B.y) || (p.y > A.y && p.y > B.y)) return false; // exclude end points if((_almostEqual(p.x, A.x) && _almostEqual(p.y, A.y)) || (_almostEqual(p.x, B.x) && _almostEqual(p.y, B.y))) return false; double cross = (p.y - A.y) * (B.x - A.x) - (p.x - A.x) * (B.y - A.y); if(abs(cross) > TOL) return false; double dot = (p.x - A.x) * (B.x - A.x) + (p.y - A.y)*(B.y - A.y); if(dot < 0 || _almostEqual(dot, 0)) return false; double len2 = (B.x - A.x)*(B.x - A.x) + (B.y - A.y)*(B.y - A.y); if(dot > len2 || _almostEqual(dot, len2)) return false; return true; } // return true if point is in the polygon, false if outside, and null if exactly on a point or edge static int pointInPolygon(const Vector& point, const Cntr& polygon) { if(polygon.size() < 3){ return 0; } bool inside = false; Coord offsetx = polygon.offsetx; Coord offsety = polygon.offsety; for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j=i++) { auto xi = polygon[i].x + offsetx; auto yi = polygon[i].y + offsety; auto xj = polygon[j].x + offsetx; auto yj = polygon[j].y + offsety; if(_almostEqual(xi, point.x) && _almostEqual(yi, point.y)){ return 0; // no result } if(_onSegment({xi, yi}, {xj, yj}, point)){ return 0; // exactly on the segment } if(_almostEqual(xi, xj) && _almostEqual(yi, yj)){ // ignore very small lines continue; } bool intersect = ((yi > point.y) != (yj > point.y)) && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside? 1 : -1; } static bool intersect(const Cntr& A, const Cntr& B){ Contour a = A, b = B; return shapelike::intersects(shapelike::create(a), shapelike::create(b)); } static Vector _normalizeVector(const Vector& v) { if(_almostEqual(v.x*v.x + v.y*v.y, Coord(1))){ return Point(v); // given vector was already a unit vector } auto len = sqrt(v.x*v.x + v.y*v.y); auto inverse = 1/len; return { Coord(v.x*inverse), Coord(v.y*inverse) }; } static double pointDistance( const Vector& p, const Vector& s1, const Vector& s2, Vector normal, bool infinite = false) { normal = _normalizeVector(normal); Vector dir = { normal.y, -normal.x }; auto pdot = p.x*dir.x + p.y*dir.y; auto s1dot = s1.x*dir.x + s1.y*dir.y; auto s2dot = s2.x*dir.x + s2.y*dir.y; auto pdotnorm = p.x*normal.x + p.y*normal.y; auto s1dotnorm = s1.x*normal.x + s1.y*normal.y; auto s2dotnorm = s2.x*normal.x + s2.y*normal.y; if(!infinite){ if (((pdots1dot || _almostEqual(pdot, s1dot)) && (pdot>s2dot || _almostEqual(pdot, s2dot)))) { // dot doesn't collide with segment, // or lies directly on the vertex return dNAN; } if ((_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot)) && (pdotnorm>s1dotnorm && pdotnorm>s2dotnorm)) { return min(pdotnorm - s1dotnorm, pdotnorm - s2dotnorm); } if ((_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot)) && (pdotnorm EFmax){ return dNAN; } double overlap = 0; if((ABmax > EFmax && ABmin < EFmin) || (EFmax > ABmax && EFmin < ABmin)) { overlap = 1; } else{ auto minMax = min(ABmax, EFmax); auto maxMin = max(ABmin, EFmin); auto maxMax = max(ABmax, EFmax); auto minMin = min(ABmin, EFmin); overlap = (minMax-maxMin)/(maxMax-minMin); } auto crossABE = (E.y - A.y) * (B.x - A.x) - (E.x - A.x) * (B.y - A.y); auto crossABF = (F.y - A.y) * (B.x - A.x) - (F.x - A.x) * (B.y - A.y); // lines are colinear if(_almostEqual(crossABE,0) && _almostEqual(crossABF,0)){ Vector ABnorm = {B.y-A.y, A.x-B.x}; Vector EFnorm = {F.y-E.y, E.x-F.x}; auto ABnormlength = sqrt(ABnorm.x*ABnorm.x + ABnorm.y*ABnorm.y); ABnorm.x /= ABnormlength; ABnorm.y /= ABnormlength; auto EFnormlength = sqrt(EFnorm.x*EFnorm.x + EFnorm.y*EFnorm.y); EFnorm.x /= EFnormlength; EFnorm.y /= EFnormlength; // segment normals must point in opposite directions if(abs(ABnorm.y * EFnorm.x - ABnorm.x * EFnorm.y) < TOL && ABnorm.y * EFnorm.y + ABnorm.x * EFnorm.x < 0){ // normal of AB segment must point in same direction as // given direction vector auto normdot = ABnorm.y * direction.y + ABnorm.x * direction.x; // the segments merely slide along eachother if(_almostEqual(normdot,0, TOL)){ return dNAN; } if(normdot < 0){ return 0.0; } } return dNAN; } std::vector distances; distances.reserve(10); // coincident points if(_almostEqual(dotA, dotE)){ distances.emplace_back(crossA-crossE); } else if(_almostEqual(dotA, dotF)){ distances.emplace_back(crossA-crossF); } else if(dotA > EFmin && dotA < EFmax){ auto d = pointDistance(A,E,F,reverse); if(!isnan(d) && _almostEqual(d, 0)) { // A currently touches EF, but AB is moving away from EF auto dB = pointDistance(B,E,F,reverse,true); if(dB < 0 || _almostEqual(dB*overlap,0)){ d = dNAN; } } if(!isnan(d)){ distances.emplace_back(d); } } if(_almostEqual(dotB, dotE)){ distances.emplace_back(crossB-crossE); } else if(_almostEqual(dotB, dotF)){ distances.emplace_back(crossB-crossF); } else if(dotB > EFmin && dotB < EFmax){ auto d = pointDistance(B,E,F,reverse); if(!isnan(d) && _almostEqual(d, 0)) { // crossA>crossB A currently touches EF, but AB is moving away from EF double dA = pointDistance(A,E,F,reverse,true); if(dA < 0 || _almostEqual(dA*overlap,0)){ d = dNAN; } } if(!isnan(d)){ distances.emplace_back(d); } } if(dotE > ABmin && dotE < ABmax){ auto d = pointDistance(E,A,B,direction); if(!isnan(d) && _almostEqual(d, 0)) { // crossF ABmin && dotF < ABmax){ auto d = pointDistance(F,A,B,direction); if(!isnan(d) && _almostEqual(d, 0)) { // && crossE 0 || _almostEqual(d, 0)){ distance = d; } } } } return distance; } static double polygonProjectionDistance(const Cntr& AA, const Cntr& BB, Vector direction) { Cntr A = AA; Cntr B = BB; auto Boffsetx = B.offsetx; auto Boffsety = B.offsety; auto Aoffsetx = A.offsetx; auto Aoffsety = A.offsety; // close the loop for polygons if(A[0] != A[A.size()-1]){ A.push(A[0]); } if(B[0] != B[B.size()-1]){ B.push(B[0]); } auto& edgeA = A; auto& edgeB = B; double distance = dNAN, d; // Vector p, s1, s2; for(size_t i = 0; i < edgeB.size(); i++) { // the shortest/most negative projection of B onto A double minprojection = dNAN; Vector minp; for(size_t j = 0; j < edgeA.size() - 1; j++){ Vector p = {x(edgeB[i]) + Boffsetx, y(edgeB[i]) + Boffsety }; Vector s1 = {x(edgeA[j]) + Aoffsetx, y(edgeA[j]) + Aoffsety }; Vector s2 = {x(edgeA[j+1]) + Aoffsetx, y(edgeA[j+1]) + Aoffsety }; if(abs((s2.y-s1.y) * direction.x - (s2.x-s1.x) * direction.y) < TOL) continue; // project point, ignore edge boundaries d = pointDistance(p, s1, s2, direction); if(!isnan(d) && (isnan(minprojection) || d < minprojection)) { minprojection = d; minp = p; } } if(!isnan(minprojection) && (isnan(distance) || minprojection > distance)){ distance = minprojection; } } return distance; } static std::pair searchStartPoint( const Cntr& AA, const Cntr& BB, bool inside, const std::vector& NFP = {}) { // clone arrays auto A = AA; auto B = BB; // // close the loop for polygons // if(A[0] != A[A.size()-1]){ // A.push(A[0]); // } // if(B[0] != B[B.size()-1]){ // B.push(B[0]); // } // returns true if point already exists in the given nfp auto inNfp = [](const Vector& p, const std::vector& nfp){ if(nfp.empty()){ return false; } for(size_t i=0; i < nfp.size(); i++){ for(size_t j = 0; j< nfp[i].size(); j++){ if(_almostEqual(p.x, nfp[i][j].x) && _almostEqual(p.y, nfp[i][j].y)){ return true; } } } return false; }; for(size_t i = 0; i < A.size() - 1; i++){ if(!A[i].marked) { A[i].marked = true; for(size_t j = 0; j < B.size(); j++){ B.offsetx = A[i].x - B[j].x; B.offsety = A[i].y - B[j].y; int Binside = 0; for(size_t k = 0; k < B.size(); k++){ int inpoly = pointInPolygon({B[k].x + B.offsetx, B[k].y + B.offsety}, A); if(inpoly != 0){ Binside = inpoly; break; } } if(Binside == 0){ // A and B are the same return {false, {}}; } auto startPoint = std::make_pair(true, Vector(B.offsetx, B.offsety)); if(((Binside && inside) || (!Binside && !inside)) && !intersect(A,B) && !inNfp(startPoint.second, NFP)){ return startPoint; } // slide B along vector auto vx = A[i+1].x - A[i].x; auto vy = A[i+1].y - A[i].y; double d1 = polygonProjectionDistance(A,B,{vx, vy}); double d2 = polygonProjectionDistance(B,A,{-vx, -vy}); double d = dNAN; // todo: clean this up if(isnan(d1) && isnan(d2)){ // nothin } else if(isnan(d1)){ d = d2; } else if(isnan(d2)){ d = d1; } else{ d = min(d1,d2); } // only slide until no longer negative // todo: clean this up if(!isnan(d) && !_almostEqual(d,0) && d > 0){ } else{ continue; } auto vd2 = vx*vx + vy*vy; if(d*d < vd2 && !_almostEqual(d*d, vd2)){ auto vd = sqrt(vx*vx + vy*vy); vx *= d/vd; vy *= d/vd; } B.offsetx += vx; B.offsety += vy; for(size_t k = 0; k < B.size(); k++){ int inpoly = pointInPolygon({B[k].x + B.offsetx, B[k].y + B.offsety}, A); if(inpoly != 0){ Binside = inpoly; break; } } startPoint = std::make_pair(true, Vector{B.offsetx, B.offsety}); if(((Binside && inside) || (!Binside && !inside)) && !intersect(A,B) && !inNfp(startPoint.second, NFP)){ return startPoint; } } } } return {false, Vector(0, 0)}; } static std::vector noFitPolygon(Cntr A, Cntr B, bool inside, bool searchEdges) { if(A.size() < 3 || B.size() < 3) { throw GeometryException(GeomErr::NFP); return {}; } A.offsetx = 0; A.offsety = 0; long i = 0, j = 0; auto minA = y(A[0]); long minAindex = 0; auto maxB = y(B[0]); long maxBindex = 0; for(i = 1; i < A.size(); i++){ A[i].marked = false; if(y(A[i]) < minA){ minA = y(A[i]); minAindex = i; } } for(i = 1; i < B.size(); i++){ B[i].marked = false; if(y(B[i]) > maxB){ maxB = y(B[i]); maxBindex = i; } } std::pair startpoint; if(!inside){ // shift B such that the bottom-most point of B is at the top-most // point of A. This guarantees an initial placement with no // intersections startpoint = { true, { x(A[minAindex]) - x(B[maxBindex]), y(A[minAindex]) - y(B[maxBindex]) } }; } else { // no reliable heuristic for inside startpoint = searchStartPoint(A, B, true); } std::vector NFPlist; struct Touch { int type; long A; long B; Touch(int t, long a, long b): type(t), A(a), B(b) {} }; while(startpoint.first) { B.offsetx = startpoint.second.x; B.offsety = startpoint.second.y; // maintain a list of touching points/edges std::vector touching; struct V { Coord x, y; Vector *start, *end; operator bool() { return start != nullptr && end != nullptr; } operator Vector() const { return {x, y}; } } prevvector = {0, 0, nullptr, nullptr}; Cntr NFP; NFP.emplace_back(x(B[0]) + B.offsetx, y(B[0]) + B.offsety); auto referencex = x(B[0]) + B.offsetx; auto referencey = y(B[0]) + B.offsety; auto startx = referencex; auto starty = referencey; unsigned counter = 0; // sanity check, prevent infinite loop while(counter < 10*(A.size() + B.size())){ touching.clear(); // find touching vertices/edges for(i = 0; i < A.size(); i++){ long nexti = (i == A.size() - 1) ? 0 : i + 1; for(j = 0; j < B.size(); j++){ long nextj = (j == B.size() - 1) ? 0 : j + 1; if( _almostEqual(A[i].x, B[j].x+B.offsetx) && _almostEqual(A[i].y, B[j].y+B.offsety)) { touching.emplace_back(0, i, j); } else if( _onSegment( A[i], A[nexti], { B[j].x+B.offsetx, B[j].y + B.offsety}) ) { touching.emplace_back(1, nexti, j); } else if( _onSegment( {B[j].x+B.offsetx, B[j].y + B.offsety}, {B[nextj].x+B.offsetx, B[nextj].y + B.offsety}, A[i]) ) { touching.emplace_back(2, i, nextj); } } } // generate translation vectors from touching vertices/edges std::vector vectors; for(i=0; i < touching.size(); i++){ auto& vertexA = A[touching[i].A]; vertexA.marked = true; // adjacent A vertices auto prevAindex = touching[i].A - 1; auto nextAindex = touching[i].A + 1; prevAindex = (prevAindex < 0) ? A.size() - 1 : prevAindex; // loop nextAindex = (nextAindex >= A.size()) ? 0 : nextAindex; // loop auto& prevA = A[prevAindex]; auto& nextA = A[nextAindex]; // adjacent B vertices auto& vertexB = B[touching[i].B]; auto prevBindex = touching[i].B-1; auto nextBindex = touching[i].B+1; prevBindex = (prevBindex < 0) ? B.size() - 1 : prevBindex; // loop nextBindex = (nextBindex >= B.size()) ? 0 : nextBindex; // loop auto& prevB = B[prevBindex]; auto& nextB = B[nextBindex]; if(touching[i].type == 0){ V vA1 = { prevA.x - vertexA.x, prevA.y - vertexA.y, &vertexA, &prevA }; V vA2 = { nextA.x - vertexA.x, nextA.y - vertexA.y, &vertexA, &nextA }; // B vectors need to be inverted V vB1 = { vertexB.x - prevB.x, vertexB.y - prevB.y, &prevB, &vertexB }; V vB2 = { vertexB.x - nextB.x, vertexB.y - nextB.y, &nextB, &vertexB }; vectors.emplace_back(vA1); vectors.emplace_back(vA2); vectors.emplace_back(vB1); vectors.emplace_back(vB2); } else if(touching[i].type == 1){ vectors.emplace_back(V{ vertexA.x-(vertexB.x+B.offsetx), vertexA.y-(vertexB.y+B.offsety), &prevA, &vertexA }); vectors.emplace_back(V{ prevA.x-(vertexB.x+B.offsetx), prevA.y-(vertexB.y+B.offsety), &vertexA, &prevA }); } else if(touching[i].type == 2){ vectors.emplace_back(V{ vertexA.x-(vertexB.x+B.offsetx), vertexA.y-(vertexB.y+B.offsety), &prevB, &vertexB }); vectors.emplace_back(V{ vertexA.x-(prevB.x+B.offsetx), vertexA.y-(prevB.y+B.offsety), &vertexB, &prevB }); } } // TODO: there should be a faster way to reject vectors that // will cause immediate intersection. For now just check them all V translate = {0, 0, nullptr, nullptr}; double maxd = 0; for(i = 0; i < vectors.size(); i++) { if(vectors[i].x == 0 && vectors[i].y == 0){ continue; } // if this vector points us back to where we came from, ignore it. // ie cross product = 0, dot product < 0 if(prevvector && vectors[i].y * prevvector.y + vectors[i].x * prevvector.x < 0){ // compare magnitude with unit vectors double vectorlength = sqrt(vectors[i].x*vectors[i].x+vectors[i].y*vectors[i].y); Vector unitv = {Coord(vectors[i].x/vectorlength), Coord(vectors[i].y/vectorlength)}; double prevlength = sqrt(prevvector.x*prevvector.x+prevvector.y*prevvector.y); Vector prevunit = { prevvector.x/prevlength, prevvector.y/prevlength}; // we need to scale down to unit vectors to normalize vector length. Could also just do a tan here if(abs(unitv.y * prevunit.x - unitv.x * prevunit.y) < 0.0001){ continue; } } V vi = vectors[i]; double d = polygonSlideDistance(A, B, vi, true); double vecd2 = vectors[i].x*vectors[i].x + vectors[i].y*vectors[i].y; if(isnan(d) || d*d > vecd2){ double vecd = sqrt(vectors[i].x*vectors[i].x + vectors[i].y*vectors[i].y); d = vecd; } if(!isnan(d) && d > maxd){ maxd = d; translate = vectors[i]; } } if(!translate || _almostEqual(maxd, 0)) { // didn't close the loop, something went wrong here NFP.clear(); break; } translate.start->marked = true; translate.end->marked = true; prevvector = translate; // trim double vlength2 = translate.x*translate.x + translate.y*translate.y; if(maxd*maxd < vlength2 && !_almostEqual(maxd*maxd, vlength2)){ double scale = sqrt((maxd*maxd)/vlength2); translate.x *= scale; translate.y *= scale; } referencex += translate.x; referencey += translate.y; if(_almostEqual(referencex, startx) && _almostEqual(referencey, starty)) { // we've made a full loop break; } // if A and B start on a touching horizontal line, // the end point may not be the start point bool looped = false; if(NFP.size() > 0) { for(i = 0; i < NFP.size() - 1; i++) { if(_almostEqual(referencex, NFP[i].x) && _almostEqual(referencey, NFP[i].y)){ looped = true; } } } if(looped){ // we've made a full loop break; } NFP.emplace_back(referencex, referencey); B.offsetx += translate.x; B.offsety += translate.y; counter++; } if(NFP.size() > 0){ NFPlist.emplace_back(NFP); } if(!searchEdges){ // only get outer NFP or first inner NFP break; } startpoint = searchStartPoint(A, B, inside, NFPlist); } return NFPlist; } }; template const double _alg::TOL = std::pow(10, -9); } } #endif // NFP_SVGNEST_HPP