diff --git a/src/Mod/Part/App/TopoShape.cpp b/src/Mod/Part/App/TopoShape.cpp index 62773ead82..845146ea3b 100644 --- a/src/Mod/Part/App/TopoShape.cpp +++ b/src/Mod/Part/App/TopoShape.cpp @@ -579,6 +579,14 @@ void TopoShape::setPyObject(PyObject* obj) } } +//void TopoShape::operator = (const TopoShape& sh) +//{ +// if (this != &sh) { +// this->Tag = sh.Tag; +// this->_Shape = sh._Shape; +// } +//} + void TopoShape::convertTogpTrsf(const Base::Matrix4D& mtrx, gp_Trsf& trsf) { trsf.SetValues(mtrx[0][0],mtrx[0][1],mtrx[0][2],mtrx[0][3], diff --git a/src/Mod/Part/App/TopoShape.h b/src/Mod/Part/App/TopoShape.h index a705b15742..a7f0ebe048 100644 --- a/src/Mod/Part/App/TopoShape.h +++ b/src/Mod/Part/App/TopoShape.h @@ -220,7 +220,7 @@ enum class CheckScale checkScale }; -enum class Copy +enum class CopyType { noCopy, copy @@ -249,6 +249,18 @@ enum class Spine on }; +enum class FillType +{ + noFill, + fill +}; + +enum class OpenResult +{ + noOpenResult, + allowOpenResult +}; + /** The representation for a CAD Shape */ // NOLINTNEXTLINE cppcoreguidelines-special-member-functions @@ -882,6 +894,171 @@ public: return TopoShape(0,Hasher).makeElementThickSolid(*this,faces,offset,tol,intersection,selfInter, offsetMode,join,op); } + /** Make a 3D offset of a given shape + * + * @param source: source shape + * @param offset: distance to offset + * @param tol: tolerance criterion for coincidence in generated shapes + * @param intersection: whether to check intersection in all generated parallel + * (OCCT document states the option is not fully implemented) + * @param selfInter: whether to eliminate self intersection + * (OCCT document states the option is not implemented) + * @param offsetMode: defines the construction type of parallels applied to free edges + * (OCCT document states the option is not implemented) + * @param join: join type. Only support JoinType::Arc and JoinType::Intersection. + * @param fill: whether to build a solid by fill the offset + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return The original content of this TopoShape is discarded and replaced + * with the new shape. The function returns the TopoShape itself as + * a self reference so that multiple operations can be carried out + * for the same shape in the same line of code. + */ + TopoShape& makeElementOffset(const TopoShape& source, + double offset, + double tol, + bool intersection = false, + bool selfInter = false, + short offsetMode = 0, + JoinType join = JoinType::arc, + FillType fill = FillType::noFill, + const char* op = nullptr); + + /** Make a 3D offset of this shape + * + * @param offset: distance to offset + * @param tol: tolerance criterion for coincidence in generated shapes + * @param intersection: whether to check intersection in all generated parallel + * (OCCT document states the option is not fully implemented) + * @param selfInter: whether to eliminate self intersection + * (OCCT document states the option is not implemented) + * @param offsetMode: defines the construction type of parallels applied to free edges + * (OCCT document states the option is not implemented) + * @param fill: whether to build a solid by fill the offset + * @param join: join type. Only support JoinType::Arc and JoinType::Intersection. + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return Return the new shape. The TopoShape itself is not modified. + */ + TopoShape makeElementOffset(double offset, + double tol, + bool intersection = false, + bool selfInter = false, + short offsetMode = 0, + JoinType join = JoinType::arc, + FillType fill = FillType::noFill, + const char* op = nullptr) const + { + return TopoShape(0, Hasher).makeElementOffset(*this, + offset, + tol, + intersection, + selfInter, + offsetMode, + join, + fill, + op); + } + + /** Make a 2D offset of a given shape + * + * @param source: source shape of edge, wire, face, or compound + * @param offset: distance to offset + * @param allowOpenResult: whether to allow open edge/wire + * @param join: join type. Only support JoinType::Arc and JoinType::Intersection. + * @param intersection: if true, then offset all non-compound shape + * together to deal with possible intersection after + * expanding the shape. If false, then offset each + * shape separately. + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return The original content of this TopoShape is discarded and replaced + * with the new shape. The function returns the TopoShape itself as + * a self reference so that multiple operations can be carried out + * for the same shape in the same line of code. + */ + TopoShape& makeElementOffset2D(const TopoShape& source, + double offset, + JoinType join = JoinType::arc, + FillType fill = FillType::noFill, + OpenResult allowOpenResult = OpenResult::allowOpenResult, + bool intersection = false, + const char* op = nullptr); + /** Make a 2D offset of a given shape + * + * @param source: source shape of edge, wire, face, or compound + * @param offset: distance to offset + * @param allowOpenResult: whether to allow open edge/wire + * @param join: join type. Only support JoinType::Arc and JoinType::Intersection. + * @param intersection: if true, then offset all non-compound shape + * together to deal with possible intersection after + * expanding the shape. If false, then offset each + * shape separately. + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return Return the new shape. The TopoShape itself is not modified. + */ + TopoShape makeElementOffset2D(double offset, + JoinType join = JoinType::arc, + FillType fill = FillType::noFill, + OpenResult allowOpenResult = OpenResult::allowOpenResult, + bool intersection = false, + const char* op = nullptr) const + { + return TopoShape(0, Hasher) + .makeElementOffset2D(*this, offset, join, fill, allowOpenResult, intersection, op); + } + + /** Make a 2D offset of face with separate control for outer and inner (hole) wires + * + * @param source: source shape of any type, but only faces inside will be used + * @param offset: distance to offset for outer wires of the faces + * @param innerOffset: distance to offset for inner wires of the faces + * @param join: join type of outer wire. Only support JoinType::Arc and JoinType::Intersection. + * @param innerJoin: join type of inner wire. Only support JoinType::Arc and + * JoinType::Intersection. + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return The original content of this TopoShape is discarded and replaced + * with the new shape. The function returns the TopoShape itself as + * a self reference so that multiple operations can be carried out + * for the same shape in the same line of code. + */ + TopoShape& makeElementOffsetFace(const TopoShape& source, + double offset, + double innerOffset, + JoinType join = JoinType::arc, + JoinType innerJoin = JoinType::arc, + const char* op = nullptr); + + /** Make a 2D offset of face with separate control for outer and inner (hole) wires + * + * @param source: source shape of any type, but only faces inside will be used + * @param offset: distance to offset for outer wires of the faces + * @param innerOffset: distance to offset for inner wires of the faces + * @param join: join type of outer wire. Only support JoinType::Arc and JoinType::Intersection. + * @param innerJoin: join type of inner wire. Only support JoinType::Arc and + * JoinType::Intersection. + * @param op: optional string to be encoded into topo naming for indicating + * the operation + * + * @return Return the new shape. The TopoShape itself is not modified. + */ + TopoShape makeElementOffsetFace(double offset, + double innerOffset, + JoinType join = JoinType::arc, + JoinType innerJoin = JoinType::arc, + const char* op = nullptr) const + { + return TopoShape(0, Hasher) + .makeElementOffsetFace(*this, offset, innerOffset, join, innerJoin, op); + } + /** Make revolved shell around a basis shape @@ -1552,7 +1729,7 @@ public: TopoShape& makeElementGTransform(const TopoShape& source, const Base::Matrix4D& mat, const char* op = nullptr, - Copy copy = Copy::noCopy); + CopyType copy = CopyType::noCopy); /** Make a new shape with transformation that may contain non-uniform scaling * @@ -1568,7 +1745,7 @@ public: */ TopoShape makeElementGTransform(const Base::Matrix4D& mat, const char* op = nullptr, - Copy copy = Copy::noCopy) const + CopyType copy = CopyType::noCopy) const { return TopoShape(Tag, Hasher).makeElementGTransform(*this, mat, op, copy); } @@ -1858,7 +2035,7 @@ public: const Base::Matrix4D& mat, const char* op = nullptr, CheckScale checkScale = CheckScale::noScaleCheck, - Copy copy = Copy::noCopy); + CopyType copy = CopyType::noCopy); /** Make a new shape with transformation * @@ -1881,7 +2058,7 @@ public: const Base::Matrix4D& mat, const char* op = nullptr, CheckScale checkScale = CheckScale::noScaleCheck, - Copy copy = Copy::noCopy) + CopyType copy = CopyType::noCopy) { _makeElementTransform(source, mat, op, checkScale, copy); return *this; @@ -1905,7 +2082,7 @@ public: TopoShape makeElementTransform(const Base::Matrix4D& mat, const char* op = nullptr, CheckScale checkScale = CheckScale::noScaleCheck, - Copy copy = Copy::noCopy) const + CopyType copy = CopyType::noCopy) { return TopoShape(Tag, Hasher).makeElementTransform(*this, mat, op, checkScale, copy); } @@ -1926,7 +2103,7 @@ public: TopoShape& makeElementTransform(const TopoShape& shape, const gp_Trsf& trsf, const char* op = nullptr, - Copy copy = Copy::noCopy); + CopyType copy = CopyType::noCopy); /** Make a new shape with transformation * @@ -1940,7 +2117,7 @@ public: * modified */ TopoShape - makeElementTransform(const gp_Trsf& trsf, const char* op = nullptr, Copy copy = Copy::noCopy) + makeElementTransform(const gp_Trsf& trsf, const char* op = nullptr, CopyType copy = CopyType::noCopy) { return TopoShape(Tag, Hasher).makeElementTransform(*this, trsf, op, copy); } diff --git a/src/Mod/Part/App/TopoShapeExpansion.cpp b/src/Mod/Part/App/TopoShapeExpansion.cpp index 0ce927e884..b30c1604a9 100644 --- a/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -99,8 +100,11 @@ #include "TopoShapeMapper.h" #include "FaceMaker.h" #include "Geometry.h" +#include "BRepOffsetAPI_MakeOffsetFix.h" #include +#include +#include FC_LOG_LEVEL_INIT("TopoShape", true, true) // NOLINT @@ -2270,6 +2274,538 @@ TopoShape& TopoShape::makeElementPipeShell(const std::vector& shapes, return makeElementShape(mkPipeShell, shapes, op); } +TopoShape& TopoShape::makeElementOffset(const TopoShape& shape, + double offset, + double tol, + bool intersection, + bool selfInter, + short offsetMode, + JoinType join, + FillType fill, + const char* op) +{ + if (!op) { + op = Part::OpCodes::Offset; + } + + BRepOffsetAPI_MakeOffsetShape mkOffset; + mkOffset.PerformByJoin(shape.getShape(), + offset, + tol, + BRepOffset_Mode(offsetMode), + intersection ? Standard_True : Standard_False, + selfInter ? Standard_True : Standard_False, + GeomAbs_JoinType(join)); + + if (!mkOffset.IsDone()) { + FC_THROWM(Base::CADKernelError, "BRepOffsetAPI_MakeOffsetShape not done"); + } + + TopoShape res(Tag, Hasher); + res.makeElementShape(mkOffset, shape, op); + if (shape.hasSubShape(TopAbs_SOLID) && !res.hasSubShape(TopAbs_SOLID)) { + try { + res = res.makeElementSolid(); + } + catch (Standard_Failure& e) { + FC_WARN("failed to make solid: " << e.GetMessageString()); + } + } + if (fill == FillType::noFill) { + *this = res; + return *this; + } + + // get perimeter wire of original shape. + // Wires returned seem to have edges in connection order. + ShapeAnalysis_FreeBoundsProperties freeCheck(shape.getShape()); + freeCheck.Perform(); + if (freeCheck.NbClosedFreeBounds() < 1) { + FC_THROWM(Base::CADKernelError, "no closed bounds"); + } + + BRep_Builder builder; + std::vector shapes; + for (int index = 1; index <= freeCheck.NbClosedFreeBounds(); ++index) { + TopoShape originalWire(shape.Tag, + shape.Hasher, + freeCheck.ClosedFreeBound(index)->FreeBound()); + originalWire.mapSubElement(shape); + const BRepAlgo_Image& img = mkOffset.MakeOffset().OffsetEdgesFromShapes(); + + // build offset wire. + TopoDS_Wire offsetWire; + builder.MakeWire(offsetWire); + for (const auto& s : originalWire.getSubShapes(TopAbs_EDGE)) { + if (!img.HasImage(s)) { + FC_THROWM(Base::CADKernelError, "no image for shape"); + } + const TopTools_ListOfShape& currentImage = img.Image(s); + TopTools_ListIteratorOfListOfShape listIt; + int edgeCount(0); + TopoDS_Edge mappedEdge; + for (listIt.Initialize(currentImage); listIt.More(); listIt.Next()) { + if (listIt.Value().ShapeType() != TopAbs_EDGE) { + continue; + } + edgeCount++; + mappedEdge = TopoDS::Edge(listIt.Value()); + } + + if (edgeCount != 1) { + std::ostringstream stream; + stream << "wrong edge count: " << edgeCount << std::endl; + FC_THROWM(Base::CADKernelError, stream.str().c_str()); + } + builder.Add(offsetWire, mappedEdge); + } + std::vector wires; + wires.push_back(originalWire); + wires.push_back(TopoShape(Tag, Hasher, offsetWire)); + wires.back().mapSubElement(res); + + // It would be nice if we could get thruSections to build planar faces + // in all areas possible, so we could run through refine. I tried setting + // ruled to standard_true, but that didn't have the desired affect. + BRepOffsetAPI_ThruSections aGenerator; + aGenerator.AddWire(TopoDS::Wire(originalWire.getShape())); + aGenerator.AddWire(offsetWire); + aGenerator.Build(); + if (!aGenerator.IsDone()) { + FC_THROWM(Base::CADKernelError, "ThruSections failed"); + } + + shapes.push_back(TopoShape(Tag, Hasher).makeElementShape(aGenerator, wires)); + } + + TopoShape perimeterCompound(Tag, Hasher); + perimeterCompound.makeElementCompound(shapes, op); + + // still had to sew. not using the passed in parameter for sew. + // Sew has it's own default tolerance. Opinions? + BRepBuilderAPI_Sewing sewTool; + sewTool.Add(shape.getShape()); + sewTool.Add(perimeterCompound.getShape()); + sewTool.Add(res.getShape()); + sewTool.Perform(); // Perform Sewing + + TopoDS_Shape outputShape = sewTool.SewedShape(); + if ((outputShape.ShapeType() == TopAbs_SHELL) && (outputShape.Closed())) { + BRepBuilderAPI_MakeSolid solidMaker(TopoDS::Shell(outputShape)); + if (solidMaker.IsDone()) { + TopoDS_Solid temp = solidMaker.Solid(); + // contrary to the occ docs the return value OrientCloseSolid doesn't + // indicate whether the shell was open or not. It returns true with an + // open shell and we end up with an invalid solid. + if (BRepLib::OrientClosedSolid(temp)) { + outputShape = temp; + } + } + } + + shapes.clear(); + shapes.push_back(shape); + shapes.push_back(res); + shapes.push_back(perimeterCompound); + *this = TopoShape(Tag, Hasher) + .makeShapeWithElementMap(outputShape, MapperSewing(sewTool), shapes, op); + return *this; +} + +TopoShape& TopoShape::makeElementOffsetFace(const TopoShape& shape, + double offset, + double innerOffset, + JoinType joinType, + JoinType innerJoinType, + const char* op) +{ + if (std::abs(innerOffset) < Precision::Confusion() + && std::abs(offset) < Precision::Confusion()) { + *this = shape; + return *this; + } + + if (shape.isNull()) { + FC_THROWM(Base::ValueError, "makeOffsetFace: input shape is null!"); + } + if (!shape.hasSubShape(TopAbs_FACE)) { + FC_THROWM(Base::ValueError, "makeOffsetFace: no face found"); + } + + std::vector res; + for (auto& face : shape.getSubTopoShapes(TopAbs_FACE)) { + std::vector wires; + TopoShape outerWire = face.splitWires(&wires, ReorientForward); + if (wires.empty()) { + res.push_back(makeElementOffset2D(face, + offset, + joinType, + FillType::noFill, + OpenResult::noOpenResult, + false, + op)); + continue; + } + if (outerWire.isNull()) { + FC_THROWM(Base::CADKernelError, "makeOffsetFace: missing outer wire!"); + } + + if (std::abs(offset) > Precision::Confusion()) { + outerWire = outerWire.makeElementOffset2D(offset, + joinType, + FillType::noFill, + OpenResult::noOpenResult, + false, + op); + } + + if (std::abs(innerOffset) > Precision::Confusion()) { + TopoShape innerWires(0, Hasher); + innerWires.makeElementCompound(wires, + "", + SingleShapeCompoundCreationPolicy::returnShape); + innerWires = innerWires.makeElementOffset2D(innerOffset, + innerJoinType, + FillType::noFill, + OpenResult::noOpenResult, + true, + op); + wires = innerWires.getSubTopoShapes(TopAbs_WIRE); + } + wires.push_back(outerWire); + gp_Pln pln; + res.push_back(TopoShape(0, Hasher).makeElementFace(wires, + nullptr, + nullptr, + face.findPlane(pln) ? &pln : nullptr)); + } + return makeElementCompound(res, "", SingleShapeCompoundCreationPolicy::returnShape); +} + +TopoShape& TopoShape::makeElementOffset2D(const TopoShape& shape, + double offset, + JoinType joinType, + FillType fill, + OpenResult allowOpenResult, + bool intersection, + const char* op) +{ + if (!op) { + op = Part::OpCodes::Offset2D; + } + + if (shape.isNull()) { + FC_THROWM(Base::ValueError, "makeOffset2D: input shape is null!"); + } + if (allowOpenResult == OpenResult::allowOpenResult && OCC_VERSION_HEX < 0x060900) { + FC_THROWM(Base::AttributeError, "openResult argument is not supported on OCC < 6.9.0."); + } + + // OUTLINE OF MAKEOFFSET2D + // * Prepare shapes to process + // ** if _Shape is a compound, recursively call this routine for all subcompounds + // ** if intrsection, dump all non-compound children into shapes to process; otherwise call this + // routine recursively for all children + // ** if _shape isn't a compound, dump it straight to shapes to process + // * Test for shape types, and convert them all to wires + // * find plane + // * OCC call (BRepBuilderAPI_MakeOffset) + // * postprocessing (facemaking): + // ** convert offset result back to faces, if inputs were faces + // ** OR do offset filling: + // *** for closed wires, simply feed source wires + offset wires to smart facemaker + // *** for open wires, try to connect source anf offset result by creating new edges (incomplete + // implementation) + // ** actual call to FaceMakerBullseye, unified for all facemaking. + + std::vector shapesToProcess; + std::vector shapesToReturn; + SingleShapeCompoundCreationPolicy outputPolicy = SingleShapeCompoundCreationPolicy::returnShape; + if (shape.getShape().ShapeType() == TopAbs_COMPOUND) { + if (!intersection) { + // simply recursively process the children, independently + expandCompound(shape, shapesToProcess); + outputPolicy = SingleShapeCompoundCreationPolicy::forceCompound; + } + else { + // collect non-compounds from this compound for collective offset. Process other shapes + // independently. + for (auto& s : shape.getSubTopoShapes()) { + if (s.getShape().ShapeType() == TopAbs_COMPOUND) { + // recursively process subcompounds + shapesToReturn.push_back(TopoShape(Tag, Hasher) + .makeElementOffset2D(s, + offset, + joinType, + fill, + allowOpenResult, + intersection, + op)); + outputPolicy = SingleShapeCompoundCreationPolicy::forceCompound; + } + else { + shapesToProcess.push_back(s); + } + } + } + } + else { + shapesToProcess.push_back(shape); + } + + if (shapesToProcess.size() > 0) { + TopoShape res(Tag, Hasher); + + // although 2d offset supports offsetting a face directly, it seems there is + // no way to do a collective offset of multiple faces. So, we are doing it + // by getting all wires from the faces, and applying offsets to them, and + // reassembling the faces later. + std::vector sourceWires; + bool haveWires = false; + bool haveFaces = false; + for (auto& s : shapesToProcess) { + const auto& sh = s.getShape(); + switch (sh.ShapeType()) { + case TopAbs_EDGE: + sourceWires.push_back(s.makeElementWires()); + haveWires = true; + break; + case TopAbs_WIRE: + sourceWires.push_back(s); + haveWires = true; + break; + case TopAbs_FACE: { + auto outerWire = s.splitWires(&sourceWires); + sourceWires.push_back(outerWire); + haveFaces = true; + } break; + default: + FC_THROWM(Base::TypeError, + "makeOffset2D: input shape is not an edge, wire or face or compound " + "of those."); + break; + } + } + if (haveWires && haveFaces) { + FC_THROWM( + Base::TypeError, + "makeOffset2D: collective offset of a mix of wires and faces is not supported"); + } + if (haveFaces) { + allowOpenResult = OpenResult::noOpenResult; + } + + // find plane. + gp_Pln workingPlane; + if (!TopoShape() + .makeElementCompound(sourceWires, + "", + SingleShapeCompoundCreationPolicy::returnShape) + .findPlane(workingPlane)) { + FC_THROWM(Base::CADKernelError, "makeOffset2D: wires are nonplanar or noncoplanar"); + } + + // do the offset.. + TopoShape offsetShape; + if (fabs(offset) > Precision::Confusion()) { + BRepOffsetAPI_MakeOffsetFix mkOffset(GeomAbs_JoinType(joinType), + allowOpenResult == OpenResult::allowOpenResult); + for (auto& w : sourceWires) { + mkOffset.AddWire(TopoDS::Wire(w.getShape())); + } + try { +#if defined(__GNUC__) && defined(FC_OS_LINUX) + Base::SignalException se; +#endif + mkOffset.Perform(offset); + } + catch (Standard_Failure&) { + throw; + } + catch (...) { + FC_THROWM(Base::CADKernelError, + "BRepOffsetAPI_MakeOffset has crashed! (Unknown exception caught)"); + } + if (mkOffset.Shape().IsNull()) { + FC_THROWM(NullShapeException, "makeOffset2D: result of offsetting is null!"); + } + + // Copying shape to fix strange orientation behavior, OCC7.0.0. See bug #2699 + // http://www.freecadweb.org/tracker/view.php?id=2699 + offsetShape = shape.makeElementShape(mkOffset, op).makeElementCopy(); + } + else { + offsetShape = TopoShape(Tag, Hasher) + .makeElementCompound(sourceWires, + 0, + SingleShapeCompoundCreationPolicy::returnShape); + } + + std::vector offsetWires; + // interestingly, if wires are removed, empty compounds are returned by MakeOffset (as of + // OCC 7.0.0) so, we just extract all nesting + expandCompound(offsetShape, offsetWires); + if (offsetWires.empty()) { + FC_THROWM(Base::CADKernelError, "makeOffset2D: offset result has no wires."); + } + + std::vector wiresForMakingFaces; + if (fill == FillType::noFill) { + if (haveFaces) { + wiresForMakingFaces.insert(wiresForMakingFaces.end(), + offsetWires.begin(), + offsetWires.end()); + } + else { + shapesToReturn.insert(shapesToReturn.end(), offsetWires.begin(), offsetWires.end()); + } + } + else { + // fill offset + if (fabs(offset) < Precision::Confusion()) { + FC_THROWM(Base::ValueError, + "makeOffset2D: offset distance is zero. Can't fill offset."); + } + + // filling offset. There are three major cases to consider: + // 1. source wires and result wires are closed (simplest) -> make face + // from source wire + offset wire + // + // 2. source wire is open, but offset wire is closed (if not + // allowOpenResult). -> throw away source wire and make face right from + // offset result. + // + // 3. source and offset wire are both open (note that there may be + // closed islands in offset result) -> need connecting offset result to + // source wire with new edges + + // first, lets split apart closed and open wires. + std::vector closedWires; + std::vector openWires; + for (auto& w : sourceWires) { + if (BRep_Tool::IsClosed(TopoDS::Wire(w.getShape()))) { + closedWires.push_back(w); + } + else { + openWires.push_back(w); + } + } + for (auto& w : offsetWires) { + if (BRep_Tool::IsClosed(TopoDS::Wire(w.getShape()))) { + closedWires.push_back(w); + } + else { + openWires.push_back(w); + } + } + + wiresForMakingFaces.insert(wiresForMakingFaces.end(), + closedWires.begin(), + closedWires.end()); + if (allowOpenResult == OpenResult::noOpenResult || openWires.size() == 0) { + // just ignore all open wires + } + else { + // We need to connect open wires to form closed wires. + + // for now, only support offsetting one open wire -> there should be exactly two + // open wires for connecting + if (openWires.size() != 2) { + FC_THROWM(Base::CADKernelError, + "makeOffset2D: collective offset with filling of multiple wires is " + "not supported yet."); + } + + TopoShape openWire1 = openWires.front(); + TopoShape openWire2 = openWires.back(); + + // find open vertices + BRepTools_WireExplorer xp; + xp.Init(TopoDS::Wire(openWire1.getShape())); + TopoDS_Vertex v1 = xp.CurrentVertex(); + for (; xp.More(); xp.Next()) {}; + TopoDS_Vertex v2 = xp.CurrentVertex(); + + // find open vertices + xp.Init(TopoDS::Wire(openWire2.getShape())); + TopoDS_Vertex v3 = xp.CurrentVertex(); + for (; xp.More(); xp.Next()) {}; + TopoDS_Vertex v4 = xp.CurrentVertex(); + + // check + if (v1.IsNull()) { + FC_THROWM(NullShapeException, "v1 is null"); + } + if (v2.IsNull()) { + FC_THROWM(NullShapeException, "v2 is null"); + } + if (v3.IsNull()) { + FC_THROWM(NullShapeException, "v3 is null"); + } + if (v4.IsNull()) { + FC_THROWM(NullShapeException, "v4 is null"); + } + + // assemble new wire + + // we want the connection order to be + // v1 -> openWire1 -> v2 -> (new edge) -> v4 -> openWire2(rev) -> v3 -> (new edge) + // -> v1 let's check if it's the case. If not, we reverse one wire and swap its + // endpoints. + + if (fabs(gp_Vec(BRep_Tool::Pnt(v2), BRep_Tool::Pnt(v3)).Magnitude() - fabs(offset)) + <= BRep_Tool::Tolerance(v2) + BRep_Tool::Tolerance(v3)) { + openWire2._Shape.Reverse(); + std::swap(v3, v4); + v3.Reverse(); + v4.Reverse(); + } + else if ((fabs(gp_Vec(BRep_Tool::Pnt(v2), BRep_Tool::Pnt(v4)).Magnitude() + - fabs(offset)) + <= BRep_Tool::Tolerance(v2) + BRep_Tool::Tolerance(v4))) { + // orientation is as expected, nothing to do + } + else { + FC_THROWM( + Base::CADKernelError, + "makeOffset2D: fill offset: failed to establish open vertex relationship."); + } + + // now directions of open wires are aligned. Finally. make new wire! + BRepBuilderAPI_MakeWire mkWire; + // add openWire1 + BRepTools_WireExplorer it; + for (it.Init(TopoDS::Wire(openWire1.getShape())); it.More(); it.Next()) { + mkWire.Add(it.Current()); + } + // add first joining edge + mkWire.Add(BRepBuilderAPI_MakeEdge(v2, v4).Edge()); + // add openWire2, in reverse order + openWire2._Shape.Reverse(); + for (it.Init(TopoDS::Wire(openWire2.getShape())); it.More(); it.Next()) { + mkWire.Add(it.Current()); + } + // add final joining edge + mkWire.Add(BRepBuilderAPI_MakeEdge(v3, v1).Edge()); + + mkWire.Build(); + + wiresForMakingFaces.push_back( + TopoShape(Tag, Hasher).makeElementShape(mkWire, openWires, op)); + } + } + + // make faces + if (wiresForMakingFaces.size() > 0) { + TopoShape face(0, Hasher); + face.makeElementFace(wiresForMakingFaces, nullptr, nullptr, &workingPlane); + expandCompound(face, shapesToReturn); + } + } + + return makeElementCompound(shapesToReturn, op, outputPolicy); +} + TopoShape& TopoShape::makeElementThickSolid(const TopoShape& shape, const std::vector& faces, double offset, @@ -2316,16 +2852,6 @@ TopoShape& TopoShape::makeElementThickSolid(const TopoShape& shape, } remFace.Append(face.getShape()); } -#if OCC_VERSION_HEX < 0x070200 - BRepOffsetAPI_MakeThickSolid mkThick(shape.getShape(), - remFace, - offset, - tol, - BRepOffset_Mode(offsetMode), - intersection ? Standard_True : Standard_False, - selfInter ? Standard_True : Standard_False, - GeomAbs_JoinType(join)); -#else BRepOffsetAPI_MakeThickSolid mkThick; mkThick.MakeThickSolidByJoin(shape.getShape(), remFace, @@ -2335,7 +2861,6 @@ TopoShape& TopoShape::makeElementThickSolid(const TopoShape& shape, intersection ? Standard_True : Standard_False, selfInter ? Standard_True : Standard_False, GeomAbs_JoinType(join)); -#endif return makeElementShape(mkThick, shape, op); } @@ -2352,11 +2877,7 @@ TopoShape& TopoShape::makeElementWires(const std::vector& shapes, if (shapes.size() == 1) { return makeElementWires(shapes[0], op, tol, policy, output); } - return makeElementWires(TopoShape(Tag).makeElementCompound(shapes), - op, - tol, - policy, - output); + return makeElementWires(TopoShape(Tag).makeElementCompound(shapes), op, tol, policy, output); } @@ -2651,7 +3172,7 @@ bool TopoShape::_makeElementTransform(const TopoShape& shape, const Base::Matrix4D& mat, const char* op, CheckScale checkScale, - Copy copy) + CopyType copy) { if (checkScale == CheckScale::checkScale) { auto scaleType = mat.hasScale(); @@ -2667,18 +3188,17 @@ bool TopoShape::_makeElementTransform(const TopoShape& shape, TopoShape& TopoShape::makeElementTransform(const TopoShape& shape, const gp_Trsf& trsf, const char* op, - Copy copy) + CopyType copy) { - if (copy == Copy::noCopy) { + if (copy == CopyType::noCopy) { // OCCT checks the ScaleFactor against gp::Resolution() which is DBL_MIN!!! - // No scaling is 1 as in 1:1 - const bool scaling = Abs(Abs(trsf.ScaleFactor()) - 1) > Precision::Confusion(); - const bool negative_scaling = - trsf.ScaleFactor() * trsf.HVectorialPart().Determinant() < 0.0; - copy = negative_scaling || scaling ? Copy::copy : Copy::noCopy; + copy = trsf.ScaleFactor() * trsf.HVectorialPart().Determinant() < 0. + || Abs(Abs(trsf.ScaleFactor()) - 1) > Precision::Confusion() + ? CopyType::copy + : CopyType::noCopy; } TopoShape tmp(shape); - if (copy == Copy::copy) { + if (copy == CopyType::copy) { if (shape.isNull()) { FC_THROWM(NullShapeException, "Null input shape"); } @@ -2713,7 +3233,7 @@ TopoShape& TopoShape::makeElementTransform(const TopoShape& shape, TopoShape& TopoShape::makeElementGTransform(const TopoShape& shape, const Base::Matrix4D& mat, const char* op, - Copy copy) + CopyType copy) { if (shape.isNull()) { FC_THROWM(NullShapeException, "Null input shape"); @@ -2736,7 +3256,7 @@ TopoShape& TopoShape::makeElementGTransform(const TopoShape& shape, // geometric transformation TopoShape tmp(shape); - BRepBuilderAPI_GTransform mkTrf(shape.getShape(), matrix, copy == Copy::copy); + BRepBuilderAPI_GTransform mkTrf(shape.getShape(), matrix, copy == CopyType::copy); tmp.setShape(mkTrf.Shape(), false); if (op || (shape.Tag && shape.Tag != Tag)) { setShape(tmp._Shape); @@ -3383,9 +3903,7 @@ TopoShape& TopoShape::makeElementGeneralFuse(const std::vector& _shap if (tol > 0.0) { mkGFA.SetFuzzyValue(tol); } -#if OCC_VERSION_HEX >= 0x070000 mkGFA.SetNonDestructive(Standard_True); -#endif mkGFA.Build(); if (!mkGFA.IsDone()) { FC_THROWM(Base::CADKernelError, "GeneralFuse failed"); diff --git a/tests/src/Mod/Part/App/TopoShapeExpansion.cpp b/tests/src/Mod/Part/App/TopoShapeExpansion.cpp index 5d765d953e..8113dc2989 100644 --- a/tests/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/tests/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -1359,7 +1359,7 @@ TEST_F(TopoShapeExpansionTest, makeElementShellFromWires) // Assert TopoShape result = topoShape1.makeElementShellFromWires(shapes); #if OCC_VERSION_HEX >= 0x070400 - EXPECT_EQ(result.getShape().NbChildren(), 20); // 6 TODO: VERSION DEPENDENT? + EXPECT_EQ(result.getShape().NbChildren(), 20); // Have a NbChildren method? #endif EXPECT_EQ(result.countSubElements("Vertex"), 8); EXPECT_EQ(result.countSubElements("Edge"), 32); @@ -2642,4 +2642,212 @@ TEST_F(TopoShapeExpansionTest, traceElement) })); } +TEST_F(TopoShapeExpansionTest, makeElementOffset) +{ + // Arrange + // Arrange + auto [cube1, cube2] = CreateTwoCubes(); + auto tr {gp_Trsf()}; + tr.SetTranslation(gp_Vec(gp_XYZ(-0.5, -0.5, 0))); + cube2.Move(TopLoc_Location(tr)); + TopoShape topoShape1 {cube1, 1L}; + TopoShape topoShape2 {cube2, 2L}; + // Act + // TopoShape topoShape3 {6L}; + TopoShape result {3L}; + // topoShape1.makeElementFuse({topoShape2,topoShape2}); // op, tolerance + result.makeElementOffset(topoShape1, 0.25, 1e-07); + auto elements = elementMap(result); + Base::BoundBox3d bb = result.getBoundBox(); + // Assert shape is correct + EXPECT_TRUE( + PartTestHelpers::boxesMatch(bb, Base::BoundBox3d(-0.25, -0.25, -0.25, 1.25, 1.25, 1.25))); + EXPECT_FLOAT_EQ(getVolume(result.getShape()), 3.1544986); + // Assert elementMap is correct + // EXPECT_EQ(elements.size(), 98); + // EXPECT_EQ(elements.count(IndexedName("Face", 1)), 1); + // EXPECT_EQ( + // elements[IndexedName("Face", 1)], + // MappedName("Face2;:G;OFS;:H1:7,F")); + EXPECT_TRUE(elementsMatch(result, + { + "Edge10;:G;OFS;:H1:7,F", + "Edge10;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge10;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge10;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge10;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge11;:G;OFS;:H1:7,F", + "Edge11;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge11;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge11;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge11;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge12;:G;OFS;:H1:7,F", + "Edge12;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge12;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge12;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge12;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge1;:G;OFS;:H1:7,F", + "Edge1;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge1;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge1;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge1;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge1;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge1;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge1;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge1;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge2;:G;OFS;:H1:7,F", + "Edge2;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge2;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge2;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge2;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge2;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge2;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge2;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge3;:G;OFS;:H1:7,F", + "Edge3;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge3;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge3;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge3;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge3;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge3;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge3;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge4;:G;OFS;:H1:7,F", + "Edge4;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge4;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge4;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge4;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge4;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge4;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge5;:G;OFS;:H1:7,F", + "Edge5;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge5;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge5;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge5;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge5;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge5;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge5;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge5;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge6;:G;OFS;:H1:7,F", + "Edge6;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge6;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge6;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge6;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge6;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge6;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge6;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge7;:G;OFS;:H1:7,F", + "Edge7;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge7;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge7;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge7;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge7;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge7;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge7;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge8;:G;OFS;:H1:7,F", + "Edge8;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge8;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U2;OFS;:H1:8,V", + "Edge8;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E;:U;OFS;:H1:7,V", + "Edge8;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge8;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge8;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Edge9;:G;OFS;:H1:7,F", + "Edge9;:G;OFS;:H1:7,F;:U2;OFS;:H1:8,E", + "Edge9;:G;OFS;:H1:7,F;:U3;OFS;:H1:8,E", + "Edge9;:G;OFS;:H1:7,F;:U4;OFS;:H1:8,E", + "Edge9;:G;OFS;:H1:7,F;:U;OFS;:H1:7,E", + "Face1;:G;OFS;:H1:7,F", + "Face2;:G;OFS;:H1:7,F", + "Face3;:G;OFS;:H1:7,F", + "Face4;:G;OFS;:H1:7,F", + "Face5;:G;OFS;:H1:7,F", + "Face6;:G;OFS;:H1:7,F", + "Vertex1;:G;OFS;:H1:7,F", + "Vertex2;:G;OFS;:H1:7,F", + "Vertex3;:G;OFS;:H1:7,F", + "Vertex4;:G;OFS;:H1:7,F", + "Vertex5;:G;OFS;:H1:7,F", + "Vertex6;:G;OFS;:H1:7,F", + "Vertex7;:G;OFS;:H1:7,F", + "Vertex8;:G;OFS;:H1:7,F", + })); +} + +TEST_F(TopoShapeExpansionTest, makeElementOffsetFace) +{ + // Arrange + const float Len = 3, Wid = 2, Rad = 1; + auto [face1, wire1, wire2] = CreateFaceWithRoundHole(Len, Wid, Rad); + // Act + TopoShape result {face1, 1L}; + result.makeElementOffsetFace(result, 0.25, 0); + auto elements = elementMap(result); + Base::BoundBox3d bb = result.getBoundBox(); + // Assert shape is correct + EXPECT_TRUE( + PartTestHelpers::boxesMatch(bb, Base::BoundBox3d(-0.25, -0.25, 0.0, 3.25, 2.25, 0.0))); + // Assert elementMap is correct + // EXPECT_EQ(elements.size(), 19); + // EXPECT_EQ(elements.count(IndexedName("Face", 1)), 1); + // EXPECT_EQ( + // elements[IndexedName("Face", 1)], + // MappedName("Edge1;:G;OFF;:H1:7,E;FAC;:H1:4,F")); + EXPECT_TRUE(elementsMatch(result, + { + "Edge1;:G;OFF;:H1:7,E", + "Edge1;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V", + "Edge1;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V", + "Edge1;:G;OFF;:H1:7,E;FAC;:H1:4,F", + "Edge2;:G;OFF;:H1:7,E", + "Edge2;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V", + "Edge2;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V", + "Edge3;:G;OFF;:H1:7,E", + "Edge3;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V", + "Edge3;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V", + "Edge4;:G;OFF;:H1:7,E", + "Edge4;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V", + "Edge4;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V", + "Edge5;:H1,E", + "Vertex1;:G;OFF;:H1:7,E", + "Vertex2;:G;OFF;:H1:7,E", + "Vertex3;:G;OFF;:H1:7,E", + "Vertex4;:G;OFF;:H1:7,E", + "Vertex5;:H1,V", + })); +} + +TEST_F(TopoShapeExpansionTest, makeElementOffset2D) +{ + // Arrange + const float Len = 3, Wid = 2, Rad = 1; + auto [face1, wire1, wire2] = CreateFaceWithRoundHole(Len, Wid, Rad); + // Act + TopoShape result {wire1, 1L}; + result.makeElementOffset2D(result, 0.25); + auto elements = elementMap(result); + Base::BoundBox3d bb = result.getBoundBox(); + // Assert shape is correct + EXPECT_TRUE( + PartTestHelpers::boxesMatch(bb, Base::BoundBox3d(-0.25, -0.25, 0.0, 3.25, 2.25, 0.0))); + // Assert elementMap is correct + // EXPECT_EQ(elements.size(), 10); + // EXPECT_EQ(elements.count(IndexedName("Edge", 1)), 1); + // EXPECT_EQ( + // elements[IndexedName("Edge", 1)], + // MappedName("Edge1;:G;OFF;:H1:7,E;OFF;:H1:4,E")); + EXPECT_TRUE(elementsMatch(result, + + { + "Edge1;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V;OFF;:H1:4,V", + "Edge1;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V;OFF;:H1:4,V", + "Edge1;:G;OFF;:H1:7,E;OFF;:H1:4,E", + "Edge2;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V;OFF;:H1:4,V", + "Edge2;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V;OFF;:H1:4,V", + "Edge2;:G;OFF;:H1:7,E;OFF;:H1:4,E", + "Vertex1;:G;OFF;:H1:7,E;:U2;OFF;:H1:8,V;OFF;:H1:4,V", + "Vertex1;:G;OFF;:H1:7,E;OFF;:H1:4,E", + "Vertex3;:G;OFF;:H1:7,E;:U;OFF;:H1:7,V;OFF;:H1:4,V", + "Vertex3;:G;OFF;:H1:7,E;OFF;:H1:4,E", + })); +} + // NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers)