/*************************************************************************** * Copyright (c) 2022 Uwe Stöhr * * * * 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" #ifndef _PreComp_ # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include #endif #include #include #include "ExtrusionHelper.h" #include "BRepOffsetAPI_MakeOffsetFix.h" using namespace Part; ExtrusionHelper::ExtrusionHelper() = default; void ExtrusionHelper::makeDraft(const TopoDS_Shape& shape, const gp_Dir& direction, const double LengthFwd, const double LengthRev, const double AngleFwd, const double AngleRev, bool isSolid, std::list& drafts, bool isPartDesign) { std::vector> wiresections; auto addWiresToWireSections = [&shape](std::vector>& wiresections) -> size_t { TopExp_Explorer ex; size_t i = 0; for (ex.Init(shape, TopAbs_WIRE); ex.More(); ex.Next(), ++i) { wiresections.emplace_back(); wiresections[i].push_back(TopoDS::Wire(ex.Current())); } return i; }; double distanceFwd = tan(AngleFwd) * LengthFwd; double distanceRev = tan(AngleRev) * LengthRev; gp_Vec vecFwd = gp_Vec(direction) * LengthFwd; gp_Vec vecRev = gp_Vec(direction.Reversed()) * LengthRev; bool bFwd = fabs(LengthFwd) > Precision::Confusion(); bool bRev = fabs(LengthRev) > Precision::Confusion(); // only if there is a 2nd direction and the negated angle is equal to the first one // we can omit the source shape as loft section bool bMid = !bFwd || !bRev || -1.0 * AngleFwd != AngleRev; if (shape.IsNull()) Standard_Failure::Raise("Not a valid shape"); // store all wires of the shape into an array size_t numWires = addWiresToWireSections(wiresections); if (numWires == 0) Standard_Failure::Raise("Extrusion: Input must not only consist if a vertex"); // to store the sections for the loft std::list list_of_sections; // we need for all found wires an offset copy of them // we store them in an array TopoDS_Wire offsetWire; std::vector> extrusionSections(wiresections.size(), std::vector()); size_t rows = 0; int numEdges = 0; // We need to find out what are outer wires and what are inner ones // methods like checking the center of mass etc. don't help us here. // As solution we build a prism with every wire, then subtract every prism from each other. // If the moment of inertia changes by a subtraction, we have an inner wire prism. // // first build the prisms std::vector resultPrisms; TopoDS_Shape singlePrism; for (auto& wireVector : wiresections) { for (auto& singleWire : wireVector) { BRepBuilderAPI_MakeFace mkFace(TopoDS::Wire(singleWire)); auto tempFace = mkFace.Shape(); BRepPrimAPI_MakePrism mkPrism(tempFace, vecFwd); if (!mkPrism.IsDone()) Standard_Failure::Raise("Extrusion: Generating prism failed"); singlePrism = mkPrism.Shape(); resultPrisms.push_back(singlePrism); } } // create an array with false to store later which wires are inner ones std::vector isInnerWire(resultPrisms.size(), false); std::vector checklist(resultPrisms.size(), true); // finally check reecursively for inner wires checkInnerWires(isInnerWire, direction, checklist, false, resultPrisms); // count the number of inner wires int numInnerWires = 0; for (auto isInner : isInnerWire) { if (isInner) ++numInnerWires; } // at first create offset wires for the reversed part of extrusion // it is important that these wires are the first loft section if (bRev) { // create an offset for all source wires rows = 0; for (auto& wireVector : wiresections) { for (auto& singleWire : wireVector) { // count number of edges numEdges = 0; TopExp_Explorer xp(singleWire, TopAbs_EDGE); while (xp.More()) { numEdges++; xp.Next(); } // create an offset copy of the wire if (!isInnerWire[rows]) { // this is an outer wire createTaperedPrismOffset(TopoDS::Wire(singleWire), vecRev, distanceRev, true, offsetWire); } else { // there is an OCC bug with single-edge wires (circles), see inside createTaperedPrismOffset if (numEdges > 1 || !isPartDesign) // inner wires must get the negated offset createTaperedPrismOffset(TopoDS::Wire(singleWire), vecRev, -distanceRev, true, offsetWire); else // circles in PartDesign must not get the negated offset createTaperedPrismOffset(TopoDS::Wire(singleWire), vecRev, distanceRev, true, offsetWire); } if (offsetWire.IsNull()) return; extrusionSections[rows].push_back(offsetWire); } ++rows; } } // add the source wire as middle section // it is important to add them after the reversed part if (bMid) { // transfer all source wires as they are to the array from which we build the shells rows = 0; for (auto& wireVector : wiresections) { for (auto& singleWire : wireVector) { extrusionSections[rows].push_back(singleWire); } rows++; } } // finally add the forward extrusion offset wires // these wires must be the last loft section if (bFwd) { rows = 0; for (auto& wireVector : wiresections) { for (auto& singleWire : wireVector) { // count number of edges numEdges = 0; TopExp_Explorer xp(singleWire, TopAbs_EDGE); while (xp.More()) { numEdges++; xp.Next(); } // create an offset copy of the wire if (!isInnerWire[rows]) { // this is an outer wire createTaperedPrismOffset(TopoDS::Wire(singleWire), vecFwd, distanceFwd, false, offsetWire); } else { // there is an OCC bug with single-edge wires (circles), see inside createTaperedPrismOffset if (numEdges > 1 || !isPartDesign) // inner wires must get the negated offset createTaperedPrismOffset(TopoDS::Wire(singleWire), vecFwd, -distanceFwd, false, offsetWire); else // circles in PartDesign must not get the negated offset createTaperedPrismOffset(TopoDS::Wire(singleWire), vecFwd, distanceFwd, false, offsetWire); } if (offsetWire.IsNull()) return; extrusionSections[rows].push_back(offsetWire); } ++rows; } } try { // build all shells std::vector shells; for (auto& wires : extrusionSections) { BRepOffsetAPI_ThruSections mkTS(isSolid, /*ruled=*/Standard_True, Precision::Confusion()); for (auto& singleWire : wires) { if (singleWire.ShapeType() == TopAbs_VERTEX) mkTS.AddVertex(TopoDS::Vertex(singleWire)); else mkTS.AddWire(TopoDS::Wire(singleWire)); } mkTS.Build(); if (!mkTS.IsDone()) Standard_Failure::Raise("Extrusion: Loft could not be built"); shells.push_back(mkTS.Shape()); } if (isSolid) { // we only need to cut if we have inner wires if (numInnerWires > 0) { // we take every outer wire prism and cut subsequently all inner wires prisms from it // every resulting shape is the final drafted extrusion shape GProp_GProps tempProperties; Standard_Real momentOfInertiaInitial; Standard_Real momentOfInertiaFinal; std::vector::iterator isInnerWireIterator = isInnerWire.begin(); std::vector::iterator isInnerWireIteratorLoop; for (auto itOuter = shells.begin(); itOuter != shells.end(); ++itOuter) { if (*isInnerWireIterator) { ++isInnerWireIterator; continue; } isInnerWireIteratorLoop = isInnerWire.begin(); for (auto itInner = shells.begin(); itInner != shells.end(); ++itInner) { if (itOuter == itInner || !*isInnerWireIteratorLoop) { ++isInnerWireIteratorLoop; continue; } // get MomentOfInertia of first shape BRepGProp::VolumeProperties(*itOuter, tempProperties); momentOfInertiaInitial = tempProperties.MomentOfInertia(gp_Ax1(gp_Pnt(), direction)); BRepAlgoAPI_Cut mkCut(*itOuter, *itInner); if (!mkCut.IsDone()) Standard_Failure::Raise("Extrusion: Final cut out failed"); BRepGProp::VolumeProperties(mkCut.Shape(), tempProperties); momentOfInertiaFinal = tempProperties.MomentOfInertia(gp_Ax1(gp_Pnt(), direction)); // if the whole shape was cut away the resulting shape is not Null but its MomentOfInertia is 0.0 // therefore we have a valid cut if the MomentOfInertia is not zero and changed if ((momentOfInertiaInitial != momentOfInertiaFinal) && (momentOfInertiaFinal > Precision::Confusion())) { // immediately update the outer shape since more inner wire prism might cut it *itOuter = mkCut.Shape(); } ++isInnerWireIteratorLoop; } drafts.push_back(*itOuter); ++isInnerWireIterator; } } else // we already have the results for (const auto & shell : shells) drafts.push_back(shell); } else { // no solid BRepBuilderAPI_Sewing sewer; sewer.SetTolerance(Precision::Confusion()); for (TopoDS_Shape& s : shells) sewer.Add(s); sewer.Perform(); drafts.push_back(sewer.SewedShape()); } } catch (Standard_Failure& e) { throw Base::RuntimeError(e.GetMessageString()); } catch (const Base::Exception& e) { throw Base::RuntimeError(e.what()); } catch (...) { throw Base::CADKernelError("Extrusion: A fatal error occurred when making the loft"); } } void ExtrusionHelper::checkInnerWires(std::vector& isInnerWire, const gp_Dir direction, std::vector&checklist, bool forInner, std::vector prisms) { // store the number of wires to be checked size_t numCheckWiresInitial = 0; for (auto checks : checklist) { if (checks) ++numCheckWiresInitial; } GProp_GProps tempProperties; Standard_Real momentOfInertiaInitial; Standard_Real momentOfInertiaFinal; size_t numCheckWires = 0; std::vector::iterator isInnerWireIterator = isInnerWire.begin(); std::vector::iterator toCheckIterator = checklist.begin(); // create an array with false used later to store what can be cancelled from the checklist std::vector toDisable(checklist.size(), false); int outer = -1; // we cut every prism to be checked from the other to be checked ones // if nothing happens, a prism can be cancelled from the checklist for (auto itOuter = prisms.begin(); itOuter != prisms.end(); ++itOuter) { ++outer; if (!*toCheckIterator) { ++isInnerWireIterator; ++toCheckIterator; continue; } auto toCheckIteratorInner = checklist.begin(); bool saveIsInnerWireIterator = *isInnerWireIterator; for (auto itInner = prisms.begin(); itInner != prisms.end(); ++itInner) { if (itOuter == itInner || !*toCheckIteratorInner) { ++toCheckIteratorInner; continue; } // get MomentOfInertia of first shape BRepGProp::VolumeProperties(*itInner, tempProperties); momentOfInertiaInitial = tempProperties.MomentOfInertia(gp_Ax1(gp_Pnt(), direction)); BRepAlgoAPI_Cut mkCut(*itInner, *itOuter); if (!mkCut.IsDone()) Standard_Failure::Raise("Extrusion: Cut out failed"); BRepGProp::VolumeProperties(mkCut.Shape(), tempProperties); momentOfInertiaFinal = tempProperties.MomentOfInertia(gp_Ax1(gp_Pnt(), direction)); // if the whole shape was cut away the resulting shape is not Null but its MomentOfInertia is 0.0 // therefore we have an inner wire if the MomentOfInertia is not zero and changed if ((momentOfInertiaInitial != momentOfInertiaFinal) && (momentOfInertiaFinal > Precision::Confusion())) { *isInnerWireIterator = !forInner; ++numCheckWires; *toCheckIterator = true; break; } ++toCheckIteratorInner; } if (saveIsInnerWireIterator == *isInnerWireIterator) // nothing was changed and we can remove it from the list to be checked // but we cannot do this before the for loop was fully run toDisable[outer] = true; ++isInnerWireIterator; ++toCheckIterator; } // cancel prisms from the checklist whose wire state did not change size_t i = 0; for (auto disable : toDisable) { if (disable) checklist[i] = false; ++i; } // if all wires are inner ones, we take the first one as outer and issue a warning if (numCheckWires == isInnerWire.size()) { isInnerWire[0] = false; checklist[0] = false; --numCheckWires; Base::Console().Warning("Extrusion: could not determine what structure is the outer one.\n\ The first input one will now be taken as outer one.\n"); } // There can be cases with several wires all intersecting each other. // Then it is impossible to find out what wire is an inner one // and we can only treat all wires in the checklist as outer ones. if (numCheckWiresInitial == numCheckWires) { i = 0; for (auto checks : checklist) { if (checks) { isInnerWire[i] = false; checklist[i] = false; --numCheckWires; } ++i; } Base::Console().Warning("Extrusion: too many self-intersection structures!\n\ Impossible to determine what structure is an inner one.\n\ All undeterminable structures will therefore be taken as outer ones.\n"); } // recursively call the function until all wires are checked if (numCheckWires > 1) checkInnerWires(isInnerWire, direction, checklist, !forInner, prisms); } void ExtrusionHelper::createTaperedPrismOffset(TopoDS_Wire sourceWire, const gp_Vec& translation, double offset, bool isSecond, TopoDS_Wire& result) { // if the wire consists of a single edge which has applied a placement // then this placement must be reset because otherwise // BRepOffsetAPI_MakeOffset shows weird behaviour by applying the placement, see // https://dev.opencascade.org/content/brepoffsetapimakeoffset-wire-and-face-odd-occt-740 // therefore we use here the workaround of BRepOffsetAPI_MakeOffsetFix and not BRepOffsetAPI_MakeOffset gp_Trsf tempTransform; tempTransform.SetTranslation(translation); TopLoc_Location loc(tempTransform); TopoDS_Wire movedSourceWire = TopoDS::Wire(sourceWire.Moved(loc)); TopoDS_Shape offsetShape; if (fabs(offset) > Precision::Confusion()) { TopLoc_Location edgeLocation; // create the offset shape BRepOffsetAPI_MakeOffsetFix mkOffset; mkOffset.Init(GeomAbs_Arc); mkOffset.Init(GeomAbs_Intersection); mkOffset.AddWire(movedSourceWire); try { mkOffset.Perform(offset); offsetShape = mkOffset.Shape(); } catch (const Base::Exception& e) { throw Base::RuntimeError(e.what()); } if (!mkOffset.IsDone()) { Standard_Failure::Raise("Extrusion: Offset could not be created"); } } else { offsetShape = movedSourceWire; } if (offsetShape.IsNull()) { if (isSecond) Base::Console().Error("Extrusion: end face of tapered against extrusion is empty\n" \ "This means most probably that the against taper angle is too large or small.\n"); else Base::Console().Error("Extrusion: end face of tapered along extrusion is empty\n" \ "This means most probably that the along taper angle is too large or small.\n"); Standard_Failure::Raise("Extrusion: end face of tapered extrusion is empty"); } // assure we return a wire and no edge TopAbs_ShapeEnum type = offsetShape.ShapeType(); if (type == TopAbs_WIRE) { result = TopoDS::Wire(offsetShape); } else if (type == TopAbs_EDGE) { BRepBuilderAPI_MakeWire mkWire2(TopoDS::Edge(offsetShape)); result = mkWire2.Wire(); } else { // this happens usually if type == TopAbs_COMPOUND and means the angle is too small // since this is a common mistake users will quickly do, issue a warning dialog // FIXME: Standard_Failure::Raise or App::DocumentObjectExecReturn don't output the message to the user result = TopoDS_Wire(); if (isSecond) Base::Console().Error("Extrusion: type of against extrusion end face is not supported.\n" \ "This means most probably that the against taper angle is too large or small.\n"); else Base::Console().Error("Extrusion: type of along extrusion is not supported.\n" \ "This means most probably that the along taper angle is too large or small.\n"); } } static TopoShape makEDraftUsingPipe(const std::vector &_wires, App::StringHasherRef hasher) { std::vector shells; std::vector frontwires, backwires; if (_wires.size() < 2) throw Base::CADKernelError("Not enough wire section"); std::vector wires; wires.reserve(_wires.size()); for (auto &wire : _wires) { // Make a copy to work around OCCT bug on offset circular shapes wires.push_back(wire.makECopy()); } GeomLineSegment line; Base::Vector3d pstart, pend; wires.front().getCenterOfGravity(pstart); gp_Pln pln; if (wires.back().findPlane(pln)) { auto dir = pln.Position().Direction(); auto base = pln.Location(); pend = pstart; pend.ProjectToPlane(Base::Vector3d(base.X(), base.Y(), base.Z()), Base::Vector3d(dir.X(), dir.Y(), dir.Z())); } else wires.back().getCenterOfGravity(pend); line.setPoints(pstart, pend); BRepBuilderAPI_MakeWire mkWire(TopoDS::Edge(line.toShape())); BRepOffsetAPI_MakePipeShell mkPS(mkWire.Wire()); mkPS.SetTolerance(Precision::Confusion()); mkPS.SetTransitionMode(BRepBuilderAPI_Transformed); mkPS.SetMode(false); for (auto &wire : wires) mkPS.Add(TopoDS::Wire(wire.getShape())); if (!mkPS.IsReady()) throw Base::CADKernelError("Shape could not be built"); TopoShape result(0,hasher); result.makEShape(mkPS,wires); if (!mkPS.Shape().Closed()) { // shell is not closed - use simulate to get the end wires TopTools_ListOfShape sim; mkPS.Simulate(2, sim); TopoShape front(sim.First()); if(front.countSubShapes(TopAbs_EDGE)==wires.front().countSubShapes(TopAbs_EDGE)) { front = wires.front(); front.setShape(sim.First(),false); }else front.Tag = -wires.front().Tag; TopoShape back(sim.Last()); if(back.countSubShapes(TopAbs_EDGE)==wires.back().countSubShapes(TopAbs_EDGE)) { back = wires.back(); back.setShape(sim.Last(),false); }else back.Tag = -wires.back().Tag; // build the end faces, sew the shell and build the final solid front = front.makEFace(); back = back.makEFace(); BRepBuilderAPI_Sewing sewer; sewer.SetTolerance(Precision::Confusion()); sewer.Add(front.getShape()); sewer.Add(back.getShape()); sewer.Add(result.getShape()); sewer.Perform(); result = result.makEShape(sewer); } result = result.makESolid(); BRepClass3d_SolidClassifier SC(result.getShape()); SC.PerformInfinitePoint(Precision::Confusion()); if (SC.State() == TopAbs_IN) { result.setShape(result.getShape().Reversed(),false); } return result; } void ExtrusionHelper::makEDraft(const ExtrusionParameters& params, const TopoShape& _shape, std::vector& drafts, App::StringHasherRef hasher) { double distanceFwd = tan(params.taperAngleFwd)*params.lengthFwd; double distanceRev = tan(params.taperAngleRev)*params.lengthRev; gp_Vec vecFwd = gp_Vec(params.dir)*params.lengthFwd; gp_Vec vecRev = gp_Vec(params.dir.Reversed())*params.lengthRev; bool bFwd = fabs(params.lengthFwd) > Precision::Confusion(); bool bRev = fabs(params.lengthRev) > Precision::Confusion(); bool bMid = !bFwd || !bRev || params.lengthFwd*params.lengthRev > 0.0; //include the source shape as loft section? TopoShape shape = _shape; TopoShape sourceWire; if (shape.isNull()) Standard_Failure::Raise("Not a valid shape"); if (params.solid && !shape.hasSubShape(TopAbs_FACE)) shape = shape.makEFace(nullptr, params.faceMakerClass.c_str()); if (shape.shapeType() == TopAbs_FACE) { std::vector wires; TopoShape outerWire = shape.splitWires(&wires, TopoShape::ReorientForward); if (outerWire.isNull()) Standard_Failure::Raise("Missing outer wire"); if (wires.empty()) shape = outerWire; else { unsigned pos = drafts.size(); makEDraft(params, outerWire, drafts, hasher); if (drafts.size() != pos+1) Standard_Failure::Raise("Failed to make drafted extrusion"); std::vector inner; TopoShape innerWires(0, hasher); innerWires.makECompound(wires,"",false); ExtrusionParameters copy = params; copy.taperAngleFwd = params.innerTaperAngleFwd; copy.taperAngleRev = params.innerTaperAngleRev; makEDraft(copy, innerWires, inner, hasher); if (inner.empty()) Standard_Failure::Raise("Failed to make drafted extrusion with inner hole"); inner.insert(inner.begin(), drafts.back()); drafts.back().makECut(inner); return; } } if (shape.shapeType() == TopAbs_WIRE) { ShapeFix_Wire aFix; aFix.Load(TopoDS::Wire(shape.getShape())); aFix.FixReorder(); aFix.FixConnected(); aFix.FixClosed(); sourceWire.setShape(aFix.Wire()); sourceWire.Tag = shape.Tag; sourceWire.mapSubElement(shape); } else if (shape.shapeType() == TopAbs_COMPOUND) { for(auto &s : shape.getSubTopoShapes()) makEDraft(params, s, drafts, hasher); } else { Standard_Failure::Raise("Only a wire or a face is supported"); } if (!sourceWire.isNull()) { std::vector list_of_sections; auto makeOffset = [&sourceWire](const gp_Vec& translation, double offset) -> TopoShape { gp_Trsf mat; mat.SetTranslation(translation); TopoShape offsetShape(sourceWire.makETransform(mat,"RV")); if (fabs(offset)>Precision::Confusion()) offsetShape = offsetShape.makEOffset2D(offset, TopoShape::JoinType::Intersection); return offsetShape; }; //first. add wire for reversed part of extrusion if (bRev){ auto offsetShape = makeOffset(vecRev, distanceRev); if (offsetShape.isNull()) Standard_Failure::Raise("Tapered shape is empty"); TopAbs_ShapeEnum type = offsetShape.getShape().ShapeType(); if (type == TopAbs_WIRE) { list_of_sections.push_back(offsetShape); } else if (type == TopAbs_EDGE) { list_of_sections.push_back(offsetShape.makEWires()); } else { Standard_Failure::Raise("Tapered shape type is not supported"); } } //next. Add source wire as middle section. Order is important. if (bMid){ list_of_sections.push_back(sourceWire); } //finally. Forward extrusion offset wire. if (bFwd){ auto offsetShape = makeOffset(vecFwd, distanceFwd); if (offsetShape.isNull()) Standard_Failure::Raise("Tapered shape is empty"); TopAbs_ShapeEnum type = offsetShape.getShape().ShapeType(); if (type == TopAbs_WIRE) { list_of_sections.push_back(offsetShape); } else if (type == TopAbs_EDGE) { list_of_sections.push_back(offsetShape.makEWires()); } else { Standard_Failure::Raise("Tapered shape type is not supported"); } } try { #if defined(__GNUC__) && defined (FC_OS_LINUX) Base::SignalException se; #endif if (params.usepipe) { drafts.push_back(makEDraftUsingPipe(list_of_sections, hasher)); return; } //make loft BRepOffsetAPI_ThruSections mkGenerator( params.solid ? Standard_True : Standard_False, /*ruled=*/Standard_True); for(auto &s : list_of_sections) mkGenerator.AddWire(TopoDS::Wire(s.getShape())); mkGenerator.Build(); drafts.push_back(TopoShape(0,hasher).makEShape(mkGenerator,list_of_sections)); } catch (Standard_Failure &){ throw; } catch (...) { throw Base::CADKernelError("Unknown exception from BRepOffsetAPI_ThruSections"); } } }