Merge branch 'lh_ms_seam_improvements'

This commit is contained in:
Lukas Matena 2024-05-14 07:33:26 +02:00
commit d8bf4fed18
60 changed files with 4847 additions and 2204 deletions

View File

@ -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<int64_t>(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);
}

View File

@ -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<PolyNode*, Allocator<PolyNode*>> 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

View File

@ -0,0 +1,276 @@
#include <stack>
#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<bool> 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<PerimeterExtrusion *> 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<const PerimeterExtrusion *> ordered_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector<const PerimeterExtrusion *> 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<const PerimeterExtrusion *> ordered_extrusions;
std::vector<bool> already_selected(extrusions.size(), false);
while (ordered_extrusions.size() < extrusions.size()) {
double nearest_distance_sqr = std::numeric_limits<double>::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<double>().squaredNorm();
if (distance_sqr < nearest_distance_sqr) {
if (extrusion_line.is_closed || (!extrusion_line.is_closed && nearest_distance_sqr != std::numeric_limits<double>::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<const PerimeterExtrusion *> extrusions;
const PerimeterExtrusion *external_perimeter_extrusion = nullptr;
};
// Returns vector of indexes that represent the order of grouped extrusions in grouped_extrusions.
static std::vector<size_t> order_of_grouped_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector<GroupedPerimeterExtrusions> 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<size_t> grouped_extrusions_order;
std::vector<bool> already_selected(grouped_extrusions.size(), false);
while (grouped_extrusions_order.size() < grouped_extrusions.size()) {
double nearest_distance_sqr = std::numeric_limits<double>::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<double>().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<double>::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<GroupedPerimeterExtrusions> grouped_extrusions;
std::stack<const PerimeterExtrusion *> stack;
std::vector<bool> 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<const PerimeterExtrusion *> 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<const PerimeterExtrusion *> 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<size_t> 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

View File

@ -0,0 +1,47 @@
#ifndef slic3r_GCode_PerimeterOrder_hpp_
#define slic3r_GCode_PerimeterOrder_hpp_
#include <Arachne/utils/ExtrusionLine.hpp>
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<PerimeterExtrusion *> 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<size_t>::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<PerimeterExtrusion>;
PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, bool external_perimeters_first);
} // namespace Slic3r::Arachne::PerimeterOrder
#endif // slic3r_GCode_Travels_hpp_

View File

@ -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<coord_t>(*vd_edge));
@ -471,7 +467,7 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys)
Point v1 = Geometry::VoronoiUtils::to_point(vd_edge->vertex0()).cast<coord_t>();
Point v2 = Geometry::VoronoiUtils::to_point(vd_edge->vertex1()).cast<coord_t>();
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<coord_t>(), end_source_point, *ending_voronoi_edge, prev_edge, start_source_point, end_source_point, segments);

View File

@ -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<Segment> &segments);
};
} // namespace Slic3r::Arachne

View File

@ -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);

View File

@ -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

View File

@ -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 <boost/log/trivial.hpp>
@ -758,99 +758,4 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &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<ExtrusionLine *> &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<LineLoc, Locator>;
GridT grid(searching_radius);
for (const ExtrusionLine *line : input)
for (const ExtrusionJunction &junction : *line) grid.insert(LineLoc{junction, line});
for (const std::pair<const SquareGrid::GridPoint, LineLoc> &pair : grid) {
const LineLoc &lineloc_here = pair.second;
const ExtrusionLine *here = lineloc_here.line;
Point loc_here = pair.second.j.p;
std::vector<LineLoc> 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

View File

@ -75,15 +75,6 @@ public:
static bool removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpaths);
using ExtrusionLineSet = ankerl::unordered_dense::set<std::pair<const ExtrusionLine *, const ExtrusionLine *>, boost::hash<std::pair<const ExtrusionLine *, const ExtrusionLine *>>>;
/*!
* 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<ExtrusionLine *> &input, bool outer_to_inner);
protected:
/*!

View File

@ -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) {}
}

View File

@ -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)

View File

@ -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<double>();
@ -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 {

View File

@ -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<ExtrusionLine> &extrusion_lines)
{
BoundingBox bbox;
@ -272,15 +266,6 @@ static BoundingBox get_extents(const std::vector<const ExtrusionLine *> &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<Points> to_points(const std::vector<const ExtrusionLine *> &extrusion_lines)
{
std::vector<Points> points;
@ -293,6 +278,8 @@ static std::vector<Points> to_points(const std::vector<const ExtrusionLine *> &e
#endif
using VariableWidthLines = std::vector<ExtrusionLine>; //<! The ExtrusionLines generated by libArachne
using Perimeter = VariableWidthLines;
using Perimeters = std::vector<Perimeter>;
} // namespace Slic3r::Arachne

View File

@ -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<ElemT>::GridPoint;
@ -68,22 +58,6 @@ void SparsePointGrid<ElemT, Locator>::insert(const Elem &elem)
SparseGrid<ElemT>::m_grid.emplace(grid_loc, elem);
}
template<class ElemT, class Locator>
const ElemT *SparsePointGrid<ElemT, Locator>::getAnyNearby(const Point &query_pt, coord_t radius)
{
const ElemT *ret = nullptr;
const std::function<bool(const ElemT &)> &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<ElemT>::processNearby(query_pt, radius, process_func);
return ret;
}
} // namespace Slic3r::Arachne
#endif // UTILS_SPARSE_POINT_GRID_H

View File

@ -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;

View File

@ -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

View File

@ -1234,7 +1234,9 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail
// Collect custom seam data from all objects.
std::function<void(void)> 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.

View File

@ -353,8 +353,7 @@ private:
std::string set_extruder(unsigned int extruder_id, double print_z);
bool line_distancer_is_required(const std::vector<unsigned int>& 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()

View File

@ -0,0 +1,301 @@
#include <boost/log/trivial.hpp>
#include "libslic3r/ShortEdgeCollapse.hpp"
#include "libslic3r/GCode/ModelVisibility.hpp"
#include "libslic3r/AABBTreeIndirect.hpp"
namespace Slic3r::ModelInfo {
namespace Impl {
CoordinateFunctor::CoordinateFunctor(const std::vector<Vec3f> *coords) : coordinates(coords) {}
CoordinateFunctor::CoordinateFunctor() : coordinates(nullptr) {}
const float &CoordinateFunctor::operator()(size_t idx, size_t dim) const {
return coordinates->operator[](idx)[dim];
}
template<typename T> 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<float> 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 &params
) {
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<Vec3f> 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<float> result(samples.positions.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, result.size()),
[&triangles, &precomputed_sample_directions, model_contains_negative_parts, negative_volumes_start_index,
&raycasting_tree, &result, &samples, &params](tbb::blocked_range<size_t> r) {
// Maintaining hits memory outside of the loop, so it does not have to be reallocated for each query.
std::vector<igl::Hit> 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 &center = 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<double>();
Vec3d ray_origin_d = (center + normal * 0.01f).cast<double>(); // 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<double>(); // 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<double>();
}
Vec3d final_ray_dir_d = final_ray_dir.cast<double>();
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 &params,
const std::function<void(void)> &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<size_t> 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;
}
}

View File

@ -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<Vec3f> *coordinates;
CoordinateFunctor(const std::vector<Vec3f> *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 &params,
const std::function<void(void)> &throw_if_canceled
);
TriangleSetSamples mesh_samples;
std::vector<float> 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_

View File

@ -0,0 +1,533 @@
#include "libslic3r/GCode/SeamAligned.hpp"
#include "libslic3r/GCode/SeamGeometry.hpp"
#include "libslic3r/GCode/ModelVisibility.hpp"
#include <fstream>
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<PointType> &types{perimeter.point_types};
const std::vector<PointClassification> &classifications{perimeter.point_classifications};
const std::vector<Vec2d> &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<std::size_t> 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<Vec2d> &positions{perimeter.positions};
const std::vector<AngleType> &angle_types{perimeter.angle_types};
std::optional<std::size_t> match;
double min_distance{std::numeric_limits<double>::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<double>::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<Vec2d> &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<std::size_t> snapped{
snap_to_angle(nearest_point.head<2>(), closest, perimeter, max_detour)};
return {
closest, adjacent, adjacent_forward, snapped, nearest_point,
};
}
std::optional<SeamChoice> LeastVisible::operator()(
const Perimeters::Perimeter &perimeter,
const PointType point_type,
const PointClassification point_classification
) const {
std::optional<size_t> chosen_index;
double visibility{std::numeric_limits<double>::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<SeamChoice> 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<float>()
);
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<Vec2d> extract_points(
const Perimeters::Perimeter &perimeter, const Perimeters::PointType point_type
) {
std::vector<Vec2d> 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<Vec2d> get_starting_positions(const Shells::Shell<> &shell) {
const Perimeters::Perimeter &perimeter{shell.front().boundary};
std::vector<Vec2d> enforcers{extract_points(perimeter, Perimeters::PointType::enforcer)};
if (!enforcers.empty()) {
return enforcers;
}
std::vector<Vec2d> 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<SeamChoice> choices;
std::vector<double> visibilities;
};
SeamCandidate get_seam_candidate(
const Shells::Shell<> &shell,
const Vec2d &starting_position,
const SeamChoiceVisibility &visibility_calculator,
const Params &params,
const std::vector<std::vector<double>> &precalculated_visibility,
const std::vector<LeastVisiblePoint> &least_visible_points
) {
using Perimeters::Perimeter, Perimeters::AngleType;
std::vector<double> choice_visibilities(shell.size(), 1.0);
std::vector<SeamChoice> 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<double>>;
std::vector<ShellVertexVisibility> get_shells_vertex_visibility(
const Shells::Shells<> &shells, const SeamChoiceVisibility &visibility_calculator
) {
std::vector<ShellVertexVisibility> 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<Vec2d> &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<LeastVisiblePoint>;
std::vector<ShellLeastVisiblePoints> get_shells_least_visible_points(
const Shells::Shells<> &shells,
const std::vector<ShellVertexVisibility> &precalculated_visibility
) {
std::vector<ShellLeastVisiblePoints> 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<Vec2d>;
std::vector<ShellStartingPositions> get_shells_starting_positions(
const Shells::Shells<> &shells
) {
std::vector<ShellStartingPositions> result;
for (const Shells::Shell<> &shell : shells) {
std::vector<Vec2d> starting_positions{get_starting_positions(shell)};
result.push_back(std::move(starting_positions));
}
return result;
}
using ShellSeamCandidates = std::vector<SeamCandidate>;
std::vector<ShellSeamCandidates> get_shells_seam_candidates(
const Shells::Shells<> &shells,
const std::vector<ShellStartingPositions> &starting_positions,
const SeamChoiceVisibility &visibility_calculator,
const std::vector<ShellVertexVisibility> &precalculated_visibility,
const std::vector<ShellLeastVisiblePoints> &least_visible_points,
const Params &params
) {
std::vector<ShellSeamCandidates> 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<SeamChoice> get_shell_seam(
const Shells::Shell<> &shell,
std::vector<SeamCandidate> seam_candidates,
const Perimeters::Perimeter::OptionalPointTree &previous_points,
const Params &params
) {
std::vector<SeamChoice> seam;
double visibility{std::numeric_limits<double>::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<Vec2d> 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<double> 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<std::vector<SeamPerimeterChoice>> get_object_seams(
Shells::Shells<> &&shells,
const SeamChoiceVisibility &visibility_calculator,
const Params &params
) {
const std::vector<ShellVertexVisibility> precalculated_visibility{
get_shells_vertex_visibility(shells, visibility_calculator)};
const std::vector<ShellLeastVisiblePoints> least_visible_points{
get_shells_least_visible_points(shells, precalculated_visibility)
};
const std::vector<ShellStartingPositions> starting_positions{
get_shells_starting_positions(shells)
};
const std::vector<ShellSeamCandidates> seam_candidates{
get_shells_seam_candidates(
shells,
starting_positions,
visibility_calculator,
precalculated_visibility,
least_visible_points,
params
)
};
std::vector<std::vector<SeamPerimeterChoice>> 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<const SeamPerimeterChoice> previous_seams{
layer_index == 0 ? tcb::span<const SeamPerimeterChoice>{} : layer_seams[layer_index - 1]};
std::vector<Vec2d> 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<SeamChoice> 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

View File

@ -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<double(const SeamChoice &, const Perimeters::Perimeter &)>;
namespace Impl {
struct SeamOptions
{
std::size_t closest;
std::size_t adjacent;
bool adjacent_forward;
std::optional<std::size_t> snapped;
Vec2d on_edge;
};
SeamChoice pick_seam_option(const Perimeters::Perimeter &perimeter, const SeamOptions &options);
std::optional<std::size_t> 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<SeamChoice> operator()(
const Perimeters::Perimeter &perimeter,
const Perimeters::PointType point_type,
const Perimeters::PointClassification point_classification
) const;
};
struct LeastVisible
{
std::optional<SeamChoice> operator()(
const Perimeters::Perimeter &perimeter,
const Perimeters::PointType point_type,
const Perimeters::PointClassification point_classification
) const;
const std::vector<double> &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<std::vector<SeamPerimeterChoice>> get_object_seams(
Shells::Shells<> &&shells,
const SeamChoiceVisibility& visibility_calculator,
const Params& params
);
} // namespace Slic3r::Seams::Aligned
#endif // libslic3r_SeamAligned_hpp_

View File

@ -0,0 +1,108 @@
#include "libslic3r/GCode/SeamChoice.hpp"
namespace Slic3r::Seams {
std::optional<SeamChoice> maybe_choose_seam_point(
const Perimeters::Perimeter &perimeter, const SeamPicker &seam_picker
) {
using Perimeters::PointType;
using Perimeters::PointClassification;
std::vector<PointType>
type_search_order{PointType::enforcer, PointType::common, PointType::blocker};
std::vector<PointClassification> 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<SeamChoice> 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<SeamChoice> 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<SeamChoice> choose_degenerate_seam_point(const Perimeters::Perimeter &perimeter) {
if (!perimeter.positions.empty()) {
return SeamChoice{0, 0, perimeter.positions.front()};
}
return std::nullopt;
}
std::optional<std::vector<SeamChoice>> maybe_get_shell_seam(
const Shells::Shell<> &shell,
const std::function<std::optional<SeamChoice>(const Perimeters::Perimeter &, std::size_t)> &chooser
) {
std::vector<SeamChoice> 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<SeamChoice> seam_choice{
choose_degenerate_seam_point(slice.boundary)}) {
result.push_back(*seam_choice);
} else {
result.emplace_back();
}
} else {
const std::optional<SeamChoice> choice{chooser(slice.boundary, i)};
if (!choice) {
return std::nullopt;
}
result.push_back(*choice);
}
}
return result;
}
std::vector<SeamChoice> get_shell_seam(
const Shells::Shell<> &shell,
const std::function<SeamChoice(const Perimeters::Perimeter &, std::size_t)> &chooser
) {
std::optional<std::vector<SeamChoice>> 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<SeamChoice>(shell.size());
}
return *seam;
}
std::vector<std::vector<SeamPerimeterChoice>> get_object_seams(
Shells::Shells<> &&shells,
const std::function<std::vector<SeamChoice>(const Shells::Shell<>&)> &get_shell_seam
) {
std::vector<std::vector<SeamPerimeterChoice>> layer_seams(get_layer_count(shells));
for (Shells::Shell<> &shell : shells) {
std::vector<SeamChoice> 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

View File

@ -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<std::optional<
SeamChoice>(const Perimeters::Perimeter &, const Perimeters::PointType, const Perimeters::PointClassification)>;
std::optional<SeamChoice> 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<SeamChoice> choose_degenerate_seam_point(const Perimeters::Perimeter &perimeter);
std::optional<std::vector<SeamChoice>> maybe_get_shell_seam(
const Shells::Shell<> &shell,
const std::function<std::optional<SeamChoice>(const Perimeters::Perimeter &, std::size_t)> &chooser
);
std::vector<SeamChoice> get_shell_seam(
const Shells::Shell<> &shell,
const std::function<SeamChoice(const Perimeters::Perimeter &, std::size_t)> &chooser
);
std::vector<std::vector<SeamPerimeterChoice>> get_object_seams(
Shells::Shells<> &&shells,
const std::function<std::vector<SeamChoice>(const Shells::Shell<> &)> &get_shell_seam
);
} // namespace Slic3r::Seams
#endif // libslic3r_SeamChoice_hpp_

View File

@ -0,0 +1,369 @@
#include "libslic3r/GCode/SeamGeometry.hpp"
#include "KDTreeIndirect.hpp"
#include "Layer.hpp"
#include <fstream>
#include <numeric>
#include <oneapi/tbb/blocked_range.h>
#include <oneapi/tbb/parallel_for.h>
namespace Slic3r::Seams::Geometry {
namespace MappingImpl {
/**
* @brief Return 0, 1, ..., size - 1.
*/
std::vector<std::size_t> range(std::size_t size) {
std::vector<std::size_t> 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<std::size_t> assign_buckets(
const std::vector<std::optional<Link>> &links, std::size_t &new_bucket_id
) {
std::vector<std::size_t> result;
std::transform(
links.begin(), links.end(), std::back_inserter(result),
[&](const std::optional<Link> &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<Vec2d> &points, const std::size_t index, const double min_arm_length
) {
std::optional<std::size_t> previous_index;
std::optional<std::size_t> 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<Vec2d, double> 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<Mapping, std::size_t> get_mapping(
const std::vector<std::size_t> &list_sizes, const MappingOperator &mapping_operator
) {
using namespace MappingImpl;
std::vector<std::vector<std::size_t>> 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<std::optional<Link>> 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<Geometry::Extrusion> 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<const ExtrusionEntityCollection *>(
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<Extrusions> get_extrusions(tcb::span<const Slic3r::Layer *const> object_layers) {
std::vector<Extrusions> 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<Extrusion> 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<Vec2d> 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::size_t>(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<Vec2d> 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<bool(std::size_t)> &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<bool(std::size_t)> &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<Vec2d> unscaled(const Points &points) {
std::vector<Vec2d> 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<Linef> unscaled(const Lines &lines) {
std::vector<Linef> 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<Vec2d> &points) {
Points result;
for (const Vec2d &point : points) {
result.push_back(Slic3r::scaled(point));
}
return result;
}
std::vector<double> get_embedding_distances(
const std::vector<Vec2d> &points, const AABBTreeLines::LinesDistancer<Linef> &perimeter_distancer
) {
std::vector<double> 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<true>(point)};
return distance < 0 ? -distance : 0.0;
});
return result;
}
std::vector<double> get_overhangs(
const std::vector<Vec2d> &points,
const AABBTreeLines::LinesDistancer<Linef> &previous_layer_perimeter_distancer,
const double layer_height
) {
std::vector<double> 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<true>(point)};
return distance > 0 ? M_PI / 2 - std::atan(layer_height / distance) : 0.0;
});
return result;
}
// Measured from outside, convex is positive
std::vector<double> get_vertex_angles(const std::vector<Vec2d> &points, const double min_arm_length) {
std::vector<double> result;
result.reserve(points.size());
for (std::size_t index{0}; index < points.size(); ++index) {
std::optional<std::size_t> previous_index;
std::optional<std::size_t> 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<std::size_t, double> pick_closest_bounding_box(
const BoundingBox &to, const BoundingBoxes &choose_from
) {
double min_distance{std::numeric_limits<double>::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

View File

@ -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 <oneapi/tbb/blocked_range.h>
#include <oneapi/tbb/parallel_for.h>
#include <vector>
#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<Extrusion>;
std::vector<Extrusions> get_extrusions(tcb::span<const Slic3r::Layer *const> object_layers);
Vec2d get_polygon_normal(
const std::vector<Vec2d> &points, const std::size_t index, const double min_arm_length
);
Vec2d get_normal(const Vec2d &vector);
std::pair<Vec2d, double> distance_to_segment_squared(const Linef &segment, const Vec2d &point);
using Mapping = std::vector<std::vector<std::size_t>>;
using MappingOperatorResult = std::optional<std::pair<std::size_t, double>>;
using MappingOperator = std::function<MappingOperatorResult(std::size_t, std::size_t)>;
/**
* @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<Mapping, std::size_t> get_mapping(
const std::vector<std::size_t> &list_sizes, const MappingOperator &mapping_operator
);
std::vector<Vec2d> oversample_edge(const Vec2d &from, const Vec2d &to, const double max_distance);
template <typename NestedVector>
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<typename NestedVector>
std::vector<std::pair<std::size_t, std::size_t>> get_flat_index2indices_table(
const NestedVector &nested_vector
) {
std::vector<std::pair<std::size_t, std::size_t>> 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 <typename NestedVector>
void iterate_nested(const NestedVector &nested_vector, const std::function<void(std::size_t, std::size_t)> &function) {
std::size_t flat_size{Geometry::get_flat_size(nested_vector)};
using Range = tbb::blocked_range<size_t>;
const Range range{0, flat_size};
std::vector<std::pair<std::size_t, std::size_t>> 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<bool(std::size_t)> &visitor
);
void visit_backward(
const std::size_t start_index,
const std::size_t loop_size,
const std::function<bool(std::size_t)> &visitor
);
std::vector<Vec2d> unscaled(const Points &points);
std::vector<Linef> unscaled(const Lines &lines);
Points scaled(const std::vector<Vec2d> &points);
std::vector<double> get_embedding_distances(
const std::vector<Vec2d> &points, const AABBTreeLines::LinesDistancer<Linef> &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<double> get_overhangs(
const std::vector<Vec2d> &points,
const AABBTreeLines::LinesDistancer<Linef> &previous_layer_perimeter_distancer,
const double layer_height
);
// Measured from outside, convex is positive
std::vector<double> get_vertex_angles(const std::vector<Vec2d> &points, const double min_arm_length);
double bounding_box_distance(const BoundingBox &a, const BoundingBox &b);
std::pair<std::size_t, double> 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_

View File

@ -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

View File

@ -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_

View File

@ -0,0 +1,409 @@
#include <fstream>
#include "ClipperUtils.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/GCode/SeamGeometry.hpp"
#include "libslic3r/GCode/SeamPerimeters.hpp"
namespace Slic3r::Seams::Perimeters::Impl {
std::vector<Vec2d> oversample_painted(
const std::vector<Vec2d> &points,
const std::function<bool(Vec3f, double)> &is_painted,
const double slice_z,
const double max_distance
) {
std::vector<Vec2d> 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<float>((point - next_point).norm())};
const Vec2d middle_point{(point + next_point) / 2.0};
Vec3f point3d{to_3d(middle_point, slice_z).cast<float>()};
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<Vec2d>, std::vector<PointType>> remove_redundant_points(
const std::vector<Vec2d> &points,
const std::vector<PointType> &point_types,
const double tolerance
) {
std::vector<Vec2d> points_result;
std::vector<PointType> 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<Vec2d> simplification_result;
douglas_peucker<double>(
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<PointType>
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<PointType> get_point_types(
const std::vector<Vec2d> &positions,
const ModelInfo::Painting &painting,
const double slice_z,
const double painting_radius
) {
std::vector<PointType> 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<float>(), static_cast<float>(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<PointClassification> classify_points(
const std::vector<double> &embeddings,
const std::optional<std::vector<double>> &overhangs,
const double overhang_threshold,
const double embedding_threshold
) {
std::vector<PointClassification> 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<AngleType> get_angle_types(
const std::vector<double> &angles, const double convex_threshold, const double concave_threshold
) {
std::vector<AngleType> 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<AngleType> merge_angle_types(
const std::vector<AngleType> &angle_types,
const std::vector<AngleType> &smooth_angle_types,
const std::vector<Vec2d> &points,
const double min_arm_length
) {
std::vector<AngleType> 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<const Slic3r::Layer* const> object_layers, const double elephant_foot_compensation
) {
LayerInfos result(object_layers.size());
using Range = tbb::blocked_range<size_t>;
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<Linef> perimeter_distancer{
to_unscaled_linesf({object_layer.lslices})};
using PreviousLayerDistancer = std::optional<AABBTreeLines::LinesDistancer<Linef>>;
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<PointType> &all_point_types,
const std::vector<PointClassification> &point_classifications,
const Perimeter::IndexToCoord &index_to_coord
) {
std::vector<std::size_t> overhang_indexes;
std::vector<std::size_t> embedded_indexes;
std::vector<std::size_t> 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<Vec2d> &&positions,
std::vector<double> &&angles,
std::vector<PointType> &&point_types,
std::vector<PointClassification> &&point_classifications,
std::vector<AngleType> &&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<Vec2d> &&points, const double slice_z, const std::size_t layer_index
) {
std::vector<PointType> point_types(points.size(), PointType::common);
std::vector<PointClassification>
point_classifications(points.size(), PointClassification::common);
std::vector<double> angles(points.size());
std::vector<AngleType> 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 &params
) {
if (polygon.size() < 3) {
return Perimeter::create_degenerate(
Geometry::unscaled(polygon.points), layer_info.slice_z, layer_info.index
);
}
std::vector<Vec2d> 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<Vec2d> perimeter_points{
Impl::oversample_painted(points, is_painted, layer_info.slice_z, params.oversampling_max_distance)};
std::vector<PointType> 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<double> embeddings{
Geometry::get_embedding_distances(perimeter_points, layer_info.distancer)};
std::optional<std::vector<double>> overhangs;
if (layer_info.previous_distancer) {
overhangs = Geometry::get_overhangs(
perimeter_points, *layer_info.previous_distancer, layer_info.height
);
}
std::vector<PointClassification> point_classifications{
Impl::classify_points(embeddings, overhangs, params.overhang_threshold, params.embedding_threshold)};
std::vector<double> smooth_angles{Geometry::get_vertex_angles(perimeter_points, params.smooth_angle_arm_length)};
std::vector<double> angles{Geometry::get_vertex_angles(perimeter_points, params.sharp_angle_arm_length)};
std::vector<AngleType> angle_types{
Impl::get_angle_types(angles, params.convex_threshold, params.concave_threshold)};
std::vector<AngleType> 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::Shell<Polygon>> &shells,
const std::vector<LayerInfo> &layer_infos,
const ModelInfo::Painting &painting,
const PerimeterParams &params
) {
std::vector<Shells::Shell<>> result;
result.reserve(shells.size());
std::transform(
shells.begin(), shells.end(), std::back_inserter(result),
[](const Shells::Shell<Polygon> &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<Polygon> &shell{shells[shell_index]};
const Shells::Slice<Polygon>& 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

View File

@ -0,0 +1,190 @@
#ifndef libslic3r_SeamPerimeters_hpp_
#define libslic3r_SeamPerimeters_hpp_
#include <tcbspan/span.hpp>
#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<Linef> distancer;
std::optional<AABBTreeLines::LinesDistancer<Linef>> previous_distancer;
std::size_t index;
double height{};
double slice_z{};
double elephant_foot_compensation;
};
using LayerInfos = std::vector<LayerInfo>;
/**
* @brief Construct LayerInfo for each of the provided layers.
*/
LayerInfos get_layer_infos(
tcb::span<const Slic3r::Layer* const> 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<Vec2d> oversample_painted(
const std::vector<Vec2d> &points,
const std::function<bool(Vec3f, double)> &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<Vec2d>, std::vector<PointType>> remove_redundant_points(
const std::vector<Vec2d> &points,
const std::vector<PointType> &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<const Vec2d> positions;
};
using PointTree = KDTreeIndirect<2, double, IndexToCoord>;
using OptionalPointTree = std::optional<PointTree>;
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<Vec2d> &&positions,
std::vector<double> &&angles,
std::vector<PointType> &&point_types,
std::vector<PointClassification> &&point_classifications,
std::vector<AngleType> &&angle_types
);
static Perimeter create(
const Polygon &polygon,
const ModelInfo::Painting &painting,
const LayerInfo &layer_info,
const PerimeterParams &params
);
static Perimeter create_degenerate(
std::vector<Vec2d> &&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<Vec2d> positions{};
std::vector<double> angles{};
IndexToCoord index_to_coord{};
std::vector<PointType> point_types{};
std::vector<PointClassification> point_classifications{};
std::vector<AngleType> 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<Perimeter> create_perimeters(
const std::vector<Shells::Shell<Polygon>> &shells,
const std::vector<LayerInfo> &layer_infos,
const ModelInfo::Painting &painting,
const PerimeterParams &params
);
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_

File diff suppressed because it is too large Load Diff

View File

@ -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 <memory>
#include <atomic>
#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<const PrintObject *, std::vector<std::vector<SeamPerimeterChoice>>>;
using LayerPerimeters = std::vector<std::vector<BoundedPerimeter>>;
using ObjectLayerPerimeters = std::unordered_map<const PrintObject *, LayerPerimeters>;
// 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<SeamCandidate> &seam_candidates) :
seam_candidates(seam_candidates) {
}
const std::vector<SeamCandidate> &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<SeamPlacerImpl::Perimeter> perimeters;
std::vector<SeamPlacerImpl::SeamCandidate> points;
std::unique_ptr<SeamCandidatesTree> points_tree;
};
// Map of PrintObjects (PO) -> vector of layers of PO -> vector of perimeter
std::vector<LayerSeams> 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<PrintObject> objects,
const Params &params,
const std::function<void(void)> &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<const PrintObject*, PrintObjectSeamData> m_seam_per_object;
void init(const Print &print, std::function<void(void)> 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<std::pair<size_t, size_t>> find_seam_string(const PrintObject *po,
std::pair<size_t, size_t> start_seam,
const SeamPlacerImpl::SeamComparator &comparator) const;
std::optional<std::pair<size_t, size_t>> find_next_seam_in_layer(
const std::vector<PrintObjectSeamData::LayerSeams> &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_

View File

@ -0,0 +1,136 @@
#include <random>
#include "libslic3r/GCode/SeamRandom.hpp"
#include "libslic3r/GCode/SeamGeometry.hpp"
namespace Slic3r::Seams::Random {
using Perimeters::PointType;
using Perimeters::PointClassification;
namespace Impl {
std::vector<PerimeterSegment> get_segments(
const Perimeters::Perimeter &perimeter,
const PointType point_type,
const PointClassification point_classification
) {
const std::vector<Vec2d> &positions{perimeter.positions};
const std::vector<PointType> &point_types{perimeter.point_types};
const std::vector<PointClassification> &point_classifications{perimeter.point_classifications};
std::optional<double> current_begin;
std::optional<std::size_t> current_begin_index;
Vec2d previous_position{positions.front()};
double distance{0.0};
std::vector<PerimeterSegment> 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<PerimeterSegment> &segments, std::mt19937 &random_engine
) {
double length{0.0};
for (const PerimeterSegment &segment : segments) {
length += segment.length();
}
std::uniform_real_distribution<double> 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<Vec2d> &positions{perimeter.positions};
if (segment.length() < std::numeric_limits<double>::epsilon()) {
return {segment.begin_index, segment.begin_index, positions[segment.begin_index]};
}
std::uniform_real_distribution<double> 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<double>::epsilon()) {
index = previous_index;
} else if (distance + edge.norm() - random_distance < std::numeric_limits<double>::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<SeamChoice> Random::operator()(
const Perimeters::Perimeter &perimeter,
const PointType point_type,
const PointClassification point_classification
) const {
std::vector<PerimeterSegment> 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<std::vector<SeamPerimeterChoice>> 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

View File

@ -0,0 +1,29 @@
#include "libslic3r/GCode/SeamChoice.hpp"
#include <random>
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<SeamChoice> operator()(
const Perimeters::Perimeter &perimeter,
const Perimeters::PointType point_type,
const Perimeters::PointClassification point_classification
) const;
};
}
std::vector<std::vector<SeamPerimeterChoice>> get_object_seams(
Shells::Shells<> &&shells, const unsigned fixed_seed
);
}

View File

@ -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<SeamChoice> get_rearest_point(
const Perimeters::Perimeter &perimeter,
const PointType point_type,
const PointClassification point_classification
) {
double max_y{-std::numeric_limits<double>::infinity()};
std::optional<std::size_t> 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<SeamChoice> StraightLine::operator()(
const Perimeters::Perimeter &perimeter,
const PointType point_type,
const PointClassification point_classification
) const {
std::vector<PerimeterLine> 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<PerimeterLine> possible_distancer{possible_lines};
const BoundingBoxf bounding_box{perimeter.positions};
const std::vector<std::pair<Vec2d, std::size_t>> intersections{
possible_distancer.intersections_with_line<true>(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<std::vector<SeamPerimeterChoice>> 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<std::vector<SeamChoice>> 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

View File

@ -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<SeamChoice> operator()(
const Perimeters::Perimeter &perimeter,
const Perimeters::PointType point_type,
const Perimeters::PointClassification point_classification
) const;
};
} // namespace Impl
std::vector<std::vector<SeamPerimeterChoice>> get_object_seams(
Shells::Shells<> &&shells,
const double rear_project_threshold
);
} // namespace Slic3r::Seams::Rear
#endif // libslic3r_SeamRear_hpp_

View File

@ -0,0 +1,114 @@
#include "libslic3r/GCode/SeamShells.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include <oneapi/tbb/blocked_range.h>
#include <oneapi/tbb/parallel_for.h>
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<BoundedPolygons> project_to_geometry(const std::vector<Geometry::Extrusions> &extrusions, const double max_bb_distance) {
std::vector<BoundedPolygons> 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<Polygon> map_to_shells(
std::vector<BoundedPolygons> &&layers, const Geometry::Mapping &mapping, const std::size_t shell_count
) {
Shells<Polygon> 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<Polygon>{std::move(perimeter), layer_index}
);
}
}
return result;
}
} // namespace Slic3r::Seams::Shells::Impl
namespace Slic3r::Seams::Shells {
Shells<Polygon> create_shells(
const std::vector<Geometry::Extrusions> &extrusions, const double max_distance
) {
std::vector<Impl::BoundedPolygons> projected{Impl::project_to_geometry(extrusions, max_distance)};
std::vector<std::size_t> 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

View File

@ -0,0 +1,47 @@
#ifndef libslic3r_SeamShells_hpp_
#define libslic3r_SeamShells_hpp_
#include <vector>
#include <tcbspan/span.hpp>
#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<BoundedPolygon>;
BoundedPolygons project_to_geometry(const Geometry::Extrusions &extrusions, const double max_bb_distance);
}
namespace Slic3r::Seams::Shells {
template<typename T = Perimeters::Perimeter> struct Slice
{
T boundary;
std::size_t layer_index;
};
template<typename T = Perimeters::Perimeter> using Shell = std::vector<Slice<T>>;
template<typename T = Perimeters::Perimeter> using Shells = std::vector<Shell<T>>;
Shells<Polygon> create_shells(
const std::vector<Geometry::Extrusions> &extrusions, const double max_distance
);
} // namespace Slic3r::Seams::Shells
#endif // libslic3r_SeamShells_hpp_

View File

@ -31,75 +31,46 @@ std::optional<Point> 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>();
double lsqr = v.squaredNorm();
const Vec2d v = (point - prev_point).cast<double>();
const double lsqr = v.squaredNorm();
if (lsqr > sqr(distance))
return std::make_optional<Point>(prev_point + (v * (distance / sqrt(lsqr))).cast<coord_t>());
return prev_point + (v * (distance / sqrt(lsqr))).cast<coord_t>();
distance -= sqrt(lsqr);
} else {
// Circular segment
float angle = Geometry::ArcWelder::arc_angle(prev_point.cast<float>(), point.cast<float>(), it->radius);
double len = std::abs(it->radius) * angle;
const float angle = Geometry::ArcWelder::arc_angle(prev_point.cast<float>(), point.cast<float>(), 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<Point>(prev_point.rotated(- angle * (distance / len),
Geometry::ArcWelder::arc_center(prev_point.cast<float>(), point.cast<float>(), it->radius, it->ccw()).cast<coord_t>()));
const Point center_pt = Geometry::ArcWelder::arc_center(prev_point.cast<float>(), point.cast<float>(), segment_it->radius, segment_it->ccw()).cast<coord_t>();
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>(point);
return point;
prev_point = point;
}
}
}
// Failed.
return {};
}
std::optional<Point> 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>();
double lsqr = v.squaredNorm();
if (lsqr > sqr(distance))
return std::make_optional<Point>(prev_point + (v * (distance / sqrt(lsqr))).cast<coord_t>());
distance -= sqrt(lsqr);
}
else {
// Circular segment
float angle = Geometry::ArcWelder::arc_angle(prev_point.cast<float>(), point.cast<float>(), 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<Point>(prev_point.rotated(-angle * (distance / len),
Geometry::ArcWelder::arc_center(prev_point.cast<float>(), point.cast<float>(), it->radius, it->ccw()).cast<coord_t>()));
}
distance -= len;
}
if (distance < 0)
return std::make_optional<Point>(point);
prev_point = point;
}
}
}
// Failed.
return {};
std::optional<Point> 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.

View File

@ -211,7 +211,7 @@ std::optional<Point> 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<Point> p = sample_path_point_at_distance_from_end(path, wipe_length); p)
if (std::optional<Point> 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<Point> 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<double>().normalized();
return std::make_optional<Point>(p_current + (v_rotated * wipe_length).cast<coord_t>());
return p_current + (v_rotated * wipe_length).cast<coord_t>();
}
return {};

View File

@ -947,6 +947,8 @@ void Layer::sort_perimeters_into_islands(
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) {

View File

@ -77,6 +77,8 @@ private:
static constexpr const uint32_t fill_region_composite_id = std::numeric_limits<uint32_t>::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.

View File

@ -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 <algorithm>
#include <cmath>
#include <cassert>
#include <cstddef>
#include <cstdlib>
#include <functional>
#include <iterator>
#include <limits>
#include <list>
#include <math.h>
#include <ostream>
#include <stack>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
@ -53,15 +44,13 @@
#include <ankerl/unordered_dense.h>
// #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 &params, const Polygons &lower_slices_polygons_cache, std::vector<PerimeterGeneratorArachneExtrusion> &pg_extrusions)
static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::Parameters &params, 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<float>(params.config.fuzzy_skin_thickness.value), scaled<float>(params.config.fuzzy_skin_point_dist.value));
fuzzy_extrusion_line(extrusion, scaled<float>(params.config.fuzzy_skin_thickness.value), scaled<float>(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<Arachne::VariableWidthLines> &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<Arachne::VariableWidthLines> 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<Arachne::VariableWidthLines> 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<Arachne::ExtrusionLine *> 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<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
ankerl::unordered_dense::map<const Arachne::ExtrusionLine *, size_t> 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<bool> 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<PerimeterGeneratorArachneExtrusion> 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<double>::max();
bool is_best_closed = false;
std::vector<size_t> 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<double>::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<double>().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<double>::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<PerimeterGeneratorArachneExtrusion *> 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<Arachne::PerimeterOrder::PerimeterExtrusion *> 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);
}

View File

@ -9,11 +9,29 @@
#include <unordered_set>
#include <random>
#include <algorithm>
#include <boost/random/uniform_int_distribution.hpp>
#include <ankerl/unordered_dense.h>
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<typename Range, typename UniformRandomNumberGenerator>
void stable_shuffle(Range &range, UniformRandomNumberGenerator &generator) {
const int n{static_cast<int>(range.size())};
for (int i{0}; i < n - 2; ++i) {
int j{boost::random::uniform_int_distribution<int>{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<size_t> vertices_index_mapping(mesh.vertices.size());
@ -102,7 +120,7 @@ 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) {

View File

@ -7,6 +7,7 @@
#include <random>
#include <tbb/parallel_for.h>
#include <tbb/blocked_range.h>
#include <boost/random/uniform_real_distribution.hpp>
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<double> fdistribution;
boost::random::uniform_real_distribution<double> fdistribution;
auto get_random = [&fdistribution, &mersenne_engine]() {
return Vec3d { fdistribution(mersenne_engine), fdistribution(mersenne_engine), fdistribution(mersenne_engine) };

View File

@ -8,6 +8,9 @@
#include "libslic3r/format.hpp" // -> format()
#include "I18N.hpp"
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/split.hpp>
#include <cmath>
#include "ImGuiWrapper.hpp"

View File

@ -8,6 +8,7 @@
#include <GL/glew.h>
#include <algorithm>
#include <boost/nowide/convert.hpp>
#include <wx/progdlg.h>
#include <boost/nowide/convert.hpp>

View File

@ -10,6 +10,7 @@
#include <string_view>
#include <vector>
#include <optional>
#include <functional>
#include <imgui/imgui.h>

View File

@ -7,6 +7,8 @@
#include "libslic3r/CustomGCode.hpp"
#include <array>
#include <functional>
#include <vector>
#include <set>

Binary file not shown.

View File

@ -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)

View File

@ -0,0 +1,124 @@
#include <catch2/catch.hpp>
#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<Shells::Shells<>> 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<Catch::Benchmark::storage_for<Visibility>> 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<Shells::Shells<>> 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<Shells::Shells<>> 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<std::pair<const Layer*, const ExtrusionLoop*>> 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<const ExtrusionEntityCollection*>(layer_region.perimeters().entities[perimeter_id])};
if (entity_collection != nullptr) {
for (const ExtrusionEntity *entity : *entity_collection) {
const auto loop{static_cast<const ExtrusionLoop*>(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});
}
});
};
}
}

View File

@ -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 <filesystem>
#include <unordered_map>
namespace Slic3r { namespace Test {
@ -39,14 +46,13 @@ enum class TestMesh {
};
// Neccessary for <c++17
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
struct TestMeshHash
{
std::size_t operator()(TestMesh tm) const { return static_cast<std::size_t>(tm); }
};
/// Mesh enumeration to name mapping
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
extern const std::unordered_map<TestMesh, const char *, TestMeshHash> 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 <typename T>
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
template<typename T> bool _equiv(const T &a, const T &b) { return std::abs(a - b) < EPSILON; }
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
template<typename T> 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<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false, unsigned duplicate_count = 1);
void init_print(std::initializer_list<TestMesh> 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<TriangleMesh> 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<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false, unsigned duplicate = 1);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false, unsigned duplicate = 1);
Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh);
void init_print(
std::vector<TriangleMesh> &&meshes,
Slic3r::Print &print,
Slic3r::Model &model,
const DynamicPrintConfig &config_in,
bool comments = false,
unsigned duplicate_count = 1
);
void init_print(
std::initializer_list<TestMesh> 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<TriangleMesh> 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<TestMesh> meshes,
Slic3r::Print &print,
Slic3r::Model &model,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
bool comments = false,
unsigned duplicate = 1
);
void init_print(
std::initializer_list<TriangleMesh> meshes,
Slic3r::Print &print,
Slic3r::Model &model,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
bool comments = false,
unsigned duplicate = 1
);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(
std::initializer_list<TestMesh> meshes,
Slic3r::Print &print,
const DynamicPrintConfig &config,
bool comments = false
);
void init_and_process_print(
std::initializer_list<TriangleMesh> meshes,
Slic3r::Print &print,
const DynamicPrintConfig &config,
bool comments = false
);
void init_and_process_print(
std::initializer_list<TestMesh> meshes,
Slic3r::Print &print,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
bool comments = false
);
void init_and_process_print(
std::initializer_list<TriangleMesh> meshes,
Slic3r::Print &print,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
bool comments = false
);
std::string gcode(Print& print);
std::string gcode(Print &print);
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string slice(
std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments = false
);
std::string slice(
std::initializer_list<TriangleMesh> meshes,
const DynamicPrintConfig &config,
bool comments = false
);
std::string slice(
std::initializer_list<TestMesh> meshes,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
bool comments = false
);
std::string slice(
std::initializer_list<TriangleMesh> meshes,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> 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<Print> process_3mf(const std::filesystem::path &path) {
DynamicPrintConfig config;
auto print{std::make_unique<Print>()};
Model model;
ConfigSubstitutionContext context{ForwardCompatibilitySubstitutionRule::Disable};
load_3mf(path.string().c_str(), config, context, &model, false);
Slic3r::Test::init_print(std::vector<TriangleMesh>{}, *print, model, config);
print->process();
return print;
}
static std::map<std::string, std::unique_ptr<Print>> 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<std::vector<Seams::SeamPerimeterChoice>> &seam) {
output << "x,y,z,layer_index" << std::endl;
for (const std::vector<Seams::SeamPerimeterChoice> &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<Seams::Geometry::Extrusions> 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<Polygon> 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

View File

@ -0,0 +1,160 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamAligned.hpp>
#include "test_data.hpp"
#include <fstream>
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<Vec2d> positions{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.5}};
std::vector<double> angles(positions.size(), -M_PI / 2.0);
angles[4] = 0.0;
std::vector<Perimeters::PointType> point_types(positions.size(), Perimeters::PointType::common);
std::vector<Perimeters::PointClassification>
point_classifications{positions.size(), Perimeters::PointClassification::common};
std::vector<Perimeters::AngleType> 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<std::size_t> 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<SeamChoice> 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<double> precalculated_visibility{};
for (std::size_t i{0}; i < perimeter.positions.size(); ++i) {
precalculated_visibility.push_back(-static_cast<double>(i));
}
Aligned::Impl::LeastVisible least_visible{precalculated_visibility};
const std::optional<SeamChoice> 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<std::vector<SeamPerimeterChoice>> 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<float>(), 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
}
}
}
}
}

