diff --git a/src/Mod/CAM/App/Area.cpp b/src/Mod/CAM/App/Area.cpp index 291aef72e5..309c5ad84f 100644 --- a/src/Mod/CAM/App/Area.cpp +++ b/src/Mod/CAM/App/Area.cpp @@ -756,7 +756,12 @@ static inline void getEndPoints(const TopoDS_Wire& wire, gp_Pnt& p1, gp_Pnt& p2) p2 = BRep_Tool::Pnt(TopoDS::Vertex(xp.CurrentVertex())); } - +// Toponaming integration note: there's a new class called WireJoiner in Mod/Part/App/ that has been +// imported from RT's fork. Is's an improved version of the following struct, therefor +// probably at some point this struct should be replaced with the new imported class. +// See https://github.com/realthunder/FreeCAD/blob/LinkStable/src/Mod/Part/App/WireJoiner.h for the +// original implementation of the class and https://github.com/FreeCAD/FreeCAD/pull/12535 for the +// import conversation. struct WireJoiner { using Box = bg::model::box; diff --git a/src/Mod/Part/App/CMakeLists.txt b/src/Mod/Part/App/CMakeLists.txt index f12e9c631c..a43945c48a 100644 --- a/src/Mod/Part/App/CMakeLists.txt +++ b/src/Mod/Part/App/CMakeLists.txt @@ -556,6 +556,8 @@ SET(Part_SRCS FaceMakerCheese.h FaceMakerBullseye.cpp FaceMakerBullseye.h + WireJoiner.cpp + WireJoiner.h ) if(FREECAD_USE_PCH) diff --git a/src/Mod/Part/App/WireJoiner.cpp b/src/Mod/Part/App/WireJoiner.cpp new file mode 100644 index 0000000000..42cc2a38e5 --- /dev/null +++ b/src/Mod/Part/App/WireJoiner.cpp @@ -0,0 +1,3166 @@ +/**************************************************************************** + * Copyright (c) 2022 Zheng Lei (realthunder) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ + +#include "PreCompiled.h" +#include +#include + +#ifndef _PreComp_ +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "WireJoiner.h" + +#include "Geometry.h" +#include "PartFeature.h" +#include "TopoShapeOpCode.h" +#include "TopoShapeMapper.h" + +namespace bg = boost::geometry; +namespace bgi = boost::geometry::index; + +const size_t RParametersNumber = 16UL; +using RParameters = bgi::linear; + +BOOST_GEOMETRY_REGISTER_POINT_3D_GET_SET( + gp_Pnt,double,bg::cs::cartesian,X,Y,Z,SetX,SetY,SetZ) + +FC_LOG_LEVEL_INIT("WireJoiner",true, true) + +using namespace Part; + +static inline void getEndPoints(const TopoDS_Edge &eForEndPoints, gp_Pnt &p1, gp_Pnt &p2) { + p1 = BRep_Tool::Pnt(TopExp::FirstVertex(eForEndPoints)); + p2 = BRep_Tool::Pnt(TopExp::LastVertex(eForEndPoints)); +} + +static inline void getEndPoints(const TopoDS_Wire &wire, gp_Pnt &p1, gp_Pnt &p2) { + BRepTools_WireExplorer xp(wire); + p1 = BRep_Tool::Pnt(TopoDS::Vertex(xp.CurrentVertex())); + for(;xp.More();xp.Next()) {}; + p2 = BRep_Tool::Pnt(TopoDS::Vertex(xp.CurrentVertex())); +} + +// Originally here there was the definition of the precompiler macro assertCheck() and of the method +// _assertCheck(), that have been replaced with the already defined precompiler macro assert(). +// See +// https://github.com/realthunder/FreeCAD/blob/6f15849be2505f98927e75d0e8352185e14e7b72/src/Mod/Part/App/WireJoiner.cpp#L107 +// for reference and https://github.com/FreeCAD/FreeCAD/pull/12535/files#r1526647457 for the +// discussion about replacing it + +class WireJoiner::WireJoinerP { +public: + double myTol = Precision::Confusion(); + double myTol2 = myTol * myTol; + double myAngularTol = Precision::Angular(); + bool doSplitEdge = true; + bool doMergeEdge = true; + bool doOutline = false; + bool doTightBound = true; + + std::string catchObject; + int catchIteration {}; + int iteration = 0; + + using Box = bg::model::box; + + bool checkBBox(const Bnd_Box &box) const + { + if (box.IsVoid()) { + return false; + } + Standard_Real xMin = Standard_Real(); + Standard_Real yMin = Standard_Real(); + Standard_Real zMin = Standard_Real(); + Standard_Real xMax = Standard_Real(); + Standard_Real yMax = Standard_Real(); + Standard_Real zMax = Standard_Real(); + box.Get(xMin, yMin, zMin, xMax, yMax, zMax); + return zMax - zMin <= myTol; + } + + WireJoinerP() + : catchObject(App::GetApplication() + .GetParameterGroupByPath("User parameter:BaseApp/Preferences/WireJoiner") + ->GetASCII("ObjectName")) + , catchIteration(static_cast( + App::GetApplication() + .GetParameterGroupByPath("User parameter:BaseApp/Preferences/WireJoiner") + ->GetInt("Iteration", 0))) + {} + + bool getBBox(const TopoDS_Shape &eForBBox, Bnd_Box &bound) { + BRepBndLib::AddOptimal(eForBBox,bound,Standard_False); + if (bound.IsVoid()) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("failed to get bound of edge"); + } + return false; + } + if (!checkBBox(bound)) { + showShape(eForBBox, "invalid"); + } + if (bound.SquareExtent() < myTol2) { + return false; + } + bound.Enlarge(myTol); + return true; + } + + bool getBBox(const TopoDS_Shape &eForBBox, Box &box) { + Bnd_Box bound; + if (!getBBox(eForBBox, bound)) { + return false; + } + Standard_Real xMin = Standard_Real(); + Standard_Real yMin = Standard_Real(); + Standard_Real zMin = Standard_Real(); + Standard_Real xMax = Standard_Real(); + Standard_Real yMax = Standard_Real(); + Standard_Real zMax = Standard_Real(); + bound.Get(xMin, yMin, zMin, xMax, yMax, zMax); + box = Box(gp_Pnt(xMin,yMin,zMin), gp_Pnt(xMax,yMax,zMax)); + return true; + } + + struct WireInfo; + struct EdgeSet; + + struct EdgeInfo { + TopoDS_Edge edge; + TopoDS_Wire superEdge; + mutable TopoDS_Shape edgeReversed; + mutable TopoDS_Shape superEdgeReversed; + gp_Pnt p1; + gp_Pnt p2; + gp_Pnt mid; + Box box; + std::array iStart {}; // adjacent list index start for p1 and p2 + std::array iEnd {}; // adjacent list index end + int iteration {}; + int iteration2 {}; + bool queryBBox; + std::shared_ptr wireInfo {}; + std::shared_ptr wireInfo2 {}; // an edge can be shared by at most two tight bound wires. + std::unique_ptr geo {}; + Standard_Real firstParam {}; + Standard_Real lastParam {}; + Handle_Geom_Curve curve; + GeomAbs_CurveType type {}; + bool isLinear; + + EdgeInfo(const TopoDS_Edge& eForInfo, + const gp_Pnt& pt1, + const gp_Pnt& pt2, + const Box& bound, + bool bbox, + bool isLinear) + : edge(eForInfo) + , p1(pt1) + , p2(pt2) + , box(bound) + , queryBBox(bbox) + , isLinear(isLinear) + { + curve = BRep_Tool::Curve(eForInfo, firstParam, lastParam); + type = GeomAdaptor_Curve(curve).GetType(); + + // Originally here there was a call to the precompiler macro assertCheck(), which has + // been replaced with the precompiler macro assert() + + assert(!curve.IsNull()); + const double halving {0.5}; + GeomLProp_CLProps prop(curve,(firstParam+lastParam)*halving,0,Precision::Confusion()); + mid = prop.Value(); + + reset(); + } + Geometry *geometry() { + if (!geo) { + geo = Geometry::fromShape(edge, /*silent*/ true); + } + return geo.get(); + } + void reset() { + wireInfo.reset(); + wireInfo2.reset(); + if (iteration >= 0) { + iteration = 0; + } + iteration2 = 0; + iStart[0] = iStart[1] = iEnd[0] = iEnd[1] = -1; + } + const TopoDS_Shape &shape(bool forward=true) const + { + if (superEdge.IsNull()) { + if (forward) { + return edge; + } + if (edgeReversed.IsNull()) { + edgeReversed = edge.Reversed(); + } + return edgeReversed; + } + if (forward) { + return superEdge; + } + if (superEdgeReversed.IsNull()) { + superEdgeReversed = superEdge.Reversed(); + } + return superEdgeReversed; + } + TopoDS_Wire wire() const + { + auto sForWire = shape(); + if (sForWire.ShapeType() == TopAbs_WIRE) { + return TopoDS::Wire(sForWire); + } + return BRepBuilderAPI_MakeWire(TopoDS::Edge(sForWire)).Wire(); + } + }; + + template + struct VectorSet { + void sort() + { + if (!sorted) { + sorted = true; + std::sort(data.begin(), data.end()); + } + } + bool contains(const T &vForContains) + { + if (!sorted) { + const size_t dataSizeMax = 30; + if (data.size() < dataSizeMax) { + return std::find(data.begin(), data.end(), vForContains) != data.end(); + } + sort(); + } + auto it = std::lower_bound(data.begin(), data.end(), vForContains); + return it!=data.end() && *it == vForContains; + } + bool intersects(const VectorSet &other) + { + if (other.size() < size()) { + return other.intersects(*this); + } + if (!sorted) { + for (const auto &vector : data) { + if (other.contains(vector)) { + return true; + } + } + } + else { + other.sort(); + auto it = other.data.begin(); + for (const auto &vertex : data) { + it = std::lower_bound(it, other.data.end(), vertex); + if (it == other.data.end()) { + return false; + } + if (*it == vertex) { + return true; + } + } + } + return false; + } + void insert(const T &vToInsert) + { + if (sorted) { + data.insert(std::upper_bound(data.begin(), data.end(), vToInsert), vToInsert); + } + else { + data.push_back(vToInsert); + } + } + bool insertUnique(const T &vToInsertUnique) + { + if (sorted) { + auto it = std::lower_bound(data.begin(), data.end(), vToInsertUnique); + bool insert = !(it != data.end() && *it == vToInsertUnique); + if (insert) { + data.insert(it, vToInsertUnique); + } + return insert; + } + + if (contains(vToInsertUnique)) { + return false; + } + data.push_back(vToInsertUnique); + return true; + } + void erase(const T &vToErase) + { + if (!sorted) { + data.erase(std::remove(data.begin(), data.end(), vToErase), data.end()); + } + else { + auto it = std::lower_bound(data.begin(), data.end(), vToErase); + auto itEnd = it; + while (itEnd != data.end() && *itEnd == vToErase) { + ++itEnd; + } + data.erase(it, itEnd); + } + const size_t dataSizeMax = 20; + if (data.size() < dataSizeMax) { + sorted = false; + } + } + void clear() + { + data.clear(); + sorted = false; + } + std::size_t size() const + { + return data.size(); + } + bool empty() const + { + return data.empty(); + } + private: + bool sorted = false; + std::vector data {}; + }; + + Handle(BRepTools_History) aHistory = new BRepTools_History; + + using Edges = std::list; + Edges edges {}; + + std::map edgesTable {}; + + struct VertexInfo { + Edges::iterator it {}; + bool start {}; + VertexInfo() = default; + VertexInfo(Edges::iterator it, bool start) + :it(it),start(start) + {} + VertexInfo reversed() const { + return {it, !start}; + } + bool operator==(const VertexInfo &other) const { + return it==other.it && start==other.start; + } + bool operator<(const VertexInfo &other) const { + auto thisInfo = edgeInfo(); + auto otherInfo = other.edgeInfo(); + if (thisInfo < otherInfo) { + return true; + } + if (thisInfo > otherInfo) { + return false; + } + return static_cast(start) < static_cast(other.start); + } + const gp_Pnt &pt() const { + return start?it->p1:it->p2; + } + gp_Pnt &pt() { + return start?it->p1:it->p2; + } + const gp_Pnt &ptOther() const { + return start?it->p2:it->p1; + } + gp_Pnt &ptOther() { + return start?it->p2:it->p1; + } + TopoDS_Vertex vertex() const { + return start ? TopExp::FirstVertex(edge()) : TopExp::LastVertex(edge()); + } + TopoDS_Vertex otherVertex() const { + return start ? TopExp::LastVertex(edge()) : TopExp::FirstVertex(edge()); + } + EdgeInfo *edgeInfo() const { + return &(*it); + } + const TopoDS_Edge &edge() const { + return it->edge; + } + }; + + struct StackInfo { + size_t iStart; + size_t iEnd; + size_t iCurrent; + explicit StackInfo(size_t idx = 0) + : iStart(idx) + , iEnd(idx) + , iCurrent(idx) + {} + }; + + std::vector stack {}; + std::vector vertexStack {}; + std::vector tmpVertices {}; + std::vector adjacentList {}; + + struct WireInfo { + std::vector vertices {}; + mutable std::vector sorted {}; + TopoDS_Wire wire; + TopoDS_Face face; + mutable Bnd_Box box; + bool done = false; + bool purge = false; + + void sort() const + { + if (sorted.size() == vertices.size()) { + return; + } + + // Originally here there was a call to the precompiler macro assertCheck(), which has + // been replaced with the precompiler macro assert() + + assert(sorted.size() < vertices.size()); + sorted.reserve(vertices.size()); + for (int i = (int)sorted.size(); i < (int)vertices.size(); ++i) { + sorted.push_back(i); + } + std::sort(sorted.begin(), sorted.end(), [&](int vA, int vB) { + return vertices[vA] < vertices[vB]; + }); + } + int find(const VertexInfo &info) const + { + const size_t verticesSizeMax = 20; + if (vertices.size() < verticesSizeMax) { + auto it = std::find(vertices.begin(), vertices.end(), info); + if (it == vertices.end()) { + return 0; + } + return (static_cast(it - vertices.begin()) + 1); + } + sort(); + auto it = std::lower_bound(sorted.begin(), sorted.end(), info, + [&](int idx, const VertexInfo &vertex) {return vertices[idx]edgeInfo() == info) { + return (static_cast(it - vertices.begin()) + 1); + } + } + return 0; + } + sort(); + auto it = std::lower_bound(sorted.begin(), sorted.end(), info, + [&](int idx, const EdgeInfo *vertex) {return vertices[idx].edgeInfo() { + }; + EdgeSet edgeSet; + + struct WireSet: VectorSet { + }; + WireSet wireSet; + + const Bnd_Box &getWireBound(const WireInfo &wireInfo) const + { + if (wireInfo.box.IsVoid()) { + for (auto& vertex : wireInfo.vertices) { + BRepBndLib::Add(vertex.it->shape(), wireInfo.box); + } + wireInfo.box.Enlarge(myTol); + } + return wireInfo.box; + } + + // This method was originally part of WireJoinerP::initWireInfo(), split to reduce cognitive + // complexity + bool initWireInfoWireClosed(const WireInfo& wireInfo) + { + if (!BRep_Tool::IsClosed(wireInfo.wire)) { + showShape(wireInfo.wire, "FailedToClose"); + FC_ERR("Wire not closed"); + for (auto& vertex : wireInfo.vertices) { + showShape(vertex.edgeInfo(), vertex.start ? "failed" : "failed_r", iteration); + } + return false; + } + return true; + } + + // This method was originally part of WireJoinerP::initWireInfo(), split to reduce cognitive + // complexity + bool initWireInfoFaceDone(WireInfo& wireInfo) + { + BRepBuilderAPI_MakeFace mkFace(wireInfo.wire); + if (!mkFace.IsDone()) { + FC_ERR("Failed to create face for wire"); + showShape(wireInfo.wire, "FailedFace"); + return false; + } + wireInfo.face = mkFace.Face(); + return true; + } + + bool initWireInfo(WireInfo &wireInfo) + { + if (!wireInfo.face.IsNull()) { + return true; + } + getWireBound(wireInfo); + if (wireInfo.wire.IsNull()) { + wireData->Clear(); + for (auto& vertex : wireInfo.vertices) { + wireData->Add(vertex.it->shape(vertex.start)); + } + wireInfo.wire = makeCleanWire(); + } + + if (!initWireInfoWireClosed(wireInfo)) { + return false; + } + + if (!initWireInfoFaceDone(wireInfo)) { + return false; + } + + return true; + } + + bool isInside(const WireInfo &wireInfo, gp_Pnt &pt) const + { + if (getWireBound(wireInfo).IsOut(pt)) { + return false; + } + BRepClass_FaceClassifier fc(wireInfo.face, pt, myTol); + return fc.State() == TopAbs_IN; + } + + bool isOutside(const WireInfo &wireInfo, gp_Pnt &pt) const + { + if (getWireBound(wireInfo).IsOut(pt)) { + return false; + } + BRepClass_FaceClassifier fc(wireInfo.face, pt, myTol); + return fc.State() == TopAbs_OUT; + } + + struct PntGetter + { + using result_type = const gp_Pnt&; + result_type operator()(const VertexInfo &vInfo) const { + return vInfo.pt(); + } + }; + + bgi::rtree vmap {}; + + struct BoxGetter + { + using result_type = const Box&; + result_type operator()(Edges::iterator it) const { + return it->box; + } + }; + bgi::rtree boxMap {}; + + BRep_Builder builder; + TopoDS_Compound compound; + + std::unordered_set sourceEdges {}; + std::vector sourceEdgeArray {}; + TopoDS_Compound openWireCompound; + + Handle(ShapeExtend_WireData) wireData = new ShapeExtend_WireData(); + + void clear() + { + aHistory->Clear(); + iteration = 0; + boxMap.clear(); + vmap.clear(); + edges.clear(); + edgeSet.clear(); + wireSet.clear(); + adjacentList.clear(); + stack.clear(); + tmpVertices.clear(); + vertexStack.clear(); + builder.MakeCompound(compound); + openWireCompound.Nullify(); + } + + Edges::iterator remove(Edges::iterator it) + { + if (it->queryBBox) { + boxMap.remove(it); + } + vmap.remove(VertexInfo(it,true)); + vmap.remove(VertexInfo(it,false)); + return edges.erase(it); + } + + void remove(EdgeInfo *info) + { + if (edgesTable.empty()) { + for (auto it = edges.begin(); it != edges.end(); ++it) { + edgesTable[&(*it)] = it; + } + } + auto it = edgesTable.find(info); + if (it != edgesTable.end()) { + remove(it->second); + edgesTable.erase(it); + } + } + + void add(Edges::iterator it) + { + vmap.insert(VertexInfo(it,true)); + vmap.insert(VertexInfo(it,false)); + if (it->queryBBox) { + boxMap.insert(it); + } + showShape(it->edge, "add"); + } + + int add(const TopoDS_Edge &eToAdd, bool queryBBox=false) + { + auto it = edges.begin(); + return add(eToAdd, queryBBox, it); + } + + int add(const TopoDS_Edge &eToAdd, bool queryBBox, Edges::iterator &it) + { + Box bbox; + if (!getBBox(eToAdd, bbox)) { + showShape(eToAdd, "small"); + aHistory->Remove(eToAdd); + return 0; + } + return add(eToAdd, queryBBox, bbox, it) ? 1 : -1; + } + + // This method was originally part of WireJoinerP::add(), split to reduce cognitive complexity + bool addNoDuplicates(const TopoDS_Edge& eToAdd, + TopoDS_Vertex& v2, + TopoDS_Edge& ev2, + const bool isLinear, + const VertexInfo& vinfo, + std::unique_ptr& geo) + { + if (v2.IsNull()) { + ev2 = vinfo.edge(); + v2 = vinfo.otherVertex(); + } + if (isLinear && vinfo.edgeInfo()->isLinear) { + showShape(eToAdd, "duplicate"); + aHistory->Remove(eToAdd); + return false; + } + if (auto geoEdge = vinfo.edgeInfo()->geometry()) { + if (!geo) { + geo = Geometry::fromShape(eToAdd, /*silent*/ true); + } + if (geo && geo->isSame(*geoEdge, myTol, myAngularTol)) { + showShape(eToAdd, "duplicate"); + aHistory->Remove(eToAdd); + return false; + } + } + return true; + } + + // This method was originally part of WireJoinerP::add(), split to reduce cognitive complexity + bool addValidEdges(const TopoDS_Edge& eToAdd, + const gp_Pnt p1, + const double tol, + TopoDS_Vertex& v1, + TopoDS_Edge& ev1, + const gp_Pnt p2, + TopoDS_Vertex& v2, + TopoDS_Edge& ev2, + const bool isLinear) + { + std::unique_ptr geo; + for (auto vit = vmap.qbegin(bgi::nearest(p1, INT_MAX)); vit != vmap.qend(); ++vit) { + auto& vinfo = *vit; + if (canShowShape()) { + FC_MSG("addcheck " << vinfo.edge().HashCode(INT_MAX)); + } + double d1 = vinfo.pt().SquareDistance(p1); + if (d1 >= tol) { + break; + } + if (v1.IsNull()) { + ev1 = vinfo.edge(); + v1 = vinfo.vertex(); + } + double d2 = vinfo.ptOther().SquareDistance(p2); + if (d2 < tol) { + if (!addNoDuplicates(eToAdd, v2, ev2, isLinear, vinfo, geo)){ + return false; + } + } + } + return true; + } + + bool add(const TopoDS_Edge &eToAdd, bool queryBBox, const Box &bbox, Edges::iterator &it) + { + gp_Pnt p1 = gp_Pnt(); + gp_Pnt p2 = gp_Pnt(); + getEndPoints(eToAdd,p1,p2); + TopoDS_Vertex v1 = TopoDS_Vertex(); + TopoDS_Vertex v2 = TopoDS_Vertex(); + TopoDS_Edge ev1 = TopoDS_Edge(); + TopoDS_Edge ev2 = TopoDS_Edge(); + double tol = myTol2; + // search for duplicate edges + showShape(eToAdd, "addcheck"); + bool isLinear = TopoShape(eToAdd).isLinearEdge(); + + if (!addValidEdges(eToAdd, p1, tol, v1, ev1, p2, v2, ev2, isLinear)){ + return false; + } + + if (v2.IsNull()) { + for (auto vit=vmap.qbegin(bgi::nearest(p2,1));vit!=vmap.qend();++vit) { + auto &vinfo = *vit; + double d1 = vinfo.pt().SquareDistance(p2); + if (d1 < tol) { + v2 = vit->vertex(); + ev2 = vit->edge(); + } + } + } + + // Make sure coincident vertices are actually the same TopoDS_Vertex, + // which is crucial for the OCC internal shape hierarchy structure. We + // achieve this by making a temp wire and let OCC do the hard work of + // replacing the vertex. + auto connectEdge = [&](TopoDS_Edge& eCurrent, + const TopoDS_Vertex& vCurrent, + const TopoDS_Edge& eOther, + const TopoDS_Vertex& vOther) { + if (vOther.IsNull()) { + return; + } + if (vCurrent.IsSame(vOther)) { + return; + } + double tol = std::max(BRep_Tool::Pnt(vCurrent).Distance(BRep_Tool::Pnt(vOther)), + BRep_Tool::Tolerance(vOther)); + if (tol >= BRep_Tool::Tolerance(vCurrent)) { + ShapeFix_ShapeTolerance fix; + const double halving {0.5}; + fix.SetTolerance(vCurrent, std::max(tol*halving, myTol), TopAbs_VERTEX); + } + BRepBuilderAPI_MakeWire mkWire(eOther); + mkWire.Add(eCurrent); + auto newEdge = mkWire.Edge(); + TopoDS_Vertex vFirst = TopExp::FirstVertex(newEdge); + TopoDS_Vertex vLast = TopExp::LastVertex(newEdge); + + // Originally here there was a call to the precompiler macro assertCheck(), which has + // been replaced with the precompiler macro assert() + + assert(vLast.IsSame(vOther) || vFirst.IsSame(vOther)); + eCurrent = newEdge; + }; + + TopoDS_Edge edge = eToAdd; + TopoDS_Vertex vFirst = TopExp::FirstVertex(eToAdd); + TopoDS_Vertex vLast = TopExp::LastVertex(eToAdd); + connectEdge(edge, vFirst, ev1, v1); + connectEdge(edge, vLast, ev2, v2); + if (!edge.IsSame(eToAdd)) { + auto itSource = sourceEdges.find(eToAdd); + if (itSource != sourceEdges.end()) { + TopoShape newEdge = *itSource; + newEdge.setShape(edge, false); + sourceEdges.erase(itSource); + sourceEdges.insert(newEdge); + } + getEndPoints(edge,p1,p2); + // Shall we also update bbox? + } + it = edges.emplace(it,edge,p1,p2,bbox,queryBBox,isLinear); + add(it); + return true; + } + + void add(const TopoDS_Shape &shape, bool queryBBox=false) + { + for (TopExp_Explorer xp(shape, TopAbs_EDGE); xp.More(); xp.Next()) { + add(TopoDS::Edge(xp.Current()), queryBBox); + } + } + + // This method was originally part of WireJoinerP::join(), split to reduce cognitive complexity + void joinMakeWire(const int idx, + BRepBuilderAPI_MakeWire& mkWire, + const Edges::iterator it, + bool& done) + { + double tol = myTol2; + gp_Pnt pstart(it->p1); + gp_Pnt pend(it->p2); + while (!edges.empty()) { + std::vector ret; + ret.reserve(1); + const gp_Pnt& pt = idx == 0 ? pstart : pend; + vmap.query(bgi::nearest(pt, 1), std::back_inserter(ret)); + + // Originally here there was a call to the precompiler macro assertCheck(), + // which has been replaced with the precompiler macro assert() + + assert(ret.size() == 1); + double dist = ret[0].pt().SquareDistance(pt); + if (dist > tol) { + break; + } + + const auto& info = *ret[0].it; + bool start = ret[0].start; + if (dist > Precision::SquareConfusion()) { + // insert a filling edge to solve the tolerance problem + const gp_Pnt& pt = ret[idx].pt(); + if (idx != 0) { + mkWire.Add(BRepBuilderAPI_MakeEdge(pend, pt).Edge()); + } + else { + mkWire.Add(BRepBuilderAPI_MakeEdge(pt, pstart).Edge()); + } + } + + if (idx == 1 && start) { + pend = info.p2; + mkWire.Add(info.edge); + } + else if (idx == 0 && !start) { + pstart = info.p1; + mkWire.Add(info.edge); + } + else if (idx == 0 && start) { + pstart = info.p2; + mkWire.Add(TopoDS::Edge(info.edge.Reversed())); + } + else { + pend = info.p1; + mkWire.Add(TopoDS::Edge(info.edge.Reversed())); + } + remove(ret[0].it); + if (pstart.SquareDistance(pend) <= Precision::SquareConfusion()) { + done = true; + break; + } + } + } + + //This algorithm tries to join connected edges into wires + // + //tol*tol>Precision::SquareConfusion() can be used to join points that are + //close but do not coincide with a line segment. The close points may be + //the results of rounding issue. + // + void join() + { + while (!edges.empty()) { + auto it = edges.begin(); + BRepBuilderAPI_MakeWire mkWire; + mkWire.Add(it->edge); + remove(it); + + bool done = false; + for (int idx=0;!done&&idx<2;++idx) { + joinMakeWire(idx, mkWire, it, done); + } + + builder.Add(compound,mkWire.Wire()); + } + } + + struct IntersectInfo { + double param; + TopoDS_Shape intersectShape; + gp_Pnt point; + IntersectInfo(double pToIntersect, const gp_Pnt& pt, TopoDS_Shape sToIntersect) + : param(pToIntersect) + , intersectShape(std::move(sToIntersect)) + , point(pt) + {} + bool operator<(const IntersectInfo &other) const { + return param < other.param; + } + }; + + void checkSelfIntersection(const EdgeInfo &info, std::set ¶ms) const + { + // Early return if checking for self intersection (only for non linear spline curves) + if (info.type <= GeomAbs_Parabola || info.isLinear) { + return; + } + IntRes2d_SequenceOfIntersectionPoint points2d; + TColgp_SequenceOfPnt points3d; + TColStd_SequenceOfReal errors; + TopoDS_Wire wire; + BRepBuilderAPI_MakeWire mkWire(info.edge); + if (!mkWire.IsDone()) { + return; + } + if (!BRep_Tool::IsClosed(mkWire.Wire())) { + BRepBuilderAPI_MakeEdge mkEdge(info.p1, info.p2); + if (!mkEdge.IsDone()) { + return; + } + mkWire.Add(mkEdge.Edge()); + } + wire = mkWire.Wire(); + BRepBuilderAPI_MakeFace mkFace(wire); + if (!mkFace.IsDone()) { + return; + } + const TopoDS_Face& face = mkFace.Face(); + ShapeAnalysis_Wire analysis(wire, face, myTol); + analysis.CheckSelfIntersectingEdge(1, points2d, points3d); + + // Originally here there was a call to the precompiler macro assertCheck(), which has been + // replaced with the precompiler macro assert() + + assert(points2d.Length() == points3d.Length()); + for (int i=1; i<=points2d.Length(); ++i) { + params.emplace(points2d(i).ParamOnFirst(), points3d(i), info.edge); + params.emplace(points2d(i).ParamOnSecond(), points3d(i), info.edge); + } + } + + // This method was originally part of WireJoinerP::checkIntersection(), split to reduce + // cognitive complexity + bool checkIntersectionPlanar(const EdgeInfo& info, + const EdgeInfo& other, + std::set& params1, + std::set& params2) + { + gp_Pln pln; + bool planar = TopoShape(info.edge).findPlane(pln); + if (!planar) { + TopoDS_Compound comp; + builder.MakeCompound(comp); + builder.Add(comp, info.edge); + builder.Add(comp, other.edge); + planar = TopoShape(comp).findPlane(pln); + if (!planar) { + BRepExtrema_DistShapeShape extss(info.edge, other.edge); + extss.Perform(); + if (extss.IsDone() && extss.NbSolution() > 0) { + if (!extss.IsDone() || extss.NbSolution() <= 0 || extss.Value() >= myTol) { + return false; + } + } + for (int i = 1; i <= extss.NbSolution(); ++i) { + Standard_Real par = Standard_Real(); + auto s1 = extss.SupportOnShape1(i); + auto s2 = extss.SupportOnShape2(i); + if (s1.ShapeType() == TopAbs_EDGE) { + extss.ParOnEdgeS1(i, par); + pushIntersection(params1, par, extss.PointOnShape1(i), other.edge); + } + if (s2.ShapeType() == TopAbs_EDGE) { + extss.ParOnEdgeS2(i, par); + pushIntersection(params2, par, extss.PointOnShape2(i), info.edge); + } + } + return false; + } + } + + return true; + } + + // This method was originally part of WireJoinerP::checkIntersection(), split to reduce + // cognitive complexity + static bool checkIntersectionEdgeDone(const BRepBuilderAPI_MakeEdge& mkEdge) + { + if (!mkEdge.IsDone()) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("Failed to build edge for checking intersection"); + } + return false; + } + return true; + } + + // This method was originally part of WireJoinerP::checkIntersection(), split to reduce + // cognitive complexity + static bool checkIntersectionWireDone(const BRepBuilderAPI_MakeWire& mkWire) + { + if (!mkWire.IsDone()) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("Failed to build wire for checking intersection"); + } + return false; + } + return true; + } + + // This method was originally part of WireJoinerP::checkIntersection(), split to reduce + // cognitive complexity + static bool checkIntersectionMakeWire(const EdgeInfo& info, + const EdgeInfo& other, + int& idx, + TopoDS_Wire& wire) + { + BRepBuilderAPI_MakeWire mkWire(info.edge); + mkWire.Add(other.edge); + if (mkWire.IsDone()) { + idx = 2; + } + else if (mkWire.Error() == BRepBuilderAPI_DisconnectedWire) { + idx = 3; + BRepBuilderAPI_MakeEdge mkEdge(info.p1, other.p1); + + if (!checkIntersectionEdgeDone(mkEdge)) { + return false; + } + + mkWire.Add(mkEdge.Edge()); + mkWire.Add(other.edge); + } + + if (!checkIntersectionWireDone(mkWire)) { + return false; + } + + wire = mkWire.Wire(); + if (!BRep_Tool::IsClosed(wire)) { + gp_Pnt p1 = gp_Pnt(); + gp_Pnt p2 = gp_Pnt(); + getEndPoints(wire, p1, p2); + BRepBuilderAPI_MakeEdge mkEdge(p1, p2); + + if (!checkIntersectionEdgeDone(mkEdge)) { + return false; + } + + mkWire.Add(mkEdge.Edge()); + } + return true; + } + + // This method was originally part of WireJoinerP::checkIntersection(), split to reduce + // cognitive complexity + static bool checkIntersectionFaceDone(const BRepBuilderAPI_MakeFace& mkFace) + { + if (!mkFace.IsDone()) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("Failed to build face for checking intersection"); + } + return false; + } + return true; + } + + void checkIntersection(const EdgeInfo &info, + const EdgeInfo &other, + std::set ¶ms1, + std::set ¶ms2) + { + if(!checkIntersectionPlanar(info, other, params1, params2)){ + return; + } + + // BRepExtrema_DistShapeShape has trouble finding all solutions for a + // spline curve. ShapeAnalysis_Wire is better. Besides, it can check + // for self intersection. It's slightly more troublesome to use, as it + // requires to build a face for the wire, so we only use it for planar + // cases. + + IntRes2d_SequenceOfIntersectionPoint points2d; + TColgp_SequenceOfPnt points3d; + TColStd_SequenceOfReal errors; + TopoDS_Wire wire; + int idx = 0; + + if (!checkIntersectionMakeWire(info, other, idx, wire)){ + return; + } + + BRepBuilderAPI_MakeFace mkFace(wire); + if (!checkIntersectionFaceDone(mkFace)) { + return; + } + + const TopoDS_Face& face = mkFace.Face(); + ShapeAnalysis_Wire analysis(wire, face, myTol); + analysis.CheckIntersectingEdges(1, idx, points2d, points3d, errors); + + // Originally here there was a call to the precompiler macro assertCheck(), which has been + // replaced with the precompiler macro assert() + + assert(points2d.Length() == points3d.Length()); + for (int i=1; i<=points2d.Length(); ++i) { + pushIntersection(params1, points2d(i).ParamOnFirst(), points3d(i), other.edge); + pushIntersection(params2, points2d(i).ParamOnSecond(), points3d(i), info.edge); + } + } + + void pushIntersection(std::set& params, + double param, + const gp_Pnt& pt, + const TopoDS_Shape& shape) + { + IntersectInfo info(param, pt, shape); + auto it = params.upper_bound(info); + if (it != params.end()) { + if (it->point.SquareDistance(pt) < myTol2) { + return; + } + } + if (it != params.begin()) { + auto itPrev = it; + --itPrev; + if (itPrev->point.SquareDistance(pt) < myTol2) { + return; + } + } + params.insert(it, info); + } + + struct SplitInfo { + TopoDS_Edge edge; + TopoDS_Shape intersectShape; + Box bbox; + }; + + // This method was originally part of WireJoinerP::splitEdges(), split to reduce cognitive + // complexity + void splitEdgesMakeEdge(const std::set::iterator& itParam, + const EdgeInfo& info, + std::vector& splits, + std::set::iterator& itPrevParam, + const TopoDS_Shape& intersectShape) + { + // Using points cause MakeEdge failure for some reason. Using + // parameters is better. + // + const gp_Pnt& p1 = itPrevParam->point; + const gp_Pnt& p2 = itParam->point; + const Standard_Real& param1 = itPrevParam->param; + const Standard_Real& param2 = itParam->param; + + BRepBuilderAPI_MakeEdge mkEdge(info.curve, param1, param2); + if (mkEdge.IsDone()) { + splits.emplace_back(); + auto& entry = splits.back(); + entry.edge = mkEdge.Edge(); + entry.intersectShape = intersectShape; + if (getBBox(entry.edge, entry.bbox)) { + itPrevParam = itParam; + } + else { + splits.pop_back(); + } + } + else if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + FC_WARN("edge split failed " << std::setprecision(16) << FC_XYZ(p1) << FC_XYZ(p2) + << ": " << mkEdge.Error()); + } + } + + // This method was originally part of WireJoinerP::splitEdges(), split to reduce cognitive + // complexity + void splitEdgesMakeEdges(std::set::iterator& itParam, + const std::set& params, + const EdgeInfo& info, + std::vector& splits) + { + for (auto itPrevParam = itParam++; itParam != params.end(); ++itParam) { + const auto& intersectShape = itParam->intersectShape.IsNull() + ? itPrevParam->intersectShape + : itParam->intersectShape; + if (intersectShape.IsNull()) { + break; + } + + splitEdgesMakeEdge(itParam, info, splits, itPrevParam, intersectShape); + } + } + + // Try splitting any edges that intersects other edge + void splitEdges() + { + std::unordered_map> intersects; + + int idx=0; + for (auto& info : edges) { + info.iteration = ++idx; + } + + std::unique_ptr seq( + new Base::SequencerLauncher("Splitting edges", edges.size())); + + idx = 0; + for (auto& info : edges) { + seq->next(true); + ++idx; + auto ¶ms = intersects[&info]; + checkSelfIntersection(info, params); + + for (auto vit=boxMap.qbegin(bgi::intersects(info.box)); vit!=boxMap.qend(); ++vit) { + const auto &other = *(*vit); + if (other.iteration <= idx) { + // means the edge is before us, and we've already checked intersection + continue; + } + checkIntersection(info, other, params, intersects[&other]); + } + } + + idx=0; + std::vector splits; + for (auto it=edges.begin(); it!=edges.end(); ) { + ++idx; + auto iter = intersects.find(&(*it)); + if (iter == intersects.end()) { + ++it; + continue; + } + auto &info = *it; + auto ¶ms = iter->second; + if (params.empty()) { + ++it; + continue; + } + + auto itParam = params.begin(); + if (itParam->point.SquareDistance(info.p1) < myTol2) { + params.erase(itParam); + } + params.emplace(info.firstParam, info.p1, TopoDS_Shape()); + itParam = params.end(); + --itParam; + if (itParam->point.SquareDistance(info.p2) < myTol2) { + params.erase(itParam); + } + params.emplace(info.lastParam, info.p2, TopoDS_Shape()); + + if (params.size() <= 2) { + ++it; + continue; + } + + splits.clear(); + itParam = params.begin(); + + splitEdgesMakeEdges(itParam, params, info, splits); + + if (splits.size() <= 1) { + ++it; + continue; + } + + showShape(info.edge, "remove"); + auto removedEdge = info.edge; + it = remove(it); + for (const auto& split : splits) { + if (!add(split.edge, false, split.bbox, it)) { + continue; + } + auto &newInfo = *it++; + aHistory->AddModified(split.intersectShape, newInfo.edge); + // if (v.intersectShape != removedEdge) + // aHistory->AddModified(removedEdge, newInfo.edge); + showShape(newInfo.edge, "split"); + } + } + } + + // This method was originally part of WireJoinerP::findSuperEdges(), split to reduce cognitive + // complexity + void findSuperEdgeFromAdjacent(std::deque& vertices, const int direction) + { + bool done = false; + auto begin = direction == 1 ? vertices.back().reversed() : vertices.front(); + while (true) { + auto currentVertex = direction == 1 ? vertices.front() : vertices.back(); + auto current = currentVertex.edgeInfo(); + // showShape(current, "siter", k); + const int idx = (currentVertex.start ? 1 : 0) ^ direction; + EdgeInfo* found = nullptr; + for (int i = current->iStart[idx]; i < current->iEnd[idx]; ++i) { + const auto& vertex = adjacentList[i]; + auto next = vertex.edgeInfo(); + if (next->iteration < 0 // skipped + || next == current) { // skip self (see how adjacent list is built) + continue; + } + if (vertex == begin) { + // closed + done = true; + break; + } + if (found // more than one branch + || edgeSet.contains(next)) // or, self intersect + { + + // Originally here there were some lines of code that have been removed + // as them are commented out. + // See + // https://github.com/realthunder/FreeCAD/blob/6f15849be2505f98927e75d0e8352185e14e7b72/src/Mod/Part/App/WireJoiner.cpp#L1141 + // for reference. + + found = nullptr; + break; + } + found = next; + currentVertex = vertex; + } + if (done || !found) { + break; + } + // showShape(found, "snext", k); + if (direction == 1) { + edgeSet.insert(current); + vertices.push_front(currentVertex.reversed()); + } + else { + edgeSet.insert(found); + vertices.push_back(currentVertex); + } + } + } + + // This method was originally part of WireJoinerP::findSuperEdges(), split to reduce cognitive + // complexity + void findSuperEdge(std::deque& vertices, const Edges::iterator it) + { + vertices.clear(); + vertices.emplace_back(it, true); + edgeSet.clear(); + + for (int direction = 0; direction < 2; ++direction) { // search in both direction + findSuperEdgeFromAdjacent(vertices, direction); + } + } + + // This method was originally part of WireJoinerP::findSuperEdges(), split to reduce cognitive + // complexity + void findSuperEdgesUpdateFirst(std::deque vertices) + { + Bnd_Box bbox; + for (const auto& vertex : vertices) { + auto current = vertex.edgeInfo(); + bbox.Add(current->box.min_corner()); + bbox.Add(current->box.max_corner()); + wireData->Add(current->shape(vertex.start)); + showShape(current, "edge"); + current->iteration = -1; + } + + auto first = vertices.front().edgeInfo(); + first->superEdge = makeCleanWire(false); + first->superEdgeReversed.Nullify(); + if (BRep_Tool::IsClosed(first->superEdge)) { + first->iteration = -2; + showShape(first, "super_done"); + } + else { + first->iteration = iteration; + showShape(first, "super"); + auto& vFirst = vertices.front(); + auto& vLast = vertices.back(); + auto last = vLast.edgeInfo(); + vFirst.ptOther() = vLast.ptOther(); + const int idx = vFirst.start ? 1 : 0; + first->iStart[idx] = last->iStart[vLast.start ? 1 : 0]; + first->iEnd[idx] = last->iEnd[vLast.start ? 1 : 0]; + + for (int i = first->iStart[idx]; i < first->iEnd[idx]; ++i) { + auto& vertex = adjacentList[i]; + if (vertex.it == vLast.it) { + vertex.it = vFirst.it; + vertex.start = !vFirst.start; + } + } + bbox.Enlarge(myTol); + first->box = Box(bbox.CornerMin(), bbox.CornerMax()); + } + } + + void findSuperEdges() + { + std::unique_ptr seq( + new Base::SequencerLauncher("Combining edges", edges.size())); + + std::deque vertices; + + ++iteration; + + // Join edges (let's call it super edge) that are connected to only one + // other edges (count == 2 counts this and the other edge) on one of + // its vertices to save traverse time. + for (auto it = edges.begin(); it != edges.end(); ++it) { + seq->next(true); + auto& info = *it; + if (info.iteration == iteration || info.iteration < 0) { + continue; + } + info.iteration = iteration; + // showShape(&info, "scheck"); + + findSuperEdge(vertices, it); + + if (vertices.size() <= 1) { + continue; + } + + wireData->Clear(); + + findSuperEdgesUpdateFirst(vertices); + } + } + + void buildAdjacentListPopulate() + { + // populate adjacent list + for (auto& info : edges) { + if (info.iteration == -2) { + + // Originally there was the following precompiler directive around assertCheck(): + // #if OCC_VERSION_HEX >= 0x070000 + // The precompiler directive has been removed as the minimum OCCT version supported + // is 7.3.0 and the precompiler macro has been replaced with assert() + + assert(BRep_Tool::IsClosed(info.shape())); + + showShape(&info, "closed"); + if (!doTightBound) { + builder.Add(compound, info.wire()); + } + continue; + } + + if (info.iteration < 0) { + continue; + } + + if (info.p1.SquareDistance(info.p2) <= myTol2) { + if (!doTightBound) { + builder.Add(compound, info.wire()); + } + info.iteration = -2; + continue; + } + + std::array pt {}; + pt[0] = info.p1; + pt[1] = info.p2; + for (int i = 0; i < 2; ++i) { + const int ic = i; + if (info.iStart[ic] >= 0) { + continue; + } + info.iEnd[ic] = info.iStart[ic] = (int)adjacentList.size(); + + for (auto vit = vmap.qbegin(bgi::nearest(pt[ic], INT_MAX)); vit != vmap.qend(); + ++vit) { + auto& vinfo = *vit; + if (vinfo.pt().SquareDistance(pt[ic]) > myTol2) { + break; + } + + // We must push ourself too, because the adjacency + // information is shared among all connected edges. + // + // if (&(*vinfo.it) == &info) + // continue; + + if (vinfo.it->iteration < 0) { + continue; + } + + adjacentList.push_back(vinfo); + ++info.iEnd[ic]; + } + + // copy the adjacent indices to all connected edges + for (int j = info.iStart[ic]; j < info.iEnd[ic]; ++j) { + auto& other = adjacentList[j]; + auto& otherInfo = *other.it; + if (&otherInfo != &info) { + const int kc = other.start ? 0 : 1; + otherInfo.iStart[kc] = info.iStart[ic]; + otherInfo.iEnd[kc] = info.iEnd[ic]; + } + } + } + } + } + + void buildAdjacentListSkipEdges() + { + bool done = false; + while (!done) { + done = true; + + if (doMergeEdge || doTightBound) { + findSuperEdges(); + } + + // Skip edges that are connected to only one end + for (auto& info : edges) { + if (info.iteration < 0) { + continue; + } + for (int k = 0; k < 2; ++k) { + const int kc = k; + int idx = 0; + for (idx = info.iStart[kc]; idx < info.iEnd[kc]; ++idx) { + const auto& vertex = adjacentList[idx]; + auto other = vertex.edgeInfo(); + if (other->iteration >= 0 && other != &info) { + break; + } + } + if (idx == info.iEnd[kc]) { + // If merge or tight bound, then repeat until no edges + // can be skipped. + done = !doMergeEdge && !doTightBound; + info.iteration = -3; + showShape(&info, "skip"); + break; + } + } + } + } + } + + void buildAdjacentList() + { + builder.MakeCompound(compound); + + for (auto& info : edges) { + info.reset(); + } + + adjacentList.clear(); + + buildAdjacentListPopulate(); + + buildAdjacentListSkipEdges(); + } + + // This algorithm tries to find a set of closed wires that includes as many + // edges (added by calling add() ) as possible. One edge may be included + // in more than one closed wires if it connects to more than one edges. + void findClosedWires(bool tightBound=false) + { + std::unique_ptr seq( + new Base::SequencerLauncher("Finding wires", edges.size())); + + for (auto &info : edges) { + info.wireInfo.reset(); + info.wireInfo2.reset(); + } + + for (auto it=edges.begin(); it!=edges.end(); ++it) { + VertexInfo beginVertex(it, true); + auto &beginInfo = *it; + seq->next(true); + ++iteration; + if (beginInfo.iteration < 0 || beginInfo.wireInfo) { + continue; + } + + VertexInfo currentVertex(it, true); + EdgeInfo *currentInfo = &beginInfo; + showShape(currentInfo, "begin"); + stack.clear(); + vertexStack.clear(); + edgeSet.clear(); + + TopoDS_Wire wire = _findClosedWires(beginVertex, currentVertex); + if (wire.IsNull()) { + continue; + } + + if (tightBound) { + + // Originally here there was a call to the precompiler macro assertCheck(), which + // has been replaced with the precompiler macro assert() + + assert(!beginInfo.wireInfo); + beginInfo.wireInfo.reset(new WireInfo()); + beginInfo.wireInfo->vertices.emplace_back(it, true); + beginInfo.wireInfo->wire = wire; + } + for (auto &entry : stack) { + const auto &vertex = vertexStack[entry.iCurrent]; + auto &info = *vertex.it; + if (tightBound) { + beginInfo.wireInfo->vertices.push_back(vertex); + } + if (!info.wireInfo) { + info.wireInfo = beginInfo.wireInfo; + // showShape(&info, "visited"); + } + } + showShape(wire,"joined"); + if (!tightBound) { + builder.Add(compound, wire); + } + } + } + + // Originally here there was the definition of the method checkStack(), which does nothing and + // therefor has been removed. See + // https://github.com/realthunder/FreeCAD/blob/6f15849be2505f98927e75d0e8352185e14e7b72/src/Mod/Part/App/WireJoiner.cpp#L1366 + // for reference + + void checkWireInfo(const WireInfo &wireInfo) + { + (void)wireInfo; + if (FC_LOG_INSTANCE.level() <= FC_LOGLEVEL_TRACE) { + return; + } + int idx = 0; + for (auto &info : edges) { + ++idx; + if (auto wire = info.wireInfo.get()) { + + // Originally here there was a call to the precompiler macro assertCheck(), which + // has been replaced with the precompiler macro assert() + + assert(wire->vertices.front().edgeInfo()->wireInfo.get() == wire); + } + } + } + + // This method was originally part of WireJoinerP::_findClosedWires(), split to reduce cognitive + // complexity + void _findClosedWiresBeginEdge(const std::shared_ptr& wireInfo, + const EdgeInfo& beginInfo, + int& idx) + { + auto info = wireInfo->vertices[idx].edgeInfo(); + showShape(info, "merge", iteration); + + if (info != &beginInfo) { + while (true) { + if (++idx == (int)wireInfo->vertices.size()) { + idx = 0; + } + + info = wireInfo->vertices[idx].edgeInfo(); + if (info == &beginInfo) { + break; + } + stack.emplace_back(vertexStack.size()); + vertexStack.push_back(wireInfo->vertices[idx]); + ++stack.back().iEnd; + + // Originally here there was a call to the method checkStack(), + // which does nothing and therefor has been removed. + } + } + } + + // This method was originally part of WireJoinerP::_findClosedWires(), split to reduce cognitive + // complexity + int _findClosedWiresWithExisting(int* idxVertex, + const std::shared_ptr& wireInfo, + int* const stackPos, + bool& proceed, + const VertexInfo& vinfo, + const int ic, + StackInfo& stackBack, + const EdgeInfo& beginInfo, + EdgeInfo& info) + { + if (wireInfo) { + // We may be called by findTightBound() with an existing wire + // to try to find a new wire by splitting the current one. So + // check if we've iterated to some edge in the existing wire. + + int idx = wireInfo->find(vinfo); + + if (idx != 0) { + vertexStack.push_back(adjacentList[ic]); + stackBack.iCurrent = stackBack.iEnd++; + --idx; + proceed = false; + if (idxVertex) { + *idxVertex = idx; + } + if (stackPos) { + *stackPos = (int)stack.size() - 2; + } + + _findClosedWiresBeginEdge(wireInfo, beginInfo, idx); + + return 1; + } + + if (wireInfo->find(VertexInfo(vinfo.it, !vinfo.start)) != 0) { + showShape(&info, "rintersect", iteration); + // Only used when exhausting tight bound. + wireInfo->purge = true; + return 2; + } + + if (isOutside(*wireInfo, info.mid)) { + showShape(&info, "outside", iteration); + return 2; + } + } + return 0; + } + + // This method was originally part of WireJoinerP::_findClosedWires(), split to reduce cognitive + // complexity + void _findClosedWiresUpdateStack(int* idxVertex, + const std::shared_ptr& wireInfo, + int* stackPos, + const EdgeInfo* currentInfo, + const int currentIdx, + bool& proceed, + const EdgeInfo& beginInfo) + { + auto& stackBack = stack.back(); + + // The loop below is to find all edges connected to pend, and save them into stack.back() + auto size = vertexStack.size(); + for (int i = currentInfo->iStart[currentIdx]; i < currentInfo->iEnd[currentIdx]; ++i) { + auto& vinfo = adjacentList[i]; + auto& info = *vinfo.it; + if (info.iteration < 0 || currentInfo == &info) { + continue; + } + + bool abort = false; + if (!wireSet.empty() && wireSet.contains(info.wireInfo.get())) { + showShape(&info, "wired", iteration); + if (wireInfo) { + wireInfo->purge = true; + } + abort = true; + } + + if (edgeSet.contains(&info)) { + showShape(&info, "intersect", iteration); + // This means the current edge connects to an + // existing edge in the middle of the stack. + // skip the current edge. + stackBack.iEnd = stackBack.iStart; + vertexStack.resize(size); + break; + } + + if (abort || currentInfo->wireInfo2) { + if (wireInfo) { + wireInfo->purge = true; + } + continue; + } + + if (info.iteration == iteration) { + continue; + } + info.iteration = iteration; + + int exists = _findClosedWiresWithExisting(idxVertex, + wireInfo, + stackPos, + proceed, + vinfo, + i, + stackBack, + beginInfo, + info); + + if (exists == 1) { + break; + } + + if (exists == 2) { + continue; + } + + vertexStack.push_back(adjacentList[i]); + ++stackBack.iEnd; + } + } + + // This method was originally part of WireJoinerP::_findClosedWires(), split to reduce cognitive + // complexity + bool _findClosedWiresUpdateEdges(VertexInfo& currentVertex, + gp_Pnt& pend, + EdgeInfo* currentInfo, + int& currentIdx, + const size_t stackEnd) + { + while (true) { + auto& stackBack = stack.back(); + if (stackBack.iCurrent < stackBack.iEnd) { + // now pick one edge from stack.back(), connect it to + // pend, then extend pend + currentVertex = vertexStack[stackBack.iCurrent]; + pend = currentVertex.ptOther(); + // update current edge info + currentInfo = currentVertex.edgeInfo(); + showShape(currentInfo, "iterate", iteration); + currentIdx = currentVertex.start ? 1 : 0; + edgeSet.insert(currentInfo); + if (!wireSet.empty()) { + wireSet.insert(currentInfo->wireInfo.get()); + } + break; + } + vertexStack.erase(vertexStack.begin() + static_cast(stackBack.iStart), vertexStack.end()); + + stack.pop_back(); + if (stack.size() == stackEnd) { + // If stack reaches the end, it means this wire is open. + return true; + } + + auto& lastInfo = *vertexStack[stack.back().iCurrent].it; + edgeSet.erase(&lastInfo); + wireSet.erase(lastInfo.wireInfo.get()); + showShape(&lastInfo, "pop", iteration); + ++stack.back().iCurrent; + } + return false; + } + + // This method was originally part of WireJoinerP::_findClosedWires(), split to reduce cognitive + // complexity + bool _findClosedWiresIsClosed(const VertexInfo& beginVertex, + const TopoDS_Wire& wire, + const EdgeInfo& beginInfo) + { + if (!BRep_Tool::IsClosed(wire)) { + FC_WARN("failed to close some wire in iteration " << iteration); + showShape(wire, "_FailedToClose", iteration); + showShape(beginInfo.shape(beginVertex.start), "failed", iteration); + for (auto& entry : stack) { + const auto& vertex = vertexStack[entry.iCurrent]; + auto& info = *vertex.it; + showShape(info.shape(vertex.start), vertex.start ? "failed" : "failed_r", iteration); + } + + // Originally here there was a call to the precompiler macro assertCheck(), which + // has been replaced with the precompiler macro assert() + + assert(false); + return false; + } + return true; + } + + TopoDS_Wire _findClosedWires(VertexInfo beginVertex, + VertexInfo currentVertex, + int *idxVertex = nullptr, + const std::shared_ptr& wireInfo = std::shared_ptr(), + int* stackPos = nullptr) + { + Base::SequencerBase::Instance().checkAbort(); + EdgeInfo &beginInfo = *beginVertex.it; + + EdgeInfo *currentInfo = currentVertex.edgeInfo(); + int currentIdx = currentVertex.start ? 1 : 0; + currentInfo->iteration = iteration; + + gp_Pnt pstart = beginVertex.pt(); + gp_Pnt pend = currentVertex.ptOther(); + + auto stackEnd = stack.size(); + + // Originally here there was a call to the method checkStack(), which does nothing and + // therefor has been removed. + + // pstart and pend is the start and end vertex of the current wire + while (true) { + // push a new stack entry + stack.emplace_back(vertexStack.size()); + showShape(currentInfo, "check", iteration); + + bool proceed = true; + + _findClosedWiresUpdateStack(idxVertex, + wireInfo, + stackPos, + currentInfo, + currentIdx, + proceed, + beginInfo); + + // Originally here there was a call to the method checkStack(), which does nothing and + // therefor has been removed. + + if (proceed) { + if (_findClosedWiresUpdateEdges(currentVertex, + pend, + currentInfo, + currentIdx, + stackEnd)) { + return {}; + } + + if (pstart.SquareDistance(pend) > myTol2) { + // if the wire is not closed yet, continue search for the + // next connected edge + continue; + } + if (wireInfo) { + if (idxVertex) { + *idxVertex = (int)wireInfo->vertices.size(); + } + if (stackPos) { + *stackPos = (int)stack.size() - 1; + } + } + } + + wireData->Clear(); + wireData->Add(beginInfo.shape(beginVertex.start)); + for (auto &entry : stack) { + const auto &vertex = vertexStack[entry.iCurrent]; + auto &info = *vertex.it; + wireData->Add(info.shape(vertex.start)); + } + TopoDS_Wire wire = makeCleanWire(); + if (!_findClosedWiresIsClosed(beginVertex, wire, beginInfo)) { + continue; + } + return wire; + } + } + + // This method was originally part of WireJoinerP::findTightBound(), split to reduce cognitive + // complexity + void findTightBoundSplitWire(const std::shared_ptr& wireInfo, + const EdgeInfo& beginInfo, + const std::vector& wireVertices, + std::shared_ptr& splitWire, + const int idxV, + int& idxStart, + const int idxEnd) + { + for (int idx = idxV; idx != idxEnd; ++idx) { + auto info = wireVertices[idx].edgeInfo(); + if (info == &beginInfo) { + showShape(*wireInfo, "exception", iteration, true); + showShape(info, "exception", iteration, true); + + // Originally here there was a call to the precompiler macro + // assertCheck(), which has been replaced with the precompiler macro + // assert() + + assert(info != &beginInfo); + } + if (info->wireInfo == wireInfo) { + if (!splitWire) { + idxStart = idx; + splitWire.reset(new WireInfo()); + } + info->wireInfo = splitWire; + } + } + } + + // This method was originally part of WireJoinerP::findTightBound(), split to reduce cognitive + // complexity + void findTightBoundWithSplit(const std::vector& wireVertices, + const int idxV, + const std::shared_ptr& splitWire, + const int idxStart, + const int idxEnd, + const int stackPos, + const int stackStart) + { + auto& splitEdges = splitWire->vertices; + gp_Pnt pstart; + gp_Pnt pt; + bool first = true; + for (int idx = idxStart; idx != idxEnd; ++idx) { + auto& vertex = wireVertices[idx]; + if (first) { + first = false; + pstart = vertex.pt(); + } + else { + + // Originally here there was a call to the precompiler macro + // assertCheck(), which has been replaced with the precompiler + // macro assert() + + assert(pt.SquareDistance(vertex.pt()) < myTol2); + } + pt = vertex.ptOther(); + splitEdges.push_back(vertex); + } + for (int i = stackPos; i >= stackStart; --i) { + const auto& vertex = vertexStack[stack[i].iCurrent]; + + // Originally here there was a call to the precompiler macro + // assertCheck(), which has been replaced with the precompiler macro + // assert() + + assert(pt.SquareDistance(vertex.ptOther()) < myTol2); + pt = vertex.pt(); + // The edges in the stack are the ones to slice + // the wire in half. We construct a new wire + // that includes the original beginning edge in + // the loop above. And this loop contains the + // other half. Note that the slicing edges + // should run in the oppsite direction, hence reversed + splitEdges.push_back(vertex.reversed()); + } + for (int idx = idxV; idx != idxStart; ++idx) { + auto& vertex = wireVertices[idx]; + + // Originally here there was a call to the precompiler macro + // assertCheck(), which has been replaced with the precompiler macro + // assert() + + assert(pt.SquareDistance(vertex.pt()) < myTol2); + pt = vertex.ptOther(); + splitEdges.push_back(vertex); + } + + // Originally here there was a call to the precompiler macro + // assertCheck(), which has been replaced with the precompiler macro + // assert() + + assert(pt.SquareDistance(pstart) < myTol2); + showShape(*splitWire, "swire", iteration); + } + + // This method was originally part of WireJoinerP::findTightBound(), split to reduce cognitive + // complexity + void findTightBoundByVertices(EdgeInfo& beginInfo, + const std::vector& wireVertices, + int& idxV, + const int iteration2, + const gp_Pnt& pstart, + const std::shared_ptr& wireInfo, + const VertexInfo& beginVertex, + std::shared_ptr& newWire) + { + const int idx = wireVertices[idxV].start ? 1 : 0; + + auto current = wireVertices[idxV].edgeInfo(); + showShape(current, "current", iteration); + + for (int vertex = current->iStart[idx]; vertex < current->iEnd[idx]; ++vertex) { + const auto& currentVertex = adjacentList[vertex]; + auto next = currentVertex.edgeInfo(); + if (next == current || next->iteration2 == iteration2 || next->iteration < 0) { + continue; + } + + showShape(next, "tcheck", iteration); + + if (!isInside(*wireInfo, next->mid)) { + showShape(next, "ninside", iteration); + next->iteration2 = iteration2; + continue; + } + + edgeSet.insert(next); + stack.emplace_back(vertexStack.size()); + ++stack.back().iEnd; + vertexStack.push_back(currentVertex); + + // Originally here there was a call to the method checkStack(), which does + // nothing and therefor has been removed. + + int idxEnd = (int)wireVertices.size(); + int stackStart = (int)stack.size() - 1; + int stackPos = (int)stack.size() - 1; + + TopoDS_Wire wire; + if (pstart.SquareDistance(currentVertex.ptOther()) > myTol2) { + wire = _findClosedWires(beginVertex, + currentVertex, + &idxEnd, + beginInfo.wireInfo, + &stackPos); + if (wire.IsNull()) { + vertexStack.pop_back(); + stack.pop_back(); + edgeSet.erase(next); + continue; + } + } + + newWire.reset(new WireInfo()); + auto& newWireVertices = newWire->vertices; + newWireVertices.push_back(beginVertex); + for (auto& entry : stack) { + const auto& vertex = vertexStack[entry.iCurrent]; + newWireVertices.push_back(vertex); + } + if (!wire.IsNull()) { + newWire->wire = wire; + } + else if (!initWireInfo(*newWire)) { + newWire.reset(); + vertexStack.pop_back(); + stack.pop_back(); + edgeSet.erase(next); + continue; + } + for (auto& vertex : newWire->vertices) { + if (vertex.edgeInfo()->wireInfo == wireInfo) { + vertex.edgeInfo()->wireInfo = newWire; + } + } + beginInfo.wireInfo = newWire; + showShape(*newWire, "nwire", iteration); + + std::shared_ptr splitWire; + if (idxEnd == 0) { + idxEnd = (int)wireVertices.size(); + } + ++idxV; + + // Originally here there was a call to the precompiler macro assertCheck(), + // which has been replaced with the precompiler macro assert() + + assert(idxV <= idxEnd); + int idxStart = idxV; + + findTightBoundSplitWire(wireInfo, + beginInfo, + wireVertices, + splitWire, + idxV, + idxStart, + idxEnd); + + if (splitWire) { + findTightBoundWithSplit(wireVertices, + idxV, + splitWire, + idxStart, + idxEnd, + stackPos, + stackStart); + } + + checkWireInfo(*newWire); + break; + } + } + + // This method was originally part of WireJoinerP::findTightBound(), split to reduce cognitive + // complexity + void findTightBoundUpdateVertices(EdgeInfo& beginInfo) + { + showShape(*beginInfo.wireInfo, "done", iteration); + beginInfo.wireInfo->done = true; + // If a wire is done, make sure all edges of this wire is + // marked as done. This can also prevent duplicated wires. + for (auto& vertex : beginInfo.wireInfo->vertices) { + auto info = vertex.edgeInfo(); + if (!info->wireInfo) { + info->wireInfo = beginInfo.wireInfo; + continue; + } + if (info->wireInfo->done) { + continue; + } + auto otherWire = info->wireInfo; + auto& otherWireVertices = info->wireInfo->vertices; + if (info == otherWireVertices.front().edgeInfo()) { + // About to change the first edge of the other wireInfo. + // Try to find a new first edge for it. + tmpVertices.clear(); + auto it = otherWireVertices.begin(); + tmpVertices.push_back(*it); + for (++it; it != otherWireVertices.end(); ++it) { + if (it->edgeInfo()->wireInfo == otherWire) { + break; + } + tmpVertices.push_back(*it); + } + if (tmpVertices.size() != otherWireVertices.size()) { + otherWireVertices.erase(otherWireVertices.begin(), it); + otherWireVertices.insert(otherWireVertices.end(), + tmpVertices.begin(), + tmpVertices.end()); + } + } + + // Originally here there was a call to the precompiler macro assertCheck(), + // which has been replaced with the precompiler macro assert() + + assert(info != &beginInfo); + info->wireInfo = beginInfo.wireInfo; + checkWireInfo(*otherWire); + } + checkWireInfo(*beginInfo.wireInfo); + } + + void findTightBound() + { + // Assumption: all edges lies on a common manifold surface + // + // Definition of 'Tight Bound': a wire that cannot be split into + // smaller wires by any intersecting edges internal to the wire. + // + // The idea of the searching algorithm is simple. The initial condition + // here is that we've found a closed wire for each edge. To find the + // tight bound, for each wire, check wire edge branches (using the + // adjacent list built earlier), and split the wire whenever possible. + + std::unique_ptr seq( + new Base::SequencerLauncher("Finding tight bound", edges.size())); + + int iteration2 = iteration; + for (auto &info : edges) { + ++iteration; + seq->next(true); + if (info.iteration < 0 || !info.wireInfo) { + continue; + } + + ++iteration2; + while(!info.wireInfo->done) { + auto wireInfo = info.wireInfo; + checkWireInfo(*wireInfo); + const auto &wireVertices = wireInfo->vertices; + auto beginVertex = wireVertices.front(); + auto &beginInfo = *beginVertex.it; + initWireInfo(*wireInfo); + showShape(wireInfo->wire, "iwire", iteration); + for (auto& vertex : wireVertices) { + vertex.it->iteration2 = iteration2; + } + + stack.clear(); + vertexStack.clear(); + edgeSet.clear(); + + std::shared_ptr newWire; + gp_Pnt pstart = beginVertex.pt(); + + int idxV = 0; + while (true) { + findTightBoundByVertices(beginInfo, + wireVertices, + idxV, + iteration2, + pstart, + wireInfo, + beginVertex, + newWire); + + if (newWire) { + ++iteration; + break; + } + + if (++idxV == (int)wireVertices.size()) { + break; + } + + stack.emplace_back(vertexStack.size()); + ++stack.back().iEnd; + vertexStack.push_back(wireVertices[idxV]); + edgeSet.insert(wireVertices[idxV].edgeInfo()); + + // Originally here there was a call to the method checkStack(), which does + // nothing and therefor has been removed. + } + + if (!newWire) { + findTightBoundUpdateVertices(beginInfo); + } + } + } + } + + // This method was originally part of WireJoinerP::exhaustTightBound(), split to reduce cognitive + // complexity + void exhaustTightBoundUpdateVertex(const int iteration2, + const VertexInfo& beginVertex, + const int idxV, + const gp_Pnt& pstart, + const std::vector& wireVertices, + std::shared_ptr& newWire, + const std::shared_ptr& wireInfo) + { + const int idx = wireVertices[idxV].start ? 1 : 0; + auto current = wireVertices[idxV].edgeInfo(); + + for (int vertex = current->iStart[idx]; vertex < current->iEnd[idx]; ++vertex) { + const auto& currentVertex = adjacentList[vertex]; + auto next = currentVertex.edgeInfo(); + if (next == current || next->iteration2 == iteration2 || next->iteration < 0) { + continue; + } + + showShape(next, "check2", iteration); + + if (!isInside(*wireInfo, next->mid)) { + showShape(next, "ninside2", iteration); + next->iteration2 = iteration2; + continue; + } + + edgeSet.insert(next); + stack.emplace_back(vertexStack.size()); + ++stack.back().iEnd; + vertexStack.push_back(currentVertex); + + // Originally here there a call to the method checkStack(), which + // does nothing and therefor has been removed. + + TopoDS_Wire wire; + if (pstart.SquareDistance(currentVertex.ptOther()) > myTol2) { + wire = _findClosedWires(beginVertex, currentVertex, nullptr, wireInfo); + if (wire.IsNull()) { + vertexStack.pop_back(); + stack.pop_back(); + edgeSet.erase(next); + wireSet.erase(next->wireInfo.get()); + continue; + } + } + + newWire.reset(new WireInfo()); + auto& newWireVertices = newWire->vertices; + newWireVertices.push_back(beginVertex); + for (auto& entry : stack) { + const auto& vertex = vertexStack[entry.iCurrent]; + newWireVertices.push_back(vertex); + } + if (!wire.IsNull()) { + newWire->wire = wire; + } + else if (!initWireInfo(*newWire)) { + newWire.reset(); + vertexStack.pop_back(); + stack.pop_back(); + edgeSet.erase(next); + wireSet.erase(next->wireInfo.get()); + continue; + } + for (auto& vertex : newWire->vertices) { + if (vertex.edgeInfo()->wireInfo == wireInfo) { + vertex.edgeInfo()->wireInfo = newWire; + } + } + showShape(*newWire, "nwire2", iteration); + checkWireInfo(*newWire); + break; + } + } + + // This method was originally part of WireJoinerP::exhaustTightBound(), split to reduce cognitive + // complexity + void exhaustTightBoundUpdateEdge(const int iteration2, + const VertexInfo& beginVertex, + const std::vector& wireVertices, + const gp_Pnt& pstart, + std::shared_ptr& wireInfo) + { + std::shared_ptr newWire; + + int idxV = 1; + while (true) { + exhaustTightBoundUpdateVertex(iteration2, + beginVertex, + idxV, + pstart, + wireVertices, + newWire, + wireInfo); + + if (newWire) { + ++iteration; + wireInfo = newWire; + break; + } + + if (++idxV == (int)wireVertices.size()) { + if (wireInfo->purge) { + showShape(*wireInfo, "discard2", iteration); + wireInfo.reset(); + } + else { + wireInfo->done = true; + showShape(*wireInfo, "done2", iteration); + } + break; + } + stack.emplace_back(vertexStack.size()); + ++stack.back().iEnd; + vertexStack.push_back(wireVertices[idxV]); + edgeSet.insert(wireVertices[idxV].edgeInfo()); + + // Originally here there a call to the method checkStack(), which does + // nothing and therefor has been removed. + } + } + + // This method was originally part of WireJoinerP::exhaustTightBound(), split to reduce cognitive + // complexity + void exhaustTightBoundWithAdjacent(const EdgeInfo& info, + int& iteration2, + const VertexInfo beginVertex, + const EdgeInfo* check) + { + const gp_Pnt& pstart = beginVertex.pt(); + const int vidx = beginVertex.start ? 1 : 0; + + edgeSet.clear(); + vertexStack.clear(); + stack.clear(); + stack.emplace_back(); + for (int i = info.iStart[vidx]; i < info.iEnd[vidx]; ++i) { + const auto& currentVertex = adjacentList[i]; + auto next = currentVertex.edgeInfo(); + if (next == &info || next == check || next->iteration < 0 || !next->wireInfo + || !next->wireInfo->done || next->wireInfo2) { + continue; + } + + showShape(next, "n2", iteration); + + stack.clear(); + stack.emplace_back(); + ++stack.back().iEnd; + vertexStack.clear(); + vertexStack.push_back(currentVertex); + + edgeSet.clear(); + edgeSet.insert(next); + wireSet.clear(); + wireSet.insert(next->wireInfo.get()); + + TopoDS_Wire wire; + if (pstart.SquareDistance(currentVertex.ptOther()) > myTol2) { + wire = _findClosedWires(beginVertex, currentVertex); + if (wire.IsNull()) { + continue; + } + } + + std::shared_ptr wireInfo(new WireInfo()); + wireInfo->vertices.push_back(beginVertex); + for (auto& entry : stack) { + const auto& vertex = vertexStack[entry.iCurrent]; + wireInfo->vertices.push_back(vertex); + } + if (!wire.IsNull()) { + wireInfo->wire = wire; + } + else if (!initWireInfo(*wireInfo)) { + continue; + } + + showShape(*wireInfo, "nw2", iteration); + + ++iteration; + ++iteration2; + + while (wireInfo && !wireInfo->done) { + showShape(next, "next2", iteration); + + vertexStack.resize(1); + stack.resize(1); + edgeSet.clear(); + edgeSet.insert(next); + wireSet.clear(); + wireSet.insert(next->wireInfo.get()); + + const auto& wireVertices = wireInfo->vertices; + initWireInfo(*wireInfo); + for (auto& vertex : wireVertices) { + vertex.it->iteration2 = iteration2; + } + + exhaustTightBoundUpdateEdge(iteration2, + beginVertex, + wireVertices, + pstart, + wireInfo); + } + + if (wireInfo && wireInfo->done) { + for (auto& vertex : wireInfo->vertices) { + auto edgeInfo = vertex.edgeInfo(); + + // Originally here there was a call to the precompiler macro + // assertCheck(), which has been replaced with the precompiler macro + // assert() + + assert(edgeInfo->wireInfo != nullptr); + if (edgeInfo->wireInfo->isSame(*wireInfo)) { + wireInfo = edgeInfo->wireInfo; + break; + } + } + for (auto& vertex : wireInfo->vertices) { + auto edgeInfo = vertex.edgeInfo(); + if (!edgeInfo->wireInfo2 && edgeInfo->wireInfo != wireInfo) { + edgeInfo->wireInfo2 = wireInfo; + } + } + + // Originally here there were two calls to the precompiler macro + // assertCheck(), which have been replaced with the precompiler macro + // assert() + + assert(info.wireInfo2 == wireInfo); + assert(info.wireInfo2 != info.wireInfo); + showShape(*wireInfo, "exhaust"); + break; + } + } + } + + // This method was originally part of WireJoinerP::exhaustTightBound(), split to reduce cognitive + // complexity + void exhaustTightBoundUpdateWire(const EdgeInfo& info, int& iteration2) + { + + showShape(*info.wireInfo, "iwire2", iteration); + showShape(&info, "begin2", iteration); + + int idx = info.wireInfo->find(&info); + + // Originally here there was a call to the precompiler macro assertCheck(), which has + // been replaced with the precompiler macro assert() + + assert(idx > 0); + const auto& vertices = info.wireInfo->vertices; + --idx; + int nextIdx = idx == (int)vertices.size() - 1 ? 0 : idx + 1; + int prevIdx = idx == 0 ? (int)vertices.size() - 1 : idx - 1; + int count = prevIdx == nextIdx ? 1 : 2; + for (int idxV = 0; idxV < count && !info.wireInfo2; ++idxV) { + auto check = vertices[idxV == 0 ? nextIdx : prevIdx].edgeInfo(); + auto beginVertex = vertices[idx]; + if (idxV == 1) { + beginVertex.start = !beginVertex.start; + } + + exhaustTightBoundWithAdjacent(info, iteration2, beginVertex, check); + } + } + + void exhaustTightBound() + { + // findTightBound() function will find a tight bound wire for each + // edge. Now we try to find all possible tight bound wires, relying on + // the important fact that an edge can be shared by at most two tight + // bound wires. + + std::unique_ptr seq( + new Base::SequencerLauncher("Exhaust tight bound", edges.size())); + + for (auto &info : edges) { + if (info.iteration < 0 || !info.wireInfo || !info.wireInfo->done) { + continue; + } + for (auto &vertex : info.wireInfo->vertices) { + auto edgeInfo = vertex.edgeInfo(); + if (edgeInfo->wireInfo != info.wireInfo) { + edgeInfo->wireInfo2 = info.wireInfo; + } + } + } + + int iteration2 = iteration; + for (auto &info : edges) { + ++iteration; + seq->next(true); + if (info.iteration < 0 + || !info.wireInfo + || !info.wireInfo->done) + { + if (info.wireInfo) { + showShape(*info.wireInfo, "iskip"); + } + else { + showShape(&info, "iskip"); + } + continue; + } + + if (info.wireInfo2 && info.wireInfo2->done) { + showShape(*info.wireInfo, "idone"); + continue; + } + + exhaustTightBoundUpdateWire(info, iteration2); + + } + wireSet.clear(); + } + + // This method was originally part of WireJoinerP::makeCleanWire(), split to reduce cognitive + // complexity + void printHistoryInit(const Handle_BRepTools_History& newHistory, + const std::vector& inputEdges) + { + FC_MSG("init:"); + for (const auto& shape : sourceEdges) { + FC_MSG(shape.getShape().TShape().get() << ", " << shape.getShape().HashCode(INT_MAX)); + } + printHistory(aHistory, sourceEdges); + printHistory(newHistory, inputEdges); + } + + // This method was originally part of WireJoinerP::makeCleanWire(), split to reduce cognitive + // complexity + void printHistoryFinal() + { + printHistory(aHistory, sourceEdges); + FC_MSG("final:"); + for (int i = 1; i <= wireData->NbEdges(); ++i) { + auto shape = wireData->Edge(i); + FC_MSG(shape.TShape().get() << ", " << shape.HashCode(INT_MAX)); + } + } + + TopoDS_Wire makeCleanWire(bool fixGap=true) + { + // Make a clean wire with sorted, oriented, connected, etc edges + TopoDS_Wire result; + std::vector inputEdges; + + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_TRACE)) { + for (int i = 1; i <= wireData->NbEdges(); ++i) { + inputEdges.emplace_back(wireData->Edge(i)); + } + } + + ShapeFix_Wire fixer; + Handle(ShapeBuild_ReShape) reshape = new ShapeBuild_ReShape(); + fixer.SetContext(reshape); + fixer.Load(wireData); + fixer.SetMaxTolerance(myTol); + fixer.ClosedWireMode() = Standard_True; + fixer.Perform(); + // fixer.FixReorder(); + // fixer.FixConnected(); + + if (fixGap) { + // Gap fixing may change vertex, but we need all concident vertexes + // to be the same one. + // + // fixer.FixGap3d(1, Standard_True); + } + + fixer.FixClosed(); + + result = fixer.Wire(); + auto newHistory = fixer.Context()->History(); + + + if (FC_LOG_INSTANCE.level() > FC_LOGLEVEL_TRACE + 1) { + printHistoryInit(newHistory, inputEdges); + } + + aHistory->Merge(newHistory); + + + if (FC_LOG_INSTANCE.level() > FC_LOGLEVEL_TRACE + 1) { + printHistoryFinal(); + } + + return result; + } + + // This method was originally part of WireJoinerP::printHistory(), split to reduce cognitive + // complexity + template + void printHistoryOfShape(const Handle(BRepTools_History)& hist, const T& shape) + { + for (TopTools_ListIteratorOfListOfShape it(hist->Modified(shape.getShape())); it.More(); + it.Next()) { + FC_MSG(shape.getShape().TShape().get() + << ", " << shape.getShape().HashCode(INT_MAX) << " -> " + << it.Value().TShape().get() << ", " << it.Value().HashCode(INT_MAX)); + } + } + + template + void printHistory(Handle(BRepTools_History) hist, const T &input) + { + FC_MSG("\nHistory:\n"); + for (const auto& shape : input) { + printHistoryOfShape(hist, shape); + } + } + + bool canShowShape(int idx=-1, bool forced=false) const + { + if (idx < 0 || catchIteration == 0 || catchIteration > idx) { + if (!forced && FC_LOG_INSTANCE.level() <= FC_LOGLEVEL_TRACE) { + return false; + } + } + return true; + } + + void showShape(const EdgeInfo* info, const char* name, int idx = -1, bool forced = false) const + { + if (!canShowShape(idx, forced)) { + return; + } + showShape(info->shape(), name, idx, forced); + } + + void showShape(WireInfo &wireInfo, const char *name, int idx=-1, bool forced=false) + { + if (!canShowShape(idx, forced)) { + return; + } + if (wireInfo.wire.IsNull()) { + initWireInfo(wireInfo); + } + showShape(wireInfo.wire, name, idx, forced); + } + + void showShape(const TopoDS_Shape& sToShow, + const char* name, + int idx = -1, + bool forced = false) const + { + if (!canShowShape(idx, forced)) { + return; + } + std::string _name; + if (idx >= 0) { + _name = name; + _name += "_"; + _name += std::to_string(idx); + _name += "_"; + name = _name.c_str(); + } + auto obj = Feature::create(sToShow, name); + FC_MSG(obj->getNameInDocument() << " " << ShapeHasher()(sToShow)); + if (catchObject == obj->getNameInDocument()) { + FC_MSG("found"); + } + } + + // This method was originally part of WireJoinerP::build(), split to reduce cognitive complexity + void buildClosedWire() + { + findClosedWires(true); + findTightBound(); + exhaustTightBound(); + bool done = !doOutline; + while (!done) { + ++iteration; + done = true; + std::unordered_map counter; + std::unordered_set wires; + for (auto& info : edges) { + if (info.iteration == -2) { + continue; + } + if (info.iteration < 0 || !info.wireInfo || !info.wireInfo->done) { + if (info.iteration >= 0) { + info.iteration = -1; + done = false; + showShape(&info, "removed", iteration); + aHistory->Remove(info.edge); + } + continue; + } + if (info.wireInfo2 && wires.insert(info.wireInfo2.get()).second) { + for (auto& vertex : info.wireInfo2->vertices) { + if (++counter[vertex.edgeInfo()] == 2) { + vertex.edgeInfo()->iteration = -1; + done = false; + showShape(vertex.edgeInfo(), "removed2", iteration); + aHistory->Remove(info.edge); + } + } + } + if (!wires.insert(info.wireInfo.get()).second) { + continue; + } + for (auto& vertex : info.wireInfo->vertices) { + if (++counter[vertex.edgeInfo()] == 2) { + vertex.edgeInfo()->iteration = -1; + done = false; + showShape(vertex.edgeInfo(), "removed1", iteration); + aHistory->Remove(info.edge); + } + } + } + findClosedWires(true); + findTightBound(); + } + + builder.MakeCompound(compound); + wireSet.clear(); + for (auto& info : edges) { + if (info.iteration == -2) { + if (!info.wireInfo) { + builder.Add(compound, info.wire()); + continue; + } + addWire(info.wireInfo); + addWire(info.wireInfo2); + } + else if (info.iteration >= 0) { + addWire(info.wireInfo2); + addWire(info.wireInfo); + } + } + wireSet.clear(); + } + + void build() + { + clear(); + sourceEdges.clear(); + sourceEdges.insert(sourceEdgeArray.begin(), sourceEdgeArray.end()); + for (const auto& edge : sourceEdgeArray) { + add(TopoDS::Edge(edge.getShape()), true); + } + + if (doTightBound || doSplitEdge) { + splitEdges(); + } + + buildAdjacentList(); + + if (!doTightBound && !doOutline) { + findClosedWires(); + } + else { + buildClosedWire(); + } + + // TODO: We choose to put open wires in a separated shape from the final + // result shape, so the history may contains some entries that are not + // presented in the final result, which will cause warning message when + // generating topo naming in TopoShape::makESHAPE(). We've lowered log + // message level to suppress the warning for the moment. The right way + // to solve the problem is to reconstruct the history and filter out + // those entries. + + bool hasOpenEdge = false; + for (const auto &info : edges) { + if (info.iteration == -3 || (!info.wireInfo && info.iteration>=0)) { + if (!hasOpenEdge) { + hasOpenEdge = true; + builder.MakeCompound(openWireCompound); + } + builder.Add(openWireCompound, info.wire()); + } + } + } + + void addWire(std::shared_ptr &wireInfo) + { + if (!wireInfo || !wireInfo->done || !wireSet.insertUnique(wireInfo.get())) { + return; + } + initWireInfo(*wireInfo); + builder.Add(compound, wireInfo->wire); + } + + bool getOpenWires(TopoShape &shape, const char *op, bool noOriginal) { + if (openWireCompound.IsNull()) { + shape.setShape(TopoShape()); + return false; + } + auto comp = openWireCompound; + if (noOriginal) { + TopoShape source(-1); + source.makeElementCompound(sourceEdgeArray); + auto wires = TopoShape(openWireCompound, -1).getSubTopoShapes(TopAbs_WIRE); + bool touched = false; + for (auto it=wires.begin(); it!=wires.end();) { + bool purge = true; + for (const auto &edge : it->getSubShapes(TopAbs_EDGE)) { + if (source.findSubShapesWithSharedVertex(TopoShape(edge, -1)).empty()) { + purge = false; + break; + } + } + if (purge) { + it = wires.erase(it); + touched = true; + } + else { + ++it; + } + } + if (touched) { + if (wires.empty()) { + shape.setShape(TopoShape()); + return false; + } + comp = TopoDS::Compound(TopoShape(-1).makeElementCompound(wires).getShape()); + } + } + shape.makeShapeWithElementMap(comp, + MapperHistory(aHistory), + {sourceEdges.begin(), sourceEdges.end()}, + op); + return true; + } + + bool getResultWires(TopoShape &shape, const char *op) { + // As compound is created by various calls to builder.MakeCompound() it looks that the + // following condition is always false. + // Probably it may be needed to add something like compound.Nullify() as done for + // openWireCompound in WireJoiner::WireJoinerP::clear() + if (compound.IsNull()) { + shape.setShape(TopoShape()); + return false; + } + shape.makeShapeWithElementMap(compound, + MapperHistory(aHistory), + {sourceEdges.begin(), sourceEdges.end()}, + op); + return true; + } +}; + + +WireJoiner::WireJoiner() + :pimpl(new WireJoinerP()) +{ +} + +WireJoiner::~WireJoiner() = default; + +void WireJoiner::addShape(const TopoShape &shape) +{ + NotDone(); + for (auto& edge : shape.getSubTopoShapes(TopAbs_EDGE)) { + pimpl->sourceEdgeArray.push_back(edge); + } +} + +void WireJoiner::addShape(const std::vector &shapes) +{ + NotDone(); + for (const auto &shape : shapes) { + for (auto& edge : shape.getSubTopoShapes(TopAbs_EDGE)) { + pimpl->sourceEdgeArray.push_back(edge); + } + } +} + +void WireJoiner::addShape(const std::vector &shapes) +{ + NotDone(); + for (const auto &shape : shapes) { + for (TopExp_Explorer xp(shape, TopAbs_EDGE); xp.More(); xp.Next()) { + pimpl->sourceEdgeArray.emplace_back(TopoDS::Edge(xp.Current()), -1); + } + } +} + +void WireJoiner::setOutline(bool enable) +{ + if (enable != pimpl->doOutline) { + NotDone(); + pimpl->doOutline = enable; + } +} + +void WireJoiner::setTightBound(bool enable) +{ + if (enable != pimpl->doTightBound) { + NotDone(); + pimpl->doTightBound = enable; + } +} + +void WireJoiner::setSplitEdges(bool enable) +{ + if (enable != pimpl->doSplitEdge) { + NotDone(); + pimpl->doSplitEdge = enable; + } +} + +void WireJoiner::setMergeEdges(bool enable) +{ + if (enable != pimpl->doSplitEdge) { + NotDone(); + pimpl->doMergeEdge = enable; + } +} + +void WireJoiner::setTolerance(double tol, double atol) +{ + if (tol >= 0 && tol != pimpl->myTol) { + NotDone(); + pimpl->myTol = tol; + pimpl->myTol2 = tol * tol; + } + if (atol >= 0 && atol != pimpl->myAngularTol) { + NotDone(); + pimpl->myAngularTol = atol; + } +} + +#if OCC_VERSION_HEX < 0x070600 +void WireJoiner::Build() +{ +#else +void WireJoiner::Build(const Message_ProgressRange& theRange) +{ + (void)theRange; +#endif + if (IsDone()) { + return; + } + pimpl->build(); + if (TopoShape(pimpl->compound).countSubShapes(TopAbs_SHAPE) > 0) { + myShape = pimpl->compound; + } + else { + myShape.Nullify(); + } + Done(); +} + +bool WireJoiner::getOpenWires(TopoShape &shape, const char *op, bool noOriginal) +{ + Build(); + return pimpl->getOpenWires(shape, op, noOriginal); +} + +bool WireJoiner::getResultWires(TopoShape &shape, const char *op) +{ + Build(); + return pimpl->getResultWires(shape, op); +} + +const TopTools_ListOfShape& WireJoiner::Generated (const TopoDS_Shape& SThatGenerates) +{ + Build(); + return pimpl->aHistory->Generated(SThatGenerates); +} + +const TopTools_ListOfShape& WireJoiner::Modified (const TopoDS_Shape& SThatModifies) +{ + Build(); + return pimpl->aHistory->Modified(SThatModifies); +} + +Standard_Boolean WireJoiner::IsDeleted (const TopoDS_Shape& SDeleted) +{ + Build(); + return pimpl->aHistory->IsRemoved(SDeleted); +} diff --git a/src/Mod/Part/App/WireJoiner.h b/src/Mod/Part/App/WireJoiner.h new file mode 100644 index 0000000000..23e23623ee --- /dev/null +++ b/src/Mod/Part/App/WireJoiner.h @@ -0,0 +1,67 @@ +/**************************************************************************** + * Copyright (c) 2022 Zheng Lei (realthunder) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ + +#ifndef PART_WIRE_JOINER_H +#define PART_WIRE_JOINER_H + +#include +#include +#include +#include "TopoShape.h" + +namespace Part{ + +class PartExport WireJoiner: public BRepBuilderAPI_MakeShape { +public: + WireJoiner(); + ~WireJoiner() override; + + void addShape(const TopoShape &shape); + void addShape(const std::vector &shapes); + void addShape(const std::vector &shapes); + + void setOutline(bool enable=true); + void setTightBound(bool enable=true); + void setSplitEdges(bool enable=true); + void setMergeEdges(bool enable=true); + void setTolerance(double tolerance, double atol=0.0); + + bool getOpenWires(TopoShape &shape, const char *op="", bool noOriginal=true); + bool getResultWires(TopoShape &shape, const char *op=""); + +#if OCC_VERSION_HEX < 0x070600 + void Build() override; +#else + void Build(const Message_ProgressRange& theRange = Message_ProgressRange()) override; +#endif + const TopTools_ListOfShape& Modified (const TopoDS_Shape& SThatModifies) override; + const TopTools_ListOfShape& Generated (const TopoDS_Shape& SThatGenerates) override; + Standard_Boolean IsDeleted (const TopoDS_Shape& SDeleted) override; + +private: + class WireJoinerP; + std::unique_ptr pimpl; +}; + +} // namespace Part + +#endif // PART_WIRE_JOINER_H diff --git a/tests/src/Mod/Part/App/CMakeLists.txt b/tests/src/Mod/Part/App/CMakeLists.txt index ac2d513352..bfd77aea3b 100644 --- a/tests/src/Mod/Part/App/CMakeLists.txt +++ b/tests/src/Mod/Part/App/CMakeLists.txt @@ -28,4 +28,5 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/TopoShapeMakeShapeWithElementMap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TopoShapeMapper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TopoShapeMakeShape.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/WireJoiner.cpp ) diff --git a/tests/src/Mod/Part/App/WireJoiner.cpp b/tests/src/Mod/Part/App/WireJoiner.cpp new file mode 100644 index 0000000000..717b8a4b3c --- /dev/null +++ b/tests/src/Mod/Part/App/WireJoiner.cpp @@ -0,0 +1,931 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gtest/gtest.h" +#include "src/App/InitApplication.h" +#include "Mod/Part/App/WireJoiner.h" +#include + +#include "PartTestHelpers.h" + +#include + +// NOLINTBEGIN(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) + +using namespace Part; +using namespace PartTestHelpers; + +class WireJoinerTest: public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + _docName = App::GetApplication().getUniqueDocumentName("test"); + App::GetApplication().newDocument(_docName.c_str(), "testUser"); + _hasher = Base::Reference(new App::StringHasher); + ASSERT_EQ(_hasher.getRefCount(), 1); + } + + void TearDown() override + { + App::GetApplication().closeDocument(_docName.c_str()); + } + + +private: + std::string _docName; + Data::ElementIDRefs _sid; + App::StringHasherRef _hasher; +}; + +TEST_F(WireJoinerTest, addShape) +{ + // Arrange + + // Create various edges that will be used to define the arguments of the various + // WireJoiner::addShape() calls + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 1.0, 0.0)).Edge()}; + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + + // A vector of TopoDS_Shape used as argument for wjvTDS.addShape() + std::vector edges {edge1, edge2, edge3, edge4}; + + // Create various TopoShapes used as arguments for wjvTS.addShape() + + auto edge1TS {TopoShape(edge1, 1)}; + auto edge2TS {TopoShape(edge2, 2)}; + + // A wire TopoShape used as argument for wjTS.addShape() + auto wireTS {TopoShape(BRepBuilderAPI_MakeWire(edge1, edge2, edge3).Wire(), 3)}; + // An empty TopoShape that will contain the shapes added by wjvTS.addShape() + auto wirevTS {TopoShape(4)}; + // An empty TopoShape that will contain the shapes added by wjvTDS.addShape() + auto wirevTDS {TopoShape(5)}; + + // Create 3 WireJoiner objects, one for every definition of WireJoiner::addShape() + + // A WireJoiner object where it will be added a TopoShape without calling WireJoiner::Build() + // afterwards + auto wjTSNotDone {WireJoiner()}; + // A WireJoiner object where it will be added a TopoShape + auto wjTS {WireJoiner()}; + // A WireJoiner object where it will be added a vector of TopoShapes + auto wjvTS {WireJoiner()}; + // A WireJoiner object where it will be added a vector of TopoDS_Shapes + auto wjvTDS {WireJoiner()}; + + // Act + + // Calling only WireJoiner::addShape(). Expected result is that wjTSNotDone.Done() is false + wjTSNotDone.addShape(wireTS); + + // Calling WireJoiner::addShape() and then WireJoiner::Build(). + // If we don't call WireJoiner::Build() we can't see the effect of WireJoiner::addShape() as it + // adds the shapes in the private member WireJoinerP::sourceEdgeArray + + wjTS.addShape(wireTS); + wjTS.Build(); + // The wire in wjTS is open, therefor to see the effect of wjTS.addShape() we must call + // wjTS.getOpenWires() and put the result in wireTS + wjTS.getOpenWires(wireTS, nullptr, false); + + wjvTS.addShape({edge1TS, edge2TS}); + wjvTS.Build(); + // The wire in wjvTS is open, therefor to see the effect of wjvTS.addShape() we must call + // wjvTS.getOpenWires() and put the result in wirevTS + wjvTS.getOpenWires(wirevTS, nullptr, false); + + wjvTDS.addShape(edges); + wjvTDS.Build(); + // The wire in wjvTDS is closed, therefor to see the effect of wjvTDS.addShape() we can smply + // call wjvTDS.Shape() to replace the shape in wirevTDS + wirevTDS.setShape(wjvTDS.Shape()); + + // Assert + + // Check the output of WireJoiner::IsDone(). + // It should be true for all the objects that executed also WireJoiner::Build() + // (All except wjTSNotDone) + EXPECT_FALSE(wjTSNotDone.IsDone()); + EXPECT_TRUE(wjTS.IsDone()); + EXPECT_TRUE(wjvTS.IsDone()); + EXPECT_TRUE(wjvTDS.IsDone()); + + // wireTS is build with 3 edges. The same quantity should be in the shape built + EXPECT_EQ(wireTS.getSubTopoShapes(TopAbs_EDGE).size(), 3); + // wirevTS is build with 2 edges. The same quantity should be in the shape built + EXPECT_EQ(wirevTS.getSubTopoShapes(TopAbs_EDGE).size(), 2); + // wirevTDS is build with 4 edges. The same quantity should be in the shape built + EXPECT_EQ(wirevTDS.getSubTopoShapes(TopAbs_EDGE).size(), 4); +} + +TEST_F(WireJoinerTest, setOutline) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, -0.1, 0.0), gp_Pnt(-0.1, 1.1, 0.0)).Edge()}; + auto edge5 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, 1.1, 0.0), gp_Pnt(1.1, 1.1, 0.0)).Edge()}; + auto edge6 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(-0.1, -0.1, 0.0)).Edge()}; + + // A vector of edges used as argument for wjNoOutline.addShape() + std::vector edgesNoOutline {edge1, edge2, edge3, edge4, edge5, edge6}; + // A vector of edges used as argument for wjOutline.addShape() + std::vector edgesOutline {edge1, edge2, edge3, edge4, edge5, edge6}; + + // To see the effect of setOutline() it is necessary to set the user parameter "Iteration" + // to a value, in this case, less than zero before the WireObjects are initialized. + // To see the correct value for this parameter refer to method WireJoinerP::canShowShape() + + auto hParam {App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/WireJoiner")}; + auto catchIteration {hParam->GetInt("Iteration", 0)}; + hParam->SetInt("Iteration", -1); + + // A document where there will WireJoiner.Build() will be called having setOutline() set to + // false + auto docNoOutline {App::GetApplication().getActiveDocument()}; + + auto _docNameOutline {App::GetApplication().getUniqueDocumentName("docOutline")}; + App::GetApplication().newDocument(_docNameOutline.c_str(), "docOutlineUser"); + // A document where there will WireJoiner.Build() will be called having setOutline() set to true + auto docOutline {App::GetApplication().getActiveDocument()}; + + // A WireJoiner object where the value of setOutline() will be changed but no shapes will be + // built + auto wjNoBuild {WireJoiner()}; + // A WireJoiner object where setOutline() will be set to false + auto wjNoOutline {WireJoiner()}; + // A WireJoiner object where setOutline() will be set to true + auto wjOutline {WireJoiner()}; + + // Reset the parameter to its previous value. + hParam->SetInt("Iteration", catchIteration); + + // Act + + // Changing only the value of setOutline(). This should set wjNoBuild.IsDone() to false + wjNoBuild.setOutline(true); + + // We can see the effect of setOutline by searching, among all the DocumentObjects created by + // the logic, if there are any with a label containing "removed" + + // Testing first the Document where WireJoiner::Build() will be executed with setOutline set to + // false + + App::GetApplication().setActiveDocument(docNoOutline); + + wjNoOutline.addShape(edgesNoOutline); + wjNoOutline.setOutline(false); + wjNoOutline.Build(); + + auto objsInDoc {docNoOutline->getObjects()}; + + auto foundRemovedNoOutline {false}; + for (const auto& obj : objsInDoc) { + foundRemovedNoOutline = + (std::string(obj->Label.getValue()).find("removed") != std::string::npos); + if (foundRemovedNoOutline) { + break; + } + } + + // Then testing the Document where WireJoiner::Build() will be executed with setOutline set to + // true + + App::GetApplication().setActiveDocument(docOutline); + + wjOutline.addShape(edgesOutline); + // same as wjOutline.setOutline(true); + wjOutline.setOutline(); + wjOutline.Build(); + + objsInDoc = docOutline->getObjects(); + + auto foundRemovedOutline {false}; + for (const auto& obj : objsInDoc) { + foundRemovedOutline = + (std::string(obj->Label.getValue()).find("removed") != std::string::npos); + if (foundRemovedOutline) { + break; + } + } + + // Assert + + // Without calling wjNoBuild.Build() the value of wjNoBuild.IsDone() should be false even if we + // only changed the value of setOutline() + EXPECT_FALSE(wjNoBuild.IsDone()); + + // In a document where WireJoiner::Build() is executed with setOutline() set to false there + // shouldn't be DocumentObjects with "removed" in their label + EXPECT_FALSE(foundRemovedNoOutline); + + // In a document where WireJoiner::Build() is executed with setOutline() set to true there + // should be at least a DocumentObject with "removed" in its label + EXPECT_TRUE(foundRemovedOutline); +} + +TEST_F(WireJoinerTest, setTightBound) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 1.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + + // A vector of edges used as argument for wjNoTightBound.addShape() + std::vector edgesNoTightBound {edge1, edge2, edge3, edge4}; + // A vector of edges used as argument for wjTightBound.addShape() + std::vector edgesTightBound {edge1, edge2, edge3, edge4}; + + // A WireJoiner object where the value of setTightBound() will be changed but no shapes will be + // built + auto wjNoBuild {WireJoiner()}; + // A WireJoiner object where setTightBound() will be set to false + auto wjNoTightBound {WireJoiner()}; + // A WireJoiner object where setTightBound() will be set to true + auto wjTightBound {WireJoiner()}; + + // An empty TopoShape that will contain the shapes returned by wjNoTightBound.getOpenWires() + auto wireNoTightBound {TopoShape(1)}; + // An empty TopoShape that will contain the shapes returned by wjTightBound.getOpenWires() + auto wireTightBound {TopoShape(2)}; + + // Act + + // Changing only the value of setTightBound(). This should set wjNoBuild.IsDone() to false + wjNoBuild.setTightBound(false); + + // To see the effect of setTightBound() we call WireJoiner::Build() and then + // WireJoiner::getOpenWires() + + wjNoTightBound.addShape(edgesNoTightBound); + wjNoTightBound.setTightBound(false); + // Calling wjNoTightBound.Build() will put all the edges inside the private object + // WireJoiner::WireJoinerP::openWireCompound. + wjNoTightBound.Build(); + wjNoTightBound.getOpenWires(wireNoTightBound, nullptr, false); + + wjTightBound.addShape(edgesTightBound); + // same as wjTightBound.setTightBound(true); + wjTightBound.setTightBound(); + // Calling wjTightBound.Build() will put inside the private object + // WireJoiner::WireJoinerP::openWireCompound only the edges that don't contribute to the + // creation of any closed wire. + wjTightBound.Build(); + wjTightBound.getOpenWires(wireTightBound, nullptr, false); + + // Assert + + // Without calling wjNoBuild.Build() the value of wjNoBuild.IsDone() should be false even if we + // only changed the value of setTightBound() + EXPECT_FALSE(wjNoBuild.IsDone()); + + // In this case the number of edges is equal to 6 because: + // edge1 and edge2 aren't modified => 2 edges + // edge3 and edge4 are both split in 2 edges => 4 edges + // The split is made at the intersection point (0.5, 0.5, 0.0) + EXPECT_EQ(wireNoTightBound.getSubTopoShapes(TopAbs_EDGE).size(), 6); + + // In this case the number of those edges is equal to 1 and that edge is the one with vertexes + // at the coordinates (0.5, 0.5, 0.0) - (0.0, 1.0, 0.0) + EXPECT_EQ(wireTightBound.getSubTopoShapes(TopAbs_EDGE).size(), 1); +} + +TEST_F(WireJoinerTest, setSplitEdges) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 1.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + + // A vector of edges used as argument for wjNoSplitEdges.addShape() + std::vector edgesNoSplitEdges {edge1, edge2}; + // A vector of edges used as argument for wjSplitEdges.addShape() + std::vector edgesSplitEdges {edge1, edge2}; + + // A WireJoiner object where the value of setSplitEdges() will be changed but no shapes will be + // built + auto wjNoBuild {WireJoiner()}; + + // A WireJoiner object where setSplitEdges() will be set to false + auto wjNoSplitEdges {WireJoiner()}; + // To see it's effect it's necessary also to call setTightBound(false) otherwise + // WireJoiner::WireJoinerP::splitEdges() will be called in any case + wjNoSplitEdges.setTightBound(false); + + // A WireJoiner object where setSplitEdges() will be set to true + auto wjSplitEdges {WireJoiner()}; + // To see it's effect it's necessary also to call setTightBound(false) otherwise + // WireJoiner::WireJoinerP::splitEdges() will be called in any case + wjSplitEdges.setTightBound(false); + + // An empty TopoShape that will contain the shapes returned by wjNoSplitEdges.getOpenWires() + auto wireNoSplitEdges {TopoShape(1)}; + // An empty TopoShape that will contain the shapes returned by wjSplitEdges.getOpenWires() + auto wireSplitEdges {TopoShape(2)}; + + // Act + + // Changing only the value of setSplitEdges(). This should set wjNoBuild.IsDone() to false + wjNoBuild.setSplitEdges(false); + + // To see the effect of setSplitEdges() we call WireJoiner::Build() and then + // WireJoiner::getOpenWires() + + wjNoSplitEdges.addShape(edgesNoSplitEdges); + wjNoSplitEdges.setSplitEdges(false); + // Calling wjNoSplitEdges.Build() will put all the edges that don't contribute to the creation + // of a closed wire inside the private object WireJoiner::WireJoinerP::openWireCompound. + wjNoSplitEdges.Build(); + wjNoSplitEdges.getOpenWires(wireNoSplitEdges, nullptr, false); + + wjSplitEdges.addShape(edgesSplitEdges); + // same as wjSplitEdges.setSplitEdges(true); + wjSplitEdges.setSplitEdges(); + // Calling wjSplitEdges.Build() will put inside the private object + // WireJoiner::WireJoinerP::openWireCompound all the edges processed by + // WireJoiner::WireJoinerP::splitEdges(). + wjSplitEdges.Build(); + wjSplitEdges.getOpenWires(wireSplitEdges, nullptr, false); + + // Assert + + // Without calling wjNoBuild.Build() the value of wjNoBuild.IsDone() should be false even if we + // only changed the value of setSplitEdges() + EXPECT_FALSE(wjNoBuild.IsDone()); + + // In this case the number of edges is equal to the number of edges added with + // wjNoSplitEdges.addShape() because none of them is used for the creation of a closed wire. + EXPECT_EQ(wireNoSplitEdges.getSubTopoShapes(TopAbs_EDGE).size(), 2); + + // In this case the number of those edges is equal to 4 because both the edges added with + // wjSplitEdges.addShape() have been split at the intersection point (0.5, 0.5, 0.0) + EXPECT_EQ(wireSplitEdges.getSubTopoShapes(TopAbs_EDGE).size(), 4); +} + +TEST_F(WireJoinerTest, setMergeEdges) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, 0.0, 0.0), gp_Pnt(1.1, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, -0.1, 0.0), gp_Pnt(1.0, 1.1, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(-0.1, -0.1, 0.0)).Edge()}; + + // A vector of edges used as argument for wjNoMergeEdges.addShape() + std::vector edgesNoMergeEdges {edge1, edge2, edge3}; + // A vector of edges used as argument for wjMergeEdges.addShape() + std::vector edgesMergeEdges {edge1, edge2, edge3}; + + // A WireJoiner object where the value of setMergeEdges() will be changed but no shapes will be + // built + auto wjNoBuild {WireJoiner()}; + + // A WireJoiner object where setMergeEdges() will be set to false + auto wjNoMergeEdges {WireJoiner()}; + // To see it's effect it's necessary also to call setTightBound(false) otherwise + // WireJoiner::WireJoinerP::MergeEdges() will be called in any case + wjNoMergeEdges.setTightBound(false); + + // A WireJoiner object where setMergeEdges() will be set to true + auto wjMergeEdges {WireJoiner()}; + // To see it's effect it's necessary also to call setTightBound(false) otherwise + // WireJoiner::WireJoinerP::MergeEdges() will be called in any case + wjMergeEdges.setTightBound(false); + + // An empty TopoShape that will contain the shapes returned by wjNoMergeEdges.getOpenWires() + auto wireNoMergeEdges {TopoShape(1)}; + // An empty TopoShape that will contain the shapes returned by wjMergeEdges.getOpenWires() + auto wireMergeEdges {TopoShape(2)}; + + // Act + + // Changing only the value of setMergeEdges(). This should set wjNoBuild.IsDone() to false + wjNoBuild.setMergeEdges(false); + + // To see the effect of setMergeEdges() we call WireJoiner::Build() and then + // WireJoiner::getOpenWires() + + wjNoMergeEdges.addShape(edgesNoMergeEdges); + wjNoMergeEdges.setMergeEdges(false); + // Calling wjNoMergeEdges.Build() will put all the edges produced by + // WireJoiner::WireJoinerP::splitEdges() in the private object + // WireJoiner::WireJoinerP::openWireCompound. + wjNoMergeEdges.Build(); + wjNoMergeEdges.getOpenWires(wireNoMergeEdges, nullptr, false); + + wjMergeEdges.addShape(edgesMergeEdges); + // same as wjMergeEdges.setMergeEdges(true); + wjMergeEdges.setMergeEdges(); + // Calling wjMergeEdges.Build() will put, among the edges produced by + // WireJoiner::WireJoinerP::splitEdges(), only the edges that are connected to only one of the + // others by a single vertex in the private object WireJoiner::WireJoinerP::openWireCompound. + // In the code those are called SuperEdges and are + // processed by WireJoiner::WireJoinerP::findSuperEdges(). + wjMergeEdges.Build(); + wjMergeEdges.getOpenWires(wireMergeEdges, nullptr, false); + + // Assert + + // Without calling wjNoBuild.Build() the value of wjNoBuild.IsDone() should be false even if we + // only changed the value of setMergeEdges() + EXPECT_FALSE(wjNoBuild.IsDone()); + + // In this case the number of edges is equal to 9 because all the 3 edges intersect the other 2 + // and are therefor split in 3 edges each. + EXPECT_EQ(wireNoMergeEdges.getSubTopoShapes(TopAbs_EDGE).size(), 9); + + // In this case the number of edges is equal to 6 because, among the 9 produced by + // WireJoiner::WireJoinerP::splitEdges(), 3 of them are connected to more than one other edge + // and therefor aren't added by WireJoiner::WireJoinerP::findSuperEdges() + EXPECT_EQ(wireMergeEdges.getSubTopoShapes(TopAbs_EDGE).size(), 6); +} + +TEST_F(WireJoinerTest, setTolerance) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto pi {acos(-1)}; + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.1, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(0.9, 0.0, 0.0)).Edge()}; + auto edge5 { + BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), + gp_Pnt(0.9 * std::cos(pi / 18), 0.9 * std::sin(pi / 18), 0.0)) + .Edge()}; + + // A vector of edges used as argument for wjNegtol.addShape() + std::vector edgesNegtol {edge1, edge2, edge3}; + // A vector of edges used as argument for wjtol.addShape() + std::vector edgestol {edge1, edge2, edge3}; + // A vector of edges used as argument for wjNegatol.addShape() + std::vector edgesNegatol {edge2, edge3, edge4, edge5}; + // A vector of edges used as argument for wjatol.addShape() + std::vector edgesatol {edge2, edge3, edge4, edge5}; + + // A WireJoiner object where the value of setTolerance() will be changed but no shapes will be + // built + auto wjNoBuild {WireJoiner()}; + + // A WireJoiner object where setTolerance() will be called passing to the argument tol a + // negative value. + auto wjNegtol {WireJoiner()}; + // A WireJoiner object where setTolerance() will be called passing to the argument tol a + // value both positive and not equal to WireJoiner::WireJoinerP::myTol + auto wjtol {WireJoiner()}; + + // A WireJoiner object where setTolerance() will be called passing to the argument atol a + // negative value. + auto wjNegatol {WireJoiner()}; + // A WireJoiner object where setTolerance() will be called passing to the argument atol a + // value both positive and not equal to WireJoiner::WireJoinerP::myAngularTol + auto wjatol {WireJoiner()}; + + // An empty TopoShape that will contain the shapes returned by wjNegtol.getOpenWires() + auto wireNegtol {TopoShape(1)}; + // An empty TopoShape that will contain the shapes returned by wjtol.getOpenWires() + auto wiretol {TopoShape(2)}; + + // An empty TopoShape that will contain the shapes returned by wjNegatol.getOpenWires() + auto wireNegatol {TopoShape(3)}; + // An empty TopoShape that will contain the shapes returned by wjatol.getOpenWires() + auto wireatol {TopoShape(4)}; + + // Act + + // Changing only the value of setTolerance(). This should set wjNoBuild.IsDone() to false + wjNoBuild.setTolerance(0.1); + + // To see the effect of setTolerance() we call WireJoiner::Build() and then + // WireJoiner::getOpenWires() to get the edges, if any, that aren't used to create a closed wire + + wjNegtol.addShape(edgesNegtol); + // Setting tol to a negative value won't have effect and therefor wjNegtol.pimpl->myTol will + // keep the default value. + // It's better also to give a negative value for the argument atol otherwise setTolerance() + // will set it to 0.0 + wjNegtol.setTolerance(-0.1, -pi); + wjNegtol.Build(); + wjNegtol.getOpenWires(wireNegtol, nullptr, false); + + wjtol.addShape(edgestol); + // Setting tol to a value that will change wjNegtol.pimpl->myTol. + // It's better also to give a negative value for the argument atol otherwise setTolerance() + // will set it to 0.0 + wjtol.setTolerance(0.2, -pi); + wjtol.Build(); + wjtol.getOpenWires(wiretol, nullptr, false); + + wjNegatol.addShape(edgesNegatol); + // Setting atol to a negative value won't have effect and therefor wjNegatol.pimpl->myAngularTol + // will keep the default value. + // The tol value must be given in any case. + wjNegatol.setTolerance(-0.1, -pi); + wjNegatol.Build(); + wjNegatol.getOpenWires(wireNegatol, nullptr, false); + + wjatol.addShape(edgesatol); + // Setting atol to a negative value won't have effect and therefor wjNegatol.pimpl->myAngularTol + // will keep the default value. + // We give also the tol value so that a closed wire can be created. + wjatol.setTolerance(0.2, pi / 9); + wjatol.Build(); + wjatol.getOpenWires(wireatol, nullptr, false); + + // Assert + + // Without calling wjNoBuild.Build() the value of wjNoBuild.IsDone() should be false even if we + // only changed the value of setTolerance() + EXPECT_FALSE(wjNoBuild.IsDone()); + + // In this case, as there's a gap between edge1 and edge3, no closed wires are created. + EXPECT_TRUE(wjNegtol.Shape().IsNull()); + // All the edges added with wjNegtol.addShape() can be extracted with wjNegtol.getOpenWires() + EXPECT_EQ(wireNegtol.getSubTopoShapes(TopAbs_EDGE).size(), 3); + + // In this case, as the gap between edge1 and edge3 is smaller than tol, a closed wire can be + // created and it contains all the edges added with wjtol.addShape(). + EXPECT_EQ(TopoShape(wjtol.Shape()).getSubTopoShapes(TopAbs_EDGE).size(), 3); + // There are no open wires and therefor no edges that create them + EXPECT_EQ(wiretol.getSubTopoShapes(TopAbs_EDGE).size(), 0); + + // In this case, as there's a gap between edge2, edge4 and edge5, no closed wires are created. + EXPECT_TRUE(wjNegatol.Shape().IsNull()); + // All the edges added with wjNegtol.addShape() can be extracted with wjNegatol.getOpenWires() + EXPECT_EQ(wireNegatol.getSubTopoShapes(TopAbs_EDGE).size(), 4); + + // In this case, as the gap between edge2, edge4 and edge5 is smaller than tol, a closed wire + // can be created. + // Because of atol, edge4 and edge5 are considerated as duplicates and therefor one of them is + // removed by WireJoiner::WireJoinerP::add(). + // The closed wire is then created using all the edges added with wjatol.addShape() except the + // removed one + EXPECT_EQ(TopoShape(wjatol.Shape()).getSubTopoShapes(TopAbs_EDGE).size(), 3); + // There are no open wires and therefor no edges that create them + EXPECT_EQ(wireatol.getSubTopoShapes(TopAbs_EDGE).size(), 0); +} + +TEST_F(WireJoinerTest, getOpenWires) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.5, 0.5, 0.0), gp_Pnt(1.5, 1.5, 0.0)).Edge()}; + + // A vector of edges used as argument for wjNoOpenWires.addShape() + std::vector edgesNoOpenWires {edge1, edge2, edge3}; + // A vector of edges used as argument for wjOriginal.addShape() + std::vector edgesOriginal {edge1, edge2, edge4}; + // A vector of edges used as argument for wjNoOriginal.addShape() + std::vector edgesNoOriginal {edge1, edge2, edge4}; + // A vector of TopoShape edges used as argument for wjNoOp.addShape(). A Tag is needed for every + // TopoShape, otherwise no element map will be created and no op can be found + std::vector edgesNoOp {TopoShape(edge2, 6), TopoShape(edge4, 7)}; + // A vector of TopoShape edges used as argument for wjOp.addShape(). A Tag is needed for every + // TopoShape, otherwise no element map will be created and no op can be found + std::vector edgesOp {TopoShape(edge2, 8), TopoShape(edge4, 9)}; + + // A WireJoiner object that will create a closed wire and no open wires + auto wjNoOpenWires {WireJoiner()}; + + // A WireJoiner object where the argument noOriginal will be set to false and that will create + // an open wire + auto wjOriginal {WireJoiner()}; + // A WireJoiner object where the argument noOriginal will be set to true and that will create an + // open wire + auto wjNoOriginal {WireJoiner()}; + + // A WireJoiner object where the argument op won't be set and that will create an open wire + auto wjNoOp {WireJoiner()}; + // A WireJoiner object where the argument op will be set and that will create an open wire + auto wjOp {WireJoiner()}; + + // An empty TopoShape that will contain the shapes returned by wjNoOpenWires.getOpenWires() + auto wireNoOpenWires {TopoShape(1)}; + + // An empty TopoShape that will contain the shapes returned by wjOriginal.getOpenWires() + auto wireOriginal {TopoShape(2)}; + // An empty TopoShape that will contain the shapes returned by wjNoOriginal.getOpenWires() + auto wireNoOriginal {TopoShape(3)}; + + // An empty TopoShape that will contain the shapes returned by wjNoOp.getOpenWires() + auto wireNoOp {TopoShape(4)}; + // An empty TopoShape that will contain the shapes returned by wjOp.getOpenWires() + auto wireOp {TopoShape(5)}; + + // Act + + wjNoOpenWires.addShape(edgesNoOpenWires); + // wjNoOpenWires.Build() is called by wjNoOpenWires.getOpenWires() + wjNoOpenWires.getOpenWires(wireNoOpenWires); + + wjOriginal.addShape(edgesOriginal); + // wjOriginal.Build() is called by wjOriginal.getOpenWires() + wjOriginal.getOpenWires(wireOriginal, nullptr, false); + + wjNoOriginal.addShape(edgesNoOriginal); + // wjNoOriginal.Build() is called by wjNoOriginal.getOpenWires() + wjNoOriginal.getOpenWires(wireNoOriginal); + + wjNoOp.addShape(edgesNoOp); + // wjNoOp.Build() is called by wjNoOp.getOpenWires() + wjNoOp.getOpenWires(wireNoOp, nullptr, false); + + wjOp.addShape(edgesOp); + // wjOp.Build() is called by wjOp.getOpenWires() + wjOp.getOpenWires(wireOp, "getOpenWires", false); + + // Assert + + // All the edges added with wjNoOpenWires.addShape() are used to create a closed wire, therefor + // wireNoOpenWires should be null + EXPECT_TRUE(wireNoOpenWires.isNull()); + + // In this case wireOriginal should contain all the edges added with wjOriginal.addShape(), + // except those ones that are split, and all the edges generated by splitting an edge with + // another one. + // edge1 and edge2 are left untouched, while edge4 is split in two at the intersection point + // (1.0, 1.0, 0.0), therefor 4 edges. + EXPECT_EQ(wireOriginal.getSubTopoShapes(TopAbs_EDGE).size(), 4); + + // In this case wireNoOriginal should contain only the edges generated by splitting one of them + // with another one. + // As edge1 and edge2 are left untouched, the only edges we should find are the ones generated + // by splitting edge4 in two at the intersection point (1.0, 1.0, 0.0) + EXPECT_EQ(wireNoOriginal.getSubTopoShapes(TopAbs_EDGE).size(), 2); + + // In this case, as we haven't set a value for op, WireJoiner::WireJoinerP::getOpenWires() will + // call TopoShape::makeShapeWithElementMap() which, without a value for op, will use + // Part::OpCodes::Maker as value for the various element maps + EXPECT_NE(wireNoOp.getElementMap()[0].name.find(Part::OpCodes::Maker), -1); + + // In this case WireJoiner::WireJoinerP::getOpenWires() will call + // TopoShape::makeShapeWithElementMap() giving "getOpenWires" as value for the op argument. + // That value should be found in the various element maps instead of Part::OpCodes::Maker + EXPECT_EQ(wireOp.getElementMap()[0].name.find(Part::OpCodes::Maker), -1); + EXPECT_NE(wireOp.getElementMap()[0].name.find("getOpenWires"), -1); +} + +TEST_F(WireJoinerTest, getResultWires) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + // Unlike calling WireJoiner::Build(), WireJoiner::getResultWires() returns a shape with edges + // only if those given as inputs crosses each others + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, 0.0, 0.0), gp_Pnt(1.1, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, -0.1, 0.0), gp_Pnt(1.0, 1.1, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(0.1, 0.1, 0.0)).Edge()}; + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(-0.1, -0.1, 0.0)).Edge()}; + + // A vector of edges used as argument for wjNoResultWires.addShape() + std::vector edgesNoResultWires {edge1, edge2, edge3}; + // A vector of TopoShape edges used as argument for wjNoOp.addShape(). A Tag is needed for every + // TopoShape, otherwise no element map will be created and no op can be found + std::vector edgesNoOp {TopoShape(edge1, 4), + TopoShape(edge2, 5), + TopoShape(edge4, 6)}; + // A vector of TopoShape edges used as argument for wjOp.addShape(). A Tag is needed for every + // TopoShape, otherwise no element map will be created and no op can be found + std::vector edgesOp {TopoShape(edge1, 7), TopoShape(edge2, 8), TopoShape(edge4, 9)}; + + // A WireJoiner object that will create no closed wires + auto wjNoResultWires {WireJoiner()}; + + // A WireJoiner object where the argument op won't be set and that will create a closed wire + auto wjNoOp {WireJoiner()}; + // A WireJoiner object where the argument op will be set and that will create a closed wire + auto wjOp {WireJoiner()}; + + // An empty TopoShape that will contain the shapes returned by + // wireNoResultWires.getResultWires() + auto wireNoResultWires {TopoShape(1)}; + + // An empty TopoShape that will contain the shapes returned by wjNoOp.getResultWires() + auto wireNoOp {TopoShape(2)}; + // An empty TopoShape that will contain the shapes returned by wjOp.getResultWires() + auto wireOp {TopoShape(3)}; + + + // Act + + wjNoResultWires.addShape(edgesNoResultWires); + // wjNoResultWires.Build() is called by wjNoResultWires.getResultWires() + wjNoResultWires.getResultWires(wireNoResultWires); + + wjNoOp.addShape(edgesNoOp); + // wjNoOp.Build() is called by wjNoOp.getResultWires() + wjNoOp.getResultWires(wireNoOp, nullptr); + + wjOp.addShape(edgesOp); + // wjOp.Build() is called by wjOp.getResultWires() + wjOp.getResultWires(wireOp, "getResultWires"); + + // Assert + + // All the edges added with wjNoResultWires.addShape() can't create a closed wire, therefor + // wireNoResultWires shouldn't have any edges + // It's not possible to get an useful result from wireNoResultWires.isNull() because + // WireJoiner::WireJoinerP::compound is always created by + // WireJoiner::WireJoinerP::builder.MakeCompound(), which doesn't create a null compound + EXPECT_EQ(wireNoResultWires.getSubTopoShapes(TopAbs_EDGE).size(), 0); + + // In this case, as we haven't set a value for op, WireJoiner::WireJoinerP::getResultWires() + // will call TopoShape::makeShapeWithElementMap() which, without a value for op, will use + // Part::OpCodes::Maker as value for the various element maps + EXPECT_NE(wireNoOp.getElementMap()[0].name.find(Part::OpCodes::Maker), -1); + + // In this case WireJoiner::WireJoinerP::getResultWires() will call + // TopoShape::makeShapeWithElementMap() giving "getResultWires" as value for the op argument. + // That value should be found in the various element maps instead of Part::OpCodes::Maker + EXPECT_EQ(wireOp.getElementMap()[0].name.find(Part::OpCodes::Maker), -1); + EXPECT_NE(wireOp.getElementMap()[0].name.find("getResultWires"), -1); +} + +#if OCC_VERSION_HEX >= 0x070600 +// WireJoiner::Build() has already been tested indirectly in the other tests. +// Here we check only the difference with OCCT versions >= 7.6.0 that add the parameter theRange +TEST_F(WireJoinerTest, Build) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, 0.0, 0.0), gp_Pnt(1.1, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, -0.1, 0.0), gp_Pnt(1.0, 1.1, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(-0.1, -0.1, 0.0)).Edge()}; + + // A vector of edges used as argument for wjtheRange.addShape() + std::vector edgestheRange {edge1, edge2, edge3}; + + // A WireJoiner object that will create no closed wires + auto wjtheRange {WireJoiner()}; + + // A Message_ProgressRange object that will be used as argument for wjtheRange.Build() + auto mpr {Message_ProgressRange()}; + + // Act + + wjtheRange.addShape(edgestheRange); + wjtheRange.Build(mpr); + + // Assert + + // theRange isn't used in WireJoiner::Build() and therefor not attached to any indicator. + // For more reference see + // https://dev.opencascade.org/doc/occt-7.6.0/refman/html/class_message___progress_range.html + // and + // https://dev.opencascade.org/doc/occt-7.6.0/refman/html/class_message___progress_indicator.html + EXPECT_FALSE(mpr.IsActive()); +} +#endif + +TEST_F(WireJoinerTest, Modified) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, 0.0, 0.0), gp_Pnt(1.1, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, -0.1, 0.0), gp_Pnt(1.0, 1.1, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(-0.1, -0.1, 0.0)).Edge()}; + + // A vector of edges used as argument for wjModified.addShape() + std::vector edges {edge1, edge2, edge3}; + + // A WireJoiner object that will have shapes modified by the added edges + auto wjModified {WireJoiner()}; + + // Act + + wjModified.addShape(edges); + wjModified.Build(); + + // Assert + + // In this case, every edge added with wjModified.addShape() modifies other shapes 3 times + + // edge1 modifies the edges with vertexes: + // (0.0, 0.0, 0.0) - (1.0, 1.0, 0.0) + // (-0.1, -0.1, 0.0) - (0.0, 0.0, 0.0) + // (1.0, -0.1, 0.0) - (1.0, 0.0, 0.0) + EXPECT_EQ(wjModified.Modified(edge1).Size(), 3); + + // edge2 modifies the edges with vertexes: + // (1.0, 1.0, 0.0) - (1.1, 1.1, 0.0) + // (0.0, 0.0, 0.0) - (1.0, 0.0, 0.0) + // (1.0, 0.0, 0.0) - (1.1, 0.0, 0.0) + EXPECT_EQ(wjModified.Modified(edge2).Size(), 3); + + // edge3 modifies the edges with vertexes: + // (1.0, 0.0, 0.0) - (1.0, 1.0, 0.0) + // (1.0, 1.0, 0.0) - (1.0, 1.1, 0.0) + // (-0.1, 0.0, 0.0) - (0.0, 0.0, 0.0) + EXPECT_EQ(wjModified.Modified(edge3).Size(), 3); +} + +TEST_F(WireJoinerTest, Generated) +{ + // Arrange + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(-0.1, 0.0, 0.0), gp_Pnt(1.1, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, -0.1, 0.0), gp_Pnt(1.0, 1.1, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.1, 1.1, 0.0), gp_Pnt(-0.1, -0.1, 0.0)).Edge()}; + + // A vector of edges used as argument for wjGenerated.addShape() + std::vector edges {edge1, edge2, edge3}; + + // A WireJoiner object that will have shapes generated by the added edges + auto wjGenerated {WireJoiner()}; + + // Act + + wjGenerated.addShape(edges); + wjGenerated.Build(); + + // Assert + + // There aren't calls to WireJoiner::WireJoinerP::aHistory->AddGenerated() or similar methods in + // WireJoiner::WireJoinerP, therefor nothing is returned by calling + // WireJoiner::WireJoinerP::aHistory->Generated(). + // There's a call to WireJoiner::WireJoinerP::aHistory->Merge() that uses the history produced + // by a ShapeFix_Wire object in WireJoiner::WireJoinerP::makeCleanWire() that however looks + // always empty as no methods in ShapeFix_Wire call AddGenerated() or similar methods + // (checked in OCCT 7.3.0 source + // https://git.dev.opencascade.org/gitweb/?p=occt.git;a=snapshot;h=42da0d5115bff683c6b596e66cdeaff957f81e7d;sf=tgz) + EXPECT_EQ(wjGenerated.Generated(edge1).Size(), 0); + EXPECT_EQ(wjGenerated.Generated(edge2).Size(), 0); + EXPECT_EQ(wjGenerated.Generated(edge3).Size(), 0); +} + +TEST_F(WireJoinerTest, IsDeleted) +{ + + // Create various edges that will be used for the WireJoiner objects tests + + auto edge1 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(1.0, 0.0, 0.0)).Edge()}; + auto edge2 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 0.0, 0.0), gp_Pnt(1.0, 1.0, 0.0)).Edge()}; + auto edge3 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + auto edge4 {BRepBuilderAPI_MakeEdge(gp_Pnt(1.0, 1.0, 0.0), gp_Pnt(0.0, 0.0, 0.0)).Edge()}; + auto edge5 {BRepBuilderAPI_MakeEdge(gp_Pnt(0.49, 0.0, 0.0), gp_Pnt(0.51, 0.0, 0.0)).Edge()}; + + // A vector of edges used as argument for wjIsDeleted.addShape() + std::vector edges {edge1, edge2, edge3, edge4, edge5}; + + // A WireJoiner object that will have shapes deleted by the added edges + auto wjIsDeleted {WireJoiner()}; + // To get all the deleted shapes in this case we also need to set the tolerance value + wjIsDeleted.setTolerance(0.03); + + // Act + + wjIsDeleted.addShape(edges); + wjIsDeleted.Build(); + + // Assert + + // In this case, edge1, edge2 and edge3 don't meet the conditions for deletion + EXPECT_FALSE(wjIsDeleted.IsDeleted(edge1)); + EXPECT_FALSE(wjIsDeleted.IsDeleted(edge2)); + EXPECT_FALSE(wjIsDeleted.IsDeleted(edge3)); + + // edge4 is a duplicate of edge3 and therefor deleted + EXPECT_TRUE(wjIsDeleted.IsDeleted(edge4)); + + // edge5 is smaller that the smallest shape that can be considered with the given value of + // tolerance and therefor deleted + EXPECT_TRUE(wjIsDeleted.IsDeleted(edge5)); +} + +// NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers)