diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index 0229f9ad18..1f16446ac8 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -1,10 +1,10 @@ /******************************************************************************* * * * Author : Angus Johnson * -* Version : 6.2.9 * -* Date : 16 February 2015 * +* Version : 6.4.2 * +* Date : 27 February 2017 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2015 * +* Copyright : Angus Johnson 2010-2017 * * * * License: * * Use, modification & distribution is subject to Boost Software License Ver 1. * @@ -325,7 +325,7 @@ inline bool SlopesEqual(const cInt dx1, const cInt dy1, const cInt dx2, const cI #endif inline bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) - { return SlopesEqual(e1.Delta.x(), e1.Delta.y(), e2.Delta.x(), e2.Delta.y(), UseFullInt64Range); } + { return SlopesEqual(e1.Top.x() - e1.Bot.x(), e1.Top.y() - e1.Bot.y(), e2.Top.x() - e2.Bot.x(), e2.Top.y() - e2.Bot.y(), UseFullInt64Range); } inline bool SlopesEqual(const IntPoint &pt1, const IntPoint &pt2, const IntPoint &pt3, bool UseFullInt64Range) { return SlopesEqual(pt1.x()-pt2.x(), pt1.y()-pt2.y(), pt2.x()-pt3.x(), pt2.y()-pt3.y(), UseFullInt64Range); } inline bool SlopesEqual(const IntPoint &pt1, const IntPoint &pt2, const IntPoint &pt3, const IntPoint &pt4, bool UseFullInt64Range) @@ -335,7 +335,7 @@ inline bool SlopesEqual(const IntPoint &pt1, const IntPoint &pt2, const IntPoint inline bool IsHorizontal(TEdge &e) { - return e.Delta.y() == 0; + return e.Dx == HORIZONTAL; } //------------------------------------------------------------------------------ @@ -368,7 +368,7 @@ void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) } int64_t y; - if (Edge1.Delta.x() == 0) + if (Edge1.Dx == 0) { ip.x() = Edge1.Bot.x(); y = IsHorizontal(Edge2) ? @@ -376,7 +376,7 @@ void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) Round(ip.x() / Edge2.Dx + Edge2.Bot.y() - (Edge2.Bot.x() / Edge2.Dx)); } - else if (Edge2.Delta.x() == 0) + else if (Edge2.Dx == 0) { ip.x() = Edge2.Bot.x(); y = IsHorizontal(Edge1) ? @@ -448,11 +448,9 @@ void InitEdge2(TEdge& e, PolyType Pt) e.Bot = e.Next->Curr; } - e.Delta.x() = (e.Top.x() - e.Bot.x()); - e.Delta.y() = (e.Top.y() - e.Bot.y()); - - if (e.Delta.y() == 0) e.Dx = HORIZONTAL; - else e.Dx = (double)(e.Delta.x()) / e.Delta.y(); + cInt dy = (e.Top.y() - e.Bot.y()); + if (dy == 0) e.Dx = HORIZONTAL; + else e.Dx = (double)(e.Top.x() - e.Bot.x()) / dy; e.PolyTyp = Pt; } @@ -934,8 +932,6 @@ bool ClipperBase::AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, b locMin.RightBound = E->Prev; leftBoundIsForward = true; //Q.nextInLML = Q.next } - locMin.LeftBound->Side = esLeft; - locMin.RightBound->Side = esRight; if (!Closed) locMin.LeftBound->WindDelta = 0; else if (locMin.LeftBound->Next == locMin.RightBound) @@ -1204,7 +1200,13 @@ void Clipper::SetWindingCount(TEdge &edge) const while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) e = e->PrevInAEL; if (!e) { - edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); + if (edge.WindDelta == 0) + { + PolyFillType pft = (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); + edge.WindCnt = (pft == pftNegative ? -1 : 1); + } + else + edge.WindCnt = edge.WindDelta; edge.WindCnt2 = 0; e = m_ActiveEdges; //ie get ready to calc WindCnt2 } @@ -1421,13 +1423,16 @@ OutPt* Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) prevE = e->PrevInAEL; } - if (prevE && prevE->OutIdx >= 0 && - (TopX(*prevE, Pt.y()) == TopX(*e, Pt.y())) && - SlopesEqual(*e, *prevE, m_UseFullRange) && - (e->WindDelta != 0) && (prevE->WindDelta != 0)) + if (prevE && prevE->OutIdx >= 0 && prevE->Top.y() < Pt.y() && e->Top.y() < Pt.y()) { - OutPt* outPt = AddOutPt(prevE, Pt); - m_Joins.emplace_back(Join(result, outPt, e->Top)); + cInt xPrev = TopX(*prevE, Pt.y()); + cInt xE = TopX(*e, Pt.y()); + if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && + SlopesEqual(IntPoint2d(xPrev, Pt.y()), prevE->Top, IntPoint2d(xE, Pt.y()), e->Top, m_UseFullRange)) + { + OutPt* outPt = AddOutPt(prevE, Pt); + m_Joins.emplace_back(Join(result, outPt, e->Top)); + } } return result; } @@ -1523,7 +1528,12 @@ void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) if (rb) { - if(IsHorizontal(*rb)) AddEdgeToSEL(rb); + if (IsHorizontal(*rb)) + { + AddEdgeToSEL(rb); + if (rb->NextInLML) + m_Scanbeam.push(rb->NextInLML->Top.y()); + } else m_Scanbeam.push(rb->Top.y()); } @@ -1543,7 +1553,7 @@ void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) if (lb->OutIdx >= 0 && lb->PrevInAEL && lb->PrevInAEL->Curr.x() == lb->Bot.x() && lb->PrevInAEL->OutIdx >= 0 && - SlopesEqual(*lb->PrevInAEL, *lb, m_UseFullRange) && + SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, m_UseFullRange) && (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) { OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); @@ -1554,7 +1564,7 @@ void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) { if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && - SlopesEqual(*rb->PrevInAEL, *rb, m_UseFullRange) && + SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, rb->Top, m_UseFullRange) && (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) { OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); @@ -1814,19 +1824,27 @@ void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { - bool IsHole = false; TEdge *e2 = e->PrevInAEL; + TEdge *eTmp = 0; while (e2) { if (e2->OutIdx >= 0 && e2->WindDelta != 0) { - IsHole = !IsHole; - if (! outrec->FirstLeft) - outrec->FirstLeft = &m_PolyOuts[e2->OutIdx]; + if (!eTmp) eTmp = e2; + else if (eTmp->OutIdx == e2->OutIdx) eTmp = 0; } e2 = e2->PrevInAEL; } - if (IsHole) outrec->IsHole = true; + if (!eTmp) + { + outrec->FirstLeft = 0; + outrec->IsHole = false; + } + else + { + outrec->FirstLeft = &m_PolyOuts[eTmp->OutIdx]; + outrec->IsHole = !outrec->FirstLeft->IsHole; + } } //------------------------------------------------------------------------------ @@ -1850,7 +1868,7 @@ OutRec* GetLowermostRec(OutRec *outRec1, OutRec *outRec2) } //------------------------------------------------------------------------------ -bool Param1RightOfParam2(OutRec* outRec1, OutRec* outRec2) +bool OutRec1RightOfOutRec2(OutRec* outRec1, OutRec* outRec2) { do { @@ -1877,9 +1895,9 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) OutRec *outRec2 = &m_PolyOuts[e2->OutIdx]; OutRec *holeStateRec; - if (Param1RightOfParam2(outRec1, outRec2)) + if (OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2; - else if (Param1RightOfParam2(outRec2, outRec1)) + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1; else holeStateRec = GetLowermostRec(outRec1, outRec2); @@ -1892,7 +1910,6 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) OutPt* p2_lft = outRec2->Pts; OutPt* p2_rt = p2_lft->Prev; - EdgeSide Side; //join e2 poly onto e1 poly and delete pointers to e2 ... if( e1->Side == esLeft ) { @@ -1914,7 +1931,6 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) p1_rt->Next = p2_lft; outRec1->Pts = p2_lft; } - Side = esLeft; } else { if( e2->Side == esRight ) @@ -1933,7 +1949,6 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) p1_lft->Prev = p2_rt; p2_rt->Next = p1_lft; } - Side = esRight; } outRec1->BottomPt = 0; @@ -1959,7 +1974,7 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) if( e->OutIdx == ObsoleteIdx ) { e->OutIdx = OKIdx; - e->Side = Side; + e->Side = e1->Side; break; } e = e->NextInAEL; @@ -2059,17 +2074,21 @@ inline bool IsIntermediate(TEdge *e, const cInt Y) inline TEdge *GetMaximaPair(TEdge *e) { - TEdge* result = 0; if ((e->Next->Top == e->Top) && !e->Next->NextInLML) - result = e->Next; + return e->Next; else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) - result = e->Prev; + return e->Prev; + else return 0; +} +//------------------------------------------------------------------------------ - if (result && (result->OutIdx == Skip || - //result is false if both NextInAEL & PrevInAEL are nil & not horizontal ... - (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) - return 0; - return result; +inline TEdge *GetMaximaPairEx(TEdge *e) +{ + //as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's horizontal) + TEdge* result = GetMaximaPair(e); + if (result && (result->OutIdx == Skip || + (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) return 0; + return result; } //------------------------------------------------------------------------------ @@ -2196,7 +2215,7 @@ void Clipper::ProcessHorizontal(TEdge *horzEdge) { Direction dir; cInt horzLeft, horzRight; - bool IsOpen = (horzEdge->OutIdx >= 0 && m_PolyOuts[horzEdge->OutIdx].IsOpen); + bool IsOpen = (horzEdge->WindDelta == 0); GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); @@ -2464,6 +2483,8 @@ void Clipper::BuildIntersectList(const cInt topY) if(e->Curr.x() > eNext->Curr.x()) { IntersectPoint(*e, *eNext, Pt); + + if (Pt.y() < topY) Pt = IntPoint2d(TopX(*e, topY), topY); m_IntersectList.emplace_back(IntersectNode(e, eNext, Pt)); SwapPositionsInSEL(e, eNext); isModified = true; @@ -2513,7 +2534,7 @@ bool Clipper::FixupIntersectionOrder() void Clipper::DoMaxima(TEdge *e) { - TEdge* eMaxPair = GetMaximaPair(e); + TEdge* eMaxPair = GetMaximaPairEx(e); if (!eMaxPair) { if (e->OutIdx >= 0) @@ -2574,7 +2595,7 @@ void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) if(IsMaximaEdge) { - TEdge* eMaxPair = GetMaximaPair(e); + TEdge* eMaxPair = GetMaximaPairEx(e); IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); } @@ -2649,7 +2670,7 @@ void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) if (ePrev && ePrev->Curr.x() == e->Bot.x() && ePrev->Curr.y() == e->Bot.y() && op && ePrev->OutIdx >= 0 && ePrev->Curr.y() > ePrev->Top.y() && - SlopesEqual(*e, *ePrev, m_UseFullRange) && + SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, m_UseFullRange) && (e->WindDelta != 0) && (ePrev->WindDelta != 0)) { OutPt* op2 = AddOutPt(ePrev, e->Bot); @@ -2658,7 +2679,7 @@ void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) else if (eNext && eNext->Curr.x() == e->Bot.x() && eNext->Curr.y() == e->Bot.y() && op && eNext->OutIdx >= 0 && eNext->Curr.y() > eNext->Top.y() && - SlopesEqual(*e, *eNext, m_UseFullRange) && + SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, m_UseFullRange) && (e->WindDelta != 0) && (eNext->WindDelta != 0)) { OutPt* op2 = AddOutPt(eNext, e->Bot); @@ -3172,27 +3193,59 @@ bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) } //---------------------------------------------------------------------- +inline OutRec* ParseFirstLeft(OutRec* FirstLeft) +{ + while (FirstLeft && !FirstLeft->Pts) + FirstLeft = FirstLeft->FirstLeft; + return FirstLeft; +} +//------------------------------------------------------------------------------ + // This is potentially very expensive! O(n^3)! void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) { //tests if NewOutRec contains the polygon before reassigning FirstLeft for (OutRec &outRec : m_PolyOuts) { - if (!outRec.Pts || !outRec.FirstLeft) continue; - OutRec* firstLeft = outRec.FirstLeft; - // Skip empty polygons. - while (firstLeft && !firstLeft->Pts) firstLeft = firstLeft->FirstLeft; - if (firstLeft == OldOutRec && Poly2ContainsPoly1(outRec.Pts, NewOutRec->Pts)) + OutRec* firstLeft = ParseFirstLeft(outRec.FirstLeft); + if (outRec.Pts && firstLeft == OldOutRec && Poly2ContainsPoly1(outRec.Pts, NewOutRec->Pts)) outRec.FirstLeft = NewOutRec; } } //---------------------------------------------------------------------- -void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) -{ +void Clipper::FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec) +{ + //A polygon has split into two such that one is now the inner of the other. + //It's possible that these polygons now wrap around other polygons, so check + //every polygon that's also contained by OuterOutRec's FirstLeft container + //(including 0) to see if they've become inner to the new inner polygon ... + OutRec* orfl = OuterOutRec->FirstLeft; + for (OutRec &outRec : m_PolyOuts) + { + if (!outRec.Pts || &outRec == OuterOutRec || &outRec == InnerOutRec) + continue; + OutRec* firstLeft = ParseFirstLeft(outRec.FirstLeft); + if (firstLeft != orfl && firstLeft != InnerOutRec && firstLeft != OuterOutRec) + continue; + if (Poly2ContainsPoly1(outRec.Pts, InnerOutRec->Pts)) + outRec.FirstLeft = InnerOutRec; + else if (Poly2ContainsPoly1(outRec.Pts, OuterOutRec->Pts)) + outRec.FirstLeft = OuterOutRec; + else if (outRec.FirstLeft == InnerOutRec || outRec.FirstLeft == OuterOutRec) + outRec.FirstLeft = orfl; + } +} +//---------------------------------------------------------------------- +void Clipper::FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec) +{ //reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon for (OutRec &outRec : m_PolyOuts) - if (outRec.FirstLeft == OldOutRec) outRec.FirstLeft = NewOutRec; + { + OutRec* firstLeft = ParseFirstLeft(outRec.FirstLeft); + if (outRec.Pts && firstLeft == OldOutRec) + outRec.FirstLeft = NewOutRec; + } } //---------------------------------------------------------------------- @@ -3210,8 +3263,8 @@ void Clipper::JoinCommonEdges() //before calling JoinPoints() ... OutRec *holeStateRec; if (outRec1 == outRec2) holeStateRec = outRec1; - else if (Param1RightOfParam2(outRec1, outRec2)) holeStateRec = outRec2; - else if (Param1RightOfParam2(outRec2, outRec1)) holeStateRec = outRec1; + else if (OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1; else holeStateRec = GetLowermostRec(outRec1, outRec2); if (!JoinPoints(&join, outRec1, outRec2)) continue; @@ -3228,23 +3281,9 @@ void Clipper::JoinCommonEdges() //update all OutRec2.Pts Idx's ... UpdateOutPtIdxs(*outRec2); - //We now need to check every OutRec.FirstLeft pointer. If it points - //to OutRec1 it may need to point to OutRec2 instead ... - if (m_UsingPolyTree) - for (size_t j = 0; j < m_PolyOuts.size() - 1; j++) - { - OutRec &oRec = m_PolyOuts[j]; - OutRec* firstLeft = oRec.FirstLeft; - while (firstLeft && !firstLeft->Pts) firstLeft = firstLeft->FirstLeft; - if (!oRec.Pts || firstLeft != outRec1 || - oRec.IsHole == outRec1->IsHole) continue; - if (Poly2ContainsPoly1(oRec.Pts, join.OutPt2)) - oRec.FirstLeft = outRec2; - } - if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) { - //outRec2 is contained by outRec1 ... + //outRec1 contains outRec2 ... outRec2->IsHole = !outRec1->IsHole; outRec2->FirstLeft = outRec1; @@ -3256,7 +3295,7 @@ void Clipper::JoinCommonEdges() } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) { - //outRec1 is contained by outRec2 ... + //outRec2 contains outRec1 ... outRec2->IsHole = outRec1->IsHole; outRec1->IsHole = !outRec2->IsHole; outRec2->FirstLeft = outRec1->FirstLeft; @@ -3294,7 +3333,7 @@ void Clipper::JoinCommonEdges() outRec2->FirstLeft = outRec1; // For each m_PolyOuts, replace FirstLeft from outRec2 to outRec1. - if (m_UsingPolyTree) FixupFirstLefts2(outRec2, outRec1); + if (m_UsingPolyTree) FixupFirstLefts3(outRec2, outRec1); } } } @@ -3975,6 +4014,7 @@ void CleanPolygon(Path& poly, double distance) void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance) { + out_polys.resize(in_polys.size()); for (Paths::size_type i = 0; i < in_polys.size(); ++i) CleanPolygon(in_polys[i], out_polys[i], distance); } diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index c88545454e..846410cf8b 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -1,10 +1,10 @@ /******************************************************************************* * * * Author : Angus Johnson * -* Version : 6.2.9 * -* Date : 16 February 2015 * +* Version : 6.4.2 * +* Date : 27 February 2017 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2015 * +* Copyright : Angus Johnson 2010-2017 * * * * License: * * Use, modification & distribution is subject to Boost Software License Ver 1. * @@ -144,7 +144,7 @@ typedef std::vector> PolyNodes; class PolyNode { public: - PolyNode() : Childs(), Parent(0), Index(0), m_IsOpen(false) {} + PolyNode() : Parent(0), Index(0), m_IsOpen(false) {} virtual ~PolyNode(){}; Path Contour; PolyNodes Childs; @@ -234,8 +234,6 @@ enum EdgeSide { esLeft = 1, esRight = 2}; IntPoint Curr; // Top point of this edge (with maximum Y). IntPoint Top; - // Vector from Bot to Top. - IntPoint Delta; // Slope (dx/dy). For horiontal edges, the slope is set to HORIZONTAL (-1.0E+40). double Dx; PolyType PolyTyp; @@ -529,7 +527,8 @@ private: void JoinCommonEdges(); void DoSimplePolygons(); void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec); - void FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec); + void FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec); + void FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec); #ifdef CLIPPERLIB_USE_XYZ void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); #endif diff --git a/src/libslic3r/Arachne/PerimeterOrder.cpp b/src/libslic3r/Arachne/PerimeterOrder.cpp new file mode 100644 index 0000000000..dd2404122c --- /dev/null +++ b/src/libslic3r/Arachne/PerimeterOrder.cpp @@ -0,0 +1,276 @@ +#include +#include "PerimeterOrder.hpp" + +namespace Slic3r::Arachne::PerimeterOrder { + +using namespace Arachne; + +static size_t get_extrusion_lines_count(const Perimeters &perimeters) { + size_t extrusion_lines_count = 0; + for (const Perimeter &perimeter : perimeters) + extrusion_lines_count += perimeter.size(); + + return extrusion_lines_count; +} + +static PerimeterExtrusions get_sorted_perimeter_extrusions_by_area(const Perimeters &perimeters) { + PerimeterExtrusions sorted_perimeter_extrusions; + sorted_perimeter_extrusions.reserve(get_extrusion_lines_count(perimeters)); + + for (const Perimeter &perimeter : perimeters) { + for (const ExtrusionLine &extrusion_line : perimeter) { + if (extrusion_line.empty()) + continue; // This shouldn't ever happen. + + const BoundingBox bbox = get_extents(extrusion_line); + // Be aware that Arachne produces contours with clockwise orientation and holes with counterclockwise orientation. + const double area = std::abs(extrusion_line.area()); + const Polygon polygon = extrusion_line.is_closed ? to_polygon(extrusion_line) : Polygon{}; + + sorted_perimeter_extrusions.emplace_back(extrusion_line, area, polygon, bbox); + } + } + + // Open extrusions have an area equal to zero, so sorting based on the area ensures that open extrusions will always be before closed ones. + std::sort(sorted_perimeter_extrusions.begin(), sorted_perimeter_extrusions.end(), + [](const PerimeterExtrusion &l, const PerimeterExtrusion &r) { return l.area < r.area; }); + + return sorted_perimeter_extrusions; +} + +// Functions fill adjacent_perimeter_extrusions field for every PerimeterExtrusion by pointers to PerimeterExtrusions that contain or are inside this PerimeterExtrusion. +static void construct_perimeter_extrusions_adjacency_graph(PerimeterExtrusions &sorted_perimeter_extrusions) { + // Construct a graph (defined using adjacent_perimeter_extrusions field) where two PerimeterExtrusion are adjacent when one is inside the other. + std::vector root_candidates(sorted_perimeter_extrusions.size(), false); + for (PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) { + const size_t perimeter_extrusion_idx = &perimeter_extrusion - sorted_perimeter_extrusions.data(); + + if (!perimeter_extrusion.is_closed()) { + root_candidates[perimeter_extrusion_idx] = true; + continue; + } + + for (PerimeterExtrusion &root_candidate : sorted_perimeter_extrusions) { + const size_t root_candidate_idx = &root_candidate - sorted_perimeter_extrusions.data(); + + if (!root_candidates[root_candidate_idx]) + continue; + + if (perimeter_extrusion.bbox.contains(root_candidate.bbox) && perimeter_extrusion.polygon.contains(root_candidate.extrusion.junctions.front().p)) { + perimeter_extrusion.adjacent_perimeter_extrusions.emplace_back(&root_candidate); + root_candidate.adjacent_perimeter_extrusions.emplace_back(&perimeter_extrusion); + root_candidates[root_candidate_idx] = false; + } + } + + root_candidates[perimeter_extrusion_idx] = true; + } +} + +// Perform the depth-first search to assign the nearest external perimeter for every PerimeterExtrusion. +// When some PerimeterExtrusion is achievable from more than one external perimeter, then we choose the +// one that comes from a contour. +static void assign_nearest_external_perimeter(PerimeterExtrusions &sorted_perimeter_extrusions) { + std::stack stack; + for (PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) { + if (perimeter_extrusion.is_external_perimeter()) { + perimeter_extrusion.depth = 0; + perimeter_extrusion.nearest_external_perimeter = &perimeter_extrusion; + stack.push(&perimeter_extrusion); + } + } + + while (!stack.empty()) { + PerimeterExtrusion *current_extrusion = stack.top(); + stack.pop(); + + for (PerimeterExtrusion *adjacent_extrusion : current_extrusion->adjacent_perimeter_extrusions) { + const size_t adjacent_extrusion_depth = current_extrusion->depth + 1; + // Update depth when the new depth is smaller or when we can achieve the same depth from a contour. + // This will ensure that the internal perimeter will be extruded before the outer external perimeter + // when there are two external perimeters and one internal. + if (adjacent_extrusion_depth < adjacent_extrusion->depth) { + adjacent_extrusion->nearest_external_perimeter = current_extrusion->nearest_external_perimeter; + adjacent_extrusion->depth = adjacent_extrusion_depth; + stack.push(adjacent_extrusion); + } else if (adjacent_extrusion_depth == adjacent_extrusion->depth && !adjacent_extrusion->nearest_external_perimeter->is_contour() && current_extrusion->is_contour()) { + adjacent_extrusion->nearest_external_perimeter = current_extrusion->nearest_external_perimeter; + stack.push(adjacent_extrusion); + } + } + } +} + +inline Point get_end_position(const ExtrusionLine &extrusion) { + if (extrusion.is_closed) + return extrusion.junctions[0].p; // We ended where we started. + else + return extrusion.junctions.back().p; // Pick the other end from where we started. +} + +// Returns ordered extrusions. +static std::vector ordered_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector extrusions) { + // Ensure that open extrusions will be placed before the closed one. + std::sort(extrusions.begin(), extrusions.end(), + [](const PerimeterExtrusion *l, const PerimeterExtrusion *r) -> bool { return l->is_closed() < r->is_closed(); }); + + std::vector ordered_extrusions; + std::vector already_selected(extrusions.size(), false); + while (ordered_extrusions.size() < extrusions.size()) { + double nearest_distance_sqr = std::numeric_limits::max(); + size_t nearest_extrusion_idx = 0; + bool is_nearest_closed = false; + + for (size_t extrusion_idx = 0; extrusion_idx < extrusions.size(); ++extrusion_idx) { + if (already_selected[extrusion_idx]) + continue; + + const ExtrusionLine &extrusion_line = extrusions[extrusion_idx]->extrusion; + const Point &extrusion_start_position = extrusion_line.junctions.front().p; + const double distance_sqr = (current_position - extrusion_start_position).cast().squaredNorm(); + if (distance_sqr < nearest_distance_sqr) { + if (extrusion_line.is_closed || (!extrusion_line.is_closed && nearest_distance_sqr != std::numeric_limits::max()) || (!extrusion_line.is_closed && !is_nearest_closed)) { + nearest_extrusion_idx = extrusion_idx; + nearest_distance_sqr = distance_sqr; + is_nearest_closed = extrusion_line.is_closed; + } + } + } + + already_selected[nearest_extrusion_idx] = true; + const PerimeterExtrusion *nearest_extrusion = extrusions[nearest_extrusion_idx]; + current_position = get_end_position(nearest_extrusion->extrusion); + ordered_extrusions.emplace_back(nearest_extrusion); + } + + return ordered_extrusions; +} + +struct GroupedPerimeterExtrusions +{ + GroupedPerimeterExtrusions() = delete; + explicit GroupedPerimeterExtrusions(const PerimeterExtrusion *external_perimeter_extrusion) + : external_perimeter_extrusion(external_perimeter_extrusion) {} + + std::vector extrusions; + const PerimeterExtrusion *external_perimeter_extrusion = nullptr; +}; + +// Returns vector of indexes that represent the order of grouped extrusions in grouped_extrusions. +static std::vector order_of_grouped_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector grouped_extrusions) { + // Ensure that holes will be placed before contour and open extrusions before the closed one. + std::sort(grouped_extrusions.begin(), grouped_extrusions.end(), [](const GroupedPerimeterExtrusions &l, const GroupedPerimeterExtrusions &r) -> bool { + return (l.external_perimeter_extrusion->is_contour() < r.external_perimeter_extrusion->is_contour()) || + (l.external_perimeter_extrusion->is_contour() == r.external_perimeter_extrusion->is_contour() && l.external_perimeter_extrusion->is_closed() < r.external_perimeter_extrusion->is_closed()); + }); + + const size_t holes_cnt = std::count_if(grouped_extrusions.begin(), grouped_extrusions.end(), [](const GroupedPerimeterExtrusions &grouped_extrusions) { + return !grouped_extrusions.external_perimeter_extrusion->is_contour(); + }); + + std::vector grouped_extrusions_order; + std::vector already_selected(grouped_extrusions.size(), false); + while (grouped_extrusions_order.size() < grouped_extrusions.size()) { + double nearest_distance_sqr = std::numeric_limits::max(); + size_t nearest_grouped_extrusions_idx = 0; + bool is_nearest_closed = false; + + // First we order all holes and then we start ordering contours. + const size_t grouped_extrusion_end = grouped_extrusions_order.size() < holes_cnt ? holes_cnt: grouped_extrusions.size(); + for (size_t grouped_extrusion_idx = 0; grouped_extrusion_idx < grouped_extrusion_end; ++grouped_extrusion_idx) { + if (already_selected[grouped_extrusion_idx]) + continue; + + const ExtrusionLine &external_perimeter_extrusion_line = grouped_extrusions[grouped_extrusion_idx].external_perimeter_extrusion->extrusion; + const Point &extrusion_start_position = external_perimeter_extrusion_line.junctions.front().p; + const double distance_sqr = (current_position - extrusion_start_position).cast().squaredNorm(); + if (distance_sqr < nearest_distance_sqr) { + if (external_perimeter_extrusion_line.is_closed || (!external_perimeter_extrusion_line.is_closed && nearest_distance_sqr != std::numeric_limits::max()) || (!external_perimeter_extrusion_line.is_closed && !is_nearest_closed)) { + nearest_grouped_extrusions_idx = grouped_extrusion_idx; + nearest_distance_sqr = distance_sqr; + is_nearest_closed = external_perimeter_extrusion_line.is_closed; + } + } + } + + grouped_extrusions_order.emplace_back(nearest_grouped_extrusions_idx); + already_selected[nearest_grouped_extrusions_idx] = true; + const GroupedPerimeterExtrusions &nearest_grouped_extrusions = grouped_extrusions[nearest_grouped_extrusions_idx]; + const ExtrusionLine &last_extrusion_line = nearest_grouped_extrusions.extrusions.back()->extrusion; + current_position = get_end_position(last_extrusion_line); + } + + return grouped_extrusions_order; +} + +static PerimeterExtrusions extract_ordered_perimeter_extrusions(const PerimeterExtrusions &sorted_perimeter_extrusions, const bool external_perimeters_first) { + // Extrusions are ordered inside each group. + std::vector grouped_extrusions; + + std::stack stack; + std::vector visited(sorted_perimeter_extrusions.size(), false); + for (const PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) { + if (!perimeter_extrusion.is_external_perimeter()) + continue; + + stack.push(&perimeter_extrusion); + visited.assign(sorted_perimeter_extrusions.size(), false); + + grouped_extrusions.emplace_back(&perimeter_extrusion); + while (!stack.empty()) { + const PerimeterExtrusion *current_extrusion = stack.top(); + const size_t current_extrusion_idx = current_extrusion - sorted_perimeter_extrusions.data(); + stack.pop(); + + if (visited[current_extrusion_idx]) + continue; + + if (current_extrusion->nearest_external_perimeter == &perimeter_extrusion) + grouped_extrusions.back().extrusions.emplace_back(current_extrusion); + + if (current_extrusion->adjacent_perimeter_extrusions.size() == 1) { + const PerimeterExtrusion *adjacent_extrusion = current_extrusion->adjacent_perimeter_extrusions.front(); + stack.push(adjacent_extrusion); + } else if (current_extrusion->adjacent_perimeter_extrusions.size() > 1) { + // When there is more than one available candidate, then order candidates to minimize distances between + // candidates and also to minimize the distance from the current_position. + std::vector available_candidates; + for (const PerimeterExtrusion *adjacent_extrusion : current_extrusion->adjacent_perimeter_extrusions) { + if (const size_t adjacent_extrusion_idx = adjacent_extrusion - sorted_perimeter_extrusions.data(); !visited[adjacent_extrusion_idx]) + available_candidates.emplace_back(adjacent_extrusion); + } + + std::vector adjacent_extrusions = ordered_perimeter_extrusions_to_minimize_distances(Point::Zero(), available_candidates); + std::reverse(adjacent_extrusions.begin(), adjacent_extrusions.end()); + for (const PerimeterExtrusion *adjacent_extrusion : adjacent_extrusions) + stack.push(adjacent_extrusion); + } + + visited[current_extrusion_idx] = true; + } + + if (!external_perimeters_first) + std::reverse(grouped_extrusions.back().extrusions.begin(), grouped_extrusions.back().extrusions.end()); + } + + const std::vector grouped_extrusion_order = order_of_grouped_perimeter_extrusions_to_minimize_distances(Point::Zero(), grouped_extrusions); + + PerimeterExtrusions ordered_extrusions; + for (size_t order_idx : grouped_extrusion_order) { + for (const PerimeterExtrusion *perimeter_extrusion : grouped_extrusions[order_idx].extrusions) + ordered_extrusions.emplace_back(*perimeter_extrusion); + } + + return ordered_extrusions; +} + +// FIXME: From the point of better patch planning, it should be better to do ordering when we have generated all extrusions (for now, when G-Code is exported). +// FIXME: It would be better to extract the adjacency graph of extrusions from the SkeletalTrapezoidation graph. +PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, const bool external_perimeters_first) { + PerimeterExtrusions sorted_perimeter_extrusions = get_sorted_perimeter_extrusions_by_area(perimeters); + construct_perimeter_extrusions_adjacency_graph(sorted_perimeter_extrusions); + assign_nearest_external_perimeter(sorted_perimeter_extrusions); + return extract_ordered_perimeter_extrusions(sorted_perimeter_extrusions, external_perimeters_first); +} + +} // namespace Slic3r::Arachne::PerimeterOrder diff --git a/src/libslic3r/Arachne/PerimeterOrder.hpp b/src/libslic3r/Arachne/PerimeterOrder.hpp new file mode 100644 index 0000000000..20fc3983d3 --- /dev/null +++ b/src/libslic3r/Arachne/PerimeterOrder.hpp @@ -0,0 +1,47 @@ +#ifndef slic3r_GCode_PerimeterOrder_hpp_ +#define slic3r_GCode_PerimeterOrder_hpp_ + +#include + +namespace Slic3r::Arachne::PerimeterOrder { + +// Data structure stores ExtrusionLine (closed and open) together with additional data. +struct PerimeterExtrusion +{ + explicit PerimeterExtrusion(const Arachne::ExtrusionLine &extrusion, const double area, const Polygon &polygon, const BoundingBox &bbox) + : extrusion(extrusion), area(area), polygon(polygon), bbox(bbox) {} + + Arachne::ExtrusionLine extrusion; + // Absolute value of the area of the polygon. The value is always non-negative, even for holes. + double area = 0; + + // Polygon is non-empty only for closed extrusions. + Polygon polygon; + BoundingBox bbox; + + std::vector adjacent_perimeter_extrusions; + + // How far is this perimeter from the nearest external perimeter. Contour is always preferred over holes. + size_t depth = std::numeric_limits::max(); + PerimeterExtrusion *nearest_external_perimeter = nullptr; + + // Should this extrusion be fuzzyfied during path generation? + bool fuzzify = false; + + // Returns if ExtrusionLine is a contour or a hole. + bool is_contour() const { return extrusion.is_contour(); } + + // Returns if ExtrusionLine is closed or opened. + bool is_closed() const { return extrusion.is_closed; } + + // Returns if ExtrusionLine is an external or an internal perimeter. + bool is_external_perimeter() const { return extrusion.is_external_perimeter(); } +}; + +using PerimeterExtrusions = std::vector; + +PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, bool external_perimeters_first); + +} // namespace Slic3r::Arachne::PerimeterOrder + +#endif // slic3r_GCode_Travels_hpp_ diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 82f86e7a78..c80e70e61c 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -152,8 +152,7 @@ void SkeletalTrapezoidation::transferEdge(Point from, Point to, const VD::edge_t assert(twin->prev->twin); // Back rib assert(twin->prev->twin->prev); // Prev segment along parabola - constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped - graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); + graph.makeRib(prev_edge, start_source_point, end_source_point); } assert(prev_edge); } @@ -203,10 +202,8 @@ void SkeletalTrapezoidation::transferEdge(Point from, Point to, const VD::edge_t p0 = p1; v0 = v1; - if (p1_idx < discretized.size() - 1) - { // Rib for last segment gets introduced outside this function! - constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped - graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); + if (p1_idx < discretized.size() - 1) { // Rib for last segment gets introduced outside this function! + graph.makeRib(prev_edge, start_source_point, end_source_point); } } assert(prev_edge); @@ -462,8 +459,7 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) node_t *starting_node = vd_node_to_he_node[starting_voronoi_edge->vertex0()]; starting_node->data.distance_to_boundary = 0; - constexpr bool is_next_to_start_or_end = true; - graph.makeRib(prev_edge, start_source_point, end_source_point, is_next_to_start_or_end); + graph.makeRib(prev_edge, start_source_point, end_source_point); for (const VD::edge_type* vd_edge = starting_voronoi_edge->next(); vd_edge != ending_voronoi_edge; vd_edge = vd_edge->next()) { assert(vd_edge->is_finite()); assert(Geometry::VoronoiUtils::is_in_range(*vd_edge)); @@ -471,7 +467,7 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) Point v1 = Geometry::VoronoiUtils::to_point(vd_edge->vertex0()).cast(); Point v2 = Geometry::VoronoiUtils::to_point(vd_edge->vertex1()).cast(); transferEdge(v1, v2, *vd_edge, prev_edge, start_source_point, end_source_point, segments); - graph.makeRib(prev_edge, start_source_point, end_source_point, vd_edge->next() == ending_voronoi_edge); + graph.makeRib(prev_edge, start_source_point, end_source_point); } transferEdge(Geometry::VoronoiUtils::to_point(ending_voronoi_edge->vertex0()).cast(), end_source_point, *ending_voronoi_edge, prev_edge, start_source_point, end_source_point, segments); diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index 20f7b59186..b4c481263b 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -572,8 +572,6 @@ protected: * Genrate small segments for local maxima where the beading would only result in a single bead */ void generateLocalMaximaSingleBeads(); - - friend bool detect_voronoi_edge_intersecting_input_segment(const VD &voronoi_diagram, const std::vector &segments); }; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp index 4629396e86..0f97b23db9 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp @@ -314,7 +314,7 @@ void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) } } -void SkeletalTrapezoidationGraph::makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point, bool is_next_to_start_or_end) +void SkeletalTrapezoidationGraph::makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point) { Point p; Line(start_source_point, end_source_point).distance_to_infinite_squared(prev_edge->to->p, &p); diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp index efda323837..41c681f3e7 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp @@ -88,7 +88,7 @@ public: */ void collapseSmallEdges(coord_t snap_dist = 5); - void makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point, bool is_next_to_start_or_end); + void makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point); /*! * Insert a node into the graph and connect it to the input polygon using ribs diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index 6c5dafdac4..472caf775d 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -6,7 +6,6 @@ #include "WallToolPaths.hpp" #include "SkeletalTrapezoidation.hpp" -#include "../ClipperUtils.hpp" #include "utils/linearAlg2D.hpp" #include "EdgeGrid.hpp" #include "utils/SparseLineGrid.hpp" @@ -14,6 +13,7 @@ #include "utils/PolylineStitcher.hpp" #include "SVG.hpp" #include "Utils.hpp" +#include "ClipperUtils.hpp" #include @@ -758,99 +758,4 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector &toolpa return toolpaths.empty(); } -/*! - * Get the order constraints of the insets when printing walls per region / hole. - * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right. - * - * Odd walls should always go after their enclosing wall polygons. - * - * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. - */ -WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector &input, const bool outer_to_inner) -{ - ExtrusionLineSet order_requirements; - - // We build a grid where we map toolpath vertex locations to toolpaths, - // so that we can easily find which two toolpaths are next to each other, - // which is the requirement for there to be an order constraint. - // - // We use a PointGrid rather than a LineGrid to save on computation time. - // In very rare cases two insets might lie next to each other without having neighboring vertices, e.g. - // \ . - // | / . - // | / . - // || . - // | \ . - // | \ . - // / . - // However, because of how Arachne works this will likely never be the case for two consecutive insets. - // On the other hand one could imagine that two consecutive insets of a very large circle - // could be simplify()ed such that the remaining vertices of the two insets don't align. - // In those cases the order requirement is not captured, - // which means that the PathOrderOptimizer *might* result in a violation of the user set path order. - // This problem is expected to be not so severe and happen very sparsely. - - coord_t max_line_w = 0u; - for (const ExtrusionLine *line : input) // compute max_line_w - for (const ExtrusionJunction &junction : *line) - max_line_w = std::max(max_line_w, junction.w); - if (max_line_w == 0u) - return order_requirements; - - struct LineLoc - { - ExtrusionJunction j; - const ExtrusionLine *line; - }; - struct Locator - { - Point operator()(const LineLoc &elem) { return elem.j.p; } - }; - - // How much farther two verts may be apart due to corners. - // This distance must be smaller than 2, because otherwise - // we could create an order requirement between e.g. - // wall 2 of one region and wall 3 of another region, - // while another wall 3 of the first region would lie in between those two walls. - // However, higher values are better against the limitations of using a PointGrid rather than a LineGrid. - constexpr float diagonal_extension = 1.9f; - const auto searching_radius = coord_t(max_line_w * diagonal_extension); - using GridT = SparsePointGrid; - GridT grid(searching_radius); - - for (const ExtrusionLine *line : input) - for (const ExtrusionJunction &junction : *line) grid.insert(LineLoc{junction, line}); - for (const std::pair &pair : grid) { - const LineLoc &lineloc_here = pair.second; - const ExtrusionLine *here = lineloc_here.line; - Point loc_here = pair.second.j.p; - std::vector nearby_verts = grid.getNearby(loc_here, searching_radius); - for (const LineLoc &lineloc_nearby : nearby_verts) { - const ExtrusionLine *nearby = lineloc_nearby.line; - if (nearby == here) - continue; - if (nearby->inset_idx == here->inset_idx) - continue; - if (nearby->inset_idx > here->inset_idx + 1) - continue; // not directly adjacent - if (here->inset_idx > nearby->inset_idx + 1) - continue; // not directly adjacent - if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) - continue; // points are too far away from each other - if (here->is_odd || nearby->is_odd) { - if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx) - order_requirements.emplace(std::make_pair(nearby, here)); - if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx) - order_requirements.emplace(std::make_pair(here, nearby)); - } else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) { - order_requirements.emplace(std::make_pair(nearby, here)); - } else { - assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); - order_requirements.emplace(std::make_pair(here, nearby)); - } - } - } - return order_requirements; -} - } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/WallToolPaths.hpp b/src/libslic3r/Arachne/WallToolPaths.hpp index 44f3affb69..bdc8593cab 100644 --- a/src/libslic3r/Arachne/WallToolPaths.hpp +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -75,15 +75,6 @@ public: static bool removeEmptyToolPaths(std::vector &toolpaths); using ExtrusionLineSet = ankerl::unordered_dense::set, boost::hash>>; - /*! - * Get the order constraints of the insets when printing walls per region / hole. - * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right. - * - * Odd walls should always go after their enclosing wall polygons. - * - * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. - */ - static ExtrusionLineSet getRegionOrder(const std::vector &input, bool outer_to_inner); protected: /*! diff --git a/src/libslic3r/Arachne/utils/ExtrusionJunction.cpp b/src/libslic3r/Arachne/utils/ExtrusionJunction.cpp deleted file mode 100644 index 3cdfa0d8d2..0000000000 --- a/src/libslic3r/Arachne/utils/ExtrusionJunction.cpp +++ /dev/null @@ -1,18 +0,0 @@ -//Copyright (c) 2020 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. - -#include "ExtrusionJunction.hpp" - -namespace Slic3r::Arachne -{ - -bool ExtrusionJunction::operator ==(const ExtrusionJunction& other) const -{ - return p == other.p - && w == other.w - && perimeter_index == other.perimeter_index; -} - -ExtrusionJunction::ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index) : p(p), w(w), perimeter_index(perimeter_index) {} - -} diff --git a/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp b/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp index 146525185f..49f721b5cc 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp @@ -37,9 +37,11 @@ struct ExtrusionJunction */ size_t perimeter_index; - ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index); + ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index) : p(p), w(w), perimeter_index(perimeter_index) {} - bool operator==(const ExtrusionJunction& other) const; + bool operator==(const ExtrusionJunction &other) const { + return p == other.p && w == other.w && perimeter_index == other.perimeter_index; + } }; inline Point operator-(const ExtrusionJunction& a, const ExtrusionJunction& b) diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp index 8631d9e17b..7626b2b4b3 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp @@ -29,15 +29,6 @@ int64_t ExtrusionLine::getLength() const return len; } -coord_t ExtrusionLine::getMinimalWidth() const -{ - return std::min_element(junctions.cbegin(), junctions.cend(), - [](const ExtrusionJunction& l, const ExtrusionJunction& r) - { - return l.w < r.w; - })->w; -} - void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const int64_t allowed_error_distance_squared, const int64_t maximum_extrusion_area_deviation) { const size_t min_path_size = is_closed ? 3 : 2; @@ -236,9 +227,10 @@ bool ExtrusionLine::is_contour() const return poly.is_clockwise(); } -double ExtrusionLine::area() const -{ - assert(this->is_closed); +double ExtrusionLine::area() const { + if (!this->is_closed) + return 0.; + double a = 0.; if (this->junctions.size() >= 3) { Vec2d p1 = this->junctions.back().p.cast(); @@ -248,9 +240,25 @@ double ExtrusionLine::area() const p1 = p2; } } + return 0.5 * a; } +Points to_points(const ExtrusionLine &extrusion_line) { + Points points; + points.reserve(extrusion_line.junctions.size()); + for (const ExtrusionJunction &junction : extrusion_line.junctions) + points.emplace_back(junction.p); + return points; +} + +BoundingBox get_extents(const ExtrusionLine &extrusion_line) { + BoundingBox bbox; + for (const ExtrusionJunction &junction : extrusion_line.junctions) + bbox.merge(junction.p); + return bbox; +} + } // namespace Slic3r::Arachne namespace Slic3r { diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp index d39e1e07b9..f153768762 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp @@ -136,11 +136,6 @@ struct ExtrusionLine return ret; } - /*! - * Get the minimal width of this path - */ - coord_t getMinimalWidth() const; - /*! * Removes vertices of the ExtrusionLines to make sure that they are not too high * resolution. @@ -192,6 +187,8 @@ struct ExtrusionLine bool is_contour() const; double area() const; + + bool is_external_perimeter() const { return this->inset_idx == 0; } }; static inline Slic3r::ThickPolyline to_thick_polyline(const Arachne::ExtrusionLine &line_junctions) @@ -237,6 +234,7 @@ static inline Slic3r::ThickPolyline to_thick_polyline(const ClipperLib_Z::Path & static inline Polygon to_polygon(const ExtrusionLine &line) { Polygon out; + assert(line.is_closed); assert(line.junctions.size() >= 3); assert(line.junctions.front().p == line.junctions.back().p); out.points.reserve(line.junctions.size() - 1); @@ -245,15 +243,11 @@ static inline Polygon to_polygon(const ExtrusionLine &line) return out; } -#if 0 -static BoundingBox get_extents(const ExtrusionLine &extrusion_line) -{ - BoundingBox bbox; - for (const ExtrusionJunction &junction : extrusion_line.junctions) - bbox.merge(junction.p); - return bbox; -} +Points to_points(const ExtrusionLine &extrusion_line); +BoundingBox get_extents(const ExtrusionLine &extrusion_line); + +#if 0 static BoundingBox get_extents(const std::vector &extrusion_lines) { BoundingBox bbox; @@ -272,15 +266,6 @@ static BoundingBox get_extents(const std::vector &extrusi return bbox; } -static Points to_points(const ExtrusionLine &extrusion_line) -{ - Points points; - points.reserve(extrusion_line.junctions.size()); - for (const ExtrusionJunction &junction : extrusion_line.junctions) - points.emplace_back(junction.p); - return points; -} - static std::vector to_points(const std::vector &extrusion_lines) { std::vector points; @@ -293,6 +278,8 @@ static std::vector to_points(const std::vector &e #endif using VariableWidthLines = std::vector; //; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/utils/SparsePointGrid.hpp b/src/libslic3r/Arachne/utils/SparsePointGrid.hpp index 7bb51d7038..127380b3eb 100644 --- a/src/libslic3r/Arachne/utils/SparsePointGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparsePointGrid.hpp @@ -39,16 +39,6 @@ public: */ void insert(const Elem &elem); - /*! - * Get just any element that's within a certain radius of a point. - * - * Rather than giving a vector of nearby elements, this function just gives - * a single element, any element, in no particular order. - * \param query_pt The point to query for an object nearby. - * \param radius The radius of what is considered "nearby". - */ - const ElemT *getAnyNearby(const Point &query_pt, coord_t radius); - protected: using GridPoint = typename SparseGrid::GridPoint; @@ -68,22 +58,6 @@ void SparsePointGrid::insert(const Elem &elem) SparseGrid::m_grid.emplace(grid_loc, elem); } -template -const ElemT *SparsePointGrid::getAnyNearby(const Point &query_pt, coord_t radius) -{ - const ElemT *ret = nullptr; - const std::function &process_func = [&ret, query_pt, radius, this](const ElemT &maybe_nearby) { - if (shorter_then(m_locator(maybe_nearby) - query_pt, radius)) { - ret = &maybe_nearby; - return false; - } - return true; - }; - SparseGrid::processNearby(query_pt, radius, process_func); - - return ret; -} - } // namespace Slic3r::Arachne #endif // UTILS_SPARSE_POINT_GRID_H diff --git a/src/libslic3r/Arachne/utils/SquareGrid.cpp b/src/libslic3r/Arachne/utils/SquareGrid.cpp index ae89965795..856eb5968b 100644 --- a/src/libslic3r/Arachne/utils/SquareGrid.cpp +++ b/src/libslic3r/Arachne/utils/SquareGrid.cpp @@ -2,7 +2,6 @@ //CuraEngine is released under the terms of the AGPLv3 or higher. #include "SquareGrid.hpp" -#include "../../Point.hpp" using namespace Slic3r::Arachne; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 09eca00720..1ec0e1c8a0 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -177,6 +177,24 @@ set(SLIC3R_SOURCES GCode/SpiralVase.hpp GCode/SeamPlacer.cpp GCode/SeamPlacer.hpp + GCode/SeamChoice.cpp + GCode/SeamChoice.hpp + GCode/SeamPerimeters.cpp + GCode/SeamPerimeters.hpp + GCode/SeamShells.cpp + GCode/SeamShells.hpp + GCode/SeamGeometry.cpp + GCode/SeamGeometry.hpp + GCode/SeamAligned.cpp + GCode/SeamAligned.hpp + GCode/SeamRear.cpp + GCode/SeamRear.hpp + GCode/SeamRandom.cpp + GCode/SeamRandom.hpp + GCode/SeamPainting.cpp + GCode/SeamPainting.hpp + GCode/ModelVisibility.cpp + GCode/ModelVisibility.hpp GCode/SmoothPath.cpp GCode/SmoothPath.hpp GCode/ToolOrdering.cpp @@ -482,7 +500,6 @@ set(SLIC3R_SOURCES Arachne/BeadingStrategy/WideningBeadingStrategy.hpp Arachne/BeadingStrategy/WideningBeadingStrategy.cpp Arachne/utils/ExtrusionJunction.hpp - Arachne/utils/ExtrusionJunction.cpp Arachne/utils/ExtrusionLine.hpp Arachne/utils/ExtrusionLine.cpp Arachne/utils/HalfEdge.hpp @@ -500,6 +517,8 @@ set(SLIC3R_SOURCES Geometry/Voronoi.cpp Geometry/VoronoiUtils.hpp Geometry/VoronoiUtils.cpp + Arachne/PerimeterOrder.hpp + Arachne/PerimeterOrder.cpp Arachne/SkeletalTrapezoidation.hpp Arachne/SkeletalTrapezoidation.cpp Arachne/SkeletalTrapezoidationEdge.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 365689a777..74ab5b4b44 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1234,7 +1234,9 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail // Collect custom seam data from all objects. std::function throw_if_canceled_func = [&print]() { print.throw_if_canceled();}; - m_seam_placer.init(print, throw_if_canceled_func); + + const Seams::Params params{Seams::Placer::get_params(print.full_print_config())}; + m_seam_placer.init(print.objects(), params, throw_if_canceled_func); if (! (has_wipe_tower && print.config().single_extruder_multi_material_priming)) { // Set initial extruder only after custom start G-code. @@ -2980,7 +2982,7 @@ std::string GCodeGenerator::extrude_loop(const ExtrusionLoop &loop_src, const GC Point seam_point = this->last_position.has_value() ? *this->last_position : Point::Zero(); if (!m_config.spiral_vase && comment_is_perimeter(description)) { assert(m_layer != nullptr); - seam_point = m_seam_placer.place_seam(m_layer, loop_src, m_config.external_perimeters_first, seam_point); + seam_point = m_seam_placer.place_seam(m_layer, loop_src, seam_point); } // Because the G-code export has 1um resolution, don't generate segments shorter than 1.5 microns, // thus empty path segments will not be produced by G-code export. diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index e25a2421e6..ed8a883995 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -353,8 +353,7 @@ private: std::string set_extruder(unsigned int extruder_id, double print_z); bool line_distancer_is_required(const std::vector& extruder_ids); - // Cache for custom seam enforcers/blockers for each layer. - SeamPlacer m_seam_placer; + Seams::Placer m_seam_placer; /* Origin of print coordinates expressed in unscaled G-code coordinates. This affects the input arguments supplied to the extrude*() and travel_to() diff --git a/src/libslic3r/GCode/ModelVisibility.cpp b/src/libslic3r/GCode/ModelVisibility.cpp new file mode 100644 index 0000000000..63f3beffc3 --- /dev/null +++ b/src/libslic3r/GCode/ModelVisibility.cpp @@ -0,0 +1,301 @@ +#include + +#include "libslic3r/ShortEdgeCollapse.hpp" +#include "libslic3r/GCode/ModelVisibility.hpp" +#include "libslic3r/AABBTreeIndirect.hpp" + +namespace Slic3r::ModelInfo { +namespace Impl { + +CoordinateFunctor::CoordinateFunctor(const std::vector *coords) : coordinates(coords) {} +CoordinateFunctor::CoordinateFunctor() : coordinates(nullptr) {} + +const float &CoordinateFunctor::operator()(size_t idx, size_t dim) const { + return coordinates->operator[](idx)[dim]; +} + + +template int sgn(T val) { + return int(T(0) < val) - int(val < T(0)); +} + +/// Coordinate frame +class Frame { +public: + Frame() { + mX = Vec3f(1, 0, 0); + mY = Vec3f(0, 1, 0); + mZ = Vec3f(0, 0, 1); + } + + Frame(const Vec3f &x, const Vec3f &y, const Vec3f &z) : + mX(x), mY(y), mZ(z) { + } + + void set_from_z(const Vec3f &z) { + mZ = z.normalized(); + Vec3f tmpZ = mZ; + Vec3f tmpX = (std::abs(tmpZ.x()) > 0.99f) ? Vec3f(0, 1, 0) : Vec3f(1, 0, 0); + mY = (tmpZ.cross(tmpX)).normalized(); + mX = mY.cross(tmpZ); + } + + Vec3f to_world(const Vec3f &a) const { + return a.x() * mX + a.y() * mY + a.z() * mZ; + } + + Vec3f to_local(const Vec3f &a) const { + return Vec3f(mX.dot(a), mY.dot(a), mZ.dot(a)); + } + + const Vec3f& binormal() const { + return mX; + } + + const Vec3f& tangent() const { + return mY; + } + + const Vec3f& normal() const { + return mZ; + } + +private: + Vec3f mX, mY, mZ; +}; + +Vec3f sample_sphere_uniform(const Vec2f &samples) { + float term1 = 2.0f * float(PI) * samples.x(); + float term2 = 2.0f * sqrt(samples.y() - samples.y() * samples.y()); + return {cos(term1) * term2, sin(term1) * term2, + 1.0f - 2.0f * samples.y()}; +} + +Vec3f sample_hemisphere_uniform(const Vec2f &samples) { + float term1 = 2.0f * float(PI) * samples.x(); + float term2 = 2.0f * sqrt(samples.y() - samples.y() * samples.y()); + return {cos(term1) * term2, sin(term1) * term2, + abs(1.0f - 2.0f * samples.y())}; +} + +Vec3f sample_power_cosine_hemisphere(const Vec2f &samples, float power) { + float term1 = 2.f * float(PI) * samples.x(); + float term2 = pow(samples.y(), 1.f / (power + 1.f)); + float term3 = sqrt(1.f - term2 * term2); + + return Vec3f(cos(term1) * term3, sin(term1) * term3, term2); +} + +std::vector raycast_visibility( + const AABBTreeIndirect::Tree<3, float> &raycasting_tree, + const indexed_triangle_set &triangles, + const TriangleSetSamples &samples, + size_t negative_volumes_start_index, + const Visibility::Params ¶ms +) { + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: raycast visibility of " << samples.positions.size() << " samples over " << triangles.indices.size() + << " triangles: end"; + + //prepare uniform samples of a hemisphere + float step_size = 1.0f / params.sqr_rays_per_sample_point; + std::vector precomputed_sample_directions( + params.sqr_rays_per_sample_point * params.sqr_rays_per_sample_point); + for (size_t x_idx = 0; x_idx < params.sqr_rays_per_sample_point; ++x_idx) { + float sample_x = x_idx * step_size + step_size / 2.0; + for (size_t y_idx = 0; y_idx < params.sqr_rays_per_sample_point; ++y_idx) { + size_t dir_index = x_idx * params.sqr_rays_per_sample_point + y_idx; + float sample_y = y_idx * step_size + step_size / 2.0; + precomputed_sample_directions[dir_index] = sample_hemisphere_uniform( { sample_x, sample_y }); + } + } + + bool model_contains_negative_parts = negative_volumes_start_index < triangles.indices.size(); + + std::vector result(samples.positions.size()); + tbb::parallel_for(tbb::blocked_range(0, result.size()), + [&triangles, &precomputed_sample_directions, model_contains_negative_parts, negative_volumes_start_index, + &raycasting_tree, &result, &samples, ¶ms](tbb::blocked_range r) { + // Maintaining hits memory outside of the loop, so it does not have to be reallocated for each query. + std::vector hits; + for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) { + result[s_idx] = 1.0f; + const float decrease_step = 1.0f + / (params.sqr_rays_per_sample_point * params.sqr_rays_per_sample_point); + + const Vec3f ¢er = samples.positions[s_idx]; + const Vec3f &normal = samples.normals[s_idx]; + // apply the local direction via Frame struct - the local_dir is with respect to +Z being forward + Frame f; + f.set_from_z(normal); + + for (const auto &dir : precomputed_sample_directions) { + Vec3f final_ray_dir = (f.to_world(dir)); + if (!model_contains_negative_parts) { + igl::Hit hitpoint; + // FIXME: This AABBTTreeIndirect query will not compile for float ray origin and + // direction. + Vec3d final_ray_dir_d = final_ray_dir.cast(); + Vec3d ray_origin_d = (center + normal * 0.01f).cast(); // start above surface. + bool hit = AABBTreeIndirect::intersect_ray_first_hit(triangles.vertices, + triangles.indices, raycasting_tree, ray_origin_d, final_ray_dir_d, hitpoint); + if (hit && its_face_normal(triangles, hitpoint.id).dot(final_ray_dir) <= 0) { + result[s_idx] -= decrease_step; + } + } else { //TODO improve logic for order based boolean operations - consider order of volumes + bool casting_from_negative_volume = samples.triangle_indices[s_idx] + >= negative_volumes_start_index; + + Vec3d ray_origin_d = (center + normal * 0.01f).cast(); // start above surface. + if (casting_from_negative_volume) { // if casting from negative volume face, invert direction, change start pos + final_ray_dir = -1.0 * final_ray_dir; + ray_origin_d = (center - normal * 0.01f).cast(); + } + Vec3d final_ray_dir_d = final_ray_dir.cast(); + bool some_hit = AABBTreeIndirect::intersect_ray_all_hits(triangles.vertices, + triangles.indices, raycasting_tree, + ray_origin_d, final_ray_dir_d, hits); + if (some_hit) { + int counter = 0; + // NOTE: iterating in reverse, from the last hit for one simple reason: We know the state of the ray at that point; + // It cannot be inside model, and it cannot be inside negative volume + for (int hit_index = int(hits.size()) - 1; hit_index >= 0; --hit_index) { + Vec3f face_normal = its_face_normal(triangles, hits[hit_index].id); + if (hits[hit_index].id >= int(negative_volumes_start_index)) { //negative volume hit + counter -= sgn(face_normal.dot(final_ray_dir)); // if volume face aligns with ray dir, we are leaving negative space + // which in reverse hit analysis means, that we are entering negative space :) and vice versa + } else { + counter += sgn(face_normal.dot(final_ray_dir)); + } + } + if (counter == 0) { + result[s_idx] -= decrease_step; + } + } + } + } + } + }); + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: raycast visibility of " << samples.positions.size() << " samples over " << triangles.indices.size() + << " triangles: end"; + + return result; +} +} + +Visibility::Visibility( + const Transform3d &obj_transform, + const ModelVolumePtrs &volumes, + const Params ¶ms, + const std::function &throw_if_canceled +) { + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: gather occlusion meshes: start"; + indexed_triangle_set triangle_set; + indexed_triangle_set negative_volumes_set; + //add all parts + for (const ModelVolume *model_volume : volumes) { + if (model_volume->type() == ModelVolumeType::MODEL_PART + || model_volume->type() == ModelVolumeType::NEGATIVE_VOLUME) { + auto model_transformation = model_volume->get_matrix(); + indexed_triangle_set model_its = model_volume->mesh().its; + its_transform(model_its, model_transformation); + if (model_volume->type() == ModelVolumeType::MODEL_PART) { + its_merge(triangle_set, model_its); + } else { + its_merge(negative_volumes_set, model_its); + } + } + } + throw_if_canceled(); + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: gather occlusion meshes: end"; + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: decimate: start"; + its_short_edge_collpase(triangle_set, params.fast_decimation_triangle_count_target); + its_short_edge_collpase(negative_volumes_set, params.fast_decimation_triangle_count_target); + + size_t negative_volumes_start_index = triangle_set.indices.size(); + its_merge(triangle_set, negative_volumes_set); + its_transform(triangle_set, obj_transform); + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: decimate: end"; + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: Compute visibility sample points: start"; + + this->mesh_samples = sample_its_uniform_parallel(params.raycasting_visibility_samples_count, + triangle_set); + this->mesh_samples_coordinate_functor = Impl::CoordinateFunctor(&this->mesh_samples.positions); + this->mesh_samples_tree = KDTreeIndirect<3, float, Impl::CoordinateFunctor>(this->mesh_samples_coordinate_functor, + this->mesh_samples.positions.size()); + + // The following code determines search area for random visibility samples on the mesh when calculating visibility of each perimeter point + // number of random samples in the given radius (area) is approximately poisson distribution + // to compute ideal search radius (area), we use exponential distribution (complementary distr to poisson) + // parameters of exponential distribution to compute area that will have with probability="probability" more than given number of samples="samples" + float probability = 0.9f; + float samples = 4; + float density = params.raycasting_visibility_samples_count / this->mesh_samples.total_area; + // exponential probability distrubtion function is : f(x) = P(X > x) = e^(l*x) where l is the rate parameter (computed as 1/u where u is mean value) + // probability that sampled area A with S samples contains more than samples count: + // P(S > samples in A) = e^-(samples/(density*A)); express A: + float search_area = samples / (-logf(probability) * density); + float search_radius = sqrt(search_area / PI); + this->mesh_samples_radius = search_radius; + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: Compute visiblity sample points: end"; + throw_if_canceled(); + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: Mesh sample raidus: " << this->mesh_samples_radius; + + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: build AABB tree: start"; + auto raycasting_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(triangle_set.vertices, + triangle_set.indices); + + throw_if_canceled(); + BOOST_LOG_TRIVIAL(debug) + << "SeamPlacer: build AABB tree: end"; + this->mesh_samples_visibility = Impl::raycast_visibility(raycasting_tree, triangle_set, this->mesh_samples, + negative_volumes_start_index, params); + throw_if_canceled(); +} + +float Visibility::calculate_point_visibility(const Vec3f &position) const { + std::vector points = find_nearby_points(mesh_samples_tree, position, mesh_samples_radius); + if (points.empty()) { + return 1.0f; + } + + auto compute_dist_to_plane = [](const Vec3f &position, const Vec3f &plane_origin, + const Vec3f &plane_normal) { + Vec3f orig_to_point = position - plane_origin; + return std::abs(orig_to_point.dot(plane_normal)); + }; + + float total_weight = 0; + float total_visibility = 0; + for (size_t i = 0; i < points.size(); ++i) { + size_t sample_idx = points[i]; + + Vec3f sample_point = this->mesh_samples.positions[sample_idx]; + Vec3f sample_normal = this->mesh_samples.normals[sample_idx]; + + float weight = mesh_samples_radius - + compute_dist_to_plane(position, sample_point, sample_normal); + weight += (mesh_samples_radius - (position - sample_point).norm()); + total_visibility += weight * mesh_samples_visibility[sample_idx]; + total_weight += weight; + } + + return total_visibility / total_weight; +} + +} diff --git a/src/libslic3r/GCode/ModelVisibility.hpp b/src/libslic3r/GCode/ModelVisibility.hpp new file mode 100644 index 0000000000..a6ce29fba2 --- /dev/null +++ b/src/libslic3r/GCode/ModelVisibility.hpp @@ -0,0 +1,51 @@ +#ifndef libslic3r_ModelVisibility_hpp_ +#define libslic3r_ModelVisibility_hpp_ + +#include "libslic3r/KDTreeIndirect.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleSetSampling.hpp" + +namespace Slic3r::ModelInfo { +namespace Impl { + +struct CoordinateFunctor +{ + const std::vector *coordinates; + CoordinateFunctor(const std::vector *coords); + CoordinateFunctor(); + + const float &operator()(size_t idx, size_t dim) const; +}; +} // namespace Impl + +struct Visibility +{ + struct Params + { + // Number of samples generated on the mesh. There are + // sqr_rays_per_sample_point*sqr_rays_per_sample_point rays casted from each samples + size_t raycasting_visibility_samples_count{}; + size_t fast_decimation_triangle_count_target{}; + // square of number of rays per sample point + size_t sqr_rays_per_sample_point{}; + }; + + Visibility( + const Transform3d &obj_transform, + const ModelVolumePtrs &volumes, + const Params ¶ms, + const std::function &throw_if_canceled + ); + + TriangleSetSamples mesh_samples; + std::vector mesh_samples_visibility; + Impl::CoordinateFunctor mesh_samples_coordinate_functor; + KDTreeIndirect<3, float, Impl::CoordinateFunctor> mesh_samples_tree{Impl::CoordinateFunctor{}}; + float mesh_samples_radius; + + float calculate_point_visibility(const Vec3f &position) const; +}; + +} // namespace Slic3r::ModelInfo +#endif // libslic3r_ModelVisibility_hpp_ diff --git a/src/libslic3r/GCode/SeamAligned.cpp b/src/libslic3r/GCode/SeamAligned.cpp new file mode 100644 index 0000000000..209a5a85d2 --- /dev/null +++ b/src/libslic3r/GCode/SeamAligned.cpp @@ -0,0 +1,533 @@ +#include "libslic3r/GCode/SeamAligned.hpp" +#include "libslic3r/GCode/SeamGeometry.hpp" +#include "libslic3r/GCode/ModelVisibility.hpp" +#include + +namespace Slic3r::Seams::Aligned { +using Perimeters::PointType; +using Perimeters::PointClassification; + +namespace Impl { +const Perimeters::Perimeter::PointTrees &pick_trees( + const Perimeters::Perimeter &perimeter, const PointType point_type +) { + switch (point_type) { + case PointType::enforcer: return perimeter.enforced_points; + case PointType::blocker: return perimeter.blocked_points; + case PointType::common: return perimeter.common_points; + } + throw std::runtime_error("Point trees for point type do not exist."); +} + +const Perimeters::Perimeter::OptionalPointTree &pick_tree( + const Perimeters::Perimeter::PointTrees &point_trees, + const PointClassification &point_classification +) { + switch (point_classification) { + case PointClassification::overhang: return point_trees.overhanging_points; + case PointClassification::embedded: return point_trees.embedded_points; + case PointClassification::common: return point_trees.common_points; + } + throw std::runtime_error("Point tree for classification does not exist."); +} + +unsigned point_value(PointType point_type, PointClassification point_classification) { + // Better be explicit than smart. + switch (point_type) { + case PointType::enforcer: + switch (point_classification) { + case PointClassification::embedded: return 9; + case PointClassification::common: return 8; + case PointClassification::overhang: return 7; + } + case PointType::common: + switch (point_classification) { + case PointClassification::embedded: return 6; + case PointClassification::common: return 5; + case PointClassification::overhang: return 4; + } + case PointType::blocker: + switch (point_classification) { + case PointClassification::embedded: return 3; + case PointClassification::common: return 2; + case PointClassification::overhang: return 1; + } + } + return 0; +} + +SeamChoice pick_seam_option(const Perimeters::Perimeter &perimeter, const SeamOptions &options) { + const std::vector &types{perimeter.point_types}; + const std::vector &classifications{perimeter.point_classifications}; + const std::vector &positions{perimeter.positions}; + + unsigned closeset_point_value = + point_value(types.at(options.closest), classifications[options.closest]); + + if (options.snapped) { + unsigned snapped_point_value = + point_value(types.at(*options.snapped), classifications[*options.snapped]); + if (snapped_point_value >= closeset_point_value) { + const Vec2d position{positions.at(*options.snapped)}; + return {*options.snapped, *options.snapped, position}; + } + } + + unsigned adjacent_point_value = + point_value(types.at(options.adjacent), classifications[options.adjacent]); + if (adjacent_point_value < closeset_point_value) { + const Vec2d position = positions[options.closest]; + return {options.closest, options.closest, position}; + } + + const std::size_t next_index{options.adjacent_forward ? options.adjacent : options.closest}; + const std::size_t previous_index{options.adjacent_forward ? options.closest : options.adjacent}; + return {previous_index, next_index, options.on_edge}; +} + +std::optional snap_to_angle( + const Vec2d &point, + const std::size_t search_start, + const Perimeters::Perimeter &perimeter, + const double max_detour +) { + using Perimeters::AngleType; + const std::vector &positions{perimeter.positions}; + const std::vector &angle_types{perimeter.angle_types}; + + std::optional match; + double min_distance{std::numeric_limits::infinity()}; + AngleType angle_type{AngleType::convex}; + + const auto visitor{[&](const std::size_t index) { + const double distance = (positions[index] - point).norm(); + if (distance > max_detour) { + return true; + } + if (angle_types[index] == angle_type && + distance < min_distance) { + match = index; + min_distance = distance; + return true; + } + return false; + }}; + Geometry::visit_backward(search_start, positions.size(), visitor); + Geometry::visit_forward(search_start, positions.size(), visitor); + if (match) { + return match; + } + + min_distance = std::numeric_limits::infinity(); + angle_type = AngleType::concave; + + Geometry::visit_backward(search_start, positions.size(), visitor); + Geometry::visit_forward(search_start, positions.size(), visitor); + + return match; +} + +SeamOptions get_seam_options( + const Perimeters::Perimeter &perimeter, + const Vec2d &prefered_position, + const Perimeters::Perimeter::PointTree &points_tree, + const double max_detour +) { + const std::vector &positions{perimeter.positions}; + + const std::size_t closest{find_closest_point(points_tree, prefered_position.head<2>())}; + std::size_t previous{closest == 0 ? positions.size() - 1 : closest - 1}; + std::size_t next{closest == positions.size() - 1 ? 0 : closest + 1}; + + const Vec2d previous_adjacent_point{positions[previous]}; + const Vec2d closest_point{positions[closest]}; + const Vec2d next_adjacent_point{positions[next]}; + + const Linef previous_segment{previous_adjacent_point, closest_point}; + const auto [previous_point, previous_distance] = + Geometry::distance_to_segment_squared(previous_segment, prefered_position); + const Linef next_segment{closest_point, next_adjacent_point}; + const auto [next_point, next_distance] = + Geometry::distance_to_segment_squared(next_segment, prefered_position); + + const bool adjacent_forward{next_distance < previous_distance}; + const Vec2d nearest_point{adjacent_forward ? next_point : previous_point}; + const std::size_t adjacent{adjacent_forward ? next : previous}; + + std::optional snapped{ + snap_to_angle(nearest_point.head<2>(), closest, perimeter, max_detour)}; + + return { + closest, adjacent, adjacent_forward, snapped, nearest_point, + }; +} + +std::optional LeastVisible::operator()( + const Perimeters::Perimeter &perimeter, + const PointType point_type, + const PointClassification point_classification +) const { + std::optional chosen_index; + double visibility{std::numeric_limits::infinity()}; + + for (std::size_t i{0}; i < perimeter.positions.size(); ++i) { + if (perimeter.point_types[i] != point_type || + perimeter.point_classifications[i] != point_classification) { + continue; + } + const Vec2d point{perimeter.positions[i]}; + const double point_visibility{precalculated_visibility[i]}; + + if (point_visibility < visibility) { + visibility = point_visibility; + chosen_index = i; + } + } + + if (chosen_index) { + return {{*chosen_index, *chosen_index, perimeter.positions[*chosen_index]}}; + } + return std::nullopt; +} + +std::optional Nearest::operator()( + const Perimeters::Perimeter &perimeter, + const PointType point_type, + const PointClassification point_classification +) const { + const Perimeters::Perimeter::PointTrees &trees{pick_trees(perimeter, point_type)}; + const Perimeters::Perimeter::OptionalPointTree &tree = pick_tree(trees, point_classification); + if (tree) { + const SeamOptions options{get_seam_options(perimeter, prefered_position, *tree, max_detour)}; + return pick_seam_option(perimeter, options); + } + return std::nullopt; +} +} // namespace Impl + +double VisibilityCalculator::operator()( + const SeamChoice &choice, const Perimeters::Perimeter &perimeter +) const { + double visibility = points_visibility.calculate_point_visibility( + to_3d(choice.position, perimeter.slice_z).cast() + ); + + const double angle{ + choice.previous_index == choice.next_index ? perimeter.angles[choice.previous_index] : 0.0}; + visibility += + get_angle_visibility_modifier(angle, convex_visibility_modifier, concave_visibility_modifier); + return visibility; +} + +double VisibilityCalculator::get_angle_visibility_modifier( + double angle, + const double convex_visibility_modifier, + const double concave_visibility_modifier +) { + const double weight_max{angle > 0 ? convex_visibility_modifier : concave_visibility_modifier}; + angle = std::abs(angle); + const double right_angle{M_PI / 2.0}; + if (angle > right_angle) { + return -weight_max; + } + const double angle_linear_weight{angle / right_angle}; + // It is smooth and at angle 0 slope is equal to `angle linear weight`, at right angle the slope is 0 and value is equal to weight max. + const double angle_smooth_weight{angle / right_angle * weight_max + (right_angle - angle) / right_angle * angle_linear_weight}; + return -angle_smooth_weight; +} + +std::vector extract_points( + const Perimeters::Perimeter &perimeter, const Perimeters::PointType point_type +) { + std::vector result; + for (std::size_t i{0}; i < perimeter.positions.size(); ++i) { + if (perimeter.point_types[i] == point_type) { + result.push_back(perimeter.positions[i]); + } + } + return result; +} + +std::vector get_starting_positions(const Shells::Shell<> &shell) { + const Perimeters::Perimeter &perimeter{shell.front().boundary}; + + std::vector enforcers{extract_points(perimeter, Perimeters::PointType::enforcer)}; + if (!enforcers.empty()) { + return enforcers; + } + std::vector common{extract_points(perimeter, Perimeters::PointType::common)}; + if (!common.empty()) { + return common; + } + return perimeter.positions; +} + +struct LeastVisiblePoint +{ + SeamChoice choice; + double visibility; +}; + +struct SeamCandidate { + std::vector choices; + std::vector visibilities; +}; + +SeamCandidate get_seam_candidate( + const Shells::Shell<> &shell, + const Vec2d &starting_position, + const SeamChoiceVisibility &visibility_calculator, + const Params ¶ms, + const std::vector> &precalculated_visibility, + const std::vector &least_visible_points +) { + using Perimeters::Perimeter, Perimeters::AngleType; + + std::vector choice_visibilities(shell.size(), 1.0); + std::vector choices{ + Seams::get_shell_seam(shell, [&, previous_position{starting_position}](const Perimeter &perimeter, std::size_t slice_index) mutable { + SeamChoice candidate{Seams::choose_seam_point( + perimeter, Impl::Nearest{previous_position, params.max_detour} + )}; + const bool is_too_far{ + (candidate.position - previous_position).norm() > params.max_detour}; + const LeastVisiblePoint &least_visible{least_visible_points[slice_index]}; + + const bool is_on_edge{ + candidate.previous_index == candidate.next_index && + perimeter.angle_types[candidate.next_index] != AngleType::smooth}; + + if (is_on_edge) { + choice_visibilities[slice_index] = precalculated_visibility[slice_index][candidate.previous_index]; + } else { + choice_visibilities[slice_index] = + visibility_calculator(candidate, perimeter); + } + const bool is_too_visible{ + choice_visibilities[slice_index] > + least_visible.visibility + params.jump_visibility_threshold}; + const bool can_be_on_edge{ + !is_on_edge && + perimeter.angle_types[least_visible.choice.next_index] != AngleType::smooth}; + if (is_too_far || (can_be_on_edge && is_too_visible)) { + candidate = least_visible.choice; + } + previous_position = candidate.position; + return candidate; + })}; + return {std::move(choices), std::move(choice_visibilities)}; +} + +using ShellVertexVisibility = std::vector>; + +std::vector get_shells_vertex_visibility( + const Shells::Shells<> &shells, const SeamChoiceVisibility &visibility_calculator +) { + std::vector result; + + result.reserve(shells.size()); + std::transform( + shells.begin(), shells.end(), std::back_inserter(result), + [](const Shells::Shell<> &shell) { return ShellVertexVisibility(shell.size()); } + ); + + Geometry::iterate_nested(shells, [&](const std::size_t shell_index, const std::size_t slice_index) { + const Shells::Shell<> &shell{shells[shell_index]}; + const Shells::Slice<> &slice{shell[slice_index]}; + const std::vector &positions{slice.boundary.positions}; + + for (std::size_t point_index{0}; point_index < positions.size(); ++point_index) { + result[shell_index][slice_index].emplace_back(visibility_calculator( + SeamChoice{point_index, point_index, positions[point_index]}, slice.boundary + )); + } + }); + return result; +} + +using ShellLeastVisiblePoints = std::vector; + +std::vector get_shells_least_visible_points( + const Shells::Shells<> &shells, + const std::vector &precalculated_visibility +) { + std::vector result; + + result.reserve(shells.size()); + std::transform( + shells.begin(), shells.end(), std::back_inserter(result), + [](const Shells::Shell<> &shell) { return ShellLeastVisiblePoints(shell.size()); } + ); + + Geometry::iterate_nested(shells, [&](const std::size_t shell_index, const std::size_t slice_index) { + const Shells::Shell<> &shell{shells[shell_index]}; + const Shells::Slice<> &slice{shell[slice_index]}; + const SeamChoice least_visibile{ + Seams::choose_seam_point(slice.boundary, Impl::LeastVisible{precalculated_visibility[shell_index][slice_index]})}; + + const double visibility{precalculated_visibility[shell_index][slice_index][least_visibile.previous_index]}; + result[shell_index][slice_index] = LeastVisiblePoint{least_visibile, visibility}; + }); + return result; +} + +using ShellStartingPositions = std::vector; + +std::vector get_shells_starting_positions( + const Shells::Shells<> &shells +) { + std::vector result; + for (const Shells::Shell<> &shell : shells) { + std::vector starting_positions{get_starting_positions(shell)}; + result.push_back(std::move(starting_positions)); + } + return result; +} + +using ShellSeamCandidates = std::vector; + +std::vector get_shells_seam_candidates( + const Shells::Shells<> &shells, + const std::vector &starting_positions, + const SeamChoiceVisibility &visibility_calculator, + const std::vector &precalculated_visibility, + const std::vector &least_visible_points, + const Params ¶ms +) { + std::vector result; + + result.reserve(starting_positions.size()); + std::transform( + starting_positions.begin(), starting_positions.end(), std::back_inserter(result), + [](const ShellStartingPositions &positions) { return ShellSeamCandidates(positions.size()); } + ); + + Geometry::iterate_nested(starting_positions, [&](const std::size_t shell_index, const std::size_t starting_position_index){ + const Shells::Shell<> &shell{shells[shell_index]}; + using Perimeters::Perimeter, Perimeters::AngleType; + + result[shell_index][starting_position_index] = get_seam_candidate( + shell, + starting_positions[shell_index][starting_position_index], + visibility_calculator, + params, + precalculated_visibility[shell_index], + least_visible_points[shell_index] + ); + }); + return result; +} + +std::vector get_shell_seam( + const Shells::Shell<> &shell, + std::vector seam_candidates, + const Perimeters::Perimeter::OptionalPointTree &previous_points, + const Params ¶ms +) { + std::vector seam; + double visibility{std::numeric_limits::infinity()}; + + for (std::size_t i{0}; i < seam_candidates.size(); ++i) { + using Perimeters::Perimeter, Perimeters::AngleType; + + SeamCandidate seam_candidate{seam_candidates[i]}; + const Vec2d first_point{seam_candidate.choices.front().position}; + + std::optional closest_point; + if (previous_points) { + std::size_t closest_point_index{find_closest_point(*previous_points, first_point)}; + Vec2d point; + point.x() = previous_points->coordinate(closest_point_index, 0); + point.y() = previous_points->coordinate(closest_point_index, 1); + closest_point = point; + } + + std::optional previous_distance; + if (closest_point) { + previous_distance = (*closest_point - first_point).norm(); + } + const bool is_near_previous{closest_point && *previous_distance < params.max_detour}; + + double seam_candidate_visibility{ + is_near_previous ? -params.continuity_modifier * + (params.max_detour - *previous_distance) / params.max_detour : + 0.0}; + for (std::size_t slice_index{}; slice_index < shell.size(); ++slice_index) { + seam_candidate_visibility += seam_candidate.visibilities[slice_index]; + } + + if (seam_candidate_visibility < visibility) { + seam = std::move(seam_candidate.choices); + visibility = seam_candidate_visibility; + } + } + + return seam; +} + +std::vector> get_object_seams( + Shells::Shells<> &&shells, + const SeamChoiceVisibility &visibility_calculator, + const Params ¶ms +) { + const std::vector precalculated_visibility{ + get_shells_vertex_visibility(shells, visibility_calculator)}; + + const std::vector least_visible_points{ + get_shells_least_visible_points(shells, precalculated_visibility) + }; + + const std::vector starting_positions{ + get_shells_starting_positions(shells) + }; + + const std::vector seam_candidates{ + get_shells_seam_candidates( + shells, + starting_positions, + visibility_calculator, + precalculated_visibility, + least_visible_points, + params + ) + }; + + std::vector> layer_seams(get_layer_count(shells)); + + for (std::size_t shell_index{0}; shell_index < shells.size(); ++shell_index) { + Shells::Shell<> &shell{shells[shell_index]}; + + if (shell.empty()) { + continue; + } + + const std::size_t layer_index{shell.front().layer_index}; + tcb::span previous_seams{ + layer_index == 0 ? tcb::span{} : layer_seams[layer_index - 1]}; + std::vector previous_seams_positions; + std::transform( + previous_seams.begin(), previous_seams.end(), + std::back_inserter(previous_seams_positions), + [](const SeamPerimeterChoice &seam) { return seam.choice.position; } + ); + + Perimeters::Perimeter::OptionalPointTree previous_seams_positions_tree; + const Perimeters::Perimeter::IndexToCoord index_to_coord{previous_seams_positions}; + if (!previous_seams_positions.empty()) { + previous_seams_positions_tree = + Perimeters::Perimeter::PointTree{index_to_coord, index_to_coord.positions.size()}; + } + + std::vector seam{ + Aligned::get_shell_seam(shell, seam_candidates[shell_index], previous_seams_positions_tree, params)}; + + for (std::size_t perimeter_id{}; perimeter_id < shell.size(); ++perimeter_id) { + const SeamChoice &choice{seam[perimeter_id]}; + Perimeters::Perimeter &perimeter{shell[perimeter_id].boundary}; + layer_seams[shell[perimeter_id].layer_index].emplace_back(choice, std::move(perimeter)); + } + } + return layer_seams; +} + +} // namespace Slic3r::Seams::Aligned diff --git a/src/libslic3r/GCode/SeamAligned.hpp b/src/libslic3r/GCode/SeamAligned.hpp new file mode 100644 index 0000000000..ee3b581ec1 --- /dev/null +++ b/src/libslic3r/GCode/SeamAligned.hpp @@ -0,0 +1,97 @@ +#ifndef libslic3r_SeamAligned_hpp_ +#define libslic3r_SeamAligned_hpp_ + +#include "libslic3r/GCode/SeamPerimeters.hpp" +#include "libslic3r/GCode/SeamChoice.hpp" +#include "libslic3r/Point.hpp" + +namespace Slic3r::ModelInfo { + struct Visibility; +} + +namespace Slic3r::Seams::Aligned { + +using SeamChoiceVisibility = std::function; + +namespace Impl { +struct SeamOptions +{ + std::size_t closest; + std::size_t adjacent; + bool adjacent_forward; + std::optional snapped; + Vec2d on_edge; +}; + +SeamChoice pick_seam_option(const Perimeters::Perimeter &perimeter, const SeamOptions &options); + +std::optional snap_to_angle( + const Vec2d &point, + const std::size_t search_start, + const Perimeters::Perimeter &perimeter, + const double max_detour +); + +SeamOptions get_seam_options( + const Perimeters::Perimeter &perimeter, + const Vec2d &prefered_position, + const Perimeters::Perimeter::PointTree &points_tree, + const double max_detour +); + +struct Nearest +{ + Vec2d prefered_position; + double max_detour; + + std::optional operator()( + const Perimeters::Perimeter &perimeter, + const Perimeters::PointType point_type, + const Perimeters::PointClassification point_classification + ) const; +}; + +struct LeastVisible +{ + std::optional operator()( + const Perimeters::Perimeter &perimeter, + const Perimeters::PointType point_type, + const Perimeters::PointClassification point_classification + ) const; + + const std::vector &precalculated_visibility; +}; +} + + +struct VisibilityCalculator +{ + const Slic3r::ModelInfo::Visibility &points_visibility; + double convex_visibility_modifier; + double concave_visibility_modifier; + + double operator()(const SeamChoice &choice, const Perimeters::Perimeter &perimeter) const; + +private: + static double get_angle_visibility_modifier( + const double angle, + const double convex_visibility_modifier, + const double concave_visibility_modifier + ); +}; + +struct Params { + double max_detour{}; + double jump_visibility_threshold{}; + double continuity_modifier{}; +}; + +std::vector> get_object_seams( + Shells::Shells<> &&shells, + const SeamChoiceVisibility& visibility_calculator, + const Params& params +); + +} // namespace Slic3r::Seams::Aligned + +#endif // libslic3r_SeamAligned_hpp_ diff --git a/src/libslic3r/GCode/SeamChoice.cpp b/src/libslic3r/GCode/SeamChoice.cpp new file mode 100644 index 0000000000..6db562e2a1 --- /dev/null +++ b/src/libslic3r/GCode/SeamChoice.cpp @@ -0,0 +1,108 @@ +#include "libslic3r/GCode/SeamChoice.hpp" + +namespace Slic3r::Seams { +std::optional maybe_choose_seam_point( + const Perimeters::Perimeter &perimeter, const SeamPicker &seam_picker +) { + using Perimeters::PointType; + using Perimeters::PointClassification; + + std::vector + type_search_order{PointType::enforcer, PointType::common, PointType::blocker}; + std::vector classification_search_order{ + PointClassification::embedded, PointClassification::common, PointClassification::overhang}; + for (const PointType point_type : type_search_order) { + for (const PointClassification point_classification : classification_search_order) { + if (std::optional seam_choice{ + seam_picker(perimeter, point_type, point_classification)}) { + return seam_choice; + } + } + } + + return std::nullopt; +} + +SeamChoice choose_seam_point(const Perimeters::Perimeter &perimeter, const SeamPicker &seam_picker) { + using Perimeters::PointType; + using Perimeters::PointClassification; + + std::optional seam_choice{maybe_choose_seam_point(perimeter, seam_picker)}; + + if (seam_choice) { + return *seam_choice; + } + + // Failed to choose any reasonable point! + return SeamChoice{0, 0, perimeter.positions.front()}; +} + +std::optional choose_degenerate_seam_point(const Perimeters::Perimeter &perimeter) { + if (!perimeter.positions.empty()) { + return SeamChoice{0, 0, perimeter.positions.front()}; + } + return std::nullopt; +} + +std::optional> maybe_get_shell_seam( + const Shells::Shell<> &shell, + const std::function(const Perimeters::Perimeter &, std::size_t)> &chooser +) { + std::vector result; + result.reserve(shell.size()); + for (std::size_t i{0}; i < shell.size(); ++i) { + const Shells::Slice<> &slice{shell[i]}; + if (slice.boundary.is_degenerate) { + if (std::optional seam_choice{ + choose_degenerate_seam_point(slice.boundary)}) { + result.push_back(*seam_choice); + } else { + result.emplace_back(); + } + } else { + const std::optional choice{chooser(slice.boundary, i)}; + if (!choice) { + return std::nullopt; + } + result.push_back(*choice); + } + } + return result; +} + +std::vector get_shell_seam( + const Shells::Shell<> &shell, + const std::function &chooser +) { + std::optional> seam{maybe_get_shell_seam( + shell, + [&](const Perimeters::Perimeter &perimeter, std::size_t slice_index) { + return chooser(perimeter, slice_index); + } + )}; + if (!seam) { + // Should be unreachable as chooser always returns a SeamChoice! + return std::vector(shell.size()); + } + return *seam; +} + +std::vector> get_object_seams( + Shells::Shells<> &&shells, + const std::function(const Shells::Shell<>&)> &get_shell_seam +) { + std::vector> layer_seams(get_layer_count(shells)); + + for (Shells::Shell<> &shell : shells) { + std::vector seam{get_shell_seam(shell)}; + + for (std::size_t perimeter_id{}; perimeter_id < shell.size(); ++perimeter_id) { + const SeamChoice &choice{seam[perimeter_id]}; + Perimeters::Perimeter &perimeter{shell[perimeter_id].boundary}; + layer_seams[shell[perimeter_id].layer_index].emplace_back(choice, std::move(perimeter)); + } + } + + return layer_seams; +} +} // namespace Slic3r::Seams diff --git a/src/libslic3r/GCode/SeamChoice.hpp b/src/libslic3r/GCode/SeamChoice.hpp new file mode 100644 index 0000000000..d33933b71f --- /dev/null +++ b/src/libslic3r/GCode/SeamChoice.hpp @@ -0,0 +1,72 @@ +#ifndef libslic3r_SeamChoice_hpp_ +#define libslic3r_SeamChoice_hpp_ + +#include "libslic3r/Polygon.hpp" +#include "libslic3r/GCode/SeamPerimeters.hpp" + +namespace Slic3r::Seams { + +/** + * When previous_index == next_index, the point is at the point. + * Otherwise the point is at the edge. + */ +struct SeamChoice +{ + std::size_t previous_index{}; + std::size_t next_index{}; + Vec2d position{Vec2d::Zero()}; +}; + +struct SeamPerimeterChoice +{ + SeamPerimeterChoice(const SeamChoice &choice, Perimeters::Perimeter &&perimeter) + : choice(choice) + , perimeter(std::move(perimeter)) + , bounding_box(Polygon{Geometry::scaled(this->perimeter.positions)}.bounding_box()) {} + + SeamChoice choice; + Perimeters::Perimeter perimeter; + BoundingBox bounding_box; +}; + +using SeamPicker = std::function(const Perimeters::Perimeter &, const Perimeters::PointType, const Perimeters::PointClassification)>; + +std::optional maybe_choose_seam_point( + const Perimeters::Perimeter &perimeter, const SeamPicker &seam_picker +); + +/** + * Go throught points on perimeter and choose the best seam point closest to + * the prefered position. + * + * Points in the perimeter can be diveded into 3x3=9 categories. An example category is + * enforced overhanging point. These categories are searched in particualr order. + * For example enforced overhang will be always choosen over common embedded point, etc. + * + * A closest point is choosen from the first non-empty category. + */ +SeamChoice choose_seam_point( + const Perimeters::Perimeter &perimeter, const SeamPicker& seam_picker +); + +std::optional choose_degenerate_seam_point(const Perimeters::Perimeter &perimeter); + +std::optional> maybe_get_shell_seam( + const Shells::Shell<> &shell, + const std::function(const Perimeters::Perimeter &, std::size_t)> &chooser +); + +std::vector get_shell_seam( + const Shells::Shell<> &shell, + const std::function &chooser +); + +std::vector> get_object_seams( + Shells::Shells<> &&shells, + const std::function(const Shells::Shell<> &)> &get_shell_seam +); + +} // namespace Slic3r::Seams + +#endif // libslic3r_SeamChoice_hpp_ diff --git a/src/libslic3r/GCode/SeamGeometry.cpp b/src/libslic3r/GCode/SeamGeometry.cpp new file mode 100644 index 0000000000..a0f5403f7d --- /dev/null +++ b/src/libslic3r/GCode/SeamGeometry.cpp @@ -0,0 +1,369 @@ +#include "libslic3r/GCode/SeamGeometry.hpp" +#include "KDTreeIndirect.hpp" +#include "Layer.hpp" +#include +#include +#include +#include + +namespace Slic3r::Seams::Geometry { + +namespace MappingImpl { + +/** + * @brief Return 0, 1, ..., size - 1. + */ +std::vector range(std::size_t size) { + std::vector result(size); + std::iota(result.begin(), result.end(), 0); + return result; +} + +/** + * @brief A link between lists. + */ +struct Link +{ + std::size_t bucket_id; + double weight; +}; + +/** + * @brief Get optional values. Replace any nullopt Links with new_bucket_id and increment new_bucket_id. + * + * @param links A list of optional links. + * @param new_bucket_id In-out parameter incremented on each nullopt replacement. + */ +std::vector assign_buckets( + const std::vector> &links, std::size_t &new_bucket_id +) { + std::vector result; + std::transform( + links.begin(), links.end(), std::back_inserter(result), + [&](const std::optional &link) { + if (link) { + return link->bucket_id; + } + return new_bucket_id++; + } + ); + return result; +} +} // namespace MappingImpl + +Vec2d get_normal(const Vec2d &vector) { return Vec2d{vector.y(), -vector.x()}.normalized(); } + +Vec2d get_polygon_normal( + const std::vector &points, const std::size_t index, const double min_arm_length +) { + std::optional previous_index; + std::optional next_index; + + visit_forward(index, points.size(), [&](const std::size_t index_candidate) { + if (index == index_candidate) { + return false; + } + const double distance{(points[index_candidate] - points[index]).norm()}; + if (distance > min_arm_length) { + next_index = index_candidate; + return true; + } + return false; + }); + visit_backward(index, points.size(), [&](const std::size_t index_candidate) { + const double distance{(points[index_candidate] - points[index]).norm()}; + if (distance > min_arm_length) { + previous_index = index_candidate; + return true; + } + return false; + }); + + if (previous_index && next_index) { + const Vec2d previous_normal{ + Geometry::get_normal(points.at(index) - points.at(*previous_index))}; + const Vec2d next_normal{Geometry::get_normal(points.at(*next_index) - points.at(index))}; + return (previous_normal + next_normal).normalized(); + } + return Vec2d::Zero(); +} + +std::pair distance_to_segment_squared(const Linef &segment, const Vec2d &point) { + Vec2d segment_point; + const double distance{line_alg::distance_to_squared(segment, point, &segment_point)}; + return {segment_point, distance}; +} + +std::pair get_mapping( + const std::vector &list_sizes, const MappingOperator &mapping_operator +) { + using namespace MappingImpl; + + std::vector> result; + result.reserve(list_sizes.size()); + result.push_back(range(list_sizes.front())); + + std::size_t new_bucket_id{result.back().size()}; + + for (std::size_t layer_index{0}; layer_index < list_sizes.size() - 1; ++layer_index) { + // Current layer is already assigned mapping. + + // Links on the next layer to the current layer. + std::vector> links(list_sizes[layer_index + 1]); + + for (std::size_t item_index{0}; item_index < list_sizes[layer_index]; ++item_index) { + const MappingOperatorResult next_item{ + mapping_operator(layer_index, item_index)}; + if (next_item) { + const auto [index, weight] = *next_item; + const Link link{result.back()[item_index], weight}; + if (!links[index] || links[index]->weight < link.weight) { + links[index] = link; + } + } + } + result.push_back(assign_buckets(links, new_bucket_id)); + } + return {result, new_bucket_id}; +} + +Extrusion::Extrusion( + Polygon &&polygon, + BoundingBox bounding_box, + const double width, + const ExPolygon &island_boundary +) + : polygon(polygon) + , bounding_box(std::move(bounding_box)) + , width(width) + , island_boundary(island_boundary) { + this->island_boundary_bounding_boxes.push_back(island_boundary.contour.bounding_box()); + + std::transform( + this->island_boundary.holes.begin(), this->island_boundary.holes.end(), + std::back_inserter(this->island_boundary_bounding_boxes), + [](const Polygon &polygon) { return polygon.bounding_box(); } + ); +} + +Geometry::Extrusions get_external_perimeters(const Slic3r::Layer &layer, const LayerSlice &slice) { + std::vector result; + for (const LayerIsland &island : slice.islands) { + const LayerRegion &layer_region = *layer.get_region(island.perimeters.region()); + for (const uint32_t perimeter_id : island.perimeters) { + const auto collection{static_cast( + layer_region.perimeters().entities[perimeter_id] + )}; + for (const ExtrusionEntity *entity : *collection) { + if (entity->role().is_external_perimeter()) { + Polygon polygon{entity->as_polyline().points}; + const BoundingBox bounding_box{polygon.bounding_box()}; + const double width{layer_region.flow(FlowRole::frExternalPerimeter).width()}; + result.emplace_back(std::move(polygon), bounding_box, width, island.boundary); + } + } + } + } + return result; +} + +std::vector get_extrusions(tcb::span object_layers) { + std::vector result; + result.reserve(object_layers.size()); + + for (const Slic3r::Layer *object_layer : object_layers) { + Extrusions extrusions; + + for (const LayerSlice &slice : object_layer->lslices_ex) { + std::vector external_perimeters{ + get_external_perimeters(*object_layer, slice)}; + for (Geometry::Extrusion &extrusion : external_perimeters) { + extrusions.push_back(std::move(extrusion)); + } + } + + result.push_back(std::move(extrusions)); + } + + return result; +} + +std::vector oversample_edge(const Vec2d &from, const Vec2d &to, const double max_distance) { + const double total_distance{(from - to).norm()}; + const auto points_count{static_cast(std::ceil(total_distance / max_distance)) + 1}; + if (points_count < 3) { + return {}; + } + const double step_size{total_distance / (points_count - 1)}; + const Vec2d step_vector{step_size * (to - from).normalized()}; + std::vector result; + result.reserve(points_count - 2); + for (std::size_t i{1}; i < points_count - 1; ++i) { + result.push_back(from + i * step_vector); + } + return result; +} + +void visit_forward( + const std::size_t start_index, + const std::size_t loop_size, + const std::function &visitor +) { + std::size_t last_index{loop_size - 1}; + std::size_t index{start_index}; + for (unsigned _{0}; _ < 30; ++_) { // Infinite loop prevention + if (visitor(index)) { + return; + } + index = index == last_index ? 0 : index + 1; + } + assert(false); +} + +void visit_backward( + const std::size_t start_index, + const std::size_t loop_size, + const std::function &visitor +) { + std::size_t last_index{loop_size - 1}; + std::size_t index{start_index == 0 ? loop_size - 1 : start_index - 1}; + for (unsigned _{0}; _ < 30; ++_) { // Infinite loop prevention + if (visitor(index)) { + return; + } + index = index == 0 ? last_index : index - 1; + } + assert(false); +} + +std::vector unscaled(const Points &points) { + std::vector result; + result.reserve(points.size()); + using std::transform, std::begin, std::end, std::back_inserter; + transform(begin(points), end(points), back_inserter(result), [](const Point &point) { + return unscaled(point); + }); + return result; +} + +std::vector unscaled(const Lines &lines) { + std::vector result; + result.reserve(lines.size()); + std::transform(lines.begin(), lines.end(), std::back_inserter(result), [](const Line &line) { + return Linef{unscaled(line.a), unscaled(line.b)}; + }); + return result; +} + +Points scaled(const std::vector &points) { + Points result; + for (const Vec2d &point : points) { + result.push_back(Slic3r::scaled(point)); + } + return result; +} + +std::vector get_embedding_distances( + const std::vector &points, const AABBTreeLines::LinesDistancer &perimeter_distancer +) { + std::vector result; + result.reserve(points.size()); + using std::transform, std::back_inserter; + transform(points.begin(), points.end(), back_inserter(result), [&](const Vec2d &point) { + const double distance{perimeter_distancer.distance_from_lines(point)}; + return distance < 0 ? -distance : 0.0; + }); + return result; +} + +std::vector get_overhangs( + const std::vector &points, + const AABBTreeLines::LinesDistancer &previous_layer_perimeter_distancer, + const double layer_height +) { + std::vector result; + result.reserve(points.size()); + using std::transform, std::back_inserter; + transform(points.begin(), points.end(), back_inserter(result), [&](const Vec2d &point) { + const double distance{previous_layer_perimeter_distancer.distance_from_lines(point)}; + return distance > 0 ? M_PI / 2 - std::atan(layer_height / distance) : 0.0; + }); + return result; +} + +// Measured from outside, convex is positive +std::vector get_vertex_angles(const std::vector &points, const double min_arm_length) { + std::vector result; + result.reserve(points.size()); + + for (std::size_t index{0}; index < points.size(); ++index) { + std::optional previous_index; + std::optional next_index; + + visit_forward(index, points.size(), [&](const std::size_t index_candidate) { + if (index == index_candidate) { + return false; + } + const double distance{(points[index_candidate] - points[index]).norm()}; + if (distance > min_arm_length) { + next_index = index_candidate; + return true; + } + return false; + }); + visit_backward(index, points.size(), [&](const std::size_t index_candidate) { + const double distance{(points[index_candidate] - points[index]).norm()}; + if (distance > min_arm_length) { + previous_index = index_candidate; + return true; + } + return false; + }); + + if (previous_index && next_index) { + const Vec2d &previous_point = points[*previous_index]; + const Vec2d &point = points[index]; + const Vec2d &next_point = points[*next_index]; + result.push_back(-angle((point - previous_point), (next_point - point))); + } else { + result.push_back(0.0); + } + } + + return result; +} + +double bounding_box_distance(const BoundingBox &a, const BoundingBox &b) { + const double bb_max_distance{unscaled(Point{a.max - b.max}).norm()}; + const double bb_min_distance{unscaled(Point{a.min - b.min}).norm()}; + return std::max(bb_max_distance, bb_min_distance); +} + +std::pair pick_closest_bounding_box( + const BoundingBox &to, const BoundingBoxes &choose_from +) { + double min_distance{std::numeric_limits::infinity()}; + std::size_t choosen_index{0}; + + for (std::size_t i{0}; i < choose_from.size(); ++i) { + const BoundingBox &candidate{choose_from[i]}; + const double distance{bounding_box_distance(candidate, to)}; + + if (distance < min_distance) { + choosen_index = i; + min_distance = distance; + } + } + return {choosen_index, min_distance}; +} + +Polygon to_polygon(const ExtrusionLoop &loop) { + Points loop_points{}; + for (const ExtrusionPath &path : loop.paths) { + for (const Point &point : path.polyline.points) { + loop_points.push_back(point); + } + } + return Polygon{loop_points}; +} +} // namespace Slic3r::Seams::Geometry diff --git a/src/libslic3r/GCode/SeamGeometry.hpp b/src/libslic3r/GCode/SeamGeometry.hpp new file mode 100644 index 0000000000..fd43854502 --- /dev/null +++ b/src/libslic3r/GCode/SeamGeometry.hpp @@ -0,0 +1,170 @@ +#ifndef libslic3r_SeamGeometry_hpp_ +#define libslic3r_SeamGeometry_hpp_ + +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/Point.hpp" +#include +#include +#include +#include "tcbspan/span.hpp" + +namespace Slic3r { +class Layer; +} + +namespace Slic3r::Seams::Geometry { + +struct Extrusion +{ + Extrusion( + Polygon &&polygon, + BoundingBox bounding_box, + const double width, + const ExPolygon &island_boundary + ); + + Extrusion(const Extrusion &) = delete; + Extrusion(Extrusion &&) = default; + Extrusion &operator=(const Extrusion &) = delete; + Extrusion &operator=(Extrusion &&) = delete; + + Polygon polygon; + BoundingBox bounding_box; + double width; + const ExPolygon &island_boundary; + + // At index 0 there is the bounding box of contour. Rest are the bounding boxes of holes in order. + BoundingBoxes island_boundary_bounding_boxes; +}; + +using Extrusions = std::vector; + +std::vector get_extrusions(tcb::span object_layers); + +Vec2d get_polygon_normal( + const std::vector &points, const std::size_t index, const double min_arm_length +); + +Vec2d get_normal(const Vec2d &vector); + +std::pair distance_to_segment_squared(const Linef &segment, const Vec2d &point); + +using Mapping = std::vector>; +using MappingOperatorResult = std::optional>; +using MappingOperator = std::function; + +/** + * @brief Indirectly map list of lists into buckets. + * + * Look for chains of items accross the lists. + * It may do this mapping: [[1, 2], [3, 4, 5], [6]] -> [[1, 4, 6], [2, 3], [5]]. + * It depends on the weights provided by the mapping operator. + * + * Same bucket cannot be choosen for multiple items in any of the inner lists. + * Bucket is choosen **based on the weight** provided by the mapping operator. Multiple items from + * the same list may want to claim the same bucket. In that case, the item with the biggest weight + * wins the bucket. For example: [[1, 2], [3]] -> [[1, 3], [2]] + * + * @param list_sizes Vector of sizes of the original lists in a list. + * @param mapping_operator Operator that takes layer index and item index on that layer as input + * and returns the best fitting item index from the next layer, along with weight, representing how + * good the fit is. It may return nullopt if there is no good fit. + * + * @return Mapping [outter_list_index][inner_list_index] -> bucket id and the number of buckets. + */ +std::pair get_mapping( + const std::vector &list_sizes, const MappingOperator &mapping_operator +); + +std::vector oversample_edge(const Vec2d &from, const Vec2d &to, const double max_distance); + +template +std::size_t get_flat_size(const NestedVector &nested_vector) { + return std::accumulate( + nested_vector.begin(), nested_vector.end(), std::size_t{0}, + [](const std::size_t sum, const auto &vector) { return sum + vector.size(); } + ); +} + +template +std::vector> get_flat_index2indices_table( + const NestedVector &nested_vector +) { + std::vector> result; + for (std::size_t parent_index{0}; parent_index < nested_vector.size(); ++parent_index) { + const auto &vector{nested_vector[parent_index]}; + for (std::size_t nested_index{0}; nested_index < vector.size(); ++nested_index) { + result.push_back({parent_index, nested_index}); + } + } + return result; +} + +template +void iterate_nested(const NestedVector &nested_vector, const std::function &function) { + std::size_t flat_size{Geometry::get_flat_size(nested_vector)}; + using Range = tbb::blocked_range; + const Range range{0, flat_size}; + + std::vector> index_table{ + get_flat_index2indices_table(nested_vector)}; + + // Iterate the shells as if it was flat. + tbb::parallel_for(range, [&](Range range) { + for (std::size_t index{range.begin()}; index < range.end(); ++index) { + const auto[parent_index, nested_index]{index_table[index]}; + function(parent_index, nested_index); + } + }); +} + +void visit_forward( + const std::size_t start_index, + const std::size_t loop_size, + const std::function &visitor +); +void visit_backward( + const std::size_t start_index, + const std::size_t loop_size, + const std::function &visitor +); + +std::vector unscaled(const Points &points); + +std::vector unscaled(const Lines &lines); + +Points scaled(const std::vector &points); + +std::vector get_embedding_distances( + const std::vector &points, const AABBTreeLines::LinesDistancer &perimeter_distancer +); + +/** + * @brief Calculate overhang angle for each of the points over the previous layer perimeters. + * + * Larger angle <=> larger overhang. E.g. floating box has overhang = PI / 2. + * + * @returns Angles in radians <0, PI / 2>. + */ +std::vector get_overhangs( + const std::vector &points, + const AABBTreeLines::LinesDistancer &previous_layer_perimeter_distancer, + const double layer_height +); + +// Measured from outside, convex is positive +std::vector get_vertex_angles(const std::vector &points, const double min_arm_length); + +double bounding_box_distance(const BoundingBox &a, const BoundingBox &b); + +std::pair pick_closest_bounding_box( + const BoundingBox &to, const BoundingBoxes &choose_from +); + +Polygon to_polygon(const ExtrusionLoop &loop); + +} // namespace Slic3r::Seams::Geometry + +#endif // libslic3r_SeamGeometry_hpp_ diff --git a/src/libslic3r/GCode/SeamPainting.cpp b/src/libslic3r/GCode/SeamPainting.cpp new file mode 100644 index 0000000000..1f78eb5098 --- /dev/null +++ b/src/libslic3r/GCode/SeamPainting.cpp @@ -0,0 +1,48 @@ +#include "libslic3r/GCode/SeamPainting.hpp" + +namespace Slic3r::Seams::ModelInfo { +Painting::Painting(const Transform3d &obj_transform, const ModelVolumePtrs &volumes) { + for (const ModelVolume *mv : volumes) { + if (mv->is_seam_painted()) { + auto model_transformation = obj_transform * mv->get_matrix(); + + indexed_triangle_set enforcers = mv->seam_facets + .get_facets(*mv, EnforcerBlockerType::ENFORCER); + its_transform(enforcers, model_transformation); + its_merge(this->enforcers, enforcers); + + indexed_triangle_set blockers = mv->seam_facets + .get_facets(*mv, EnforcerBlockerType::BLOCKER); + its_transform(blockers, model_transformation); + its_merge(this->blockers, blockers); + } + } + + this->enforcers_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + this->enforcers.vertices, this->enforcers.indices + ); + this->blockers_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + this->blockers.vertices, this->blockers.indices + ); +} + +bool Painting::is_enforced(const Vec3f &position, float radius) const { + if (enforcers.empty()) { + return false; + } + float radius_sqr = radius * radius; + return AABBTreeIndirect::is_any_triangle_in_radius( + enforcers.vertices, enforcers.indices, enforcers_tree, position, radius_sqr + ); +} + +bool Painting::is_blocked(const Vec3f &position, float radius) const { + if (blockers.empty()) { + return false; + } + float radius_sqr = radius * radius; + return AABBTreeIndirect::is_any_triangle_in_radius( + blockers.vertices, blockers.indices, blockers_tree, position, radius_sqr + ); +} +} // namespace Slic3r::Seams::ModelInfo diff --git a/src/libslic3r/GCode/SeamPainting.hpp b/src/libslic3r/GCode/SeamPainting.hpp new file mode 100644 index 0000000000..a6fa6ff8d0 --- /dev/null +++ b/src/libslic3r/GCode/SeamPainting.hpp @@ -0,0 +1,24 @@ +#ifndef libslic3r_GlobalModelInfo_hpp_ +#define libslic3r_GlobalModelInfo_hpp_ + +#include "libslic3r/AABBTreeIndirect.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Model.hpp" + +namespace Slic3r::Seams::ModelInfo { +class Painting +{ +public: + Painting(const Transform3d &obj_transform, const ModelVolumePtrs &volumes); + + bool is_enforced(const Vec3f &position, float radius) const; + bool is_blocked(const Vec3f &position, float radius) const; + +private: + indexed_triangle_set enforcers; + indexed_triangle_set blockers; + AABBTreeIndirect::Tree<3, float> enforcers_tree; + AABBTreeIndirect::Tree<3, float> blockers_tree; +}; +} // namespace Slic3r::Seams::ModelInfo +#endif // libslic3r_GlobalModelInfo_hpp_ diff --git a/src/libslic3r/GCode/SeamPerimeters.cpp b/src/libslic3r/GCode/SeamPerimeters.cpp new file mode 100644 index 0000000000..a2db061d16 --- /dev/null +++ b/src/libslic3r/GCode/SeamPerimeters.cpp @@ -0,0 +1,409 @@ +#include + +#include "ClipperUtils.hpp" + +#include "libslic3r/Layer.hpp" + +#include "libslic3r/GCode/SeamGeometry.hpp" +#include "libslic3r/GCode/SeamPerimeters.hpp" + +namespace Slic3r::Seams::Perimeters::Impl { + +std::vector oversample_painted( + const std::vector &points, + const std::function &is_painted, + const double slice_z, + const double max_distance +) { + std::vector result; + + for (std::size_t index{0}; index < points.size(); ++index) { + const Vec2d &point{points[index]}; + + result.push_back(point); + + const std::size_t next_index{index == points.size() - 1 ? 0 : index + 1}; + const Vec2d &next_point{points[next_index]}; + const float next_point_distance{static_cast((point - next_point).norm())}; + const Vec2d middle_point{(point + next_point) / 2.0}; + Vec3f point3d{to_3d(middle_point, slice_z).cast()}; + if (is_painted(point3d, next_point_distance / 2.0)) { + for (const Vec2d &edge_point : + Geometry::oversample_edge(point, next_point, max_distance)) { + result.push_back(edge_point); + } + } + } + return result; +} + +std::pair, std::vector> remove_redundant_points( + const std::vector &points, + const std::vector &point_types, + const double tolerance +) { + std::vector points_result; + std::vector point_types_result; + + auto range_start{points.begin()}; + + for (auto iterator{points.begin()}; iterator != points.end(); ++iterator) { + const std::int64_t index{std::distance(points.begin(), iterator)}; + if (next(iterator) == points.end() || point_types[index] != point_types[index + 1]) { + std::vector simplification_result; + douglas_peucker( + range_start, next(iterator), std::back_inserter(simplification_result), tolerance, + [](const Vec2d &point) { return point; } + ); + + points_result.insert( + points_result.end(), simplification_result.begin(), simplification_result.end() + ); + const std::vector + point_types_to_add(simplification_result.size(), point_types[index]); + point_types_result.insert( + point_types_result.end(), point_types_to_add.begin(), point_types_to_add.end() + ); + + range_start = next(iterator); + } + } + + return {points_result, point_types_result}; +} + +std::vector get_point_types( + const std::vector &positions, + const ModelInfo::Painting &painting, + const double slice_z, + const double painting_radius +) { + std::vector result; + result.reserve(positions.size()); + using std::transform, std::back_inserter; + transform( + positions.begin(), positions.end(), back_inserter(result), + [&](const Vec2d &point) { + const Vec3f point3d{to_3d(point.cast(), static_cast(slice_z))}; + if (painting.is_blocked(point3d, painting_radius)) { + return PointType::blocker; + } + if (painting.is_enforced(point3d, painting_radius)) { + return PointType::enforcer; + } + return PointType::common; + } + ); + return result; +} + +std::vector classify_points( + const std::vector &embeddings, + const std::optional> &overhangs, + const double overhang_threshold, + const double embedding_threshold +) { + std::vector result; + result.reserve(embeddings.size()); + using std::transform, std::back_inserter; + transform( + embeddings.begin(), embeddings.end(), back_inserter(result), + [&, i = 0](const double embedding) mutable { + const unsigned index = i++; + if (overhangs && overhangs->operator[](index) > overhang_threshold) { + return PointClassification::overhang; + } + if (embedding > embedding_threshold) { + return PointClassification::embedded; + } + return PointClassification::common; + } + ); + return result; +} + +std::vector get_angle_types( + const std::vector &angles, const double convex_threshold, const double concave_threshold +) { + std::vector result; + using std::transform, std::back_inserter; + transform(angles.begin(), angles.end(), back_inserter(result), [&](const double angle) { + if (angle > convex_threshold) { + return AngleType::convex; + } + if (angle < -concave_threshold) { + return AngleType::concave; + } + return AngleType::smooth; + }); + return result; +} + +std::vector merge_angle_types( + const std::vector &angle_types, + const std::vector &smooth_angle_types, + const std::vector &points, + const double min_arm_length +) { + std::vector result; + result.reserve(angle_types.size()); + for (std::size_t index{0}; index < angle_types.size(); ++index) { + const AngleType &angle_type{angle_types[index]}; + const AngleType &smooth_angle_type{smooth_angle_types[index]}; + + AngleType resulting_type{angle_type}; + + if (smooth_angle_type != angle_type && smooth_angle_type != AngleType::smooth) { + resulting_type = smooth_angle_type; + + // Check if there is a sharp angle in the vicinity. If so, do not use the smooth angle. + Geometry::visit_forward(index, angle_types.size(), [&](const std::size_t forward_index) { + const double distance{(points[forward_index] - points[index]).norm()}; + if (distance > min_arm_length) { + return true; + } + if (angle_types[forward_index] == smooth_angle_type) { + resulting_type = angle_type; + } + return false; + }); + Geometry::visit_backward(index, angle_types.size(), [&](const std::size_t backward_index) { + const double distance{(points[backward_index] - points[index]).norm()}; + if (distance > min_arm_length) { + return true; + } + if (angle_types[backward_index] == smooth_angle_type) { + resulting_type = angle_type; + } + return false; + }); + } + result.push_back(resulting_type); + } + return result; +} + +} // namespace Slic3r::Seams::Perimeters::Impl + +namespace Slic3r::Seams::Perimeters { + +LayerInfos get_layer_infos( + tcb::span object_layers, const double elephant_foot_compensation +) { + LayerInfos result(object_layers.size()); + + using Range = tbb::blocked_range; + const Range range{0, object_layers.size()}; + tbb::parallel_for(range, [&](Range range) { + for (std::size_t layer_index{range.begin()}; layer_index < range.end(); ++layer_index) { + result[layer_index] = LayerInfo::create( + *object_layers[layer_index], layer_index, elephant_foot_compensation + ); + } + }); + return result; +} + +LayerInfo LayerInfo::create( + const Slic3r::Layer &object_layer, + const std::size_t index, + const double elephant_foot_compensation +) { + AABBTreeLines::LinesDistancer perimeter_distancer{ + to_unscaled_linesf({object_layer.lslices})}; + + using PreviousLayerDistancer = std::optional>; + PreviousLayerDistancer previous_layer_perimeter_distancer; + if (object_layer.lower_layer != nullptr) { + previous_layer_perimeter_distancer = PreviousLayerDistancer{ + to_unscaled_linesf(object_layer.lower_layer->lslices)}; + } + + return { + std::move(perimeter_distancer), + std::move(previous_layer_perimeter_distancer), + index, + object_layer.height, + object_layer.slice_z, + index == 0 ? elephant_foot_compensation : 0.0}; +} + +double Perimeter::IndexToCoord::operator()(const size_t index, size_t dim) const { + return positions[index][dim]; +} + +Perimeter::PointTrees get_kd_trees( + const PointType point_type, + const std::vector &all_point_types, + const std::vector &point_classifications, + const Perimeter::IndexToCoord &index_to_coord +) { + std::vector overhang_indexes; + std::vector embedded_indexes; + std::vector common_indexes; + for (std::size_t i{0}; i < all_point_types.size(); ++i) { + if (all_point_types[i] == point_type) { + switch (point_classifications[i]) { + case PointClassification::overhang: overhang_indexes.push_back(i); break; + case PointClassification::embedded: embedded_indexes.push_back(i); break; + case PointClassification::common: common_indexes.push_back(i); break; + } + } + } + Perimeter::PointTrees trees; + if (!overhang_indexes.empty()) { + trees.overhanging_points = Perimeter::PointTree{index_to_coord}; + trees.overhanging_points->build(overhang_indexes); + } + if (!embedded_indexes.empty()) { + trees.embedded_points = Perimeter::PointTree{index_to_coord}; + trees.embedded_points->build(embedded_indexes); + } + if (!common_indexes.empty()) { + trees.common_points = Perimeter::PointTree{index_to_coord}; + trees.common_points->build(common_indexes); + } + return trees; +} + +Perimeter::Perimeter( + const double slice_z, + const std::size_t layer_index, + const bool is_hole, + std::vector &&positions, + std::vector &&angles, + std::vector &&point_types, + std::vector &&point_classifications, + std::vector &&angle_types +) + : slice_z(slice_z) + , layer_index(layer_index) + , is_hole(is_hole) + , positions(std::move(positions)) + , angles(std::move(angles)) + , index_to_coord(IndexToCoord{tcb::span{this->positions}}) + , point_types(std::move(point_types)) + , point_classifications(std::move(point_classifications)) + , angle_types(std::move(angle_types)) + , enforced_points(get_kd_trees( + PointType::enforcer, this->point_types, this->point_classifications, this->index_to_coord + )) + , common_points(get_kd_trees( + PointType::common, this->point_types, this->point_classifications, this->index_to_coord + )) + , blocked_points(get_kd_trees( + PointType::blocker, this->point_types, this->point_classifications, this->index_to_coord + )) {} + +Perimeter Perimeter::create_degenerate( + std::vector &&points, const double slice_z, const std::size_t layer_index +) { + std::vector point_types(points.size(), PointType::common); + std::vector + point_classifications(points.size(), PointClassification::common); + std::vector angles(points.size()); + std::vector angle_types(points.size(), AngleType::smooth); + Perimeter perimeter{ + slice_z, + layer_index, + false, + std::move(points), + std::move(angles), + std::move(point_types), + std::move(point_classifications), + std::move(angle_types)}; + perimeter.is_degenerate = true; + return perimeter; +} + +Perimeter Perimeter::create( + const Polygon &polygon, + const ModelInfo::Painting &painting, + const LayerInfo &layer_info, + const PerimeterParams ¶ms +) { + if (polygon.size() < 3) { + return Perimeter::create_degenerate( + Geometry::unscaled(polygon.points), layer_info.slice_z, layer_info.index + ); + } + std::vector points; + if (layer_info.elephant_foot_compensation > 0) { + const Polygons expanded{expand(polygon, scaled(layer_info.elephant_foot_compensation))}; + if (expanded.empty()) { + points = Geometry::unscaled(polygon.points); + } else { + points = Geometry::unscaled(expanded.front().points); + } + } else { + points = Geometry::unscaled(polygon.points); + } + + auto is_painted{[&](const Vec3f &point, const double radius) { + return painting.is_enforced(point, radius) || painting.is_blocked(point, radius); + }}; + + std::vector perimeter_points{ + Impl::oversample_painted(points, is_painted, layer_info.slice_z, params.oversampling_max_distance)}; + + std::vector point_types{ + Impl::get_point_types(perimeter_points, painting, layer_info.slice_z, params.painting_radius)}; + + std::tie(perimeter_points, point_types) = + Impl::remove_redundant_points(perimeter_points, point_types, params.simplification_epsilon); + + const std::vector embeddings{ + Geometry::get_embedding_distances(perimeter_points, layer_info.distancer)}; + std::optional> overhangs; + if (layer_info.previous_distancer) { + overhangs = Geometry::get_overhangs( + perimeter_points, *layer_info.previous_distancer, layer_info.height + ); + } + std::vector point_classifications{ + Impl::classify_points(embeddings, overhangs, params.overhang_threshold, params.embedding_threshold)}; + + std::vector smooth_angles{Geometry::get_vertex_angles(perimeter_points, params.smooth_angle_arm_length)}; + std::vector angles{Geometry::get_vertex_angles(perimeter_points, params.sharp_angle_arm_length)}; + std::vector angle_types{ + Impl::get_angle_types(angles, params.convex_threshold, params.concave_threshold)}; + std::vector smooth_angle_types{ + Impl::get_angle_types(smooth_angles, params.convex_threshold, params.concave_threshold)}; + angle_types = Impl::merge_angle_types(angle_types, smooth_angle_types, perimeter_points, params.smooth_angle_arm_length); + + const bool is_hole{polygon.is_clockwise()}; + + return Perimeter{ + layer_info.slice_z, + layer_info.index, + is_hole, + std::move(perimeter_points), + std::move(angles), + std::move(point_types), + std::move(point_classifications), + std::move(angle_types)}; +} + +Shells::Shells<> create_perimeters( + const std::vector> &shells, + const std::vector &layer_infos, + const ModelInfo::Painting &painting, + const PerimeterParams ¶ms +) { + std::vector> result; + result.reserve(shells.size()); + std::transform( + shells.begin(), shells.end(), std::back_inserter(result), + [](const Shells::Shell &shell) { return Shells::Shell<>(shell.size()); } + ); + + Geometry::iterate_nested(shells, [&](const std::size_t shell_index, const std::size_t polygon_index){ + const Shells::Shell &shell{shells[shell_index]}; + const Shells::Slice& slice{shell[polygon_index]}; + const Polygon &polygon{slice.boundary}; + const LayerInfo &layer_info{layer_infos[slice.layer_index]}; + result[shell_index][polygon_index] = {Perimeter::create(polygon, painting, layer_info, params), slice.layer_index}; + }); + return result; +} +} // namespace Slic3r::Seams::Perimeter diff --git a/src/libslic3r/GCode/SeamPerimeters.hpp b/src/libslic3r/GCode/SeamPerimeters.hpp new file mode 100644 index 0000000000..aaebe6e5a6 --- /dev/null +++ b/src/libslic3r/GCode/SeamPerimeters.hpp @@ -0,0 +1,190 @@ +#ifndef libslic3r_SeamPerimeters_hpp_ +#define libslic3r_SeamPerimeters_hpp_ + +#include + +#include "libslic3r/GCode/SeamPainting.hpp" +#include "libslic3r/KDTreeIndirect.hpp" + +#include "libslic3r/GCode/SeamShells.hpp" + +namespace Slic3r { + class Layer; +} + +namespace Slic3r::Seams::ModelInfo { +class Painting; +} + +namespace Slic3r::Seams::Perimeters { +enum class AngleType; +enum class PointType; +enum class PointClassification; +struct Perimeter; +struct PerimeterParams; + +struct LayerInfo +{ + static LayerInfo create( + const Slic3r::Layer &object_layer, std::size_t index, const double elephant_foot_compensation + ); + + AABBTreeLines::LinesDistancer distancer; + std::optional> previous_distancer; + std::size_t index; + double height{}; + double slice_z{}; + double elephant_foot_compensation; +}; + +using LayerInfos = std::vector; + +/** + * @brief Construct LayerInfo for each of the provided layers. + */ +LayerInfos get_layer_infos( + tcb::span object_layers, const double elephant_foot_compensation +); +} // namespace Slic3r::Seams::Perimeters + +namespace Slic3r::Seams::Perimeters::Impl { + + +/** + * @brief Split edges between points into multiple points if there is a painted point anywhere on + * the edge. + * + * The edge will be split by points no more than max_distance apart. + * Smaller max_distance -> more points. + * + * @return All the points (original and added) in order along the edges. + */ +std::vector oversample_painted( + const std::vector &points, + const std::function &is_painted, + const double slice_z, + const double max_distance +); + +/** + * @brief Call Duglas-Peucker for consecutive points of the same type. + * + * It never removes the first point and last point. + * + * @param tolerance Douglas-Peucker epsilon. + */ +std::pair, std::vector> remove_redundant_points( + const std::vector &points, + const std::vector &point_types, + const double tolerance +); + +} // namespace Slic3r::Seams::Perimeters::Impl + +namespace Slic3r::Seams::Perimeters { + +enum class AngleType { convex, concave, smooth }; + +enum class PointType { enforcer, blocker, common }; + +enum class PointClassification { overhang, embedded, common }; + +struct PerimeterParams +{ + double elephant_foot_compensation{}; + double oversampling_max_distance{}; + double embedding_threshold{}; + double overhang_threshold{}; + double convex_threshold{}; + double concave_threshold{}; + double painting_radius{}; + double simplification_epsilon{}; + double smooth_angle_arm_length{}; + double sharp_angle_arm_length{}; +}; + +struct Perimeter +{ + struct IndexToCoord + { + double operator()(const size_t index, size_t dim) const; + + tcb::span positions; + }; + + using PointTree = KDTreeIndirect<2, double, IndexToCoord>; + using OptionalPointTree = std::optional; + + struct PointTrees + { + OptionalPointTree embedded_points; + OptionalPointTree common_points; + OptionalPointTree overhanging_points; + }; + + Perimeter() = default; + + Perimeter( + const double slice_z, + const std::size_t layer_index, + const bool is_hole, + std::vector &&positions, + std::vector &&angles, + std::vector &&point_types, + std::vector &&point_classifications, + std::vector &&angle_types + ); + + static Perimeter create( + const Polygon &polygon, + const ModelInfo::Painting &painting, + const LayerInfo &layer_info, + const PerimeterParams ¶ms + ); + + static Perimeter create_degenerate( + std::vector &&points, const double slice_z, const std::size_t layer_index + ); + + bool is_degenerate{false}; + double slice_z{}; + bool is_hole{false}; + std::size_t layer_index{}; + std::vector positions{}; + std::vector angles{}; + IndexToCoord index_to_coord{}; + std::vector point_types{}; + std::vector point_classifications{}; + std::vector angle_types{}; + + PointTrees enforced_points{}; + PointTrees common_points{}; + PointTrees blocked_points{}; +}; + +/** + * @brief Create a Perimeter for each polygon in each of the shells. + */ +Shells::Shells create_perimeters( + const std::vector> &shells, + const std::vector &layer_infos, + const ModelInfo::Painting &painting, + const PerimeterParams ¶ms +); + +inline std::size_t get_layer_count( + const Shells::Shells<> &shells +) { + std::size_t layer_count{0}; + for (const Shells::Shell<> &shell : shells) { + for (const Shells::Slice<>& slice : shell) { + if (slice.layer_index >= layer_count) { + layer_count = slice.layer_index + 1; + } + } + } + return layer_count; +} +} // namespace Slic3r::Seams::Perimeters + +#endif // libslic3r_SeamPerimeters_hpp_ diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index ed0d480027..4fc380523e 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -1,1616 +1,360 @@ -///|/ Copyright (c) Prusa Research 2020 - 2023 VojtÄ›ch Bubník @bubnikv, Lukáš MatÄ›na @lukasmatena, Pavel MikuÅ¡ @Godrak +///|/ Copyright (c) Prusa Research 2020 - 2023 VojtÄ›ch Bubník @bubnikv, Lukáš MatÄ›na @lukasmatena, +/// Pavel MikuÅ¡ @Godrak ///|/ Copyright (c) SuperSlicer 2023 Remi Durand @supermerill ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ + +#include +#include #include "SeamPlacer.hpp" -#include "Color.hpp" -#include "Polygon.hpp" -#include "PrintConfig.hpp" -#include "tbb/parallel_for.h" -#include "tbb/blocked_range.h" -#include "tbb/parallel_reduce.h" -#include -#include -#include -#include +#include "libslic3r/GCode/SeamAligned.hpp" +#include "libslic3r/GCode/SeamRear.hpp" +#include "libslic3r/GCode/SeamRandom.hpp" +#include "libslic3r/GCode/ModelVisibility.hpp" +#include "libslic3r/GCode/SeamGeometry.hpp" -#include "libslic3r/AABBTreeLines.hpp" -#include "libslic3r/KDTreeIndirect.hpp" -#include "libslic3r/ExtrusionEntity.hpp" -#include "libslic3r/Print.hpp" -#include "libslic3r/BoundingBox.hpp" -#include "libslic3r/ClipperUtils.hpp" -#include "libslic3r/Layer.hpp" +namespace Slic3r::Seams { -#include "libslic3r/Geometry/Curves.hpp" -#include "libslic3r/ShortEdgeCollapse.hpp" -#include "libslic3r/TriangleSetSampling.hpp" +using ObjectShells = std::vector>>; +using ObjectPainting = std::map; -#include "libslic3r/Utils.hpp" +ObjectShells partition_to_shells( + SpanOfConstPtrs objects, + const Params ¶ms, + const ObjectPainting& object_painting, + const std::function &throw_if_canceled +) { + ObjectShells result; -//#define DEBUG_FILES + for (const PrintObject *print_object : objects) { + const ModelInfo::Painting &painting{object_painting.at(print_object)}; + throw_if_canceled(); -#ifdef DEBUG_FILES -#include -#include -#endif + const std::vector extrusions{ + Geometry::get_extrusions(print_object->layers())}; + const Perimeters::LayerInfos layer_infos{Perimeters::get_layer_infos( + print_object->layers(), params.perimeter.elephant_foot_compensation + )}; + Shells::Shells shell_polygons{ + Shells::create_shells(extrusions, params.max_distance)}; -namespace Slic3r { - -namespace SeamPlacerImpl { - -template int sgn(T val) { - return int(T(0) < val) - int(val < T(0)); -} - -// base function: ((e^(((1)/(x^(2)+1)))-1)/(e-1)) -// checkout e.g. here: https://www.geogebra.org/calculator -float gauss(float value, float mean_x_coord, float mean_value, float falloff_speed) { - float shifted = value - mean_x_coord; - float denominator = falloff_speed * shifted * shifted + 1.0f; - float exponent = 1.0f / denominator; - return mean_value * (std::exp(exponent) - 1.0f) / (std::exp(1.0f) - 1.0f); -} - -float compute_angle_penalty(float ccw_angle) { - // This function is used: - // ((ℯ^(((1)/(x^(2)*3+1)))-1)/(ℯ-1))*1+((1)/(2+ℯ^(-x))) - // looks scary, but it is gaussian combined with sigmoid, - // so that concave points have much smaller penalty over convex ones - // https://github.com/prusa3d/PrusaSlicer/tree/master/doc/seam_placement/corner_penalty_function.png - return gauss(ccw_angle, 0.0f, 1.0f, 3.0f) + - 1.0f / (2 + std::exp(-ccw_angle)); -} - -/// Coordinate frame -class Frame { -public: - Frame() { - mX = Vec3f(1, 0, 0); - mY = Vec3f(0, 1, 0); - mZ = Vec3f(0, 0, 1); + Shells::Shells<> perimeters{ + Perimeters::create_perimeters(shell_polygons, layer_infos, painting, params.perimeter)}; + throw_if_canceled(); + result.emplace_back(print_object, std::move(perimeters)); } - - Frame(const Vec3f &x, const Vec3f &y, const Vec3f &z) : - mX(x), mY(y), mZ(z) { - } - - void set_from_z(const Vec3f &z) { - mZ = z.normalized(); - Vec3f tmpZ = mZ; - Vec3f tmpX = (std::abs(tmpZ.x()) > 0.99f) ? Vec3f(0, 1, 0) : Vec3f(1, 0, 0); - mY = (tmpZ.cross(tmpX)).normalized(); - mX = mY.cross(tmpZ); - } - - Vec3f to_world(const Vec3f &a) const { - return a.x() * mX + a.y() * mY + a.z() * mZ; - } - - Vec3f to_local(const Vec3f &a) const { - return Vec3f(mX.dot(a), mY.dot(a), mZ.dot(a)); - } - - const Vec3f& binormal() const { - return mX; - } - - const Vec3f& tangent() const { - return mY; - } - - const Vec3f& normal() const { - return mZ; - } - -private: - Vec3f mX, mY, mZ; -}; - -Vec3f sample_sphere_uniform(const Vec2f &samples) { - float term1 = 2.0f * float(PI) * samples.x(); - float term2 = 2.0f * sqrt(samples.y() - samples.y() * samples.y()); - return {cos(term1) * term2, sin(term1) * term2, - 1.0f - 2.0f * samples.y()}; -} - -Vec3f sample_hemisphere_uniform(const Vec2f &samples) { - float term1 = 2.0f * float(PI) * samples.x(); - float term2 = 2.0f * sqrt(samples.y() - samples.y() * samples.y()); - return {cos(term1) * term2, sin(term1) * term2, - abs(1.0f - 2.0f * samples.y())}; -} - -Vec3f sample_power_cosine_hemisphere(const Vec2f &samples, float power) { - float term1 = 2.f * float(PI) * samples.x(); - float term2 = pow(samples.y(), 1.f / (power + 1.f)); - float term3 = sqrt(1.f - term2 * term2); - - return Vec3f(cos(term1) * term3, sin(term1) * term3, term2); -} - -std::vector raycast_visibility(const AABBTreeIndirect::Tree<3, float> &raycasting_tree, - const indexed_triangle_set &triangles, - const TriangleSetSamples &samples, - size_t negative_volumes_start_index) { - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: raycast visibility of " << samples.positions.size() << " samples over " << triangles.indices.size() - << " triangles: end"; - - //prepare uniform samples of a hemisphere - float step_size = 1.0f / SeamPlacer::sqr_rays_per_sample_point; - std::vector precomputed_sample_directions( - SeamPlacer::sqr_rays_per_sample_point * SeamPlacer::sqr_rays_per_sample_point); - for (size_t x_idx = 0; x_idx < SeamPlacer::sqr_rays_per_sample_point; ++x_idx) { - float sample_x = x_idx * step_size + step_size / 2.0; - for (size_t y_idx = 0; y_idx < SeamPlacer::sqr_rays_per_sample_point; ++y_idx) { - size_t dir_index = x_idx * SeamPlacer::sqr_rays_per_sample_point + y_idx; - float sample_y = y_idx * step_size + step_size / 2.0; - precomputed_sample_directions[dir_index] = sample_hemisphere_uniform( { sample_x, sample_y }); - } - } - - bool model_contains_negative_parts = negative_volumes_start_index < triangles.indices.size(); - - std::vector result(samples.positions.size()); - tbb::parallel_for(tbb::blocked_range(0, result.size()), - [&triangles, &precomputed_sample_directions, model_contains_negative_parts, negative_volumes_start_index, - &raycasting_tree, &result, &samples](tbb::blocked_range r) { - // Maintaining hits memory outside of the loop, so it does not have to be reallocated for each query. - std::vector hits; - for (size_t s_idx = r.begin(); s_idx < r.end(); ++s_idx) { - result[s_idx] = 1.0f; - constexpr float decrease_step = 1.0f - / (SeamPlacer::sqr_rays_per_sample_point * SeamPlacer::sqr_rays_per_sample_point); - - const Vec3f ¢er = samples.positions[s_idx]; - const Vec3f &normal = samples.normals[s_idx]; - // apply the local direction via Frame struct - the local_dir is with respect to +Z being forward - Frame f; - f.set_from_z(normal); - - for (const auto &dir : precomputed_sample_directions) { - Vec3f final_ray_dir = (f.to_world(dir)); - if (!model_contains_negative_parts) { - igl::Hit hitpoint; - // FIXME: This AABBTTreeIndirect query will not compile for float ray origin and - // direction. - Vec3d final_ray_dir_d = final_ray_dir.cast(); - Vec3d ray_origin_d = (center + normal * 0.01f).cast(); // start above surface. - bool hit = AABBTreeIndirect::intersect_ray_first_hit(triangles.vertices, - triangles.indices, raycasting_tree, ray_origin_d, final_ray_dir_d, hitpoint); - if (hit && its_face_normal(triangles, hitpoint.id).dot(final_ray_dir) <= 0) { - result[s_idx] -= decrease_step; - } - } else { //TODO improve logic for order based boolean operations - consider order of volumes - bool casting_from_negative_volume = samples.triangle_indices[s_idx] - >= negative_volumes_start_index; - - Vec3d ray_origin_d = (center + normal * 0.01f).cast(); // start above surface. - if (casting_from_negative_volume) { // if casting from negative volume face, invert direction, change start pos - final_ray_dir = -1.0 * final_ray_dir; - ray_origin_d = (center - normal * 0.01f).cast(); - } - Vec3d final_ray_dir_d = final_ray_dir.cast(); - bool some_hit = AABBTreeIndirect::intersect_ray_all_hits(triangles.vertices, - triangles.indices, raycasting_tree, - ray_origin_d, final_ray_dir_d, hits); - if (some_hit) { - int counter = 0; - // NOTE: iterating in reverse, from the last hit for one simple reason: We know the state of the ray at that point; - // It cannot be inside model, and it cannot be inside negative volume - for (int hit_index = int(hits.size()) - 1; hit_index >= 0; --hit_index) { - Vec3f face_normal = its_face_normal(triangles, hits[hit_index].id); - if (hits[hit_index].id >= int(negative_volumes_start_index)) { //negative volume hit - counter -= sgn(face_normal.dot(final_ray_dir)); // if volume face aligns with ray dir, we are leaving negative space - // which in reverse hit analysis means, that we are entering negative space :) and vice versa - } else { - counter += sgn(face_normal.dot(final_ray_dir)); - } - } - if (counter == 0) { - result[s_idx] -= decrease_step; - } - } - } - } - } - }); - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: raycast visibility of " << samples.positions.size() << " samples over " << triangles.indices.size() - << " triangles: end"; - return result; } -std::vector calculate_polygon_angles_at_vertices(const Polygon &polygon, const std::vector &lengths, - float min_arm_length) { - std::vector result(polygon.size()); +ObjectSeams precalculate_seams( + const Params ¶ms, + ObjectShells &&seam_data, + const std::function &throw_if_canceled +) { + ObjectSeams result; - if (polygon.size() == 1) { - result[0] = 0.0f; - } + for (auto &[print_object, shells] : seam_data) { + switch (params.seam_preference) { + case spAligned: { + const Transform3d transformation{print_object->trafo_centered()}; + const ModelVolumePtrs &volumes{print_object->model_object()->volumes}; - size_t idx_prev = 0; - size_t idx_curr = 0; - size_t idx_next = 0; + Slic3r::ModelInfo::Visibility + points_visibility{transformation, volumes, params.visibility, throw_if_canceled}; + throw_if_canceled(); + const Aligned::VisibilityCalculator visibility_calculator{ + points_visibility, params.convex_visibility_modifier, + params.concave_visibility_modifier}; - float distance_to_prev = 0; - float distance_to_next = 0; - - //push idx_prev far enough back as initialization - while (distance_to_prev < min_arm_length) { - idx_prev = Slic3r::prev_idx_modulo(idx_prev, polygon.size()); - distance_to_prev += lengths[idx_prev]; - } - - for (size_t _i = 0; _i < polygon.size(); ++_i) { - // pull idx_prev to current as much as possible, while respecting the min_arm_length - while (distance_to_prev - lengths[idx_prev] > min_arm_length) { - distance_to_prev -= lengths[idx_prev]; - idx_prev = Slic3r::next_idx_modulo(idx_prev, polygon.size()); + result[print_object] = Aligned::get_object_seams( + std::move(shells), visibility_calculator, params.aligned + ); + break; } - - //push idx_next forward as far as needed - while (distance_to_next < min_arm_length) { - distance_to_next += lengths[idx_next]; - idx_next = Slic3r::next_idx_modulo(idx_next, polygon.size()); + case spRear: { + result[print_object] = Rear::get_object_seams(std::move(shells), params.rear_project_threshold); + break; } - - // Calculate angle between idx_prev, idx_curr, idx_next. - const Point &p0 = polygon.points[idx_prev]; - const Point &p1 = polygon.points[idx_curr]; - const Point &p2 = polygon.points[idx_next]; - result[idx_curr] = float(angle(p1 - p0, p2 - p1)); - - // increase idx_curr by one - float curr_distance = lengths[idx_curr]; - idx_curr++; - distance_to_prev += curr_distance; - distance_to_next -= curr_distance; + case spRandom: { + result[print_object] = Random::get_object_seams(std::move(shells), params.random_seed); + break; + } + case spNearest: { + throw std::runtime_error("Cannot precalculate seams for nearest position!"); + } + } + throw_if_canceled(); } - return result; } -struct CoordinateFunctor { - const std::vector *coordinates; - CoordinateFunctor(const std::vector *coords) : - coordinates(coords) { - } - CoordinateFunctor() : - coordinates(nullptr) { +Params Placer::get_params(const DynamicPrintConfig &config) { + Params params{}; + + params.perimeter.elephant_foot_compensation = config.opt_float("elefant_foot_compensation"); + if (config.opt_int("raft_layers") > 0) { + params.perimeter.elephant_foot_compensation = 0.0; } + params.random_seed = 1653710332u; - const float& operator()(size_t idx, size_t dim) const { - return coordinates->operator [](idx)[dim]; - } -}; + params.aligned.max_detour = 1.0; + params.aligned.continuity_modifier = 2.0; + params.convex_visibility_modifier = 1.1; + params.concave_visibility_modifier = 0.9; + params.perimeter.overhang_threshold = Slic3r::Geometry::deg2rad(55.0); + params.perimeter.convex_threshold = Slic3r::Geometry::deg2rad(10.0); + params.perimeter.concave_threshold = Slic3r::Geometry::deg2rad(15.0); -// structure to store global information about the model - occlusion hits, enforcers, blockers -struct GlobalModelInfo { - TriangleSetSamples mesh_samples; - std::vector mesh_samples_visibility; - CoordinateFunctor mesh_samples_coordinate_functor; - KDTreeIndirect<3, float, CoordinateFunctor> mesh_samples_tree { CoordinateFunctor { } }; - float mesh_samples_radius; + params.seam_preference = config.opt_enum("seam_position"); + params.staggered_inner_seams = config.opt_bool("staggered_inner_seams"); - indexed_triangle_set enforcers; - indexed_triangle_set blockers; - AABBTreeIndirect::Tree<3, float> enforcers_tree; - AABBTreeIndirect::Tree<3, float> blockers_tree; + params.max_nearest_detour = 1.0; + params.rear_project_threshold = 0.05; // % + params.aligned.jump_visibility_threshold = 0.6; + params.max_distance = 5.0; + params.perimeter.oversampling_max_distance = 0.2; + params.perimeter.embedding_threshold = 0.5; + params.perimeter.painting_radius = 0.1; + params.perimeter.simplification_epsilon = 0.001; + params.perimeter.smooth_angle_arm_length = 0.2; + params.perimeter.sharp_angle_arm_length = 0.05; - bool is_enforced(const Vec3f &position, float radius) const { - if (enforcers.empty()) { - return false; - } - float radius_sqr = radius * radius; - return AABBTreeIndirect::is_any_triangle_in_radius(enforcers.vertices, enforcers.indices, - enforcers_tree, position, radius_sqr); - } + params.visibility.raycasting_visibility_samples_count = 30000; + params.visibility.fast_decimation_triangle_count_target = 16000; + params.visibility.sqr_rays_per_sample_point = 5; - bool is_blocked(const Vec3f &position, float radius) const { - if (blockers.empty()) { - return false; - } - float radius_sqr = radius * radius; - return AABBTreeIndirect::is_any_triangle_in_radius(blockers.vertices, blockers.indices, - blockers_tree, position, radius_sqr); - } - - float calculate_point_visibility(const Vec3f &position) const { - std::vector points = find_nearby_points(mesh_samples_tree, position, mesh_samples_radius); - if (points.empty()) { - return 1.0f; - } - - auto compute_dist_to_plane = [](const Vec3f &position, const Vec3f &plane_origin, const Vec3f &plane_normal) { - Vec3f orig_to_point = position - plane_origin; - return std::abs(orig_to_point.dot(plane_normal)); - }; - - float total_weight = 0; - float total_visibility = 0; - for (size_t i = 0; i < points.size(); ++i) { - size_t sample_idx = points[i]; - - Vec3f sample_point = this->mesh_samples.positions[sample_idx]; - Vec3f sample_normal = this->mesh_samples.normals[sample_idx]; - - float weight = mesh_samples_radius - compute_dist_to_plane(position, sample_point, sample_normal); - weight += (mesh_samples_radius - (position - sample_point).norm()); - total_visibility += weight * mesh_samples_visibility[sample_idx]; - total_weight += weight; - } - - return total_visibility / total_weight; - - } - -#ifdef DEBUG_FILES - void debug_export(const indexed_triangle_set &obj_mesh) const { - - indexed_triangle_set divided_mesh = obj_mesh; - Slic3r::CNumericLocalesSetter locales_setter; - - { - auto filename = debug_out_path("visiblity.obj"); - FILE *fp = boost::nowide::fopen(filename.c_str(), "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "stl_write_obj: Couldn't open " << filename << " for writing"; - return; - } - - for (size_t i = 0; i < divided_mesh.vertices.size(); ++i) { - float visibility = calculate_point_visibility(divided_mesh.vertices[i]); - Vec3f color = value_to_rgbf(0.0f, 1.0f, visibility); - fprintf(fp, "v %f %f %f %f %f %f\n", - divided_mesh.vertices[i](0), divided_mesh.vertices[i](1), divided_mesh.vertices[i](2), - color(0), color(1), color(2)); - } - for (size_t i = 0; i < divided_mesh.indices.size(); ++i) - fprintf(fp, "f %d %d %d\n", divided_mesh.indices[i][0] + 1, divided_mesh.indices[i][1] + 1, - divided_mesh.indices[i][2] + 1); - fclose(fp); - } - - { - auto filename = debug_out_path("visiblity_samples.obj"); - FILE *fp = boost::nowide::fopen(filename.c_str(), "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "stl_write_obj: Couldn't open " << filename << " for writing"; - return; - } - - for (size_t i = 0; i < mesh_samples.positions.size(); ++i) { - float visibility = mesh_samples_visibility[i]; - Vec3f color = value_to_rgbf(0.0f, 1.0f, visibility); - fprintf(fp, "v %f %f %f %f %f %f\n", - mesh_samples.positions[i](0), mesh_samples.positions[i](1), mesh_samples.positions[i](2), - color(0), color(1), color(2)); - } - fclose(fp); - } - - } -#endif + return params; } -; -//Extract perimeter polygons of the given layer -Polygons extract_perimeter_polygons(const Layer *layer, std::vector &corresponding_regions_out) { - Polygons polygons; +ObjectLayerPerimeters sort_to_layers(ObjectShells &&object_shells) { + ObjectLayerPerimeters result; + for (auto &[print_object, shells] : object_shells) { + const std::size_t layer_count{print_object->layer_count()}; + result[print_object] = LayerPerimeters(layer_count); + + for (Shells::Shell<> &shell : shells) { + for (Shells::Slice<> &slice : shell) { + const BoundingBox bounding_box{Geometry::scaled(slice.boundary.positions)}; + result[print_object][slice.layer_index].push_back( + BoundedPerimeter{std::move(slice.boundary), bounding_box} + ); + } + } + } + return result; +} + +void Placer::init( + SpanOfConstPtrs objects, + const Params ¶ms, + const std::function &throw_if_canceled +) { + BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: init: start"; + + ObjectPainting object_painting; + for (const PrintObject *print_object : objects) { + const Transform3d transformation{print_object->trafo_centered()}; + const ModelVolumePtrs &volumes{print_object->model_object()->volumes}; + object_painting.emplace(print_object, ModelInfo::Painting{transformation, volumes}); + } + + ObjectShells seam_data{partition_to_shells(objects, params, object_painting, throw_if_canceled)}; + this->params = params; + + if (this->params.seam_preference != spNearest) { + this->seams_per_object = + precalculate_seams(params, std::move(seam_data), throw_if_canceled); + } else { + this->perimeters_per_layer = sort_to_layers(std::move(seam_data)); + } + + BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: init: end"; +} + +const SeamPerimeterChoice &choose_closest_seam( + const std::vector &seams, const Polygon &loop_polygon +) { + BoundingBoxes choose_from; + choose_from.reserve(seams.size()); + for (const SeamPerimeterChoice &choice : seams) { + choose_from.push_back(choice.bounding_box); + } + + const std::size_t choice_index{ + Geometry::pick_closest_bounding_box(loop_polygon.bounding_box(), choose_from).first}; + + return seams[choice_index]; +} + +std::pair project_to_extrusion_loop( + const SeamChoice &seam_choice, const Perimeters::Perimeter &perimeter, const Linesf &loop_lines +) { + const AABBTreeLines::LinesDistancer distancer{loop_lines}; + + const bool is_at_vertex{seam_choice.previous_index == seam_choice.next_index}; + const Vec2d edge{ + perimeter.positions[seam_choice.next_index] - + perimeter.positions[seam_choice.previous_index]}; + const Vec2d normal{ + is_at_vertex ? + Geometry::get_polygon_normal(perimeter.positions, seam_choice.previous_index, 0.1) : + Geometry::get_normal(edge)}; + + double depth{distancer.distance_from_lines(seam_choice.position)}; + const Vec2d final_position{seam_choice.position - normal * depth}; + + auto [_, loop_line_index, loop_point] = distancer.distance_from_lines_extra(final_position + ); + return {loop_line_index, loop_point}; +} + +std::optional offset_along_loop_lines( + const Vec2d &point, + const std::size_t loop_line_index, + const Linesf &loop_lines, + const double offset +) { + double distance{0}; + Vec2d previous_point{point}; + std::optional offset_point; + Geometry::visit_forward(loop_line_index, loop_lines.size(), [&](std::size_t index) { + const Vec2d next_point{loop_lines[index].b}; + const Vec2d edge{next_point - previous_point}; + + if (distance + edge.norm() > offset) { + const double remaining_distance{offset - distance}; + offset_point = previous_point + remaining_distance * edge.normalized(); + return true; + } + + distance += edge.norm(); + previous_point = next_point; + + return false; + }); + + return offset_point; +} + +double get_angle(const SeamChoice &seam_choice, const Perimeters::Perimeter &perimeter) { + const bool is_at_vertex{seam_choice.previous_index == seam_choice.next_index}; + return is_at_vertex ? perimeter.angles[seam_choice.previous_index] : 0.0; +} + +Point finalize_seam_position( + const Polygon &loop_polygon, + const SeamChoice &seam_choice, + const Perimeters::Perimeter &perimeter, + const double loop_width, + const bool do_staggering +) { + const Linesf loop_lines{to_unscaled_linesf({ExPolygon{loop_polygon}})}; + const auto [loop_line_index, loop_point]{ + project_to_extrusion_loop(seam_choice, perimeter, loop_lines)}; + + // ExtrusionRole::Perimeter is inner perimeter. + if (do_staggering) { + const double depth = (loop_point - seam_choice.position).norm() - + loop_width / 2.0; + const double angle{get_angle(seam_choice, perimeter)}; + const double initial_offset{angle > 0 ? angle / 2.0 * depth : 0.0}; + const double additional_offset{angle < 0 ? std::cos(angle / 2.0) * depth : depth}; + + const double staggering_offset{initial_offset + additional_offset}; + + std::optional staggered_point{ + offset_along_loop_lines(loop_point, loop_line_index, loop_lines, staggering_offset)}; + + if (staggered_point) { + return scaled(*staggered_point); + } + } + + return scaled(loop_point); +} + +std::pair place_seam_near( + const std::vector &layer_perimeters, + const ExtrusionLoop &loop, + const Point &position, + const double max_detour +) { + BoundingBoxes choose_from; + choose_from.reserve(layer_perimeters.size()); + for (const BoundedPerimeter &perimeter : layer_perimeters) { + choose_from.push_back(perimeter.bounding_box); + } + + const Polygon loop_polygon{Geometry::to_polygon(loop)}; + + const std::size_t choice_index{ + Geometry::pick_closest_bounding_box(loop_polygon.bounding_box(), choose_from).first}; + + Seams::Aligned::Impl::Nearest nearest{unscaled(position), max_detour}; + + const SeamChoice choice{Seams::choose_seam_point(layer_perimeters[choice_index].perimeter, nearest)}; + + return {choice, choice_index}; +} + +int get_perimeter_count(const Layer *layer){ + int count{0}; for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters()) { if (ex_entity->is_collection()) { //collection of inner, outer, and overhang perimeters - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - ExtrusionRole role = perimeter->role(); - if (perimeter->is_loop()) { - for (const ExtrusionPath &path : static_cast(perimeter)->paths) { - if (path.role() == ExtrusionRole::ExternalPerimeter) { - role = ExtrusionRole::ExternalPerimeter; - } - } - } - - if (role == ExtrusionRole::ExternalPerimeter) { - Points p; - perimeter->collect_points(p); - polygons.emplace_back(std::move(p)); - corresponding_regions_out.push_back(layer_region); - } - } - if (polygons.empty()) { - Points p; - ex_entity->collect_points(p); - polygons.emplace_back(std::move(p)); - corresponding_regions_out.push_back(layer_region); - } - } else { - Points p; - ex_entity->collect_points(p); - polygons.emplace_back(std::move(p)); - corresponding_regions_out.push_back(layer_region); + count += static_cast(ex_entity)->entities.size(); + } + else { + count += 1; } } } - - if (polygons.empty()) { // If there are no perimeter polygons for whatever reason (disabled perimeters .. ) insert dummy point - // it is easier than checking everywhere if the layer is not emtpy, no seam will be placed to this layer anyway - polygons.emplace_back(Points{ { 0, 0 } }); - corresponding_regions_out.push_back(nullptr); - } - - return polygons; + return count; } -// Insert SeamCandidates created from perimeter polygons in to the result vector. -// Compute its type (Enfrocer,Blocker), angle, and position -//each SeamCandidate also contains pointer to shared Perimeter structure representing the polygon -// if Custom Seam modifiers are present, oversamples the polygon if necessary to better fit user intentions -void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const LayerRegion *region, - const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result) { - if (orig_polygon.size() == 0) { - return; - } - Polygon polygon = orig_polygon; - bool was_clockwise = polygon.make_counter_clockwise(); - float angle_arm_len = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).nozzle_diameter() : 0.5f; - - std::vector lengths { }; - for (size_t point_idx = 0; point_idx < polygon.size() - 1; ++point_idx) { - lengths.push_back((unscale(polygon[point_idx]) - unscale(polygon[point_idx + 1])).norm()); - } - lengths.push_back(std::max((unscale(polygon[0]) - unscale(polygon[polygon.size() - 1])).norm(), 0.1)); - std::vector polygon_angles = calculate_polygon_angles_at_vertices(polygon, lengths, - angle_arm_len); - - result.perimeters.push_back( { }); - Perimeter &perimeter = result.perimeters.back(); - - std::queue orig_polygon_points { }; - for (size_t index = 0; index < polygon.size(); ++index) { - Vec2f unscaled_p = unscale(polygon[index]).cast(); - orig_polygon_points.emplace(unscaled_p.x(), unscaled_p.y(), z_coord); - } - Vec3f first = orig_polygon_points.front(); - std::queue oversampled_points { }; - size_t orig_angle_index = 0; - perimeter.start_index = result.points.size(); - perimeter.flow_width = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).width() : 0.0f; - bool some_point_enforced = false; - while (!orig_polygon_points.empty() || !oversampled_points.empty()) { - EnforcedBlockedSeamPoint type = EnforcedBlockedSeamPoint::Neutral; - Vec3f position; - float local_ccw_angle = 0; - bool orig_point = false; - if (!oversampled_points.empty()) { - position = oversampled_points.front(); - oversampled_points.pop(); - } else { - position = orig_polygon_points.front(); - orig_polygon_points.pop(); - local_ccw_angle = was_clockwise ? -polygon_angles[orig_angle_index] : polygon_angles[orig_angle_index]; - orig_angle_index++; - orig_point = true; - } - - if (global_model_info.is_enforced(position, perimeter.flow_width)) { - type = EnforcedBlockedSeamPoint::Enforced; - } - - if (global_model_info.is_blocked(position, perimeter.flow_width)) { - type = EnforcedBlockedSeamPoint::Blocked; - } - some_point_enforced = some_point_enforced || type == EnforcedBlockedSeamPoint::Enforced; - - if (orig_point) { - Vec3f pos_of_next = orig_polygon_points.empty() ? first : orig_polygon_points.front(); - float distance_to_next = (position - pos_of_next).norm(); - if (global_model_info.is_enforced(position, distance_to_next)) { - Vec3f vec_to_next = (pos_of_next - position).normalized(); - float step_size = SeamPlacer::enforcer_oversampling_distance; - float step = step_size; - while (step < distance_to_next) { - oversampled_points.push(position + vec_to_next * step); - step += step_size; - } - } - } - - result.points.emplace_back(position, perimeter, local_ccw_angle, type); - } - - perimeter.end_index = result.points.size(); - - if (some_point_enforced) { - // We will patches of enforced points (patch: continuous section of enforced points), choose - // the longest patch, and select the middle point or sharp point (depending on the angle) - // this point will have high priority on this perimeter - size_t perimeter_size = perimeter.end_index - perimeter.start_index; - const auto next_index = [&](size_t idx) { - return perimeter.start_index + Slic3r::next_idx_modulo(idx - perimeter.start_index, perimeter_size); - }; - - std::vector patches_starts_ends; - for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) { - if (result.points[i].type != EnforcedBlockedSeamPoint::Enforced && - result.points[next_index(i)].type == EnforcedBlockedSeamPoint::Enforced) { - patches_starts_ends.push_back(next_index(i)); - } - if (result.points[i].type == EnforcedBlockedSeamPoint::Enforced && - result.points[next_index(i)].type != EnforcedBlockedSeamPoint::Enforced) { - patches_starts_ends.push_back(next_index(i)); - } - } - //if patches_starts_ends are empty, it means that the whole perimeter is enforced.. don't do anything in that case - if (!patches_starts_ends.empty()) { - //if the first point in the patches is not enforced, it marks a patch end. in that case, put it to the end and start on next - // to simplify the processing - assert(patches_starts_ends.size() % 2 == 0); - bool start_on_second = false; - if (result.points[patches_starts_ends[0]].type != EnforcedBlockedSeamPoint::Enforced) { - start_on_second = true; - patches_starts_ends.push_back(patches_starts_ends[0]); - } - //now pick the longest patch - std::pair longest_patch { 0, 0 }; - auto patch_len = [perimeter_size](const std::pair &start_end) { - if (start_end.second < start_end.first) { - return start_end.first + (perimeter_size - start_end.second); - } else { - return start_end.second - start_end.first; - } - }; - for (size_t patch_idx = start_on_second ? 1 : 0; patch_idx < patches_starts_ends.size(); patch_idx += 2) { - std::pair current_patch { patches_starts_ends[patch_idx], patches_starts_ends[patch_idx - + 1] }; - if (patch_len(longest_patch) < patch_len(current_patch)) { - longest_patch = current_patch; - } - } - std::vector viable_points_indices; - std::vector large_angle_points_indices; - for (size_t point_idx = longest_patch.first; point_idx != longest_patch.second; - point_idx = next_index(point_idx)) { - viable_points_indices.push_back(point_idx); - if (std::abs(result.points[point_idx].local_ccw_angle) - > SeamPlacer::sharp_angle_snapping_threshold) { - large_angle_points_indices.push_back(point_idx); - } - } - assert(viable_points_indices.size() > 0); - if (large_angle_points_indices.empty()) { - size_t central_idx = viable_points_indices[viable_points_indices.size() / 2]; - result.points[central_idx].central_enforcer = true; - } else { - size_t central_idx = large_angle_points_indices.size() / 2; - result.points[large_angle_points_indices[central_idx]].central_enforcer = true; - } - } - } - -} - -// Get index of previous and next perimeter point of the layer. Because SeamCandidates of all polygons of the given layer -// are sequentially stored in the vector, each perimeter contains info about start and end index. These vales are used to -// deduce index of previous and next neigbour in the corresponding perimeter. -std::pair find_previous_and_next_perimeter_point(const std::vector &perimeter_points, - size_t point_index) { - const SeamCandidate ¤t = perimeter_points[point_index]; - int prev = point_index - 1; //for majority of points, it is true that neighbours lie behind and in front of them in the vector - int next = point_index + 1; - - if (point_index == current.perimeter.start_index) { - // if point_index is equal to start, it means that the previous neighbour is at the end - prev = current.perimeter.end_index; - } - - if (point_index == current.perimeter.end_index - 1) { - // if point_index is equal to end, than next neighbour is at the start - next = current.perimeter.start_index; - } - - assert(prev >= 0); - assert(next >= 0); - return {size_t(prev),size_t(next)}; -} - -// Computes all global model info - transforms object, performs raycasting -void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po, - std::function throw_if_canceled) { - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: gather occlusion meshes: start"; - auto obj_transform = po->trafo_centered(); - indexed_triangle_set triangle_set; - indexed_triangle_set negative_volumes_set; - //add all parts - for (const ModelVolume *model_volume : po->model_object()->volumes) { - if (model_volume->type() == ModelVolumeType::MODEL_PART - || model_volume->type() == ModelVolumeType::NEGATIVE_VOLUME) { - auto model_transformation = model_volume->get_matrix(); - indexed_triangle_set model_its = model_volume->mesh().its; - its_transform(model_its, model_transformation); - if (model_volume->type() == ModelVolumeType::MODEL_PART) { - its_merge(triangle_set, model_its); - } else { - its_merge(negative_volumes_set, model_its); - } - } - } - throw_if_canceled(); - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: gather occlusion meshes: end"; - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: decimate: start"; - its_short_edge_collpase(triangle_set, SeamPlacer::fast_decimation_triangle_count_target); - its_short_edge_collpase(negative_volumes_set, SeamPlacer::fast_decimation_triangle_count_target); - - size_t negative_volumes_start_index = triangle_set.indices.size(); - its_merge(triangle_set, negative_volumes_set); - its_transform(triangle_set, obj_transform); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: decimate: end"; - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: Compute visibility sample points: start"; - - result.mesh_samples = sample_its_uniform_parallel(SeamPlacer::raycasting_visibility_samples_count, - triangle_set); - result.mesh_samples_coordinate_functor = CoordinateFunctor(&result.mesh_samples.positions); - result.mesh_samples_tree = KDTreeIndirect<3, float, CoordinateFunctor>(result.mesh_samples_coordinate_functor, - result.mesh_samples.positions.size()); - - // The following code determines search area for random visibility samples on the mesh when calculating visibility of each perimeter point - // number of random samples in the given radius (area) is approximately poisson distribution - // to compute ideal search radius (area), we use exponential distribution (complementary distr to poisson) - // parameters of exponential distribution to compute area that will have with probability="probability" more than given number of samples="samples" - float probability = 0.9f; - float samples = 4; - float density = SeamPlacer::raycasting_visibility_samples_count / result.mesh_samples.total_area; - // exponential probability distrubtion function is : f(x) = P(X > x) = e^(l*x) where l is the rate parameter (computed as 1/u where u is mean value) - // probability that sampled area A with S samples contains more than samples count: - // P(S > samples in A) = e^-(samples/(density*A)); express A: - float search_area = samples / (-logf(probability) * density); - float search_radius = sqrt(search_area / PI); - result.mesh_samples_radius = search_radius; - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: Compute visiblity sample points: end"; - throw_if_canceled(); - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: Mesh sample raidus: " << result.mesh_samples_radius; - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: build AABB tree: start"; - auto raycasting_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(triangle_set.vertices, - triangle_set.indices); - - throw_if_canceled(); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: build AABB tree: end"; - result.mesh_samples_visibility = raycast_visibility(raycasting_tree, triangle_set, result.mesh_samples, - negative_volumes_start_index); - throw_if_canceled(); -#ifdef DEBUG_FILES - result.debug_export(triangle_set); -#endif -} - -void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) { - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start"; - - auto obj_transform = po->trafo_centered(); - - for (const ModelVolume *mv : po->model_object()->volumes) { - if (mv->is_seam_painted()) { - auto model_transformation = obj_transform * mv->get_matrix(); - - indexed_triangle_set enforcers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER); - its_transform(enforcers, model_transformation); - its_merge(result.enforcers, enforcers); - - indexed_triangle_set blockers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::BLOCKER); - its_transform(blockers, model_transformation); - its_merge(result.blockers, blockers); - } - } - - result.enforcers_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(result.enforcers.vertices, - result.enforcers.indices); - result.blockers_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(result.blockers.vertices, - result.blockers.indices); - - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: end"; -} - -struct SeamComparator { - SeamPosition setup; - float angle_importance; - explicit SeamComparator(SeamPosition setup) : - setup(setup) { - angle_importance = - setup == spNearest ? SeamPlacer::angle_importance_nearest : SeamPlacer::angle_importance_aligned; - } - - // Standard comparator, must respect the requirements of comparators (e.g. give same result on same inputs) for sorting usage - // should return if a is better seamCandidate than b - bool is_first_better(const SeamCandidate &a, const SeamCandidate &b, const Vec2f &preffered_location = Vec2f { 0.0f, - 0.0f }) const { - if (setup == SeamPosition::spAligned && a.central_enforcer != b.central_enforcer) { - return a.central_enforcer; - } - - // Blockers/Enforcers discrimination, top priority - if (a.type != b.type) { - return a.type > b.type; - } - - //avoid overhangs - if (a.overhang > 0.0f || b.overhang > 0.0f) { - return a.overhang < b.overhang; - } - - // prefer hidden points (more than 0.5 mm inside) - if (a.embedded_distance < -0.5f && b.embedded_distance > -0.5f) { - return true; - } - if (b.embedded_distance < -0.5f && a.embedded_distance > -0.5f) { - return false; - } - - if (setup == SeamPosition::spRear && a.position.y() != b.position.y()) { - return a.position.y() > b.position.y(); - } - - float distance_penalty_a = 0.0f; - float distance_penalty_b = 0.0f; - if (setup == spNearest) { - distance_penalty_a = 1.0f - gauss((a.position.head<2>() - preffered_location).norm(), 0.0f, 1.0f, 0.005f); - distance_penalty_b = 1.0f - gauss((b.position.head<2>() - preffered_location).norm(), 0.0f, 1.0f, 0.005f); - } - - // the penalites are kept close to range [0-1.x] however, it should not be relied upon - float penalty_a = a.overhang + a.visibility + - angle_importance * compute_angle_penalty(a.local_ccw_angle) - + distance_penalty_a; - float penalty_b = b.overhang + b.visibility + - angle_importance * compute_angle_penalty(b.local_ccw_angle) - + distance_penalty_b; - - return penalty_a < penalty_b; - } - - // Comparator used during alignment. If there is close potential aligned point, it is compared to the current - // seam point of the perimeter, to find out if the aligned point is not much worse than the current seam - // Also used by the random seam generator. - bool is_first_not_much_worse(const SeamCandidate &a, const SeamCandidate &b) const { - // Blockers/Enforcers discrimination, top priority - if (setup == SeamPosition::spAligned && a.central_enforcer != b.central_enforcer) { - // Prefer centers of enforcers. - return a.central_enforcer; - } - - if (a.type == EnforcedBlockedSeamPoint::Enforced) { - return true; - } - - if (a.type == EnforcedBlockedSeamPoint::Blocked) { - return false; - } - - if (a.type != b.type) { - return a.type > b.type; - } - - //avoid overhangs - if ((a.overhang > 0.0f || b.overhang > 0.0f) - && abs(a.overhang - b.overhang) > (0.1f * a.perimeter.flow_width)) { - return a.overhang < b.overhang; - } - - // prefer hidden points (more than 0.5 mm inside) - if (a.embedded_distance < -0.5f && b.embedded_distance > -0.5f) { - return true; - } - if (b.embedded_distance < -0.5f && a.embedded_distance > -0.5f) { - return false; - } - - if (setup == SeamPosition::spRandom) { - return true; - } - - if (setup == SeamPosition::spRear) { - return a.position.y() + SeamPlacer::seam_align_score_tolerance * 5.0f > b.position.y(); - } - - float penalty_a = a.overhang + a.visibility - + angle_importance * compute_angle_penalty(a.local_ccw_angle); - float penalty_b = b.overhang + b.visibility + - angle_importance * compute_angle_penalty(b.local_ccw_angle); - - return penalty_a <= penalty_b || penalty_a - penalty_b < SeamPlacer::seam_align_score_tolerance; - } - - bool are_similar(const SeamCandidate &a, const SeamCandidate &b) const { - return is_first_not_much_worse(a, b) && is_first_not_much_worse(b, a); - } -}; - -#ifdef DEBUG_FILES -void debug_export_points(const std::vector &layers, - const BoundingBox &bounding_box, const SeamComparator &comparator) { - for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { - std::string angles_file_name = debug_out_path( - ("angles_" + std::to_string(layer_idx) + ".svg").c_str()); - SVG angles_svg { angles_file_name, bounding_box }; - float min_vis = 0; - float max_vis = min_vis; - - float min_weight = std::numeric_limits::min(); - float max_weight = min_weight; - - for (const SeamCandidate &point : layers[layer_idx].points) { - Vec3i color = value_to_rgbi(-PI, PI, point.local_ccw_angle); - std::string fill = "rgb(" + std::to_string(color.x()) + "," + std::to_string(color.y()) + "," - + std::to_string(color.z()) + ")"; - angles_svg.draw(scaled(Vec2f(point.position.head<2>())), fill); - min_vis = std::min(min_vis, point.visibility); - max_vis = std::max(max_vis, point.visibility); - - min_weight = std::min(min_weight, -compute_angle_penalty(point.local_ccw_angle)); - max_weight = std::max(max_weight, -compute_angle_penalty(point.local_ccw_angle)); - - } - - std::string visiblity_file_name = debug_out_path( - ("visibility_" + std::to_string(layer_idx) + ".svg").c_str()); - SVG visibility_svg { visiblity_file_name, bounding_box }; - std::string weights_file_name = debug_out_path( - ("weight_" + std::to_string(layer_idx) + ".svg").c_str()); - SVG weight_svg { weights_file_name, bounding_box }; - std::string overhangs_file_name = debug_out_path( - ("overhang_" + std::to_string(layer_idx) + ".svg").c_str()); - SVG overhangs_svg { overhangs_file_name, bounding_box }; - - for (const SeamCandidate &point : layers[layer_idx].points) { - Vec3i color = value_to_rgbi(min_vis, max_vis, point.visibility); - std::string visibility_fill = "rgb(" + std::to_string(color.x()) + "," + std::to_string(color.y()) + "," - + std::to_string(color.z()) + ")"; - visibility_svg.draw(scaled(Vec2f(point.position.head<2>())), visibility_fill); - - Vec3i weight_color = value_to_rgbi(min_weight, max_weight, - -compute_angle_penalty(point.local_ccw_angle)); - std::string weight_fill = "rgb(" + std::to_string(weight_color.x()) + "," + std::to_string(weight_color.y()) - + "," - + std::to_string(weight_color.z()) + ")"; - weight_svg.draw(scaled(Vec2f(point.position.head<2>())), weight_fill); - - Vec3i overhang_color = value_to_rgbi(-0.5, 0.5, std::clamp(point.overhang, -0.5f, 0.5f)); - std::string overhang_fill = "rgb(" + std::to_string(overhang_color.x()) + "," - + std::to_string(overhang_color.y()) - + "," - + std::to_string(overhang_color.z()) + ")"; - overhangs_svg.draw(scaled(Vec2f(point.position.head<2>())), overhang_fill); - } - } -} -#endif - -// Pick best seam point based on the given comparator -void pick_seam_point(std::vector &perimeter_points, size_t start_index, - const SeamComparator &comparator) { - size_t end_index = perimeter_points[start_index].perimeter.end_index; - - size_t seam_index = start_index; - for (size_t index = start_index; index < end_index; ++index) { - if (comparator.is_first_better(perimeter_points[index], perimeter_points[seam_index])) { - seam_index = index; - } - } - perimeter_points[start_index].perimeter.seam_index = seam_index; -} - -size_t pick_nearest_seam_point_index(const std::vector &perimeter_points, size_t start_index, - const Vec2f &preffered_location) { - size_t end_index = perimeter_points[start_index].perimeter.end_index; - SeamComparator comparator { spNearest }; - - size_t seam_index = start_index; - for (size_t index = start_index; index < end_index; ++index) { - if (comparator.is_first_better(perimeter_points[index], perimeter_points[seam_index], preffered_location)) { - seam_index = index; - } - } - return seam_index; -} - -// picks random seam point uniformly, respecting enforcers blockers and overhang avoidance. -void pick_random_seam_point(const std::vector &perimeter_points, size_t start_index) { - SeamComparator comparator { spRandom }; - - // algorithm keeps a list of viable points and their lengths. If it finds a point - // that is much better than the viable_example_index (e.g. better type, no overhang; see is_first_not_much_worse) - // then it throws away stored lists and starts from start - // in the end, the list should contain points with same type (Enforced > Neutral > Blocked) and also only those which are not - // big overhang. - size_t viable_example_index = start_index; - size_t end_index = perimeter_points[start_index].perimeter.end_index; - struct Viable { - // Candidate seam point index. - size_t index; - float edge_length; - Vec3f edge; - }; - std::vector viables; - - const Vec3f pseudornd_seed = perimeter_points[viable_example_index].position; - float rand = std::abs(sin(pseudornd_seed.dot(Vec3f(12.9898f,78.233f, 133.3333f))) * 43758.5453f); - rand = rand - (int) rand; - - for (size_t index = start_index; index < end_index; ++index) { - if (comparator.are_similar(perimeter_points[index], perimeter_points[viable_example_index])) { - // index ok, push info into viables - Vec3f edge_to_next { perimeter_points[index == end_index - 1 ? start_index : index + 1].position - - perimeter_points[index].position }; - float dist_to_next = edge_to_next.norm(); - viables.push_back( { index, dist_to_next, edge_to_next }); - } else if (comparator.is_first_not_much_worse(perimeter_points[viable_example_index], - perimeter_points[index])) { - // index is worse then viable_example_index, skip this point - } else { - // index is better than viable example index, update example, clear gathered info, start again - // clear up all gathered info, start from scratch, update example index - viable_example_index = index; - viables.clear(); - - Vec3f edge_to_next = (perimeter_points[index == end_index - 1 ? start_index : index + 1].position - - perimeter_points[index].position); - float dist_to_next = edge_to_next.norm(); - viables.push_back( { index, dist_to_next, edge_to_next }); - } - } - - // now pick random point from the stored options - float len_sum = std::accumulate(viables.begin(), viables.end(), 0.0f, [](const float acc, const Viable &v) { - return acc + v.edge_length; - }); - float picked_len = len_sum * rand; - - size_t point_idx = 0; - while (picked_len - viables[point_idx].edge_length > 0) { - picked_len = picked_len - viables[point_idx].edge_length; - point_idx++; - } - - Perimeter &perimeter = perimeter_points[start_index].perimeter; - perimeter.seam_index = viables[point_idx].index; - perimeter.final_seam_position = perimeter_points[perimeter.seam_index].position - + viables[point_idx].edge.normalized() * picked_len; - perimeter.finalized = true; -} - -} // namespace SeamPlacerImpl - -// Parallel process and extract each perimeter polygon of the given print object. -// Gather SeamCandidates of each layer into vector and build KDtree over them -// Store results in the SeamPlacer variables m_seam_per_object -void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info) { - using namespace SeamPlacerImpl; - PrintObjectSeamData &seam_data = m_seam_per_object.emplace(po, PrintObjectSeamData { }).first->second; - seam_data.layers.resize(po->layer_count()); - - tbb::parallel_for(tbb::blocked_range(0, po->layers().size()), - [po, &global_model_info, &seam_data] - (tbb::blocked_range r) { - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - PrintObjectSeamData::LayerSeams &layer_seams = seam_data.layers[layer_idx]; - const Layer *layer = po->get_layer(layer_idx); - auto unscaled_z = layer->slice_z; - std::vector regions; - //NOTE corresponding region ptr may be null, if the layer has zero perimeters - Polygons polygons = extract_perimeter_polygons(layer, regions); - for (size_t poly_index = 0; poly_index < polygons.size(); ++poly_index) { - process_perimeter_polygon(polygons[poly_index], unscaled_z, - regions[poly_index], global_model_info, layer_seams); - } - auto functor = SeamCandidateCoordinateFunctor { layer_seams.points }; - seam_data.layers[layer_idx].points_tree = - std::make_unique(functor, - layer_seams.points.size()); - } - } - ); -} - -void SeamPlacer::calculate_candidates_visibility(const PrintObject *po, - const SeamPlacerImpl::GlobalModelInfo &global_model_info) { - using namespace SeamPlacerImpl; - - std::vector &layers = m_seam_per_object[po].layers; - tbb::parallel_for(tbb::blocked_range(0, layers.size()), - [&layers, &global_model_info](tbb::blocked_range r) { - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - for (auto &perimeter_point : layers[layer_idx].points) { - perimeter_point.visibility = global_model_info.calculate_point_visibility( - perimeter_point.position); - } - } - }); -} - -void SeamPlacer::calculate_overhangs_and_layer_embedding(const PrintObject *po) { - using namespace SeamPlacerImpl; - using PerimeterDistancer = AABBTreeLines::LinesDistancer; - - std::vector &layers = m_seam_per_object[po].layers; - tbb::parallel_for(tbb::blocked_range(0, layers.size()), - [po, &layers](tbb::blocked_range r) { - std::unique_ptr prev_layer_distancer; - if (r.begin() > 0) { // previous layer exists - prev_layer_distancer = std::make_unique(to_unscaled_linesf(po->layers()[r.begin() - 1]->lslices)); - } - - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - size_t regions_with_perimeter = 0; - for (const LayerRegion *region : po->layers()[layer_idx]->regions()) { - if (region->perimeters().size() > 0) { - regions_with_perimeter++; - } - }; - bool should_compute_layer_embedding = regions_with_perimeter > 1; - std::unique_ptr current_layer_distancer = std::make_unique( - to_unscaled_linesf(po->layers()[layer_idx]->lslices)); - - for (SeamCandidate &perimeter_point : layers[layer_idx].points) { - Vec2f point = Vec2f { perimeter_point.position.head<2>() }; - if (prev_layer_distancer.get() != nullptr) { - perimeter_point.overhang = prev_layer_distancer->distance_from_lines(point.cast()) - + 0.6f * perimeter_point.perimeter.flow_width - - tan(SeamPlacer::overhang_angle_threshold) - * po->layers()[layer_idx]->height; - perimeter_point.overhang = - perimeter_point.overhang < 0.0f ? 0.0f : perimeter_point.overhang; - } - - if (should_compute_layer_embedding) { // search for embedded perimeter points (points hidden inside the print ,e.g. multimaterial join, best position for seam) - perimeter_point.embedded_distance = current_layer_distancer->distance_from_lines(point.cast()) - + 0.6f * perimeter_point.perimeter.flow_width; - } - } - - prev_layer_distancer.swap(current_layer_distancer); - } - } - ); - } - -// Estimates, if there is good seam point in the layer_idx which is close to last_point_pos -// uses comparator.is_first_not_much_worse method to compare current seam with the closest point -// (if current seam is too far away ) -// If the current chosen stream is close enough, it is stored in seam_string. returns true and updates last_point_pos -// If the closest point is good enough to replace current chosen seam, it is stored in potential_string_seams, returns true and updates last_point_pos -// Otherwise does nothing, returns false -// Used by align_seam_points(). -std::optional> SeamPlacer::find_next_seam_in_layer( - const std::vector &layers, - const Vec3f &projected_position, - const size_t layer_idx, const float max_distance, - const SeamPlacerImpl::SeamComparator &comparator) const { - using namespace SeamPlacerImpl; - std::vector nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position, - max_distance); - - if (nearby_points_indices.empty()) { - return {}; - } - - size_t best_nearby_point_index = nearby_points_indices[0]; - size_t nearest_point_index = nearby_points_indices[0]; - - // Now find best nearby point, nearest point, and corresponding indices - for (const size_t &nearby_point_index : nearby_points_indices) { - const SeamCandidate &point = layers[layer_idx].points[nearby_point_index]; - if (point.perimeter.finalized) { - continue; // skip over finalized perimeters, try to find some that is not finalized - } - if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index], - projected_position.head<2>()) - || layers[layer_idx].points[best_nearby_point_index].perimeter.finalized) { - best_nearby_point_index = nearby_point_index; - } - if ((point.position - projected_position).squaredNorm() - < (layers[layer_idx].points[nearest_point_index].position - projected_position).squaredNorm() - || layers[layer_idx].points[nearest_point_index].perimeter.finalized) { - nearest_point_index = nearby_point_index; - } - } - - const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index]; - const SeamCandidate &nearest_point = layers[layer_idx].points[nearest_point_index]; - - if (nearest_point.perimeter.finalized) { - //all points are from already finalized perimeter, skip - return {}; - } - - //from the nearest_point, deduce index of seam in the next layer - const SeamCandidate &next_layer_seam = layers[layer_idx].points[nearest_point.perimeter.seam_index]; - - // First try to pick central enforcer if any present - if (next_layer_seam.central_enforcer - && (next_layer_seam.position - projected_position).squaredNorm() - < sqr(3 * max_distance)) { - return {std::pair {layer_idx, nearest_point.perimeter.seam_index}}; - } - - // First try to align the nearest, then try the best nearby - if (comparator.is_first_not_much_worse(nearest_point, next_layer_seam)) { - return {std::pair {layer_idx, nearest_point_index}}; - } - // If nearest point is not good enough, try it with the best nearby point. - if (comparator.is_first_not_much_worse(best_nearby_point, next_layer_seam)) { - return {std::pair {layer_idx, best_nearby_point_index}}; - } - - return {}; -} - -std::vector> SeamPlacer::find_seam_string(const PrintObject *po, - std::pair start_seam, const SeamPlacerImpl::SeamComparator &comparator) const { - const std::vector &layers = m_seam_per_object.find(po)->second.layers; - int layer_idx = start_seam.first; - - //initialize searching for seam string - cluster of nearby seams on previous and next layers - int next_layer = layer_idx + 1; - int step = 1; - std::pair prev_point_index = start_seam; - std::vector> seam_string { start_seam }; - - auto reverse_lookup_direction = [&]() { - step = -1; - prev_point_index = start_seam; - next_layer = layer_idx - 1; - }; - - while (next_layer >= 0) { - if (next_layer >= int(layers.size())) { - reverse_lookup_direction(); - if (next_layer < 0) { - break; - } - } - float max_distance = SeamPlacer::seam_align_tolerable_dist_factor * - layers[start_seam.first].points[start_seam.second].perimeter.flow_width; - Vec3f prev_position = layers[prev_point_index.first].points[prev_point_index.second].position; - Vec3f projected_position = prev_position; - projected_position.z() = float(po->get_layer(next_layer)->slice_z); - - std::optional> maybe_next_seam = find_next_seam_in_layer(layers, projected_position, - next_layer, - max_distance, comparator); - - if (maybe_next_seam.has_value()) { - // For old macOS (pre 10.14), std::optional does not have .value() method, so the code is using operator*() instead. - seam_string.push_back(maybe_next_seam.operator*()); - prev_point_index = seam_string.back(); - //String added, prev_point_index updated - } else { - if (step == 1) { - reverse_lookup_direction(); - if (next_layer < 0) { - break; - } - } else { - break; - } - } - next_layer += step; - } - return seam_string; -} - -// clusters already chosen seam points into strings across multiple layers, and then -// aligns the strings via polynomial fit -// Does not change the positions of the SeamCandidates themselves, instead stores -// the new aligned position into the shared Perimeter structure of each perimeter -// Note that this position does not necesarilly lay on the perimeter. -void SeamPlacer::align_seam_points(const PrintObject *po, const SeamPlacerImpl::SeamComparator &comparator) { - using namespace SeamPlacerImpl; - - // Prepares Debug files for writing. -#ifdef DEBUG_FILES - Slic3r::CNumericLocalesSetter locales_setter; - auto clusters_f = debug_out_path("seam_clusters.obj"); - FILE *clusters = boost::nowide::fopen(clusters_f.c_str(), "w"); - if (clusters == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "stl_write_obj: Couldn't open " << clusters_f << " for writing"; - return; - } - auto aligned_f = debug_out_path("aligned_clusters.obj"); - FILE *aligns = boost::nowide::fopen(aligned_f.c_str(), "w"); - if (aligns == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "stl_write_obj: Couldn't open " << clusters_f << " for writing"; - return; - } -#endif - - //gather vector of all seams on the print_object - pair of layer_index and seam__index within that layer - const std::vector &layers = m_seam_per_object[po].layers; - std::vector> seams; - for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { - const std::vector &layer_perimeter_points = layers[layer_idx].points; - size_t current_point_index = 0; - while (current_point_index < layer_perimeter_points.size()) { - seams.emplace_back(layer_idx, layer_perimeter_points[current_point_index].perimeter.seam_index); - current_point_index = layer_perimeter_points[current_point_index].perimeter.end_index; - } - } - - //sort them before alignment. Alignment is sensitive to initializaion, this gives it better chance to choose something nice - std::stable_sort(seams.begin(), seams.end(), - [&comparator, &layers](const std::pair &left, - const std::pair &right) { - return comparator.is_first_better(layers[left.first].points[left.second], - layers[right.first].points[right.second]); - } - ); - - //align the seam points - start with the best, and check if they are aligned, if yes, skip, else start alignment - // Keeping the vectors outside, so with a bit of luck they will not get reallocated after couple of for loop iterations. - std::vector> seam_string; - std::vector> alternative_seam_string; - std::vector observations; - std::vector observation_points; - std::vector weights; - - int global_index = 0; - while (global_index < int(seams.size())) { - size_t layer_idx = seams[global_index].first; - size_t seam_index = seams[global_index].second; - global_index++; - const std::vector &layer_perimeter_points = layers[layer_idx].points; - if (layer_perimeter_points[seam_index].perimeter.finalized) { - // This perimeter is already aligned, skip seam - continue; - } else { - seam_string = this->find_seam_string(po, { layer_idx, seam_index }, comparator); - size_t step_size = 1 + seam_string.size() / 20; - for (size_t alternative_start = 0; alternative_start < seam_string.size(); alternative_start += step_size) { - size_t start_layer_idx = seam_string[alternative_start].first; - size_t seam_idx = - layers[start_layer_idx].points[seam_string[alternative_start].second].perimeter.seam_index; - alternative_seam_string = this->find_seam_string(po, - std::pair(start_layer_idx, seam_idx), comparator); - if (alternative_seam_string.size() > seam_string.size()) { - seam_string = std::move(alternative_seam_string); - } - } - if (seam_string.size() < seam_align_minimum_string_seams) { - //string NOT long enough to be worth aligning, skip - continue; - } - - // String is long enough, all string seams and potential string seams gathered, now do the alignment - //sort by layer index - std::sort(seam_string.begin(), seam_string.end(), - [](const std::pair &left, const std::pair &right) { - return left.first < right.first; - }); - - //repeat the alignment for the current seam, since it could be skipped due to alternative path being aligned. - global_index--; - - // gather all positions of seams and their weights - observations.resize(seam_string.size()); - observation_points.resize(seam_string.size()); - weights.resize(seam_string.size()); - - auto angle_3d = [](const Vec3f& a, const Vec3f& b){ - return std::abs(acosf(a.normalized().dot(b.normalized()))); - }; - - auto angle_weight = [](float angle){ - return 1.0f / (0.1f + compute_angle_penalty(angle)); - }; - - //gather points positions and weights - float total_length = 0.0f; - Vec3f last_point_pos = layers[seam_string[0].first].points[seam_string[0].second].position; - for (size_t index = 0; index < seam_string.size(); ++index) { - const SeamCandidate ¤t = layers[seam_string[index].first].points[seam_string[index].second]; - float layer_angle = 0.0f; - if (index > 0 && index < seam_string.size() - 1) { - layer_angle = angle_3d( - current.position - - layers[seam_string[index - 1].first].points[seam_string[index - 1].second].position, - layers[seam_string[index + 1].first].points[seam_string[index + 1].second].position - - current.position - ); - } - observations[index] = current.position.head<2>(); - observation_points[index] = current.position.z(); - weights[index] = angle_weight(current.local_ccw_angle); - float curling_influence = layer_angle > 2.0 * std::abs(current.local_ccw_angle) ? -0.8f : 1.0f; - if (current.type == EnforcedBlockedSeamPoint::Enforced) { - curling_influence = 1.0f; - weights[index] += 3.0f; - } - total_length += curling_influence * (last_point_pos - current.position).norm(); - last_point_pos = current.position; - } - - if (comparator.setup == spRear) { - total_length *= 0.3f; - } - - // Curve Fitting - size_t number_of_segments = std::max(size_t(1), - size_t(std::max(0.0f,total_length) / SeamPlacer::seam_align_mm_per_segment)); - auto curve = Geometry::fit_cubic_bspline(observations, observation_points, weights, number_of_segments); - - // Do alignment - compute fitted point for each point in the string from its Z coord, and store the position into - // Perimeter structure of the point; also set flag aligned to true - for (size_t index = 0; index < seam_string.size(); ++index) { - const auto &pair = seam_string[index]; - float t = std::min(1.0f, std::pow(std::abs(layers[pair.first].points[pair.second].local_ccw_angle) - / SeamPlacer::sharp_angle_snapping_threshold, 3.0f)); - if (layers[pair.first].points[pair.second].type == EnforcedBlockedSeamPoint::Enforced){ - t = std::max(0.4f, t); - } - - Vec3f current_pos = layers[pair.first].points[pair.second].position; - Vec2f fitted_pos = curve.get_fitted_value(current_pos.z()); - - //interpolate between current and fitted position, prefer current pos for large weights. - Vec3f final_position = t * current_pos + (1.0f - t) * to_3d(fitted_pos, current_pos.z()); - - Perimeter &perimeter = layers[pair.first].points[pair.second].perimeter; - perimeter.seam_index = pair.second; - perimeter.final_seam_position = final_position; - perimeter.finalized = true; - } - -#ifdef DEBUG_FILES - auto randf = []() { - return float(rand()) / float(RAND_MAX); - }; - Vec3f color { randf(), randf(), randf() }; - for (size_t i = 0; i < seam_string.size(); ++i) { - auto orig_seam = layers[seam_string[i].first].points[seam_string[i].second]; - fprintf(clusters, "v %f %f %f %f %f %f \n", orig_seam.position[0], - orig_seam.position[1], - orig_seam.position[2], color[0], color[1], - color[2]); - } - - color = Vec3f { randf(), randf(), randf() }; - for (size_t i = 0; i < seam_string.size(); ++i) { - const Perimeter &perimeter = layers[seam_string[i].first].points[seam_string[i].second].perimeter; - fprintf(aligns, "v %f %f %f %f %f %f \n", perimeter.final_seam_position[0], - perimeter.final_seam_position[1], - perimeter.final_seam_position[2], color[0], color[1], - color[2]); - } -#endif - } - } - -#ifdef DEBUG_FILES - fclose(clusters); - fclose(aligns); -#endif - -} - -void SeamPlacer::init(const Print &print, std::function throw_if_canceled_func) { - using namespace SeamPlacerImpl; - m_seam_per_object.clear(); - - for (const PrintObject *po : print.objects()) { - throw_if_canceled_func(); - SeamPosition configured_seam_preference = po->config().seam_position.value; - SeamComparator comparator { configured_seam_preference }; - - { - GlobalModelInfo global_model_info { }; - gather_enforcers_blockers(global_model_info, po); - throw_if_canceled_func(); - if (configured_seam_preference == spAligned || configured_seam_preference == spNearest) { - compute_global_occlusion(global_model_info, po, throw_if_canceled_func); - } - throw_if_canceled_func(); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: gather_seam_candidates: start"; - gather_seam_candidates(po, global_model_info); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: gather_seam_candidates: end"; - throw_if_canceled_func(); - if (configured_seam_preference == spAligned || configured_seam_preference == spNearest) { - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: calculate_candidates_visibility : start"; - calculate_candidates_visibility(po, global_model_info); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: calculate_candidates_visibility : end"; - } - } // destruction of global_model_info (large structure, no longer needed) - throw_if_canceled_func(); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: calculate_overhangs and layer embdedding : start"; - calculate_overhangs_and_layer_embedding(po); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: calculate_overhangs and layer embdedding: end"; - throw_if_canceled_func(); - if (configured_seam_preference != spNearest) { // For spNearest, the seam is picked in the place_seam method with actual nozzle position information - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: pick_seam_point : start"; - //pick seam point - std::vector &layers = m_seam_per_object[po].layers; - tbb::parallel_for(tbb::blocked_range(0, layers.size()), - [&layers, configured_seam_preference, comparator](tbb::blocked_range r) { - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - std::vector &layer_perimeter_points = layers[layer_idx].points; - for (size_t current = 0; current < layer_perimeter_points.size(); - current = layer_perimeter_points[current].perimeter.end_index) - if (configured_seam_preference == spRandom) - pick_random_seam_point(layer_perimeter_points, current); - else - pick_seam_point(layer_perimeter_points, current, comparator); - } - }); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: pick_seam_point : end"; - } - throw_if_canceled_func(); - if (configured_seam_preference == spAligned || configured_seam_preference == spRear) { - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: align_seam_points : start"; - align_seam_points(po, comparator); - BOOST_LOG_TRIVIAL(debug) - << "SeamPlacer: align_seam_points : end"; - } - -#ifdef DEBUG_FILES - debug_export_points(m_seam_per_object[po].layers, po->bounding_box(), comparator); -#endif - } -} - -Point SeamPlacer::place_seam(const Layer *layer, const ExtrusionLoop &loop, bool external_first, - const Point &last_pos) const { - using namespace SeamPlacerImpl; +Point Placer::place_seam(const Layer *layer, const ExtrusionLoop &loop, const Point &last_pos) const { const PrintObject *po = layer->object(); // Must not be called with supprot layer. - assert(dynamic_cast(layer) == nullptr); + assert(dynamic_cast(layer) == nullptr); // Object layer IDs are incremented by the number of raft layers. assert(layer->id() >= po->slicing_parameters().raft_layers()); const size_t layer_index = layer->id() - po->slicing_parameters().raft_layers(); - const double unscaled_z = layer->slice_z; - auto get_next_loop_point = [loop](ExtrusionLoop::ClosestPathPoint current) { - current.segment_idx += 1; - if (current.segment_idx >= loop.paths[current.path_idx].polyline.points.size()) { - current.path_idx = next_idx_modulo(current.path_idx, loop.paths.size()); - current.segment_idx = 0; - } - current.foot_pt = loop.paths[current.path_idx].polyline.points[current.segment_idx]; - return current; - }; + const Polygon loop_polygon{Geometry::to_polygon(loop)}; - const PrintObjectSeamData::LayerSeams &layer_perimeters = - m_seam_per_object.find(layer->object())->second.layers[layer_index]; + const bool do_staggering{this->params.staggered_inner_seams && loop.role() == ExtrusionRole::Perimeter}; + const double loop_width{loop.paths.empty() ? 0.0 : loop.paths.front().width()}; - // Find the closest perimeter in the SeamPlacer to this loop. - // Repeat search until two consecutive points of the loop are found, that result in the same closest_perimeter - // This is beacuse with arachne, T-Junctions may exist and sometimes the wrong perimeter was chosen - size_t closest_perimeter_point_index = 0; - { // local space for the closest_perimeter_point_index - Perimeter *closest_perimeter = nullptr; - ExtrusionLoop::ClosestPathPoint closest_point{0,0,loop.paths[0].polyline.points[0]}; - size_t points_count = std::accumulate(loop.paths.begin(), loop.paths.end(), 0, [](size_t acc,const ExtrusionPath& p) { - return acc + p.polyline.points.size(); - }); - for (size_t i = 0; i < points_count; ++i) { - Vec2f unscaled_p = unscaled(closest_point.foot_pt); - closest_perimeter_point_index = find_closest_point(*layer_perimeters.points_tree.get(), - to_3d(unscaled_p, float(unscaled_z))); - if (closest_perimeter != &layer_perimeters.points[closest_perimeter_point_index].perimeter) { - closest_perimeter = &layer_perimeters.points[closest_perimeter_point_index].perimeter; - closest_point = get_next_loop_point(closest_point); - } else { - break; - } - } - } - Vec3f seam_position; - size_t seam_index; - if (const Perimeter &perimeter = layer_perimeters.points[closest_perimeter_point_index].perimeter; - perimeter.finalized) { - seam_position = perimeter.final_seam_position; - seam_index = perimeter.seam_index; + if (this->params.seam_preference == spNearest) { + const std::vector &perimeters{this->perimeters_per_layer.at(po)[layer_index]}; + const auto [seam_choice, perimeter_index] = place_seam_near(perimeters, loop, last_pos, this->params.max_nearest_detour); + return finalize_seam_position(loop_polygon, seam_choice, perimeters[perimeter_index].perimeter, loop_width, do_staggering); } else { - seam_index = - po->config().seam_position == spNearest ? - pick_nearest_seam_point_index(layer_perimeters.points, perimeter.start_index, - unscaled(last_pos)) : - perimeter.seam_index; - seam_position = layer_perimeters.points[seam_index].position; - } + const std::vector &seams_on_perimeters{this->seams_per_object.at(po)[layer_index]}; - Point seam_point = Point::new_scale(seam_position.x(), seam_position.y()); - - if (loop.role() == ExtrusionRole::Perimeter) { //Hopefully inner perimeter - const SeamCandidate &perimeter_point = layer_perimeters.points[seam_index]; - ExtrusionLoop::ClosestPathPoint projected_point = loop.get_closest_path_and_point(seam_point, false); - // determine depth of the seam point. - float depth = (float) unscale(Point(seam_point - projected_point.foot_pt)).norm(); - float beta_angle = cos(perimeter_point.local_ccw_angle / 2.0f); - size_t index_of_prev = - seam_index == perimeter_point.perimeter.start_index ? - perimeter_point.perimeter.end_index - 1 : - seam_index - 1; - size_t index_of_next = - seam_index == perimeter_point.perimeter.end_index - 1 ? - perimeter_point.perimeter.start_index : - seam_index + 1; - - if ((seam_position - perimeter_point.position).squaredNorm() < depth && // seam is on perimeter point - perimeter_point.local_ccw_angle < -EPSILON // In concave angles - ) { // In this case, we are at internal perimeter, where the external perimeter has seam in concave angle. We want to align - // the internal seam into the concave corner, and not on the perpendicular projection on the closest edge (which is what the split_at function does) - Vec2f dir_to_middle = - ((perimeter_point.position - layer_perimeters.points[index_of_prev].position).head<2>().normalized() - + (perimeter_point.position - layer_perimeters.points[index_of_next].position).head<2>().normalized()) - * 0.5; - depth = 1.4142 * depth / beta_angle; - // There are some nice geometric identities in determination of the correct depth of new seam point. - //overshoot the target depth, in concave angles it will correctly snap to the corner; TODO: find out why such big overshoot is needed. - Vec2f final_pos = perimeter_point.position.head<2>() + depth * dir_to_middle; - projected_point = loop.get_closest_path_and_point(Point::new_scale(final_pos.x(), final_pos.y()), false); - } else { // not concave angle, in that case the nearest point is the good candidate - // but for staggering, we also need to recompute depth of the inner perimter, because in convex corners, the distance is larger than layer width - // we want the perpendicular depth, not distance to nearest point - depth = depth * beta_angle / 1.4142; - } - - seam_point = projected_point.foot_pt; - - //lastly, for internal perimeters, do the staggering if requested - if (po->config().staggered_inner_seams && loop.length() > 0.0) { - //fix depth, it is sometimes strongly underestimated - depth = std::max(loop.paths[projected_point.path_idx].width(), depth); - - while (depth > 0.0f) { - auto next_point = get_next_loop_point(projected_point); - Vec2f a = unscale(projected_point.foot_pt).cast(); - Vec2f b = unscale(next_point.foot_pt).cast(); - float dist = (a - b).norm(); - if (dist > depth) { - Vec2f final_pos = a + (b - a) * depth / dist; - next_point.foot_pt = Point::new_scale(final_pos.x(), final_pos.y()); - } - depth -= dist; - projected_point = next_point; + // Special case. + // If there are only two perimeters and the current perimeter is hole (clockwise). + const int perimeter_count{get_perimeter_count(layer)}; + const bool has_2_or_3_perimeters{perimeter_count == 2 || perimeter_count == 3}; + if (has_2_or_3_perimeters) { + if (seams_on_perimeters.size() == 2 && + seams_on_perimeters[0].perimeter.is_hole != + seams_on_perimeters[1].perimeter.is_hole) { + const SeamPerimeterChoice &seam_perimeter_choice{ + seams_on_perimeters[0].perimeter.is_hole ? seams_on_perimeters[1] : + seams_on_perimeters[0]}; + return finalize_seam_position( + loop_polygon, seam_perimeter_choice.choice, seam_perimeter_choice.perimeter, + loop_width, do_staggering + ); } - seam_point = projected_point.foot_pt; } + + const SeamPerimeterChoice &seam_perimeter_choice{choose_closest_seam(seams_on_perimeters, loop_polygon)}; + return finalize_seam_position(loop_polygon, seam_perimeter_choice.choice, seam_perimeter_choice.perimeter, loop_width, do_staggering); } - - return seam_point; } - -} // namespace Slic3r +} // namespace Slic3r::Seams diff --git a/src/libslic3r/GCode/SeamPlacer.hpp b/src/libslic3r/GCode/SeamPlacer.hpp index c4b1edc00b..625170944a 100644 --- a/src/libslic3r/GCode/SeamPlacer.hpp +++ b/src/libslic3r/GCode/SeamPlacer.hpp @@ -1,4 +1,5 @@ -///|/ Copyright (c) Prusa Research 2020 - 2022 Pavel MikuÅ¡ @Godrak, Lukáš MatÄ›na @lukasmatena, VojtÄ›ch Bubník @bubnikv +///|/ Copyright (c) Prusa Research 2020 - 2022 Pavel MikuÅ¡ @Godrak, Lukáš MatÄ›na @lukasmatena, +/// VojtÄ›ch Bubník @bubnikv ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ @@ -10,159 +11,62 @@ #include #include -#include "libslic3r/libslic3r.h" -#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/GCode/SeamAligned.hpp" #include "libslic3r/Polygon.hpp" -#include "libslic3r/PrintConfig.hpp" -#include "libslic3r/BoundingBox.hpp" -#include "libslic3r/AABBTreeIndirect.hpp" -#include "libslic3r/KDTreeIndirect.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/GCode/SeamPerimeters.hpp" +#include "libslic3r/GCode/SeamChoice.hpp" +#include "libslic3r/GCode/ModelVisibility.hpp" -namespace Slic3r { +namespace Slic3r::Seams { -class PrintObject; -class ExtrusionLoop; -class Print; -class Layer; - -namespace EdgeGrid { -class Grid; -} - -namespace SeamPlacerImpl { - - -struct GlobalModelInfo; -struct SeamComparator; - -enum class EnforcedBlockedSeamPoint { - Blocked = 0, - Neutral = 1, - Enforced = 2, +struct BoundedPerimeter { + Perimeters::Perimeter perimeter; + BoundingBox bounding_box; }; -// struct representing single perimeter loop -struct Perimeter { - size_t start_index{}; - size_t end_index{}; //inclusive! - size_t seam_index{}; - float flow_width{}; +using ObjectSeams = + std::unordered_map>>; +using LayerPerimeters = std::vector>; +using ObjectLayerPerimeters = std::unordered_map; - // During alignment, a final position may be stored here. In that case, finalized is set to true. - // Note that final seam position is not limited to points of the perimeter loop. In theory it can be any position - // Random position also uses this flexibility to set final seam point position - bool finalized = false; - Vec3f final_seam_position = Vec3f::Zero(); -}; - -//Struct over which all processing of perimeters is done. For each perimeter point, its respective candidate is created, -// then all the needed attributes are computed and finally, for each perimeter one point is chosen as seam. -// This seam position can be then further aligned -struct SeamCandidate { - SeamCandidate(const Vec3f &pos, Perimeter &perimeter, - float local_ccw_angle, - EnforcedBlockedSeamPoint type) : - position(pos), perimeter(perimeter), visibility(0.0f), overhang(0.0f), embedded_distance(0.0f), local_ccw_angle( - local_ccw_angle), type(type), central_enforcer(false) { - } - const Vec3f position; - // pointer to Perimeter loop of this point. It is shared across all points of the loop - Perimeter &perimeter; - float visibility; - float overhang; - // distance inside the merged layer regions, for detecting perimeter points which are hidden indside the print (e.g. multimaterial join) - // Negative sign means inside the print, comes from EdgeGrid structure - float embedded_distance; - float local_ccw_angle; - EnforcedBlockedSeamPoint type; - bool central_enforcer; //marks this candidate as central point of enforced segment on the perimeter - important for alignment -}; - -struct SeamCandidateCoordinateFunctor { - SeamCandidateCoordinateFunctor(const std::vector &seam_candidates) : - seam_candidates(seam_candidates) { - } - const std::vector &seam_candidates; - float operator()(size_t index, size_t dim) const { - return seam_candidates[index].position[dim]; - } -}; -} // namespace SeamPlacerImpl - -struct PrintObjectSeamData +struct Params { - using SeamCandidatesTree = KDTreeIndirect<3, float, SeamPlacerImpl::SeamCandidateCoordinateFunctor>; - - struct LayerSeams - { - Slic3r::deque perimeters; - std::vector points; - std::unique_ptr points_tree; - }; - // Map of PrintObjects (PO) -> vector of layers of PO -> vector of perimeter - std::vector layers; - // Map of PrintObjects (PO) -> vector of layers of PO -> unique_ptr to KD - // tree of all points of the given layer - - void clear() - { - layers.clear(); - } + double max_nearest_detour; + double rear_project_threshold; + Aligned::Params aligned; + double max_distance{}; + unsigned random_seed{}; + double convex_visibility_modifier{}; + double concave_visibility_modifier{}; + Perimeters::PerimeterParams perimeter; + Slic3r::ModelInfo::Visibility::Params visibility; + SeamPosition seam_preference; + bool staggered_inner_seams; }; -class SeamPlacer { +std::ostream& operator<<(std::ostream& os, const Params& params); + +class Placer +{ public: - // Number of samples generated on the mesh. There are sqr_rays_per_sample_point*sqr_rays_per_sample_point rays casted from each samples - static constexpr size_t raycasting_visibility_samples_count = 30000; - static constexpr size_t fast_decimation_triangle_count_target = 16000; - //square of number of rays per sample point - static constexpr size_t sqr_rays_per_sample_point = 5; + static Params get_params(const DynamicPrintConfig &config); - // snapping angle - angles larger than this value will be snapped to during seam painting - static constexpr float sharp_angle_snapping_threshold = 55.0f * float(PI) / 180.0f; - // overhang angle for seam placement that still yields good results, in degrees, measured from vertical direction - static constexpr float overhang_angle_threshold = 50.0f * float(PI) / 180.0f; + void init( + SpanOfConstPtrs objects, + const Params ¶ms, + const std::function &throw_if_canceled + ); - // determines angle importance compared to visibility ( neutral value is 1.0f. ) - static constexpr float angle_importance_aligned = 0.6f; - static constexpr float angle_importance_nearest = 1.0f; // use much higher angle importance for nearest mode, to combat the visibility info noise - - // For long polygon sides, if they are close to the custom seam drawings, they are oversampled with this step size - static constexpr float enforcer_oversampling_distance = 0.2f; - - // When searching for seam clusters for alignment: - // following value describes, how much worse score can point have and still be picked into seam cluster instead of original seam point on the same layer - static constexpr float seam_align_score_tolerance = 0.3f; - // seam_align_tolerable_dist_factor - how far to search for seam from current position, final dist is seam_align_tolerable_dist_factor * flow_width - static constexpr float seam_align_tolerable_dist_factor = 4.0f; - // minimum number of seams needed in cluster to make alignment happen - static constexpr size_t seam_align_minimum_string_seams = 6; - // millimeters covered by spline; determines number of splines for the given string - static constexpr size_t seam_align_mm_per_segment = 4.0f; - - //The following data structures hold all perimeter points for all PrintObject. - std::unordered_map m_seam_per_object; - - void init(const Print &print, std::function throw_if_canceled_func); - - Point place_seam(const Layer *layer, const ExtrusionLoop &loop, bool external_first, const Point &last_pos) const; + Point place_seam(const Layer *layer, const ExtrusionLoop &loop, const Point &last_pos) const; private: - void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info); - void calculate_candidates_visibility(const PrintObject *po, - const SeamPlacerImpl::GlobalModelInfo &global_model_info); - void calculate_overhangs_and_layer_embedding(const PrintObject *po); - void align_seam_points(const PrintObject *po, const SeamPlacerImpl::SeamComparator &comparator); - std::vector> find_seam_string(const PrintObject *po, - std::pair start_seam, - const SeamPlacerImpl::SeamComparator &comparator) const; - std::optional> find_next_seam_in_layer( - const std::vector &layers, - const Vec3f& projected_position, - const size_t layer_idx, const float max_distance, - const SeamPlacerImpl::SeamComparator &comparator) const; + Params params; + ObjectSeams seams_per_object; + ObjectLayerPerimeters perimeters_per_layer; }; -} // namespace Slic3r +} // namespace Slic3r::Seams #endif // libslic3r_SeamPlacer_hpp_ diff --git a/src/libslic3r/GCode/SeamRandom.cpp b/src/libslic3r/GCode/SeamRandom.cpp new file mode 100644 index 0000000000..27f01ca6be --- /dev/null +++ b/src/libslic3r/GCode/SeamRandom.cpp @@ -0,0 +1,136 @@ +#include + +#include "libslic3r/GCode/SeamRandom.hpp" +#include "libslic3r/GCode/SeamGeometry.hpp" + +namespace Slic3r::Seams::Random { +using Perimeters::PointType; +using Perimeters::PointClassification; + +namespace Impl { +std::vector get_segments( + const Perimeters::Perimeter &perimeter, + const PointType point_type, + const PointClassification point_classification +) { + const std::vector &positions{perimeter.positions}; + const std::vector &point_types{perimeter.point_types}; + const std::vector &point_classifications{perimeter.point_classifications}; + + std::optional current_begin; + std::optional current_begin_index; + Vec2d previous_position{positions.front()}; + double distance{0.0}; + std::vector result; + for (std::size_t index{0}; index < positions.size(); ++index) { + distance += (positions[index] - previous_position).norm(); + previous_position = positions[index]; + + if (point_types[index] == point_type && + point_classifications[index] == point_classification) { + if (!current_begin) { + current_begin = distance; + current_begin_index = index; + } + } else { + if (current_begin) { + result.push_back(PerimeterSegment{*current_begin, distance, *current_begin_index}); + } + current_begin = std::nullopt; + current_begin_index = std::nullopt; + } + } + + if (current_begin) { + result.push_back(PerimeterSegment{*current_begin, distance, *current_begin_index}); + } + return result; +} + +PerimeterSegment pick_random_segment( + const std::vector &segments, std::mt19937 &random_engine +) { + double length{0.0}; + for (const PerimeterSegment &segment : segments) { + length += segment.length(); + } + + std::uniform_real_distribution distribution{0.0, length}; + double random_distance{distribution(random_engine)}; + + double distance{0.0}; + return *std::find_if(segments.begin(), segments.end(), [&](const PerimeterSegment &segment) { + if (random_distance >= distance && random_distance <= distance + segment.length()) { + return true; + } + distance += segment.length(); + return false; + }); +} + +SeamChoice pick_random_point( + const PerimeterSegment &segment, const Perimeters::Perimeter &perimeter, std::mt19937 &random_engine +) { + const std::vector &positions{perimeter.positions}; + + if (segment.length() < std::numeric_limits::epsilon()) { + return {segment.begin_index, segment.begin_index, positions[segment.begin_index]}; + } + + std::uniform_real_distribution distribution{0.0, segment.length()}; + const double random_distance{distribution(random_engine)}; + + double distance{0.0}; + std::size_t previous_index{segment.begin_index}; + for (std::size_t index{segment.begin_index + 1}; index < perimeter.positions.size(); ++index) { + const Vec2d edge{positions[index] - positions[previous_index]}; + + if (distance + edge.norm() >= random_distance) { + if (random_distance - distance < std::numeric_limits::epsilon()) { + index = previous_index; + } else if (distance + edge.norm() - random_distance < std::numeric_limits::epsilon()) { + previous_index = index; + } + + const double remaining_distance{random_distance - distance}; + const Vec2d position{positions[previous_index] + remaining_distance * edge.normalized()}; + return {previous_index, index, position}; + } + + distance += edge.norm(); + previous_index = index; + } + + // Should be unreachable. + return {segment.begin_index, segment.begin_index, positions[segment.begin_index]}; +} + +std::optional Random::operator()( + const Perimeters::Perimeter &perimeter, + const PointType point_type, + const PointClassification point_classification +) const { + std::vector segments{ + get_segments(perimeter, point_type, point_classification)}; + + if (!segments.empty()) { + const PerimeterSegment segment{pick_random_segment(segments, random_engine)}; + return pick_random_point(segment, perimeter, random_engine); + } + return std::nullopt; +} +} // namespace Impl + +std::vector> get_object_seams( + Shells::Shells<> &&shells, const unsigned fixed_seed +) { + std::mt19937 random_engine{fixed_seed}; + const Impl::Random random{random_engine}; + + return Seams::get_object_seams(std::move(shells), [&](const Shells::Shell<> &shell) { + return Seams::get_shell_seam(shell, [&](const Perimeters::Perimeter &perimeter, std::size_t) { + return Seams::choose_seam_point(perimeter, random); + }); + }); +} +} // namespace Slic3r::Seams::Random diff --git a/src/libslic3r/GCode/SeamRandom.hpp b/src/libslic3r/GCode/SeamRandom.hpp new file mode 100644 index 0000000000..81c8dc4342 --- /dev/null +++ b/src/libslic3r/GCode/SeamRandom.hpp @@ -0,0 +1,29 @@ +#include "libslic3r/GCode/SeamChoice.hpp" +#include + +namespace Slic3r::Seams::Random { +namespace Impl { +struct PerimeterSegment +{ + double begin{}; + double end{}; + std::size_t begin_index{}; + + double length() const { return end - begin; } +}; + +struct Random +{ + std::mt19937 &random_engine; + + std::optional operator()( + const Perimeters::Perimeter &perimeter, + const Perimeters::PointType point_type, + const Perimeters::PointClassification point_classification + ) const; +}; +} +std::vector> get_object_seams( + Shells::Shells<> &&shells, const unsigned fixed_seed +); +} diff --git a/src/libslic3r/GCode/SeamRear.cpp b/src/libslic3r/GCode/SeamRear.cpp new file mode 100644 index 0000000000..d189b5ad08 --- /dev/null +++ b/src/libslic3r/GCode/SeamRear.cpp @@ -0,0 +1,125 @@ +#include "libslic3r/GCode/SeamRear.hpp" +#include "libslic3r/GCode/SeamGeometry.hpp" + +namespace Slic3r::Seams::Rear { +using Perimeters::PointType; +using Perimeters::PointClassification; + +namespace Impl { + +BoundingBoxf get_bounding_box(const Shells::Shell<> &shell) { + BoundingBoxf result; + for (const Shells::Slice<> &slice : shell) { + result.merge(BoundingBoxf{slice.boundary.positions}); + } + return result; +} + +std::optional get_rearest_point( + const Perimeters::Perimeter &perimeter, + const PointType point_type, + const PointClassification point_classification +) { + double max_y{-std::numeric_limits::infinity()}; + std::optional choosen_index; + for (std::size_t i{0}; i < perimeter.positions.size(); ++i) { + const Perimeters::PointType _point_type{perimeter.point_types[i]}; + const Perimeters::PointClassification _point_classification{perimeter.point_classifications[i]}; + + if (point_type == _point_type && point_classification == _point_classification) { + const Vec2d &position{perimeter.positions[i]}; + if (position.y() > max_y) { + max_y = position.y(); + choosen_index = i; + } + } + } + if (choosen_index) { + return SeamChoice{*choosen_index, *choosen_index, perimeter.positions[*choosen_index]}; + } + return std::nullopt; +} + +std::optional StraightLine::operator()( + const Perimeters::Perimeter &perimeter, + const PointType point_type, + const PointClassification point_classification +) const { + std::vector possible_lines; + for (std::size_t i{0}; i < perimeter.positions.size() - 1; ++i) { + if (perimeter.point_types[i] != point_type) { + continue; + } + if (perimeter.point_classifications[i] != point_classification) { + continue; + } + if (perimeter.point_types[i + 1] != point_type) { + continue; + } + if (perimeter.point_classifications[i + 1] != point_classification) { + continue; + } + possible_lines.push_back(PerimeterLine{perimeter.positions[i], perimeter.positions[i+1], i, i + 1}); + } + if (possible_lines.empty()) { + return std::nullopt; + } + + const AABBTreeLines::LinesDistancer possible_distancer{possible_lines}; + const BoundingBoxf bounding_box{perimeter.positions}; + + const std::vector> intersections{ + possible_distancer.intersections_with_line(PerimeterLine{ + this->prefered_position, Vec2d{this->prefered_position.x(), bounding_box.min.y()}, + 0, 0})}; + if (!intersections.empty()) { + const auto[position, line_index]{intersections.front()}; + if (position.y() < bounding_box.max.y() - + this->rear_project_threshold * (bounding_box.max.y() - bounding_box.min.y())) { + return std::nullopt; + } + const PerimeterLine &intersected_line{possible_lines[line_index]}; + const SeamChoice intersected_choice{intersected_line.previous_index, intersected_line.next_index, position}; + return intersected_choice; + } + return std::nullopt; +} +} // namespace Impl + +std::vector> get_object_seams( + Shells::Shells<> &&shells, + const double rear_project_threshold +) { + double average_x_center{0.0}; + std::size_t count{0}; + for (const Shells::Shell<> &shell : shells) { + for (const Shells::Slice<> &slice : shell) { + if (slice.boundary.positions.empty()) { + continue; + } + const BoundingBoxf slice_bounding_box{slice.boundary.positions}; + average_x_center += (slice_bounding_box.min.x() + slice_bounding_box.max.x()) / 2.0; + count++; + } + } + average_x_center /= count; + return Seams::get_object_seams(std::move(shells), [&](const Shells::Shell<> &shell) { + BoundingBoxf bounding_box{Impl::get_bounding_box(shell)}; + const Vec2d back_center{average_x_center, bounding_box.max.y()}; + std::optional> straight_seam { + Seams::maybe_get_shell_seam(shell, [&](const Perimeters::Perimeter &perimeter, std::size_t) { + return Seams::maybe_choose_seam_point( + perimeter, + Impl::StraightLine{back_center, rear_project_threshold} + ); + }) + }; + if (!straight_seam) { + return Seams::get_shell_seam(shell, [&](const Perimeters::Perimeter &perimeter, std::size_t) { + return Seams::choose_seam_point(perimeter, Impl::get_rearest_point); + }); + } + return *straight_seam; + }); +} +} // namespace Slic3r::Seams::Rear diff --git a/src/libslic3r/GCode/SeamRear.hpp b/src/libslic3r/GCode/SeamRear.hpp new file mode 100644 index 0000000000..a313432780 --- /dev/null +++ b/src/libslic3r/GCode/SeamRear.hpp @@ -0,0 +1,40 @@ +#ifndef libslic3r_SeamRear_hpp_ +#define libslic3r_SeamRear_hpp_ + +#include "libslic3r/GCode/SeamPerimeters.hpp" +#include "libslic3r/GCode/SeamChoice.hpp" + +namespace Slic3r::Seams::Rear { +namespace Impl { +struct PerimeterLine +{ + Vec2d a; + Vec2d b; + std::size_t previous_index; + std::size_t next_index; + + using Scalar = Vec2d::Scalar; + static const constexpr int Dim = 2; +}; + +struct StraightLine +{ + Vec2d prefered_position; + double rear_project_threshold; + + std::optional operator()( + const Perimeters::Perimeter &perimeter, + const Perimeters::PointType point_type, + const Perimeters::PointClassification point_classification + ) const; +}; + +} // namespace Impl + +std::vector> get_object_seams( + Shells::Shells<> &&shells, + const double rear_project_threshold +); +} // namespace Slic3r::Seams::Rear + +#endif // libslic3r_SeamRear_hpp_ diff --git a/src/libslic3r/GCode/SeamShells.cpp b/src/libslic3r/GCode/SeamShells.cpp new file mode 100644 index 0000000000..65e9149c82 --- /dev/null +++ b/src/libslic3r/GCode/SeamShells.cpp @@ -0,0 +1,114 @@ +#include "libslic3r/GCode/SeamShells.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include +#include + +namespace Slic3r::Seams::Shells::Impl { + +BoundedPolygons project_to_geometry(const Geometry::Extrusions &external_perimeters, const double max_bb_distance) { + BoundedPolygons result; + result.reserve(external_perimeters.size()); + + using std::transform, std::back_inserter; + + transform( + external_perimeters.begin(), external_perimeters.end(), back_inserter(result), + [&](const Geometry::Extrusion &external_perimeter) { + const auto [choosen_index, _]{Geometry::pick_closest_bounding_box( + external_perimeter.bounding_box, + external_perimeter.island_boundary_bounding_boxes + )}; + + const double distance{Geometry::bounding_box_distance( + external_perimeter.island_boundary_bounding_boxes[choosen_index], + external_perimeter.bounding_box + )}; + + if (distance > max_bb_distance) { + Polygons expanded_extrusion{expand(external_perimeter.polygon, external_perimeter.width / 2.0)}; + if (!expanded_extrusion.empty()) { + return BoundedPolygon{ + expanded_extrusion.front(), expanded_extrusion.front().bounding_box(), external_perimeter.polygon.is_clockwise() + }; + } + } + + const bool is_hole{choosen_index != 0}; + const Polygon &adjacent_boundary{ + !is_hole ? external_perimeter.island_boundary.contour : + external_perimeter.island_boundary.holes[choosen_index - 1]}; + return BoundedPolygon{adjacent_boundary, external_perimeter.island_boundary_bounding_boxes[choosen_index], is_hole}; + } + ); + return result; +} + +std::vector project_to_geometry(const std::vector &extrusions, const double max_bb_distance) { + std::vector result(extrusions.size()); + + for (std::size_t layer_index{0}; layer_index < extrusions.size(); ++layer_index) { + result[layer_index] = project_to_geometry(extrusions[layer_index], max_bb_distance); + } + + return result; +} + +Shells map_to_shells( + std::vector &&layers, const Geometry::Mapping &mapping, const std::size_t shell_count +) { + Shells result(shell_count); + for (std::size_t layer_index{0}; layer_index < layers.size(); ++layer_index) { + BoundedPolygons &perimeters{layers[layer_index]}; + for (std::size_t perimeter_index{0}; perimeter_index < perimeters.size(); + perimeter_index++) { + Polygon &perimeter{perimeters[perimeter_index].polygon}; + result[mapping[layer_index][perimeter_index]].push_back( + Slice{std::move(perimeter), layer_index} + ); + } + } + return result; +} +} // namespace Slic3r::Seams::Shells::Impl + +namespace Slic3r::Seams::Shells { +Shells create_shells( + const std::vector &extrusions, const double max_distance +) { + std::vector projected{Impl::project_to_geometry(extrusions, max_distance)}; + + std::vector layer_sizes; + layer_sizes.reserve(projected.size()); + for (const Impl::BoundedPolygons &perimeters : projected) { + layer_sizes.push_back(perimeters.size()); + } + + const auto &[shell_mapping, shell_count]{Geometry::get_mapping( + layer_sizes, + [&](const std::size_t layer_index, + const std::size_t item_index) -> Geometry::MappingOperatorResult { + const Impl::BoundedPolygons &layer{projected[layer_index]}; + const Impl::BoundedPolygons &next_layer{projected[layer_index + 1]}; + if (next_layer.empty()) { + return std::nullopt; + } + + BoundingBoxes next_layer_bounding_boxes; + for (const Impl::BoundedPolygon &bounded_polygon : next_layer) { + next_layer_bounding_boxes.emplace_back(bounded_polygon.bounding_box); + } + + const auto [perimeter_index, distance] = Geometry::pick_closest_bounding_box( + layer[item_index].bounding_box, next_layer_bounding_boxes + ); + + if (distance > max_distance) { + return std::nullopt; + } + return std::pair{perimeter_index, 1.0 / distance}; + } + )}; + + return Impl::map_to_shells(std::move(projected), shell_mapping, shell_count); +} +} // namespace Slic3r::Seams::Shells diff --git a/src/libslic3r/GCode/SeamShells.hpp b/src/libslic3r/GCode/SeamShells.hpp new file mode 100644 index 0000000000..ea81053506 --- /dev/null +++ b/src/libslic3r/GCode/SeamShells.hpp @@ -0,0 +1,47 @@ +#ifndef libslic3r_SeamShells_hpp_ +#define libslic3r_SeamShells_hpp_ + +#include +#include + +#include "libslic3r/Polygon.hpp" +#include "libslic3r/GCode/SeamGeometry.hpp" + +namespace Slic3r { +class Layer; +} + +namespace Slic3r::Seams::Perimeters { +struct Perimeter; +} + +namespace Slic3r::Seams::Shells::Impl { + +struct BoundedPolygon { + Polygon polygon; + BoundingBox bounding_box; + bool is_hole{false}; +}; + +using BoundedPolygons = std::vector; + +BoundedPolygons project_to_geometry(const Geometry::Extrusions &extrusions, const double max_bb_distance); +} + +namespace Slic3r::Seams::Shells { +template struct Slice +{ + T boundary; + std::size_t layer_index; +}; + +template using Shell = std::vector>; + +template using Shells = std::vector>; + +Shells create_shells( + const std::vector &extrusions, const double max_distance +); +} // namespace Slic3r::Seams::Shells + +#endif // libslic3r_SeamShells_hpp_ diff --git a/src/libslic3r/GCode/SmoothPath.cpp b/src/libslic3r/GCode/SmoothPath.cpp index 7c2e8b999c..30361aa3a7 100644 --- a/src/libslic3r/GCode/SmoothPath.cpp +++ b/src/libslic3r/GCode/SmoothPath.cpp @@ -31,75 +31,46 @@ std::optional sample_path_point_at_distance_from_start(const SmoothPath & { if (distance >= 0) { for (const SmoothPathElement &el : path) { - auto it = el.path.begin(); - auto end = el.path.end(); - Point prev_point = it->point; - for (++ it; it != end; ++ it) { - Point point = it->point; - if (it->linear()) { + Point prev_point = el.path.front().point; + for (auto segment_it = el.path.begin() + 1; segment_it != el.path.end(); ++segment_it) { + Point point = segment_it->point; + if (segment_it->linear()) { // Linear segment - Vec2d v = (point - prev_point).cast(); - double lsqr = v.squaredNorm(); + const Vec2d v = (point - prev_point).cast(); + const double lsqr = v.squaredNorm(); if (lsqr > sqr(distance)) - return std::make_optional(prev_point + (v * (distance / sqrt(lsqr))).cast()); + return prev_point + (v * (distance / sqrt(lsqr))).cast(); distance -= sqrt(lsqr); } else { // Circular segment - float angle = Geometry::ArcWelder::arc_angle(prev_point.cast(), point.cast(), it->radius); - double len = std::abs(it->radius) * angle; + const float angle = Geometry::ArcWelder::arc_angle(prev_point.cast(), point.cast(), segment_it->radius); + const double len = std::abs(segment_it->radius) * angle; if (len > distance) { - // Rotate the segment end point in reverse towards the start point. - return std::make_optional(prev_point.rotated(- angle * (distance / len), - Geometry::ArcWelder::arc_center(prev_point.cast(), point.cast(), it->radius, it->ccw()).cast())); + const Point center_pt = Geometry::ArcWelder::arc_center(prev_point.cast(), point.cast(), segment_it->radius, segment_it->ccw()).cast(); + const float rotation_dir = (segment_it->ccw() ? 1.f : -1.f); + // Rotate the segment start point based on the arc orientation. + return prev_point.rotated(rotation_dir * angle * (distance / len), center_pt); } + distance -= len; } + if (distance < 0) - return std::make_optional(point); + return point; + prev_point = point; } } } + // Failed. return {}; } -std::optional sample_path_point_at_distance_from_end(const SmoothPath &path, double distance) -{ - if (distance >= 0) { - for (const SmoothPathElement& el : path) { - auto it = el.path.begin(); - auto end = el.path.end(); - Point prev_point = it->point; - for (++it; it != end; ++it) { - Point point = it->point; - if (it->linear()) { - // Linear segment - Vec2d v = (point - prev_point).cast(); - double lsqr = v.squaredNorm(); - if (lsqr > sqr(distance)) - return std::make_optional(prev_point + (v * (distance / sqrt(lsqr))).cast()); - distance -= sqrt(lsqr); - } - else { - // Circular segment - float angle = Geometry::ArcWelder::arc_angle(prev_point.cast(), point.cast(), it->radius); - double len = std::abs(it->radius) * angle; - if (len > distance) { - // Rotate the segment end point in reverse towards the start point. - return std::make_optional(prev_point.rotated(-angle * (distance / len), - Geometry::ArcWelder::arc_center(prev_point.cast(), point.cast(), it->radius, it->ccw()).cast())); - } - distance -= len; - } - if (distance < 0) - return std::make_optional(point); - prev_point = point; - } - } - } - // Failed. - return {}; +std::optional sample_path_point_at_distance_from_end(const SmoothPath &path, double distance) { + SmoothPath path_reversed = path; + reverse(path_reversed); + return sample_path_point_at_distance_from_start(path_reversed, distance); } // Clip length of a smooth path, for seam hiding. diff --git a/src/libslic3r/GCode/Wipe.cpp b/src/libslic3r/GCode/Wipe.cpp index c773b0f8c3..b5f9151b03 100644 --- a/src/libslic3r/GCode/Wipe.cpp +++ b/src/libslic3r/GCode/Wipe.cpp @@ -211,7 +211,7 @@ std::optional wipe_hide_seam(const SmoothPath &path, bool is_hole, double // Wipe move cannot be calculated, the loop is not long enough. This should not happen due to the longer_than() test above. return {}; } - if (std::optional p = sample_path_point_at_distance_from_end(path, wipe_length); p) + if (std::optional p = sample_path_point_at_distance_from_start(path, wipe_length); p) p_prev = *p; else // Wipe move cannot be calculated, the loop is not long enough. This should not happen due to the longer_than() test above. @@ -231,7 +231,7 @@ std::optional wipe_hide_seam(const SmoothPath &path, bool is_hole, double } // Rotate the forward segment inside by 1/3 of the wedge angle. auto v_rotated = Eigen::Rotation2D(angle_inside) * (p_next - p_current).cast().normalized(); - return std::make_optional(p_current + (v_rotated * wipe_length).cast()); + return p_current + (v_rotated * wipe_length).cast(); } return {}; diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index e4b4d6539a..804999c796 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -944,9 +944,11 @@ void Layer::sort_perimeters_into_islands( auto insert_into_island = [ // Region where the perimeters, gap fills and fill expolygons are stored. - region_id, + region_id, // Whether there are infills with different regions generated for this LayerSlice. has_multiple_regions, + // Layer split into surfaces + &slices, // Perimeters and gap fills to be sorted into islands. &perimeter_and_gapfill_ranges, // Infill regions to be sorted into islands. @@ -959,6 +961,7 @@ void Layer::sort_perimeters_into_islands( lslices_ex[lslice_idx].islands.push_back({}); LayerIsland &island = lslices_ex[lslice_idx].islands.back(); island.perimeters = LayerExtrusionRange(region_id, perimeter_and_gapfill_ranges[source_slice_idx].first); + island.boundary = slices.surfaces[source_slice_idx].expolygon; island.thin_fills = perimeter_and_gapfill_ranges[source_slice_idx].second; if (ExPolygonRange fill_range = fill_expolygons_ranges[source_slice_idx]; ! fill_range.empty()) { if (has_multiple_regions) { diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 6b45c38925..0a2e457df9 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -77,6 +77,8 @@ private: static constexpr const uint32_t fill_region_composite_id = std::numeric_limits::max(); public: + // Boundary of the LayerIsland before perimeter generation. + ExPolygon boundary; // Perimeter extrusions in LayerRegion belonging to this island. LayerExtrusionRange perimeters; // Thin fills of the same region as perimeters. Generated by classic perimeter generator, while Arachne puts them into perimeters. diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index fff8be41b3..722294321b 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -6,7 +6,6 @@ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #include "PerimeterGenerator.hpp" -#include "AABBTreeIndirect.hpp" #include "AABBTreeLines.hpp" #include "BoundingBox.hpp" #include "BridgeDetector.hpp" @@ -14,8 +13,6 @@ #include "ExPolygon.hpp" #include "ExtrusionEntity.hpp" #include "ExtrusionEntityCollection.hpp" -#include "Geometry/MedialAxis.hpp" -#include "KDTreeIndirect.hpp" #include "MultiPoint.hpp" #include "Point.hpp" #include "Polygon.hpp" @@ -25,9 +22,9 @@ #include "Surface.hpp" #include "Geometry/ConvexHull.hpp" -#include "SurfaceCollection.hpp" #include "clipper/clipper_z.hpp" +#include "Arachne/PerimeterOrder.hpp" #include "Arachne/WallToolPaths.hpp" #include "Arachne/utils/ExtrusionLine.hpp" #include "Arachne/utils/ExtrusionJunction.hpp" @@ -36,16 +33,10 @@ #include #include #include -#include #include -#include #include #include -#include -#include -#include #include -#include #include #include #include @@ -53,15 +44,13 @@ #include -// #define ARACHNE_DEBUG +//#define ARACHNE_DEBUG #ifdef ARACHNE_DEBUG #include "SVG.hpp" #include "Utils.hpp" #endif -#include "SVG.hpp" - namespace Slic3r { ExtrusionMultiPath PerimeterGenerator::thick_polyline_to_multi_path(const ThickPolyline &thick_polyline, ExtrusionRole role, const Flow &flow, const float tolerance, const float merge_tolerance) @@ -503,29 +492,20 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path &subject, con return clipped_paths; } -struct PerimeterGeneratorArachneExtrusion -{ - Arachne::ExtrusionLine *extrusion = nullptr; - // Indicates if closed ExtrusionLine is a contour or a hole. Used it only when ExtrusionLine is a closed loop. - bool is_contour = false; - // Should this extrusion be fuzzyfied on path generation? - bool fuzzify = false; -}; - -static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::Parameters ¶ms, const Polygons &lower_slices_polygons_cache, std::vector &pg_extrusions) +static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::Parameters ¶ms, const Polygons &lower_slices_polygons_cache, Arachne::PerimeterOrder::PerimeterExtrusions &pg_extrusions) { ExtrusionEntityCollection extrusion_coll; - for (PerimeterGeneratorArachneExtrusion &pg_extrusion : pg_extrusions) { - Arachne::ExtrusionLine *extrusion = pg_extrusion.extrusion; - if (extrusion->empty()) + for (Arachne::PerimeterOrder::PerimeterExtrusion &pg_extrusion : pg_extrusions) { + Arachne::ExtrusionLine &extrusion = pg_extrusion.extrusion; + if (extrusion.empty()) continue; - const bool is_external = extrusion->inset_idx == 0; + const bool is_external = extrusion.inset_idx == 0; ExtrusionRole role_normal = is_external ? ExtrusionRole::ExternalPerimeter : ExtrusionRole::Perimeter; ExtrusionRole role_overhang = role_normal | ExtrusionRoleModifier::Bridge; if (pg_extrusion.fuzzify) - fuzzy_extrusion_line(*extrusion, scaled(params.config.fuzzy_skin_thickness.value), scaled(params.config.fuzzy_skin_point_dist.value)); + fuzzy_extrusion_line(extrusion, scaled(params.config.fuzzy_skin_thickness.value), scaled(params.config.fuzzy_skin_point_dist.value)); ExtrusionPaths paths; // detect overhanging/bridging perimeters @@ -534,9 +514,9 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::P params.object_config.support_material_contact_distance.value == 0)) { ClipperLib_Z::Path extrusion_path; - extrusion_path.reserve(extrusion->size()); + extrusion_path.reserve(extrusion.size()); BoundingBox extrusion_path_bbox; - for (const Arachne::ExtrusionJunction &ej : extrusion->junctions) { + for (const Arachne::ExtrusionJunction &ej : extrusion.junctions) { extrusion_path.emplace_back(ej.p.x(), ej.p.y(), ej.w); extrusion_path_bbox.merge(Point{ej.p.x(), ej.p.y()}); } @@ -574,7 +554,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::P // Arachne sometimes creates extrusion with zero-length (just two same endpoints); if (!paths.empty()) { Point start_point = paths.front().first_point(); - if (!extrusion->is_closed) { + if (!extrusion.is_closed) { // Especially for open extrusion, we need to select a starting point that is at the start // or the end of the extrusions to make one continuous line. Also, we prefer a non-overhang // starting point. @@ -607,15 +587,15 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::P chain_and_reorder_extrusion_paths(paths, &start_point); } } else { - extrusion_paths_append(paths, *extrusion, role_normal, is_external ? params.ext_perimeter_flow : params.perimeter_flow); + extrusion_paths_append(paths, extrusion, role_normal, is_external ? params.ext_perimeter_flow : params.perimeter_flow); } // Append paths to collection. if (!paths.empty()) { - if (extrusion->is_closed) { + if (extrusion.is_closed) { ExtrusionLoop extrusion_loop(std::move(paths)); // Restore the orientation of the extrusion loop. - if (pg_extrusion.is_contour == extrusion_loop.is_clockwise()) + if (pg_extrusion.is_contour() == extrusion_loop.is_clockwise()) extrusion_loop.reverse_loop(); for (auto it = std::next(extrusion_loop.paths.begin()); it != extrusion_loop.paths.end(); ++it) { @@ -654,7 +634,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::P } #ifdef ARACHNE_DEBUG -static void export_perimeters_to_svg(const std::string &path, const Polygons &contours, const std::vector &perimeters, const ExPolygons &infill_area) +static void export_perimeters_to_svg(const std::string &path, const Polygons &contours, const Arachne::Perimeters &perimeters, const ExPolygons &infill_area) { coordf_t stroke_width = scale_(0.03); BoundingBox bbox = get_extents(contours); @@ -663,7 +643,7 @@ static void export_perimeters_to_svg(const std::string &path, const Polygons &co svg.draw(infill_area, "cyan"); - for (const Arachne::VariableWidthLines &perimeter : perimeters) + for (const Arachne::Perimeter &perimeter : perimeters) for (const Arachne::ExtrusionLine &extrusion_line : perimeter) { ThickPolyline thick_polyline = to_thick_polyline(extrusion_line); svg.draw({thick_polyline}, "green", "blue", stroke_width); @@ -1130,8 +1110,8 @@ void PerimeterGenerator::process_arachne( ExPolygons last = offset_ex(surface.expolygon.simplify_p(params.scaled_resolution), - float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); Polygons last_p = to_polygons(last); Arachne::WallToolPaths wall_tool_paths(last_p, ext_perimeter_spacing, perimeter_spacing, coord_t(loop_number + 1), 0, params.layer_height, params.object_config, params.print_config); - std::vector perimeters = wall_tool_paths.getToolPaths(); - ExPolygons infill_contour = union_ex(wall_tool_paths.getInnerContour()); + Arachne::Perimeters perimeters = wall_tool_paths.getToolPaths(); + ExPolygons infill_contour = union_ex(wall_tool_paths.getInnerContour()); // Check if there are some remaining perimeters to generate (the number of perimeters // is greater than one together with enabled the single perimeter on top surface feature). @@ -1168,7 +1148,7 @@ void PerimeterGenerator::process_arachne( const Polygons not_top_polygons = to_polygons(not_top_expolygons); Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, params.layer_height, params.object_config, params.print_config); - std::vector inner_perimeters = inner_wall_tool_paths.getToolPaths(); + Arachne::Perimeters inner_perimeters = inner_wall_tool_paths.getToolPaths(); // Recalculate indexes of inner perimeters before merging them. if (!perimeters.empty()) { @@ -1197,7 +1177,7 @@ void PerimeterGenerator::process_arachne( #ifdef ARACHNE_DEBUG { static int iRun = 0; - export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); + export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", params.layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); } #endif @@ -1205,107 +1185,20 @@ void PerimeterGenerator::process_arachne( // But in rare cases, Arachne produce ExtrusionLine marked as closed but without // equal the first and the last point. assert([&perimeters = std::as_const(perimeters)]() -> bool { - for (const Arachne::VariableWidthLines &perimeter : perimeters) + for (const Arachne::Perimeter &perimeter : perimeters) for (const Arachne::ExtrusionLine &el : perimeter) if (el.is_closed && el.junctions.front().p != el.junctions.back().p) return false; return true; }()); - int start_perimeter = int(perimeters.size()) - 1; - int end_perimeter = -1; - int direction = -1; - - if (params.config.external_perimeters_first) { - start_perimeter = 0; - end_perimeter = int(perimeters.size()); - direction = 1; - } - - std::vector all_extrusions; - for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) { - if (perimeters[perimeter_idx].empty()) - continue; - for (Arachne::ExtrusionLine &wall : perimeters[perimeter_idx]) - all_extrusions.emplace_back(&wall); - } - - // Find topological order with constraints from extrusions_constrains. - std::vector blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion. - std::vector> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion. - ankerl::unordered_dense::map map_extrusion_to_idx; - for (size_t idx = 0; idx < all_extrusions.size(); idx++) - map_extrusion_to_idx.emplace(all_extrusions[idx], idx); - - Arachne::WallToolPaths::ExtrusionLineSet extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, params.config.external_perimeters_first); - for (auto [before, after] : extrusions_constrains) { - auto after_it = map_extrusion_to_idx.find(after); - ++blocked[after_it->second]; - blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second); - } - - std::vector processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed. - Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position. - std::vector ordered_extrusions; // To store our result in. At the end we'll std::swap. - ordered_extrusions.reserve(all_extrusions.size()); - - while (ordered_extrusions.size() < all_extrusions.size()) { - size_t best_candidate = 0; - double best_distance_sqr = std::numeric_limits::max(); - bool is_best_closed = false; - - std::vector available_candidates; - for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) { - if (processed[candidate] || blocked[candidate]) - continue; // Not a valid candidate. - available_candidates.push_back(candidate); - } - - std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool { - return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed; - }); - - for (const size_t candidate_path_idx : available_candidates) { - auto& path = all_extrusions[candidate_path_idx]; - - if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end. - if (best_distance_sqr == std::numeric_limits::max()) { - best_candidate = candidate_path_idx; - is_best_closed = path->is_closed; - } - continue; - } - - const Point candidate_position = path->junctions.front().p; - double distance_sqr = (current_position - candidate_position).cast().norm(); - if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far. - if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits::max()) || (!path->is_closed && !is_best_closed)) { - best_candidate = candidate_path_idx; - best_distance_sqr = distance_sqr; - is_best_closed = path->is_closed; - } - } - } - - auto &best_path = all_extrusions[best_candidate]; - ordered_extrusions.push_back({best_path, best_path->is_contour(), false}); - processed[best_candidate] = true; - for (size_t unlocked_idx : blocking[best_candidate]) - blocked[unlocked_idx]--; - - if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then. - if(best_path->is_closed) - current_position = best_path->junctions[0].p; //We end where we started. - else - current_position = best_path->junctions.back().p; //Pick the other end from where we started. - } - } + Arachne::PerimeterOrder::PerimeterExtrusions ordered_extrusions = Arachne::PerimeterOrder::ordered_perimeter_extrusions(perimeters, params.config.external_perimeters_first); if (params.layer_id > 0 && params.config.fuzzy_skin != FuzzySkinType::None) { - std::vector closed_loop_extrusions; - for (PerimeterGeneratorArachneExtrusion &extrusion : ordered_extrusions) - if (extrusion.extrusion->inset_idx == 0) { - if (extrusion.extrusion->is_closed && params.config.fuzzy_skin == FuzzySkinType::External) { + std::vector closed_loop_extrusions; + for (Arachne::PerimeterOrder::PerimeterExtrusion &extrusion : ordered_extrusions) + if (extrusion.extrusion.inset_idx == 0) { + if (extrusion.extrusion.is_closed && params.config.fuzzy_skin == FuzzySkinType::External) { closed_loop_extrusions.emplace_back(&extrusion); } else { extrusion.fuzzify = true; @@ -1316,11 +1209,11 @@ void PerimeterGenerator::process_arachne( ClipperLib_Z::Paths loops_paths; loops_paths.reserve(closed_loop_extrusions.size()); for (const auto &cl_extrusion : closed_loop_extrusions) { - assert(cl_extrusion->extrusion->junctions.front() == cl_extrusion->extrusion->junctions.back()); + assert(cl_extrusion->extrusion.junctions.front() == cl_extrusion->extrusion.junctions.back()); size_t loop_idx = &cl_extrusion - &closed_loop_extrusions.front(); ClipperLib_Z::Path loop_path; - loop_path.reserve(cl_extrusion->extrusion->junctions.size() - 1); - for (auto junction_it = cl_extrusion->extrusion->junctions.begin(); junction_it != std::prev(cl_extrusion->extrusion->junctions.end()); ++junction_it) + loop_path.reserve(cl_extrusion->extrusion.junctions.size() - 1); + for (auto junction_it = cl_extrusion->extrusion.junctions.begin(); junction_it != std::prev(cl_extrusion->extrusion.junctions.end()); ++junction_it) loop_path.emplace_back(junction_it->p.x(), junction_it->p.y(), loop_idx); loops_paths.emplace_back(loop_path); } diff --git a/src/libslic3r/ShortEdgeCollapse.cpp b/src/libslic3r/ShortEdgeCollapse.cpp index 6917b24c7c..9089bae6a7 100644 --- a/src/libslic3r/ShortEdgeCollapse.cpp +++ b/src/libslic3r/ShortEdgeCollapse.cpp @@ -9,11 +9,29 @@ #include #include #include +#include #include namespace Slic3r { +/** + * Simple implementation of Fisher-Yates algorithm using uniform int + * distribution from boost, ensurinng the result is the same + * accross platforms. + * + * DO NOT EXPECT IT TO BE PERFORMANT! Use it only when std::shuffle is + * not applicable. + */ +template +void stable_shuffle(Range &range, UniformRandomNumberGenerator &generator) { + const int n{static_cast(range.size())}; + for (int i{0}; i < n - 2; ++i) { + int j{boost::random::uniform_int_distribution{i, n-1}(generator)}; + std::swap(range[i], range[j]); + } +} + void its_short_edge_collpase(indexed_triangle_set &mesh, size_t target_triangle_count) { // whenever vertex is removed, its mapping is update to the index of vertex with wich it merged std::vector vertices_index_mapping(mesh.vertices.size()); @@ -102,8 +120,8 @@ void its_short_edge_collpase(indexed_triangle_set &mesh, size_t target_triangle_ float max_edge_len_squared = edge_len * edge_len; //shuffle the faces and traverse in random order, this MASSIVELY improves the quality of the result - std::shuffle(face_indices.begin(), face_indices.end(), generator); - + stable_shuffle(face_indices, generator); + int allowed_face_removals = int(face_indices.size()) - int(target_triangle_count); for (const size_t &face_idx : face_indices) { if (face_removal_flags[face_idx]) { diff --git a/src/libslic3r/TriangleSetSampling.cpp b/src/libslic3r/TriangleSetSampling.cpp index 363df8659c..dd968fce77 100644 --- a/src/libslic3r/TriangleSetSampling.cpp +++ b/src/libslic3r/TriangleSetSampling.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace Slic3r { @@ -33,8 +34,9 @@ TriangleSetSamples sample_its_uniform_parallel(size_t samples_count, const index } std::mt19937_64 mersenne_engine { 27644437 }; + // Use boost instead of std to ensure stability accross platforms! // random numbers on interval [0, 1) - std::uniform_real_distribution fdistribution; + boost::random::uniform_real_distribution fdistribution; auto get_random = [&fdistribution, &mersenne_engine]() { return Vec3d { fdistribution(mersenne_engine), fdistribution(mersenne_engine), fdistribution(mersenne_engine) }; diff --git a/src/slic3r/GUI/DoubleSliderForLayers.cpp b/src/slic3r/GUI/DoubleSliderForLayers.cpp index ce9216c95d..d9c3bde007 100644 --- a/src/slic3r/GUI/DoubleSliderForLayers.cpp +++ b/src/slic3r/GUI/DoubleSliderForLayers.cpp @@ -8,6 +8,9 @@ #include "libslic3r/format.hpp" // -> format() #include "I18N.hpp" +#include +#include +#include #include #include "ImGuiWrapper.hpp" diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 9a6dd4a288..82f7644300 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include diff --git a/src/slic3r/GUI/ImGuiPureWrap.hpp b/src/slic3r/GUI/ImGuiPureWrap.hpp index ef1f04c8a5..e883d6cce5 100644 --- a/src/slic3r/GUI/ImGuiPureWrap.hpp +++ b/src/slic3r/GUI/ImGuiPureWrap.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include diff --git a/src/slic3r/GUI/TickCodesManager.hpp b/src/slic3r/GUI/TickCodesManager.hpp index 4eb28dce0f..d36dae989a 100644 --- a/src/slic3r/GUI/TickCodesManager.hpp +++ b/src/slic3r/GUI/TickCodesManager.hpp @@ -7,6 +7,8 @@ #include "libslic3r/CustomGCode.hpp" +#include +#include #include #include diff --git a/tests/data/seam_test_object.3mf b/tests/data/seam_test_object.3mf new file mode 100644 index 0000000000..c216b30725 Binary files /dev/null and b/tests/data/seam_test_object.3mf differ diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index da1d8b54c6..2872aa464b 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -14,6 +14,13 @@ add_executable(${_TEST_NAME}_tests test_gaps.cpp test_gcode.cpp test_gcode_travels.cpp + test_seam_perimeters.cpp + test_seam_shells.cpp + test_seam_geometry.cpp + test_seam_aligned.cpp + test_seam_rear.cpp + test_seam_random.cpp + benchmark_seams.cpp test_gcodefindreplace.cpp test_gcodewriter.cpp test_cancel_object.cpp @@ -33,6 +40,7 @@ add_executable(${_TEST_NAME}_tests ) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") +target_compile_definitions(${_TEST_NAME}_tests PUBLIC CATCH_CONFIG_ENABLE_BENCHMARKING) if (WIN32) prusaslicer_copy_dlls(${_TEST_NAME}_tests) diff --git a/tests/fff_print/benchmark_seams.cpp b/tests/fff_print/benchmark_seams.cpp new file mode 100644 index 0000000000..337c730a6d --- /dev/null +++ b/tests/fff_print/benchmark_seams.cpp @@ -0,0 +1,124 @@ +#include +#include "test_data.hpp" + +#include "libslic3r/GCode/SeamGeometry.hpp" +#include "libslic3r/GCode/SeamAligned.hpp" +#include "libslic3r/GCode/SeamRear.hpp" +#include "libslic3r/GCode/SeamRandom.hpp" + +TEST_CASE_METHOD(Slic3r::Test::SeamsFixture, "Seam benchmarks", "[Seams][.Benchmarks]") { + BENCHMARK_ADVANCED("Create extrusions benchy")(Catch::Benchmark::Chronometer meter) { + meter.measure([&] { return Slic3r::Seams::Geometry::get_extrusions(print_object->layers()); }); + }; + + using namespace Slic3r::Seams; + BENCHMARK_ADVANCED("Create shells benchy")(Catch::Benchmark::Chronometer meter) { + meter.measure([&] { return Shells::create_shells(extrusions, params.max_distance); }); + }; + + + BENCHMARK_ADVANCED("Get layer infos benchy")(Catch::Benchmark::Chronometer meter) { + meter.measure([&] { + return Perimeters::get_layer_infos( + print_object->layers(), params.perimeter.elephant_foot_compensation + ); + }); + }; + + BENCHMARK_ADVANCED("Create perimeters benchy")(Catch::Benchmark::Chronometer meter) { + meter.measure([&] { + return Perimeters::create_perimeters(shell_polygons, layer_infos, painting, params.perimeter); + }); + }; + + BENCHMARK_ADVANCED("Generate aligned seam benchy")(Catch::Benchmark::Chronometer meter) { + std::vector> inputs; + inputs.reserve(meter.runs()); + std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() { + return Perimeters::create_perimeters( + shell_polygons, layer_infos, painting, params.perimeter + ); + }); + meter.measure([&](const int i) { + return Aligned::get_object_seams( + std::move(inputs[i]), visibility_calculator, params.aligned + ); + }); + }; + + BENCHMARK_ADVANCED("Visibility constructor")(Catch::Benchmark::Chronometer meter) { + using Visibility = Slic3r::ModelInfo::Visibility; + std::vector> storage(meter.runs()); + meter.measure([&](const int i) { + storage[i].construct(transformation, volumes, params.visibility, []() {}); + }); + }; + + BENCHMARK_ADVANCED("Generate rear seam benchy")(Catch::Benchmark::Chronometer meter) { + std::vector> inputs; + inputs.reserve(meter.runs()); + std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() { + return create_perimeters( + shell_polygons, layer_infos, painting, params.perimeter + ); + }); + meter.measure([&](const int i) { + return Rear::get_object_seams(std::move(inputs[i]), params.rear_project_threshold); + }); + }; + + BENCHMARK_ADVANCED("Generate random seam benchy")(Catch::Benchmark::Chronometer meter) { + std::vector> inputs; + inputs.reserve(meter.runs()); + std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() { + return Perimeters::create_perimeters( + shell_polygons, layer_infos, painting, params.perimeter + ); + }); + meter.measure([&](const int i) { + return Random::get_object_seams(std::move(inputs[i]), params.random_seed); + }); + }; + + Placer placer; + BENCHMARK_ADVANCED("Init seam placer aligned")(Catch::Benchmark::Chronometer meter) { + meter.measure([&] { + return placer.init(print->objects(), params, [](){}); + }); + }; + + SECTION("Place seam"){ + using namespace Slic3r; + Placer placer; + placer.init(print->objects(), params, [](){}); + std::vector> loops; + + const PrintObject* object{print->objects().front()}; + for (const Layer* layer :object->layers()) { + for (const LayerSlice& lslice : layer->lslices_ex) { + for (const LayerIsland &island : lslice.islands) { + const LayerRegion &layer_region = *layer->get_region(island.perimeters.region()); + for (uint32_t perimeter_id : island.perimeters) { + const auto *entity_collection{static_cast(layer_region.perimeters().entities[perimeter_id])}; + if (entity_collection != nullptr) { + for (const ExtrusionEntity *entity : *entity_collection) { + const auto loop{static_cast(entity)}; + if (loop == nullptr) { + continue; + } + loops.emplace_back(layer, loop); + } + } + } + } + } + } + BENCHMARK_ADVANCED("Place seam benchy")(Catch::Benchmark::Chronometer meter) { + meter.measure([&] { + for (const auto &[layer, loop] : loops) { + placer.place_seam(layer, *loop, {0, 0}); + } + }); + }; + } +} diff --git a/tests/fff_print/test_data.hpp b/tests/fff_print/test_data.hpp index 12ff0f551d..61e1b92d42 100644 --- a/tests/fff_print/test_data.hpp +++ b/tests/fff_print/test_data.hpp @@ -2,12 +2,19 @@ #define SLIC3R_TEST_DATA_HPP #include "libslic3r/Config.hpp" +#include "libslic3r/Format/3mf.hpp" +#include "libslic3r/GCode/ModelVisibility.hpp" +#include "libslic3r/GCode/SeamGeometry.hpp" +#include "libslic3r/GCode/SeamPerimeters.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/GCode/SeamPlacer.hpp" +#include "libslic3r/GCode/SeamAligned.hpp" +#include #include namespace Slic3r { namespace Test { @@ -39,14 +46,13 @@ enum class TestMesh { }; // Neccessary for (tm); - } +struct TestMeshHash +{ + std::size_t operator()(TestMesh tm) const { return static_cast(tm); } }; /// Mesh enumeration to name mapping -extern const std::unordered_map mesh_names; +extern const std::unordered_map mesh_names; /// Port of Slic3r::Test::mesh /// Basic cubes/boxes should call TriangleMesh::make_cube() directly and rescale/translate it @@ -56,35 +62,173 @@ TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0 TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0); /// Templated function to see if two values are equivalent (+/- epsilon) -template -bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; } +template bool _equiv(const T &a, const T &b) { return std::abs(a - b) < EPSILON; } -template -bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; } +template bool _equiv(const T &a, const T &b, double epsilon) { + return abs(a - b) < epsilon; +} -Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh); -void init_print(std::vector &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false, unsigned duplicate_count = 1); -void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false, unsigned duplicate_count = 1); -void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false, unsigned duplicate = 1); -void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list config_items, bool comments = false, unsigned duplicate = 1); -void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list config_items, bool comments = false, unsigned duplicate = 1); +Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh); +void init_print( + std::vector &&meshes, + Slic3r::Print &print, + Slic3r::Model &model, + const DynamicPrintConfig &config_in, + bool comments = false, + unsigned duplicate_count = 1 +); +void init_print( + std::initializer_list meshes, + Slic3r::Print &print, + Slic3r::Model &model, + const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), + bool comments = false, + unsigned duplicate_count = 1 +); +void init_print( + std::initializer_list meshes, + Slic3r::Print &print, + Slic3r::Model &model, + const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), + bool comments = false, + unsigned duplicate = 1 +); +void init_print( + std::initializer_list meshes, + Slic3r::Print &print, + Slic3r::Model &model, + std::initializer_list config_items, + bool comments = false, + unsigned duplicate = 1 +); +void init_print( + std::initializer_list meshes, + Slic3r::Print &print, + Slic3r::Model &model, + std::initializer_list config_items, + bool comments = false, + unsigned duplicate = 1 +); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list config_items, bool comments = false); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list config_items, bool comments = false); +void init_and_process_print( + std::initializer_list meshes, + Slic3r::Print &print, + const DynamicPrintConfig &config, + bool comments = false +); +void init_and_process_print( + std::initializer_list meshes, + Slic3r::Print &print, + const DynamicPrintConfig &config, + bool comments = false +); +void init_and_process_print( + std::initializer_list meshes, + Slic3r::Print &print, + std::initializer_list config_items, + bool comments = false +); +void init_and_process_print( + std::initializer_list meshes, + Slic3r::Print &print, + std::initializer_list config_items, + bool comments = false +); -std::string gcode(Print& print); +std::string gcode(Print &print); -std::string slice(std::initializer_list meshes, const DynamicPrintConfig &config, bool comments = false); -std::string slice(std::initializer_list meshes, const DynamicPrintConfig &config, bool comments = false); -std::string slice(std::initializer_list meshes, std::initializer_list config_items, bool comments = false); -std::string slice(std::initializer_list meshes, std::initializer_list config_items, bool comments = false); +std::string slice( + std::initializer_list meshes, const DynamicPrintConfig &config, bool comments = false +); +std::string slice( + std::initializer_list meshes, + const DynamicPrintConfig &config, + bool comments = false +); +std::string slice( + std::initializer_list meshes, + std::initializer_list config_items, + bool comments = false +); +std::string slice( + std::initializer_list meshes, + std::initializer_list config_items, + bool comments = false +); bool contains(const std::string &data, const std::string &pattern); bool contains_regex(const std::string &data, const std::string &pattern); -} } // namespace Slic3r::Test +inline std::unique_ptr process_3mf(const std::filesystem::path &path) { + DynamicPrintConfig config; + auto print{std::make_unique()}; + Model model; + ConfigSubstitutionContext context{ForwardCompatibilitySubstitutionRule::Disable}; + load_3mf(path.string().c_str(), config, context, &model, false); + + Slic3r::Test::init_print(std::vector{}, *print, model, config); + print->process(); + + return print; +} + +static std::map> prints_3mfs; +// Lazy getter, to avoid processing the 3mf multiple times, it already takes ages. +inline Print *get_print(const std::filesystem::path &file_path) { + if (!prints_3mfs.count(file_path.string())) { + prints_3mfs[file_path.string()] = process_3mf(file_path.string()); + } + return prints_3mfs[file_path.string()].get(); +} + +inline void serialize_seam(std::ostream &output, const std::vector> &seam) { + output << "x,y,z,layer_index" << std::endl; + + for (const std::vector &layer : seam) { + if (layer.empty()) { + continue; + } + const Seams::SeamPerimeterChoice &choice{layer.front()}; + + // clang-format off + output + << choice.choice.position.x() << "," + << choice.choice.position.y() << "," + << choice.perimeter.slice_z << "," + << choice.perimeter.layer_index << std::endl; + // clang-format on + } +} + +struct SeamsFixture +{ + const std::filesystem::path file_3mf{ + std::filesystem::path{TEST_DATA_DIR} / std::filesystem::path{"seam_test_object.3mf"}}; + const Print *print{Test::get_print(file_3mf)}; + const PrintObject *print_object{print->objects()[0]}; + + Seams::Params params{Seams::Placer::get_params(print->full_print_config())}; + + const Transform3d transformation{print_object->trafo_centered()}; + const ModelVolumePtrs &volumes{print_object->model_object()->volumes}; + Seams::ModelInfo::Painting painting{transformation, volumes}; + + const std::vector extrusions{ + Seams::Geometry::get_extrusions(print_object->layers())}; + const Seams::Perimeters::LayerInfos layer_infos{Seams::Perimeters::get_layer_infos( + print_object->layers(), params.perimeter.elephant_foot_compensation + )}; + Seams::Shells::Shells shell_polygons{ + Seams::Shells::create_shells(extrusions, params.max_distance)}; + + const std::size_t shell_index{15}; + + const ModelInfo::Visibility visibility{transformation, volumes, params.visibility, [](){}}; + Seams::Aligned::VisibilityCalculator + visibility_calculator{visibility, params.convex_visibility_modifier, params.concave_visibility_modifier}; +}; + +}} // namespace Slic3r::Test #endif // SLIC3R_TEST_DATA_HPP diff --git a/tests/fff_print/test_seam_aligned.cpp b/tests/fff_print/test_seam_aligned.cpp new file mode 100644 index 0000000000..1ab755cfaf --- /dev/null +++ b/tests/fff_print/test_seam_aligned.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include "test_data.hpp" +#include + +using namespace Slic3r; +using namespace Slic3r::Seams; + +constexpr bool debug_files{false}; + +namespace AlignedTest { +Perimeters::Perimeter get_perimeter() { + const double slice_z{1.0}; + const std::size_t layer_index{}; + std::vector positions{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.5}}; + std::vector angles(positions.size(), -M_PI / 2.0); + angles[4] = 0.0; + std::vector point_types(positions.size(), Perimeters::PointType::common); + std::vector + point_classifications{positions.size(), Perimeters::PointClassification::common}; + std::vector angle_type(positions.size(), Perimeters::AngleType::concave); + angle_type[4] = Perimeters::AngleType::smooth; + + return { + slice_z, + layer_index, + false, + std::move(positions), + std::move(angles), + std::move(point_types), + std::move(point_classifications), + std::move(angle_type)}; +} +} // namespace AlignedTest + +TEST_CASE("Snap to angle", "[Seams][SeamAligned]") { + const Vec2d point{0.0, 0.4}; + const std::size_t search_start{4}; + const Perimeters::Perimeter perimeter{AlignedTest::get_perimeter()}; + + std::optional snapped_to{ + Aligned::Impl::snap_to_angle(point, search_start, perimeter, 0.5)}; + + REQUIRE(snapped_to); + CHECK(*snapped_to == 0); + + snapped_to = Aligned::Impl::snap_to_angle(point, search_start, perimeter, 0.3); + REQUIRE(!snapped_to); +} + +TEST_CASE("Get seam options", "[Seams][SeamAligned]") { + Perimeters::Perimeter perimeter{AlignedTest::get_perimeter()}; + const Vec2d prefered_position{0.0, 0.3}; + + Aligned::Impl::SeamOptions options{Aligned::Impl::get_seam_options( + perimeter, prefered_position, *perimeter.common_points.common_points, 0.4 + )}; + + CHECK(options.closest == 4); + CHECK(options.adjacent == 0); + CHECK((options.on_edge - Vec2d{0.0, 0.3}).norm() == Approx(0.0)); + REQUIRE(options.snapped); + CHECK(options.snapped == 0); +} + +struct PickSeamOptionFixture +{ + Perimeters::Perimeter perimeter{AlignedTest::get_perimeter()}; + + Aligned::Impl::SeamOptions options{ + 4, // closest + 0, // adjacent + true, // forward + false, // snapped + Vec2d{0.0, 0.3}, // on_edge + }; +}; + +TEST_CASE_METHOD(PickSeamOptionFixture, "Pick seam option", "[Seams][SeamAligned]") { + auto [previous_index, next_index, position]{pick_seam_option(perimeter, options)}; + CHECK(previous_index == next_index); + CHECK((position - Vec2d{0.0, 0.0}).norm() == Approx(0.0)); +} + +TEST_CASE_METHOD(PickSeamOptionFixture, "Pick seam option picks enforcer", "[Seams][SeamAligned]") { + perimeter.point_types[4] = Perimeters::PointType::enforcer; + + auto [previous_index, next_index, position]{pick_seam_option(perimeter, options)}; + CHECK(previous_index == next_index); + CHECK((position - Vec2d{0.0, 0.5}).norm() == Approx(0.0)); +} + +TEST_CASE_METHOD(PickSeamOptionFixture, "Nearest point", "[Seams][SeamAligned]") { + const std::optional result{Aligned::Impl::Nearest{Vec2d{0.4, -0.1}, 0.2}( + perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common + )}; + CHECK(result->previous_index == 0); + CHECK(result->next_index == 1); + CHECK((result->position - Vec2d{0.4, 0.0}).norm() == Approx(0.0)); +} + +TEST_CASE_METHOD(PickSeamOptionFixture, "Least visible point", "[Seams][SeamAligned]") { + std::vector precalculated_visibility{}; + for (std::size_t i{0}; i < perimeter.positions.size(); ++i) { + precalculated_visibility.push_back(-static_cast(i)); + } + Aligned::Impl::LeastVisible least_visible{precalculated_visibility}; + const std::optional result{least_visible( + perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common + )}; + CHECK(result->previous_index == 4); + CHECK(result->next_index == 4); + CHECK((result->position - Vec2d{0.0, 0.5}).norm() == Approx(0.0)); +} + +TEST_CASE_METHOD(Test::SeamsFixture, "Generate aligned seam", "[Seams][SeamAligned][Integration]") { + Shells::Shells<> perimeters{ + Perimeters::create_perimeters(shell_polygons, layer_infos, painting, params.perimeter)}; + Shells::Shells<> shell_perimeters; + shell_perimeters.push_back(std::move(perimeters[shell_index])); + const std::vector> seam{ + Aligned::get_object_seams(std::move(shell_perimeters), visibility_calculator, params.aligned)}; + REQUIRE(seam.size() == 125); + + if constexpr (debug_files) { + std::ofstream csv{"aligned_seam.csv"}; + Test::serialize_seam(csv, seam); + } +} + +TEST_CASE_METHOD(Test::SeamsFixture, "Calculate visibility", "[Seams][SeamAligned][Integration]") { + if constexpr (debug_files) { + std::ofstream csv{"visibility.csv"}; + csv << "x,y,z,visibility,total_visibility" << std::endl; + Shells::Shells<> perimeters{ + Perimeters::create_perimeters(shell_polygons, layer_infos, painting, params.perimeter)}; + for (const Shells::Shell<> &shell : perimeters) { + for (const Shells::Slice<> &slice : shell) { + for (std::size_t index{0}; index < slice.boundary.positions.size(); ++index) { + const Vec2d &position{slice.boundary.positions[index]}; + const double point_visibility{visibility.calculate_point_visibility( + to_3d(position.cast(), slice.boundary.slice_z) + )}; + const double total_visibility{ + visibility_calculator(SeamChoice{index, index, position}, slice.boundary)}; + + // clang-format off + csv << + position.x() << "," << + position.y() << "," << + slice.boundary.slice_z << "," << + point_visibility << "," << + total_visibility << std::endl; + // clang-format on + } + } + } + } +} diff --git a/tests/fff_print/test_seam_geometry.cpp b/tests/fff_print/test_seam_geometry.cpp new file mode 100644 index 0000000000..a12fbdb3d6 --- /dev/null +++ b/tests/fff_print/test_seam_geometry.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include + +using namespace Slic3r; + +TEST_CASE("Lists mapping", "[Seams][SeamGeometry]") { + // clang-format off + std::vector> list_of_lists{ + {}, + {7, 2, 3}, + {9, 6, 3, 6, 7}, + {1, 1, 3}, + {1}, + {3}, + {1}, + {}, + {3} + }; + // clang-format on + + std::vector sizes; + sizes.reserve(list_of_lists.size()); + for (const std::vector &list : list_of_lists) { + sizes.push_back(list.size()); + } + + const auto [mapping, bucket_cout]{Seams::Geometry::get_mapping( + sizes, + [&](const std::size_t layer_index, + const std::size_t item_index) -> Seams::Geometry::MappingOperatorResult { + unsigned max_diff{0}; + std::optional index; + const std::vector &layer{list_of_lists[layer_index]}; + const std::vector &next_layer{list_of_lists[layer_index + 1]}; + for (std::size_t i{0}; i < next_layer.size(); ++i) { + const long diff{std::abs(next_layer[i] - layer[item_index])}; + if (diff > max_diff) { + max_diff = diff; + index = i; + } + } + if (!index) { + return std::nullopt; + } + return std::pair{*index, static_cast(max_diff)}; + } + )}; + + // clang-format off + CHECK(mapping == std::vector>{ + {}, + {0, 1, 2}, + {1, 3, 0, 4, 5}, + {1, 6, 7}, + {7}, + {7}, + {7}, + {}, + {8} + }); + // clang-format on +} + +TEST_CASE("Vertex angle calculation counterclockwise", "[Seams][SeamGeometry]") { + std::vector points{Vec2d{0, 0}, Vec2d{1, 0}, Vec2d{1, 1}, Vec2d{0, 1}}; + std::vector angles{Seams::Geometry::get_vertex_angles(points, 0.1)}; + + CHECK(angles.size() == 4); + for (const double angle : angles) { + CHECK(angle == Approx(-M_PI / 2)); + } +} + +TEST_CASE("Vertex angle calculation clockwise", "[Seams][SeamGeometry]") { + std::vector points = {Vec2d{0, 0}, Vec2d{0, 1}, Vec2d{1, 1}, Vec2d{1, 0}}; + std::vector angles = Seams::Geometry::get_vertex_angles(points, 0.1); + + CHECK(angles.size() == 4); + for (const double angle : angles) { + CHECK(angle == Approx(M_PI / 2)); + } +} + +TEST_CASE("Vertex angle calculation small convex", "[Seams][SeamGeometry]") { + std::vector points = {Vec2d{0, 0}, Vec2d{-0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}}; + std::vector angles = Seams::Geometry::get_vertex_angles(points, 0.1); + + CHECK(angles.size() == 4); + CHECK(angles[1] > 0); + CHECK(angles[1] < 0.02); +} + +TEST_CASE("Vertex angle calculation small concave", "[Seams][SeamGeometry]") { + std::vector points = {Vec2d{0, 0}, Vec2d{0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}}; + std::vector angles = Seams::Geometry::get_vertex_angles(points, 0.1); + + CHECK(angles.size() == 4); + CHECK(angles[1] < 0); + CHECK(angles[1] > -0.02); +} + +TEST_CASE("Vertex angle is rotation agnostic", "[Seams][SeamGeometry]") { + std::vector points = {Vec2d{0, 0}, Vec2d{0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}}; + std::vector angles = Seams::Geometry::get_vertex_angles(points, 0.1); + + Points polygon_points; + using std::transform, std::back_inserter; + transform(points.begin(), points.end(), back_inserter(polygon_points), [](const Vec2d &point) { + return scaled(point); + }); + Polygon polygon{polygon_points}; + polygon.rotate(M_PI - Slic3r::Geometry::deg2rad(10.0)); + + std::vector rotated_points; + using std::transform, std::back_inserter; + transform( + polygon.points.begin(), polygon.points.end(), back_inserter(rotated_points), + [](const Point &point) { return unscaled(point); } + ); + + std::vector rotated_angles = Seams::Geometry::get_vertex_angles(points, 0.1); + CHECK(rotated_angles[1] == Approx(angles[1])); +} + +TEST_CASE("Calculate overhangs", "[Seams][SeamGeometry]") { + const ExPolygon square{ + scaled(Vec2d{0.0, 0.0}), + scaled(Vec2d{1.0, 0.0}), + scaled(Vec2d{1.0, 1.0}), + scaled(Vec2d{0.0, 1.0}) + }; + const std::vector points{Seams::Geometry::unscaled(square.contour.points)}; + ExPolygon previous_layer{square}; + previous_layer.translate(scaled(Vec2d{-0.5, 0})); + AABBTreeLines::LinesDistancer previous_layer_distancer{ + to_unscaled_linesf({previous_layer})}; + const std::vector overhangs{ + Seams::Geometry::get_overhangs(points, previous_layer_distancer, 0.5)}; + REQUIRE(overhangs.size() == points.size()); + CHECK_THAT(overhangs, Catch::Matchers::Approx(std::vector{ + 0.0, M_PI / 4.0, M_PI / 4.0, 0.0 + })); +} diff --git a/tests/fff_print/test_seam_perimeters.cpp b/tests/fff_print/test_seam_perimeters.cpp new file mode 100644 index 0000000000..405eb3731d --- /dev/null +++ b/tests/fff_print/test_seam_perimeters.cpp @@ -0,0 +1,181 @@ +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/GCode/SeamPerimeters.hpp" +#include "libslic3r/Layer.hpp" +#include "libslic3r/Point.hpp" +#include +#include +#include +#include + +#include "test_data.hpp" + +using namespace Slic3r; +using namespace Slic3r::Seams; + +constexpr bool debug_files{false}; + +const ExPolygon square{ + scaled(Vec2d{0.0, 0.0}), scaled(Vec2d{1.0, 0.0}), scaled(Vec2d{1.0, 1.0}), + scaled(Vec2d{0.0, 1.0})}; + +TEST_CASE("Oversample painted", "[Seams][SeamPerimeters]") { + auto is_painted{[](const Vec3f &position, float radius) { + return (position - Vec3f{0.5, 0.0, 1.0}).norm() < radius; + }}; + std::vector points{Perimeters::Impl::oversample_painted( + Seams::Geometry::unscaled(square.contour.points), is_painted, 1.0, 0.2 + )}; + + REQUIRE(points.size() == 8); + CHECK((points[1] - Vec2d{0.2, 0.0}).norm() == Approx(0.0)); + + points = Perimeters::Impl::oversample_painted( + Seams::Geometry::unscaled(square.contour.points), is_painted, 1.0, 0.199 + ); + CHECK(points.size() == 9); +} + +TEST_CASE("Remove redundant points", "[Seams][SeamPerimeters]") { + using Perimeters::PointType; + using Perimeters::PointClassification; + + std::vector points{{0.0, 0.0}, {1.0, 0.0}, {2.0, 0.0}, {3.0, 0.0}, + {3.0, 1.0}, {3.0, 2.0}, {0.0, 2.0}}; + std::vector point_types{PointType::common, + PointType::enforcer, // Should keep this. + PointType::enforcer, // Should keep this. + PointType::blocker, + PointType::blocker, // Should remove this. + PointType::blocker, PointType::common}; + + const auto [resulting_points, resulting_point_types]{ + Perimeters::Impl::remove_redundant_points(points, point_types, 0.1)}; + + REQUIRE(resulting_points.size() == 6); + REQUIRE(resulting_point_types.size() == 6); + CHECK((resulting_points[3] - Vec2d{3.0, 0.0}).norm() == Approx(0.0)); + CHECK((resulting_points[4] - Vec2d{3.0, 2.0}).norm() == Approx(0.0)); + CHECK(resulting_point_types[3] == PointType::blocker); + CHECK(resulting_point_types[4] == PointType::blocker); +} + +TEST_CASE("Perimeter constructs KD trees", "[Seams][SeamPerimeters]") { + using Perimeters::PointType; + using Perimeters::PointClassification; + using Perimeters::AngleType; + + std::vector positions{Vec2d{0.0, 0.0}, Vec2d{1.0, 0.0}, Vec2d{1.0, 1.0}, Vec2d{0.0, 1.0}}; + std::vector angles(4, -M_PI / 2.0); + std::vector + point_types{PointType::enforcer, PointType::blocker, PointType::common, PointType::common}; + std::vector point_classifications{ + PointClassification::overhang, PointClassification::embedded, PointClassification::embedded, + PointClassification::common}; + std::vector + angle_types{AngleType::convex, AngleType::concave, AngleType::smooth, AngleType::smooth}; + Perimeters::Perimeter perimeter{ + 3.0, + 2, + false, + std::move(positions), + std::move(angles), + std::move(point_types), + std::move(point_classifications), + std::move(angle_types)}; + + CHECK(perimeter.enforced_points.overhanging_points); + CHECK(perimeter.blocked_points.embedded_points); + CHECK(perimeter.common_points.common_points); + CHECK(perimeter.common_points.embedded_points); +} + +using std::filesystem::path; + +constexpr const char *to_string(Perimeters::PointType point_type) { + using Perimeters::PointType; + + switch (point_type) { + case PointType::enforcer: return "enforcer"; + case PointType::blocker: return "blocker"; + case PointType::common: return "common"; + } + throw std::runtime_error("Unreachable"); +} + +constexpr const char *to_string(Perimeters::PointClassification point_classification) { + using Perimeters::PointClassification; + + switch (point_classification) { + case PointClassification::embedded: return "embedded"; + case PointClassification::overhang: return "overhang"; + case PointClassification::common: return "common"; + } + throw std::runtime_error("Unreachable"); +} + +constexpr const char *to_string(Perimeters::AngleType angle_type) { + using Perimeters::AngleType; + + switch (angle_type) { + case AngleType::convex: return "convex"; + case AngleType::concave: return "concave"; + case AngleType::smooth: return "smooth"; + } + throw std::runtime_error("Unreachable"); +} + +void serialize_shell(std::ostream &output, const Shells::Shell &shell) { + output << "x,y,z,point_type,point_classification,angle_type,layer_index," + "point_index,distance,distance_to_previous,is_degenerate" + << std::endl; + + for (std::size_t perimeter_index{0}; perimeter_index < shell.size(); ++perimeter_index) { + const Shells::Slice<> &slice{shell[perimeter_index]}; + const Perimeters::Perimeter &perimeter{slice.boundary}; + const std::vector &points{perimeter.positions}; + + double total_distance{0.0}; + for (std::size_t point_index{0}; point_index < perimeter.point_types.size(); ++point_index) { + const Vec3d point{to_3d(points[point_index], perimeter.slice_z)}; + const Perimeters::PointType point_type{perimeter.point_types[point_index]}; + const Perimeters::PointClassification point_classification{ + perimeter.point_classifications[point_index]}; + const Perimeters::AngleType angle_type{perimeter.angle_types[point_index]}; + const std::size_t layer_index{slice.layer_index}; + const std::size_t previous_index{point_index == 0 ? points.size() - 1 : point_index - 1}; + const double distance_to_previous{(points[point_index] - points[previous_index]).norm()}; + total_distance += point_index == 0 ? 0.0 : distance_to_previous; + const double distance{total_distance}; + const bool is_degenerate{perimeter.is_degenerate}; + + // clang-format off + output + << point.x() << "," + << point.y() << "," + << point.z() << "," + << to_string(point_type) << "," + << to_string(point_classification) << "," + << to_string(angle_type) << "," + << layer_index << "," + << point_index << "," + << distance << "," + << distance_to_previous << "," + << is_degenerate << std::endl; + // clang-format on + } + } +} + +TEST_CASE_METHOD(Test::SeamsFixture, "Create perimeters", "[Seams][SeamPerimeters][Integration]") { + const Shells::Shells<> perimeters{ + create_perimeters(shell_polygons, layer_infos, painting, params.perimeter)}; + + const Shells::Shell<> &shell{perimeters[shell_index]}; + + if constexpr (debug_files) { + std::ofstream csv{"perimeters.csv"}; + serialize_shell(csv, shell); + } + + REQUIRE(shell.size() == 54); +} diff --git a/tests/fff_print/test_seam_random.cpp b/tests/fff_print/test_seam_random.cpp new file mode 100644 index 0000000000..8a69c6b66a --- /dev/null +++ b/tests/fff_print/test_seam_random.cpp @@ -0,0 +1,99 @@ +#include +#include +#include +#include "test_data.hpp" +#include + +using namespace Slic3r; +using namespace Slic3r::Seams; + +constexpr bool debug_files{false}; + +namespace RandomTest { +Perimeters::Perimeter get_perimeter() { + const double slice_z{1.0}; + const std::size_t layer_index{}; + std::vector positions{{0.0, 0.0}, {0.5, 0.0}, {1.0, 0.0}}; + std::vector angles(positions.size(), -M_PI / 2.0); + std::vector point_types(positions.size(), Perimeters::PointType::common); + std::vector + point_classifications{positions.size(), Perimeters::PointClassification::common}; + std::vector angle_type(positions.size(), Perimeters::AngleType::concave); + + return { + slice_z, + layer_index, + false, + std::move(positions), + std::move(angles), + std::move(point_types), + std::move(point_classifications), + std::move(angle_type)}; +} +} // namespace RandomTest + +double get_chi2_uniform(const std::vector &data, double min, double max, const std::size_t bin_count) { + std::vector bins(bin_count); + const double bin_size{(max - min) / bin_count}; + const double expected_frequncy{static_cast(data.size()) / bin_count}; + + for (const double value : data) { + auto bin{static_cast(std::floor((value - min) / bin_size))}; + bins[bin]++; + } + + return std::accumulate(bins.begin(), bins.end(), 0.0, [&](const double total, const std::size_t count_in_bin){ + return total + std::pow(static_cast(count_in_bin - expected_frequncy), 2.0) / expected_frequncy; + }); +} + +TEST_CASE("Random is uniform", "[Seams][SeamRandom]") { + const int seed{42}; + std::mt19937 random_engine{seed}; + const Random::Impl::Random random{random_engine}; + Perimeters::Perimeter perimeter{RandomTest::get_perimeter()}; + + std::vector x_positions; + const std::size_t count{1001}; + x_positions.reserve(count); + std::generate_n(std::back_inserter(x_positions), count, [&]() { + std::optional choice{ + random(perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common)}; + return choice->position.x(); + }); + const std::size_t degrees_of_freedom{10}; + const double critical{18.307}; // dof 10, significance 0.05 + + CHECK(get_chi2_uniform(x_positions, 0.0, 1.0, degrees_of_freedom + 1) < critical); +} + +TEST_CASE("Random respects point type", "[Seams][SeamRandom]") { + const int seed{42}; + std::mt19937 random_engine{seed}; + const Random::Impl::Random random{random_engine}; + Perimeters::Perimeter perimeter{RandomTest::get_perimeter()}; + std::optional choice{ + random(perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common)}; + + REQUIRE(choice); + const std::size_t picked_index{choice->previous_index}; + perimeter.point_types[picked_index] = Perimeters::PointType::blocker; + choice = random(perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common); + REQUIRE(choice); + CHECK(choice->previous_index != picked_index); +} + +TEST_CASE_METHOD(Test::SeamsFixture, "Generate random seam", "[Seams][SeamRandom][Integration]") { + Shells::Shells<> perimeters{ + Seams::Perimeters::create_perimeters(shell_polygons, layer_infos, painting, params.perimeter)}; + Shells::Shells<> shell_perimeters; + shell_perimeters.push_back(std::move(perimeters[shell_index])); + const std::vector> seam{ + Random::get_object_seams(std::move(shell_perimeters), params.random_seed)}; + REQUIRE(seam.size() == 125); + + if constexpr (debug_files) { + std::ofstream csv{"random_seam.csv"}; + Test::serialize_seam(csv, seam); + } +} diff --git a/tests/fff_print/test_seam_rear.cpp b/tests/fff_print/test_seam_rear.cpp new file mode 100644 index 0000000000..c6ee01c5b3 --- /dev/null +++ b/tests/fff_print/test_seam_rear.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include "test_data.hpp" +#include + +using namespace Slic3r; +using namespace Slic3r::Seams; + +constexpr bool debug_files{false}; + +namespace RearTest { +Perimeters::Perimeter get_perimeter() { + const double slice_z{1.0}; + const std::size_t layer_index{}; + std::vector positions{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.5, 1.0}, {0.0, 1.0}}; + std::vector angles(positions.size(), -M_PI / 2.0); + angles[3] = 0.0; + std::vector point_types(positions.size(), Perimeters::PointType::common); + std::vector + point_classifications{positions.size(), Perimeters::PointClassification::common}; + std::vector angle_type(positions.size(), Perimeters::AngleType::concave); + angle_type[3] = Perimeters::AngleType::smooth; + + return { + slice_z, + layer_index, + false, + std::move(positions), + std::move(angles), + std::move(point_types), + std::move(point_classifications), + std::move(angle_type)}; +} +} // namespace RearTest + +TEST_CASE("StraightLine operator places seam point near the prefered position", "[Seams][SeamRear]") { + const Rear::Impl::StraightLine rearest{Vec2d{0.7, 2.0}}; + std::optional choice{rearest(RearTest::get_perimeter(), Perimeters::PointType::common, Perimeters::PointClassification::common)}; + + REQUIRE(choice); + CHECK(scaled(choice->position) == scaled(Vec2d{0.7, 1.0})); + CHECK(choice->previous_index == 2); + CHECK(choice->next_index == 3); +} + +TEST_CASE_METHOD(Test::SeamsFixture, "Generate rear seam", "[Seams][SeamRear][Integration]") { + Shells::Shells<> perimeters{ + Perimeters::create_perimeters(shell_polygons, layer_infos, painting, params.perimeter)}; + Shells::Shells<> shell_perimeters; + shell_perimeters.push_back(std::move(perimeters[shell_index])); + const std::vector> seam{ + Rear::get_object_seams(std::move(shell_perimeters), params.rear_project_threshold)}; + REQUIRE(seam.size() == 125); + + if constexpr (debug_files) { + std::ofstream csv{"rear_seam.csv"}; + Test::serialize_seam(csv, seam); + } +} diff --git a/tests/fff_print/test_seam_shells.cpp b/tests/fff_print/test_seam_shells.cpp new file mode 100644 index 0000000000..b514304fbc --- /dev/null +++ b/tests/fff_print/test_seam_shells.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/GCode/SeamPainting.hpp" +#include "test_data.hpp" + +#include "libslic3r/GCode/SeamShells.hpp" + +using namespace Slic3r; +using namespace Slic3r::Seams; + +constexpr bool debug_files{false}; + +struct ProjectionFixture +{ + Polygon extrusion_path{ + Point{scaled(Vec2d{-1.0, -1.0})}, Point{scaled(Vec2d{1.0, -1.0})}, + Point{scaled(Vec2d{1.0, 1.0})}, Point{scaled(Vec2d{-1.0, 1.0})}}; + + ExPolygon island_boundary; + Seams::Geometry::Extrusions extrusions; + double extrusion_width{0.2}; + + ProjectionFixture() { + extrusions.emplace_back(Polygon{extrusion_path}, extrusion_path.bounding_box(), extrusion_width, island_boundary); + } +}; + +TEST_CASE_METHOD(ProjectionFixture, "Project to geometry matches", "[Seams][SeamShells]") { + Polygon boundary_polygon{extrusion_path}; + // Add + 0.1 to check that boundary polygon has been picked. + boundary_polygon.scale(1.0 + extrusion_width / 2.0 + 0.1); + island_boundary.contour = boundary_polygon; + + Shells::Impl::BoundedPolygons result{Shells::Impl::project_to_geometry(extrusions, 5.0)}; + REQUIRE(result.size() == 1); + REQUIRE(result[0].polygon.size() == 4); + // Boundary polygon is picked. + CHECK(result[0].polygon[0].x() == Approx(scaled(-(1.0 + extrusion_width / 2.0 + 0.1)))); +} + +TEST_CASE_METHOD(ProjectionFixture, "Project to geometry does not match", "[Seams][SeamShells]") { + Polygon boundary_polygon{extrusion_path}; + + // Island boundary is far from the extrusion. + boundary_polygon.scale(5.0); + + island_boundary.contour = boundary_polygon; + + Shells::Impl::BoundedPolygons result{Shells::Impl::project_to_geometry(extrusions, 1.0)}; + REQUIRE(result.size() == 1); + REQUIRE(result[0].polygon.size() == 4); + + const Polygon expanded{expand(extrusions.front().polygon, extrusion_width / 2.0).front()}; + + // The extrusion is expanded and returned. + CHECK(result[0].polygon == expanded); +} + +void serialize_shells( + std::ostream &out, const Shells::Shells &shells, const double layer_height +) { + out << "x,y,z,layer_index,slice_id,shell_id" << std::endl; + for (std::size_t shell_id{}; shell_id < shells.size(); ++shell_id) { + const Shells::Shell &shell{shells[shell_id]}; + for (std::size_t slice_id{}; slice_id < shell.size(); ++slice_id) { + const Shells::Slice &slice{shell[slice_id]}; + for (const Point &point : slice.boundary) { + // clang-format off + out + << point.x() << "," + << point.y() << "," + << slice.layer_index * 1e6 * layer_height << "," + << slice.layer_index << "," + << slice_id << "," + << shell_id << std::endl; + // clang-format on + } + } + } +} + +TEST_CASE_METHOD(Test::SeamsFixture, "Create shells", "[Seams][SeamShells][Integration]") { + if constexpr (debug_files) { + std::ofstream csv{"shells.csv"}; + serialize_shells(csv, shell_polygons, print->full_print_config().opt_float("layer_height")); + } + + CHECK(shell_polygons.size() == 36); +}