diff --git a/src/Mod/Part/App/TopoShape.h b/src/Mod/Part/App/TopoShape.h index a705b15742..b704c5edfe 100644 --- a/src/Mod/Part/App/TopoShape.h +++ b/src/Mod/Part/App/TopoShape.h @@ -882,6 +882,137 @@ 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 &makEOffset(const TopoShape &source, double offset, double tol, + bool intersection = false, bool selfInter = false, short offsetMode = 0, + JoinType join = JoinType::Arc, bool fill = false, 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 makEOffset(double offset, double tol, bool intersection = false, bool selfInter = false, + short offsetMode=0, JoinType join=JoinType::Arc, bool fill=false, const char *op=nullptr) const { + return TopoShape(0,Hasher).makEOffset(*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 &makEOffset2D(const TopoShape &source, double offset, JoinType join=JoinType::Arc, bool fill=false, + bool allowOpenResult=false, 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 makEOffset2D(double offset, JoinType join=JoinType::Arc, bool fill=false, bool allowOpenResult=false, + bool intersection=false, const char *op=nullptr) const { + return TopoShape(0,Hasher).makEOffset2D(*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 &makEOffsetFace(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 makEOffsetFace(double offset, + double innerOffset, + JoinType join = JoinType::Arc, + JoinType innerJoin = JoinType::Arc, + const char *op = nullptr) const + { + return TopoShape(0,Hasher).makEOffsetFace(*this,offset,innerOffset,join,innerJoin,op); + } + /** Make revolved shell around a basis shape diff --git a/src/Mod/Part/App/TopoShapeExpansion.cpp b/src/Mod/Part/App/TopoShapeExpansion.cpp index fe8c7a52aa..220363d9ea 100644 --- a/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -2271,6 +2271,439 @@ TopoShape& TopoShape::makeElementPipeShell(const std::vector& shapes, return makeElementShape(mkPipeShell, shapes, op); } +TopoShape &TopoShape::makEOffset(const TopoShape &shape, + double offset, double tol, bool intersection, bool selfInter, + short offsetMode, JoinType join, bool fill, const char *op) +{ + if(!op) op = Part::OpCodes::Offset; + +#if OCC_VERSION_HEX < 0x070200 + BRepOffsetAPI_MakeOffsetShape mkOffset(shape.getShape(), offset, tol, BRepOffset_Mode(offsetMode), + intersection ? Standard_True : Standard_False, + selfInter ? Standard_True : Standard_False, + GeomAbs_JoinType(join)); +#else + 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)); +#endif + + if (!mkOffset.IsDone()) + FC_THROWM(Base::CADKernelError,"BRepOffsetAPI_MakeOffsetShape not done"); + + TopoShape res(Tag,Hasher); + res.makEShape(mkOffset,shape,op); + if(shape.hasSubShape(TopAbs_SOLID) && !res.hasSubShape(TopAbs_SOLID)) { + try { + res = res.makESolid(); + }catch (Standard_Failure &e) { + FC_WARN("failed to make solid: " << e.GetMessageString()); + } + } + if (!fill) { + *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).makEShape(aGenerator,wires)); + } + + TopoShape perimeterCompound(Tag,Hasher); + perimeterCompound.makECompound(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).makESHAPE(outputShape,MapperSewing(sewTool),shapes,op); + return *this; +} + +TopoShape &TopoShape::makEOffsetFace(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(makEOffset2D(face, offset, joinType, false, false, false, op)); + continue; + } + if (outerWire.isNull()) + FC_THROWM(Base::CADKernelError, "makeOffsetFace: missing outer wire!"); + + if (std::abs(offset) > Precision::Confusion()) + outerWire = outerWire.makEOffset2D(offset, joinType, false, false, false, op); + + if (std::abs(innerOffset) > Precision::Confusion()) { + TopoShape innerWires(0, Hasher); + innerWires.makECompound(wires, "", false); + innerWires = innerWires.makEOffset2D(innerOffset, innerJoinType, false, false, true, op); + wires = innerWires.getSubTopoShapes(TopAbs_WIRE); + } + wires.push_back(outerWire); + gp_Pln pln; + res.push_back(TopoShape(0, Hasher).makEFace(wires, + nullptr, + nullptr, + face.findPlane(pln) ? &pln : nullptr)); + } + return makECompound(res, "", false); +} + +TopoShape &TopoShape::makEOffset2D(const TopoShape &shape, double offset, JoinType joinType, + bool fill, bool 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 && 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; + bool forceOutputCompound = false; + + if (shape.getShape().ShapeType() == TopAbs_COMPOUND){ + if (!intersection){ + //simply recursively process the children, independently + expandCompound(shape,shapesToProcess); + forceOutputCompound = true; + } 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).makEOffset2D( + s, offset, joinType, fill, allowOpenResult, intersection, op)); + forceOutputCompound = true; + } 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.makEWires()); + 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 = false; + + //find plane. + gp_Pln workingPlane; + if (!TopoShape().makECompound(sourceWires,"",false).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); + 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.makEShape(mkOffset,op).makECopy(); + + } else { + offsetShape = TopoShape(Tag,Hasher).makECompound(sourceWires,0,false); + } + + 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){ + 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 || 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).makEShape(mkWire,openWires,op)); + } + } + + //make faces + if (wiresForMakingFaces.size()>0) { + TopoShape face(0, Hasher); + face.makEFace(wiresForMakingFaces, nullptr, nullptr, &workingPlane); + expandCompound(face, shapesToReturn); + } + } + + return makECompound(shapesToReturn,op,forceOutputCompound); +} + TopoShape& TopoShape::makeElementThickSolid(const TopoShape& shape, const std::vector& faces, double offset,