/*************************************************************************** * Copyright (c) 2015 Stefan Tröger * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "FeaturePipe.h" #include "Mod/Part/App/TopoShapeOpCode.h" #include "Mod/Part/App/TopoShapeMapper.h" #include "FeatureLoft.h" FC_LOG_LEVEL_INIT("PartDesign", true, true); using namespace PartDesign; const char* Pipe::TypeEnums[] = {"FullPath", "UpToFace", nullptr}; const char* Pipe::TransitionEnums[] = {"Transformed", "Right corner", "Round corner", nullptr}; const char* Pipe::ModeEnums[] = {"Standard", "Fixed", "Frenet", "Auxiliary", "Binormal", nullptr}; const char* Pipe::TransformEnums[] = {"Constant", "Multisection", "Linear", "S-shape", "Interpolation", nullptr}; PROPERTY_SOURCE(PartDesign::Pipe, PartDesign::ProfileBased) Pipe::Pipe() { ADD_PROPERTY_TYPE(Sections, (nullptr), "Sweep", App::Prop_None, "List of sections"); Sections.setValue(nullptr); ADD_PROPERTY_TYPE(Spine, (nullptr), "Sweep", App::Prop_None, "Path to sweep along"); ADD_PROPERTY_TYPE(SpineTangent, (false), "Sweep", App::Prop_None, "Include tangent edges into path"); ADD_PROPERTY_TYPE(AuxiliarySpine, (nullptr), "Sweep", App::Prop_None, "Secondary path to orient sweep"); ADD_PROPERTY_TYPE( AuxiliarySpineTangent, (false), "Sweep", App::Prop_None, "Include tangent edges into secondary path" ); ADD_PROPERTY_TYPE( AuxiliaryCurvilinear, (true), "Sweep", App::Prop_None, "Calculate normal between equidistant points on both spines" ); ADD_PROPERTY_TYPE(Mode, (long(0)), "Sweep", App::Prop_None, "Profile mode"); ADD_PROPERTY_TYPE( Binormal, (Base::Vector3d()), "Sweep", App::Prop_None, "Binormal vector for corresponding orientation mode" ); ADD_PROPERTY_TYPE(Transition, (long(0)), "Sweep", App::Prop_None, "Transition mode"); ADD_PROPERTY_TYPE(Transformation, (long(0)), "Sweep", App::Prop_None, "Section transformation mode"); Mode.setEnums(ModeEnums); Transition.setEnums(TransitionEnums); Transformation.setEnums(TransformEnums); } short Pipe::mustExecute() const { if (Sections.isTouched()) { return 1; } if (Spine.isTouched()) { return 1; } if (Mode.isTouched()) { return 1; } if (Transition.isTouched()) { return 1; } return ProfileBased::mustExecute(); } App::DocumentObjectExecReturn* Pipe::execute() { if (onlyHaveRefined()) { return App::DocumentObject::StdReturn; } auto getSectionShape = [](App::DocumentObject* feature, const std::vector& subs) -> TopoDS_Shape { if (!feature || !feature->isDerivedFrom()) { throw Base::TypeError("Pipe: Invalid profile/section"); } auto subName = subs.empty() ? "" : subs.front(); // only take the entire shape when we have a sketch selected, but // not a point of the sketch if (feature->isDerivedFrom() && subName.compare(0, 6, "Vertex") != 0) { return static_cast(feature)->Shape.getValue(); } else { if (subName.empty()) { throw Base::ValueError("Pipe: No valid subelement linked in Part::Feature"); } return static_cast(feature)->Shape.getShape().getSubShape(subName.c_str()); } }; auto addWiresToWireSections = [](TopoDS_Shape& section, std::vector>& wiresections) -> size_t { TopExp_Explorer ex; size_t i = 0; bool initialWireSectionsEmpty = wiresections.empty(); for (ex.Init(section, TopAbs_WIRE); ex.More(); ex.Next(), ++i) { // if profile was just a point then this is where we can first set our list if (i >= wiresections.size()) { if (initialWireSectionsEmpty) { wiresections.emplace_back(1, ex.Current()); } else { throw Base::ValueError( "Pipe: Sections need to have the same amount of inner wires (except " "profile and last section, which can be points)" ); } } else { wiresections[i].push_back(TopoDS::Wire(ex.Current())); } } return i; }; // TODO: currently we can only allow planar faces, so add that check. // The reason for this is that with other faces in front, we could not use the // current simulate approach and build the start and end face from the wires. // As the shell begins always at the spine and not the profile, the sketchshape // cannot be used directly as front face. We would need a method to translate // the front shape to match the shell starting position somehow... TopoDS_Shape profilePoint; // if the Base property has a valid shape, fuse the pipe into it TopoShape base; try { base = getBaseTopoShape(); } catch (const Base::Exception&) { base = TopoShape(); } auto hasher = getDocument()->getStringHasher(); try { // setup the location this->positionByPrevious(); TopLoc_Location invObjLoc = this->getLocation().Inverted(); if (!base.isNull()) { base.move(invObjLoc); } // setup the profile section TopoDS_Shape profileShape = getSectionShape(Profile.getValue(), Profile.getSubValues()); if (profileShape.IsNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Pipe: Could not obtain profile shape") ); } // build the paths App::DocumentObject* spine = Spine.getValue(); if (!(spine && spine->isDerivedFrom())) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP("Exception", "No spine linked")); } std::vector subedge = Spine.getSubValues(); TopoDS_Shape path; const Part::TopoShape& shape = static_cast(spine)->Shape.getValue(); buildPipePath(shape, subedge, path); path.Move(invObjLoc); // auxiliary TopoDS_Shape auxpath; if (Mode.getValue() == 3) { App::DocumentObject* auxspine = AuxiliarySpine.getValue(); if (!(auxspine && auxspine->isDerivedFrom())) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "No auxiliary spine linked.") ); } std::vector auxsubedge = AuxiliarySpine.getSubValues(); const Part::TopoShape& auxshape = static_cast(auxspine)->Shape.getValue(); buildPipePath(auxshape, auxsubedge, auxpath); auxpath.Move(invObjLoc); } // build up multisections auto multisections = Sections.getSubListValues(); std::vector> wiresections; size_t numWires = addWiresToWireSections(profileShape, wiresections); if (numWires == 0) { // profileShape had no wires so only other valid option is single point section TopExp_Explorer ex; size_t i = 0; for (ex.Init(profileShape, TopAbs_VERTEX); ex.More(); ex.Next(), ++i) { profilePoint = ex.Current(); } if (i > 1) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP( "Exception", "Pipe: Only one isolated point is needed if using a sketch with isolated " "points for section" )); } } if (!profilePoint.IsNull() && (Transformation.getValue() != 1 || multisections.empty())) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP( "Exception", "Pipe: At least one section is needed when using a single point for profile" )); } // maybe we need a scaling law Handle(Law_Function) scalinglaw; bool isLastSectionVertex = false; // see if we shall use multiple sections if (Transformation.getValue() == 1) { // TODO: we need to order the sections to prevent occ from crashing, // as makepipeshell connects the sections in the order of adding for (auto& subSet : multisections) { if (!subSet.first->isDerivedFrom()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Pipe: All sections need to be Part features") ); } // if the section is an object's face then take just the face TopoDS_Shape shape = getSectionShape(subSet.first, subSet.second); if (shape.IsNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Pipe: Could not obtain section shape") ); } size_t nWiresAdded = addWiresToWireSections(shape, wiresections); if (nWiresAdded == 0) { TopExp_Explorer ex; size_t i = 0; for (ex.Init(shape, TopAbs_VERTEX); ex.More(); ex.Next(), ++i) { if (isLastSectionVertex) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP( "Exception", "Pipe: Only the profile and last section can be vertices" )); } isLastSectionVertex = true; for (auto& wires : wiresections) { wires.push_back(ex.Current()); } } } if (!isLastSectionVertex && nWiresAdded < wiresections.size()) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP( "Exception", "Multisections need to have the same amount of inner wires as the base " "section" )); } } } /*//build the law functions instead else if (Transformation.getValue() == 2) { if (ScalingData.getValues().size()<1) return new App::DocumentObjectExecReturn("No valid data given for linear scaling mode"); Handle(Law_Linear) lin = new Law_Linear(); lin->Set(0, 1, 1, ScalingData[0].x); scalinglaw = lin; } else if (Transformation.getValue() == 3) { if (ScalingData.getValues().size()<1) return new App::DocumentObjectExecReturn("No valid data given for S-shape scaling mode"); Handle(Law_S) s = new Law_S(); s->Set(0, 1, ScalingData[0].y, 1, ScalingData[0].x, ScalingData[0].z); scalinglaw = s; }*/ // Verify that path is not a null shape if (path.IsNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Path must not be a null shape") ); } // build all shells std::vector shells; TopoDS_Shape copyProfilePoint(profilePoint); if (!profilePoint.IsNull()) { copyProfilePoint.Move(invObjLoc); } std::vector frontwires, backwires; for (auto& wires : wiresections) { BRepOffsetAPI_MakePipeShell mkPS(TopoDS::Wire(path)); setupAlgorithm(mkPS, auxpath); if (!scalinglaw) { if (!profilePoint.IsNull()) { mkPS.Add(copyProfilePoint); } for (auto& wire : wires) { wire.Move(invObjLoc); mkPS.Add(wire); } } else { if (!profilePoint.IsNull()) { mkPS.SetLaw(copyProfilePoint, scalinglaw); } for (auto& wire : wires) { wire.Move(invObjLoc); mkPS.SetLaw(wire, scalinglaw); } } if (!mkPS.IsReady()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Pipe could not be built") ); } shells.emplace_back(mkPS.Shape()); if (!mkPS.Shape().Closed()) { // shell is not closed - use simulate to get the end wires TopTools_ListOfShape sim; mkPS.Simulate(2, sim); // Note that while we call them front and back, these sections // appear to correspond to the front or back of the path. When one // or both ends of the pipe are points, one or both of these wires // (and eventually faces) will be null. frontwires.push_back(TopoDS::Wire(sim.First())); backwires.push_back(TopoDS::Wire(sim.Last())); } } TopoShape result(0, hasher); if (!frontwires.empty() || !backwires.empty()) { BRepBuilderAPI_Sewing sewer; sewer.SetTolerance(Precision::Confusion()); // build the end faces, sew the shell and build the final solid if (!frontwires.empty()) { TopoDS_Shape front = Part::FaceMakerCheese::makeFace(frontwires); sewer.Add(front); shells.emplace_back(front); } if (!backwires.empty()) { TopoDS_Shape back = Part::FaceMakerCheese::makeFace(backwires); sewer.Add(back); shells.emplace_back(back); } for (TopoShape& s : shells) { sewer.Add(s.getShape()); } sewer.Perform(); result = result.makeShapeWithElementMap( sewer.SewedShape(), Part::MapperSewing(sewer), shells, Part::OpCodes::Sewing ); } else { // shells are already closed - add them directly BRepBuilderAPI_MakeSolid mkSolid; for (TopoShape& s : shells) { mkSolid.Add(TopoDS::Shell(s.getShape())); } if (!mkSolid.IsDone()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Result is not a solid") ); } result.setShape(mkSolid.Shape()); } if (!result.countSubShapes(TopAbs_SHELL)) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Loft: Failed to create shell") ); } auto shapes = result.getSubTopoShapes(TopAbs_SHELL); for (auto& s : shapes) { // build the solid s = s.makeElementSolid(); BRepClass3d_SolidClassifier SC(s.getShape()); SC.PerformInfinitePoint(Precision::Confusion()); if (SC.State() == TopAbs_IN) { s.setShape(s.getShape().Reversed(), false); } } AddSubShape.setValue(result.makeElementCompound( shapes, nullptr, Part::TopoShape::SingleShapeCompoundCreationPolicy::returnShape )); if (shapes.size() > 1) { result.makeElementFuse(shapes); } else { result = shapes.front(); } if (base.isNull()) { if (getAddSubType() == FeatureAddSub::Subtractive) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Pipe: There is nothing to subtract from") ); } if (!isSingleSolidRuleSatisfied(result.getShape())) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP( "Exception", "Result has multiple solids: enable 'Allow Compound' in the active body." )); } // store shape before refinement this->rawShape = result; result = refineShapeIfActive(result); Shape.setValue(getSolid(result)); return App::DocumentObject::StdReturn; } TopoShape boolOp(0, getDocument()->getStringHasher()); const char* maker; switch (getAddSubType()) { case Additive: maker = Part::OpCodes::Fuse; break; case Subtractive: maker = Part::OpCodes::Cut; break; default: return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Unknown operation type") ); } try { boolOp.makeElementBoolean(maker, {base, result}); } catch (Standard_Failure&) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Failed to perform boolean operation") ); } TopoShape solid = getSolid(boolOp); // lets check if the result is a solid if (solid.isNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Resulting shape is not a solid") ); } // store shape before refinement this->rawShape = boolOp; boolOp = refineShapeIfActive(boolOp); if (!isSingleSolidRuleSatisfied(boolOp.getShape())) { return new App::DocumentObjectExecReturn(QT_TRANSLATE_NOOP( "Exception", "Result has multiple solids: enable 'Allow Compound' in the active body." )); } boolOp = getSolid(boolOp); Shape.setValue(boolOp); return App::DocumentObject::StdReturn; } catch (Standard_Failure& e) { return new App::DocumentObjectExecReturn(e.GetMessageString()); } catch (...) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "A fatal error occurred when making the pipe") ); } } void Pipe::setupAlgorithm(BRepOffsetAPI_MakePipeShell& mkPipeShell, const TopoDS_Shape& auxshape) { mkPipeShell.SetTolerance(Precision::Confusion()); switch (Transition.getValue()) { case 0: mkPipeShell.SetTransitionMode(BRepBuilderAPI_Transformed); break; case 1: mkPipeShell.SetTransitionMode(BRepBuilderAPI_RightCorner); break; case 2: mkPipeShell.SetTransitionMode(BRepBuilderAPI_RoundCorner); break; } bool auxiliary = false; const Base::Vector3d& bVec = Binormal.getValue(); switch (Mode.getValue()) { case 1: mkPipeShell.SetMode(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1), gp_Dir(1, 0, 0))); break; case 2: mkPipeShell.SetMode(true); break; case 3: auxiliary = true; break; case 4: mkPipeShell.SetMode(gp_Dir(bVec.x, bVec.y, bVec.z)); break; } if (auxiliary) { mkPipeShell.SetMode(TopoDS::Wire(auxshape), AuxiliaryCurvilinear.getValue()); // mkPipeShell.SetMode(TopoDS::Wire(auxshape), AuxiliaryCurvilinear.getValue(), // BRepFill_ContactOnBorder); } } void Pipe::getContinuousEdges(Part::TopoShape /*TopShape*/, std::vector& /*SubNames*/) { /* TopTools_IndexedMapOfShape mapOfEdges; TopTools_IndexedDataMapOfShapeListOfShape mapEdgeEdge; TopExp::MapShapesAndAncestors(TopShape.getShape(), TopAbs_EDGE, TopAbs_EDGE, mapEdgeEdge); TopExp::MapShapes(TopShape.getShape(), TopAbs_EDGE, mapOfEdges); Base::Console().message("Initial edges:\n"); for (int i=0; i(SubNames.at(i)); if (aSubName.compare(0, 4, "Edge") == 0) { TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(aSubName.c_str())); const TopTools_ListOfShape& los = mapEdgeEdge.FindFromKey(edge); if (los.Extent() != 2) { SubNames.erase(SubNames.begin()+i); continue; } const TopoDS_Shape& face1 = los.First(); const TopoDS_Shape& face2 = los.Last(); GeomAbs_Shape cont = BRep_Tool::Continuity(TopoDS::Edge(edge), TopoDS::Face(face1), TopoDS::Face(face2)); if (cont != GeomAbs_C0) { SubNames.erase(SubNames.begin()+i); continue; } i++; } // empty name or any other sub-element else { SubNames.erase(SubNames.begin()+i); } } Base::Console().message("Final edges:\n"); for (int i=0; i& subedge, TopoDS_Shape& path ) { if (!shape.getShape().IsNull()) { try { if (!subedge.empty()) { // if (SpineTangent.getValue()) // getContinuousEdges(shape, subedge); BRepBuilderAPI_MakeWire mkWire; for (const auto& it : subedge) { TopoDS_Shape subshape = shape.getSubShape(it.c_str()); mkWire.Add(TopoDS::Edge(subshape)); } path = mkWire.Wire(); } else if (shape.getShape().ShapeType() == TopAbs_EDGE) { path = shape.getShape(); } else if (shape.getShape().ShapeType() == TopAbs_WIRE) { BRepBuilderAPI_MakeWire mkWire(TopoDS::Wire(shape.getShape())); path = mkWire.Wire(); } else if (shape.getShape().ShapeType() == TopAbs_COMPOUND) { TopoDS_Iterator it(shape.getShape()); for (; it.More(); it.Next()) { if (it.Value().IsNull()) { throw Base::ValueError( QT_TRANSLATE_NOOP("Exception", "Invalid element in spine.") ); } if ((it.Value().ShapeType() != TopAbs_EDGE) && (it.Value().ShapeType() != TopAbs_WIRE)) { throw Base::TypeError(QT_TRANSLATE_NOOP( "Exception", "Element in spine is neither an edge nor a wire." )); } } Handle(TopTools_HSequenceOfShape) hEdges = new TopTools_HSequenceOfShape(); Handle(TopTools_HSequenceOfShape) hWires = new TopTools_HSequenceOfShape(); for (TopExp_Explorer xp(shape.getShape(), TopAbs_EDGE); xp.More(); xp.Next()) { hEdges->Append(xp.Current()); } ShapeAnalysis_FreeBounds::ConnectEdgesToWires( hEdges, Precision::Confusion(), Standard_True, hWires ); int len = hWires->Length(); if (len != 1) { throw Base::ValueError(QT_TRANSLATE_NOOP("Exception", "Spine is not connected.")); } path = hWires->Value(1); } else { throw Base::TypeError( QT_TRANSLATE_NOOP("Exception", "Spine is neither an edge nor a wire.") ); } } catch (Standard_Failure&) { throw Base::CADKernelError(QT_TRANSLATE_NOOP("Exception", "Invalid spine.")); } } } PROPERTY_SOURCE(PartDesign::AdditivePipe, PartDesign::Pipe) AdditivePipe::AdditivePipe() { addSubType = Additive; } PROPERTY_SOURCE(PartDesign::SubtractivePipe, PartDesign::Pipe) SubtractivePipe::SubtractivePipe() { addSubType = Subtractive; } void Pipe::handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName, App::Property* prop) { // property Sections had the App::PropertyLinkList and was changed to App::PropertyXLinkSubList if (prop == &Sections && strcmp(TypeName, "App::PropertyLinkList") == 0) { Sections.upgrade(reader, TypeName); } else { ProfileBased::handleChangedPropertyType(reader, TypeName, prop); } } void Pipe::handleChangedPropertyName(Base::XMLReader& reader, const char* TypeName, const char* PropName) { // The AuxiliarySpine property was AuxillerySpine in the past std::string strAuxillerySpine("AuxillerySpine"); // The AuxiliarySpineTangent property was AuxillerySpineTangent in the past std::string strAuxillerySpineTangent("AuxillerySpineTangent"); // The AuxiliaryCurvilinear property was AuxilleryCurvelinear in the past std::string strAuxilleryCurvelinear("AuxilleryCurvelinear"); Base::Type type = Base::Type::fromName(TypeName); if (AuxiliarySpine.getClassTypeId() == type && strAuxillerySpine == PropName) { AuxiliarySpine.Restore(reader); } else if (AuxiliarySpineTangent.getClassTypeId() == type && strAuxillerySpineTangent == PropName) { AuxiliarySpineTangent.Restore(reader); } else if (AuxiliaryCurvilinear.getClassTypeId() == type && strAuxilleryCurvelinear == PropName) { AuxiliaryCurvilinear.Restore(reader); } else { ProfileBased::handleChangedPropertyName(reader, TypeName, PropName); } }