View File

@ -0,0 +1,145 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamGeometry.hpp>
#include <libslic3r/Geometry.hpp>
using namespace Slic3r;
TEST_CASE("Lists mapping", "[Seams][SeamGeometry]") {
// clang-format off
std::vector<std::vector<int>> list_of_lists{
{},
{7, 2, 3},
{9, 6, 3, 6, 7},
{1, 1, 3},
{1},
{3},
{1},
{},
{3}
};
// clang-format on
std::vector<std::size_t> sizes;
sizes.reserve(list_of_lists.size());
for (const std::vector<int> &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<std::size_t> index;
const std::vector<int> &layer{list_of_lists[layer_index]};
const std::vector<int> &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<double>(max_diff)};
}
)};
// clang-format off
CHECK(mapping == std::vector<std::vector<std::size_t>>{
{},
{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<Vec2d> points{Vec2d{0, 0}, Vec2d{1, 0}, Vec2d{1, 1}, Vec2d{0, 1}};
std::vector<double> 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<Vec2d> points = {Vec2d{0, 0}, Vec2d{0, 1}, Vec2d{1, 1}, Vec2d{1, 0}};
std::vector<double> 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<Vec2d> points = {Vec2d{0, 0}, Vec2d{-0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}};
std::vector<double> 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<Vec2d> points = {Vec2d{0, 0}, Vec2d{0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}};
std::vector<double> 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<Vec2d> points = {Vec2d{0, 0}, Vec2d{0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}};
std::vector<double> 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<Vec2d> 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<double> 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<Vec2d> points{Seams::Geometry::unscaled(square.contour.points)};
ExPolygon previous_layer{square};
previous_layer.translate(scaled(Vec2d{-0.5, 0}));
AABBTreeLines::LinesDistancer<Linef> previous_layer_distancer{
to_unscaled_linesf({previous_layer})};
const std::vector<double> overhangs{
Seams::Geometry::get_overhangs(points, previous_layer_distancer, 0.5)};
REQUIRE(overhangs.size() == points.size());
CHECK_THAT(overhangs, Catch::Matchers::Approx(std::vector<double>{
0.0, M_PI / 4.0, M_PI / 4.0, 0.0
}));
}

View File

@ -0,0 +1,181 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/SeamPerimeters.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Point.hpp"
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamGeometry.hpp>
#include <libslic3r/Geometry.hpp>
#include <fstream>
#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<Vec2d> 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<Vec2d> 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<PointType> 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<Vec2d> positions{Vec2d{0.0, 0.0}, Vec2d{1.0, 0.0}, Vec2d{1.0, 1.0}, Vec2d{0.0, 1.0}};
std::vector<double> angles(4, -M_PI / 2.0);
std::vector<PointType>
point_types{PointType::enforcer, PointType::blocker, PointType::common, PointType::common};
std::vector<PointClassification> point_classifications{
PointClassification::overhang, PointClassification::embedded, PointClassification::embedded,
PointClassification::common};
std::vector<AngleType>
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<Perimeters::Perimeter> &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<Vec2d> &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);
}

View File

@ -0,0 +1,99 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamRandom.hpp>
#include "test_data.hpp"
#include <fstream>
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<Vec2d> positions{{0.0, 0.0}, {0.5, 0.0}, {1.0, 0.0}};
std::vector<double> angles(positions.size(), -M_PI / 2.0);
std::vector<Perimeters::PointType> point_types(positions.size(), Perimeters::PointType::common);
std::vector<Perimeters::PointClassification>
point_classifications{positions.size(), Perimeters::PointClassification::common};
std::vector<Perimeters::AngleType> 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<double> &data, double min, double max, const std::size_t bin_count) {
std::vector<std::size_t> bins(bin_count);
const double bin_size{(max - min) / bin_count};
const double expected_frequncy{static_cast<double>(data.size()) / bin_count};
for (const double value : data) {
auto bin{static_cast<int>(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<double>(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<double> x_positions;
const std::size_t count{1001};
x_positions.reserve(count);
std::generate_n(std::back_inserter(x_positions), count, [&]() {
std::optional<SeamChoice> 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<SeamChoice> 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<std::vector<SeamPerimeterChoice>> 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);
}
}

View File

@ -0,0 +1,60 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamRear.hpp>
#include "test_data.hpp"
#include <fstream>
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<Vec2d> positions{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.5, 1.0}, {0.0, 1.0}};
std::vector<double> angles(positions.size(), -M_PI / 2.0);
angles[3] = 0.0;
std::vector<Perimeters::PointType> point_types(positions.size(), Perimeters::PointType::common);
std::vector<Perimeters::PointClassification>
point_classifications{positions.size(), Perimeters::PointClassification::common};
std::vector<Perimeters::AngleType> 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<SeamChoice> 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<std::vector<SeamPerimeterChoice>> 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);
}
}

View File

@ -0,0 +1,91 @@
#include <catch2/catch.hpp>
#include <filesystem>
#include <fstream>
#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<Polygon> &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<Polygon> &shell{shells[shell_id]};
for (std::size_t slice_id{}; slice_id < shell.size(); ++slice_id) {
const Shells::Slice<Polygon> &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);
}