From 2fcee8ea82f4c0c665dccc93a62af97243e79bb5 Mon Sep 17 00:00:00 2001 From: Ajinkya Dahale Date: Wed, 17 Nov 2021 22:20:35 -0500 Subject: [PATCH] [PD] Support "punctual" sections for PD Pipe Similar to PR #5170 for loft. This commit squashes the following commits. [PD] Refactor `Pipe::execute` and support point sections [PD] Allow point profile in selection-based pipe workflow [PD] Only add sketch subs if it is vertex [PD] Make both end faces of pipe regardless of point sections Earlier we were checking if these faces correspond to point sections, but apparently the end faces are independent of the order in which the sections are added, so the "front" may be the face closest to the last added section, rather than the "profile". Creating null faces and adding them for sewing together into a solid does not appear to have side-effects so far. --- src/Mod/PartDesign/App/FeaturePipe.cpp | 210 ++++++++++++++++--------- src/Mod/PartDesign/Gui/Command.cpp | 38 +++-- 2 files changed, 161 insertions(+), 87 deletions(-) diff --git a/src/Mod/PartDesign/App/FeaturePipe.cpp b/src/Mod/PartDesign/App/FeaturePipe.cpp index 77976627c3..f512f34b90 100644 --- a/src/Mod/PartDesign/App/FeaturePipe.cpp +++ b/src/Mod/PartDesign/App/FeaturePipe.cpp @@ -71,8 +71,8 @@ using namespace PartDesign; -const char* Pipe::TypeEnums[] = {"FullPath","UpToFace",NULL}; -const char* Pipe::TransitionEnums[] = {"Transformed","Right corner", "Round corner",NULL}; +const char* Pipe::TypeEnums[] = {"FullPath", "UpToFace", NULL}; +const char* Pipe::TransitionEnums[] = {"Transformed", "Right corner", "Round corner", NULL}; const char* Pipe::ModeEnums[] = {"Standard", "Fixed", "Frenet", "Auxiliary", "Binormal", NULL}; const char* Pipe::TransformEnums[] = {"Constant", "Multisection", "Linear", "S-shape", "Interpolation", NULL}; @@ -112,26 +112,54 @@ short Pipe::mustExecute() const App::DocumentObjectExecReturn *Pipe::execute(void) { - std::vector wires; - try { - wires = getProfileWires(); - } catch (const Base::Exception& e) { - return new App::DocumentObjectExecReturn(e.what()); - } + auto getSectionShape = + [](App::DocumentObject* feature, const std::vector &subs) -> TopoDS_Shape { + if (!feature || + !feature->isDerivedFrom(Part::Feature::getClassTypeId())) + throw Base::TypeError("Pipe: Invalid profile/section"); - TopoDS_Shape sketchshape = getVerifiedFace(); - if (sketchshape.IsNull()) - return new App::DocumentObjectExecReturn("Pipe: No valid sketch or face as first section"); - else { - //TODO: currently we only allow planar faces. 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_Face face = TopoDS::Face(sketchshape); - BRepAdaptor_Surface adapt(face); - if (adapt.GetType() != GeomAbs_Plane) - return new App::DocumentObjectExecReturn("Pipe: Only planar faces supported"); - } + 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(Part::Part2DObject::getClassTypeId()) && + !(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... + std::vector wires; + TopoDS_Shape profilePoint; // if the Base property has a valid shape, fuse the pipe into it TopoDS_Shape base; @@ -142,16 +170,22 @@ App::DocumentObjectExecReturn *Pipe::execute(void) } try { - //setup the location + // setup the location this->positionByPrevious(); TopLoc_Location invObjLoc = this->getLocation().Inverted(); if (!base.IsNull()) base.Move(invObjLoc); - //build the paths + // setup the profile section + TopoDS_Shape profileShape = getSectionShape(Profile.getValue(), + Profile.getSubValues()); + if (profileShape.IsNull()) + return new App::DocumentObjectExecReturn("Pipe: Could not obtain profile shape"); + + // build the paths App::DocumentObject* spine = Spine.getValue(); if (!(spine && spine->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId()))) - return new App::DocumentObjectExecReturn("No spine linked."); + return new App::DocumentObjectExecReturn("No spine linked"); std::vector subedge = Spine.getSubValues(); TopoDS_Shape path; @@ -159,7 +193,6 @@ App::DocumentObjectExecReturn *Pipe::execute(void) buildPipePath(shape, subedge, path); path.Move(invObjLoc); - // auxiliary TopoDS_Shape auxpath; if (Mode.getValue()==3) { @@ -173,47 +206,57 @@ App::DocumentObjectExecReturn *Pipe::execute(void) auxpath.Move(invObjLoc); } - //build up multisections - auto multisections = Sections.getValues(); - std::vector> wiresections; - for (TopoDS_Wire& wire : wires) - wiresections.emplace_back(1, wire); - //maybe we need a sacling law + // 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("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("Pipe: At least one section is needed when using a single point for profile"); + + // maybe we need a scaling law Handle(Law_Function) scalinglaw; - //see if we shall use multiple sections + 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 crahsing, as makepieshell connects - //the sections in the order of adding - - for (App::DocumentObject* obj : multisections) { - if (!obj->isDerivedFrom(Part::Feature::getClassTypeId())) - return new App::DocumentObjectExecReturn("All sections need to be part features"); + // 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(Part::Feature::getClassTypeId())) + return new App::DocumentObjectExecReturn("Pipe: All sections need to be part features"); // if the section is an object's face then take just the face - TopoDS_Shape shape; - if (obj->isDerivedFrom(Part::Part2DObject::getClassTypeId())) - shape = static_cast(obj)->Shape.getValue(); - else { - auto subValues = Sections.getSubValues(obj); - if (subValues.empty()) - throw Base::ValueError("Pipe: No valid subelement in multisection"); + TopoDS_Shape shape = getSectionShape(subSet.first, subSet.second); + if (shape.IsNull()) + return new App::DocumentObjectExecReturn("Pipe: Could not obtain section shape"); - shape = static_cast(obj)->Shape.getShape(). getSubShape(subValues[0].c_str()); + 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("Pipe: Only the profile and last section can be vertices"); + isLastSectionVertex = true; + for (auto &wires : wiresections) + wires.push_back(ex.Current()); + } } - TopExp_Explorer ex; - size_t i=0; - for (ex.Init(shape, TopAbs_WIRE); ex.More(); ex.Next()) { - if (i>=wiresections.size()) - return new App::DocumentObjectExecReturn("Multisections need to have the same amount of inner wires as the base section"); - wiresections[i].push_back(TopoDS::Wire(ex.Current())); - - ++i; - } - - if (iSet(0,1,1,ScalingData[0].x); + lin->Set(0, 1, 1, ScalingData[0].x); scalinglaw = lin; } @@ -232,7 +275,7 @@ App::DocumentObjectExecReturn *Pipe::execute(void) 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); + s->Set(0, 1, ScalingData[0].y, 1, ScalingData[0].x, ScalingData[0].z); scalinglaw = s; }*/ @@ -243,20 +286,30 @@ App::DocumentObjectExecReturn *Pipe::execute(void) // build all shells std::vector shells; - std::vector frontwires, backwires; - for (std::vector& wires : wiresections) { + 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) { - for (TopoDS_Wire& wire : wires) { + if (!profilePoint.IsNull()) + mkPS.Add(copyProfilePoint); + + for (auto& wire : wires) { wire.Move(invObjLoc); mkPS.Add(wire); } } else { - for (TopoDS_Wire& wire : wires) { + if (!profilePoint.IsNull()) + mkPS.SetLaw(copyProfilePoint, scalinglaw); + + for (auto& wire : wires) { wire.Move(invObjLoc); mkPS.SetLaw(wire, scalinglaw); } @@ -272,6 +325,10 @@ App::DocumentObjectExecReturn *Pipe::execute(void) 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())); } @@ -279,16 +336,19 @@ App::DocumentObjectExecReturn *Pipe::execute(void) BRepBuilderAPI_MakeSolid mkSolid; - if (!frontwires.empty()) { - // build the end faces, sew the shell and build the final solid - TopoDS_Shape front = Part::FaceMakerCheese::makeFace(frontwires); - TopoDS_Shape back = Part::FaceMakerCheese::makeFace(backwires); - + if (!frontwires.empty() || !backwires.empty()) { BRepBuilderAPI_Sewing sewer; sewer.SetTolerance(Precision::Confusion()); - sewer.Add(front); - sewer.Add(back); + // 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); + } + if (!backwires.empty()) { + TopoDS_Shape back = Part::FaceMakerCheese::makeFace(backwires); + sewer.Add(back); + } for (TopoDS_Shape& s : shells) sewer.Add(s); @@ -393,7 +453,7 @@ void Pipe::setupAlgorithm(BRepOffsetAPI_MakePipeShell& mkPipeShell, TopoDS_Shape 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))); + 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); @@ -402,13 +462,13 @@ void Pipe::setupAlgorithm(BRepOffsetAPI_MakePipeShell& mkPipeShell, TopoDS_Shape auxiliary = true; break; case 4: - mkPipeShell.SetMode(gp_Dir(bVec.x,bVec.y,bVec.z)); + mkPipeShell.SetMode(gp_Dir(bVec.x, bVec.y, bVec.z)); break; } if (auxiliary) { mkPipeShell.SetMode(TopoDS::Wire(auxshape), AuxilleryCurvelinear.getValue()); - //mkPipeShell.SetMode(TopoDS::Wire(auxshape), AuxilleryCurvelinear.getValue(), BRepFill_ContactOnBorder); + // mkPipeShell.SetMode(TopoDS::Wire(auxshape), AuxilleryCurvelinear.getValue(), BRepFill_ContactOnBorder); } } @@ -430,7 +490,7 @@ void Pipe::getContinuousEdges(Part::TopoShape /*TopShape*/, std::vector< std::st { std::string aSubName = static_cast(SubNames.at(i)); - if (aSubName.size() > 4 && aSubName.substr(0,4) == "Edge") { + if (aSubName.size() > 4 && aSubName.substr(0, 4) == "Edge") { TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(aSubName.c_str())); const TopTools_ListOfShape& los = mapEdgeEdge.FindFromKey(edge); diff --git a/src/Mod/PartDesign/Gui/Command.cpp b/src/Mod/PartDesign/Gui/Command.cpp index 27a1b754a7..2518013471 100644 --- a/src/Mod/PartDesign/Gui/Command.cpp +++ b/src/Mod/PartDesign/Gui/Command.cpp @@ -996,7 +996,7 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons if (feature->isTouched()) feature->recomputeFeature(); - std::string FeatName = cmd->getUniqueObjectName(which.c_str(),pcActiveBody); + std::string FeatName = cmd->getUniqueObjectName(which.c_str(), pcActiveBody); Gui::Command::openCommand((std::string("Make ") + which).c_str()); @@ -1021,7 +1021,8 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons FCMD_OBJ_CMD(Feat,"Profile = (" << objCmd << ", [" << ss.str() << "])"); }; - if (which.compare("AdditiveLoft") == 0 || which.compare("SubtractiveLoft") == 0) { + if (which.compare("AdditiveLoft") == 0 || + which.compare("SubtractiveLoft") == 0) { // for additive and subtractive lofts set subvalues even for sketches // when a vertex is first selected auto subName = subs.empty() ? "" : subs.front(); @@ -1030,7 +1031,7 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons // just the sub-shapes if they are set. So when whole sketches are // desired, don not set sub-values. if (feature->isDerivedFrom(Part::Part2DObject::getClassTypeId()) && - !(subName.size() > 6 && subName.substr(0,6) == "Vertex")) + !(subName.size() > 6 && subName.substr(0, 6) == "Vertex")) runProfileCmd(); else runProfileCmdWithSubs(); @@ -1048,15 +1049,22 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons } } } - else { - if (feature->isDerivedFrom(Part::Part2DObject::getClassTypeId()) || subs.empty()) + else if (which.compare("AdditivePipe") == 0 || + which.compare("SubtractivePipe") == 0) { + // for additive and subtractive pipes set subvalues even for sketches + // to support point sections + auto subName = subs.empty() ? "" : subs.front(); + + // `ProfileBased::getProfileShape()` and other methods will return + // just the sub-shapes if they are set. So when whole sketches are + // desired, don not set sub-values. + if (feature->isDerivedFrom(Part::Part2DObject::getClassTypeId()) && + !(subName.size() > 6 && subName.substr(0, 6) == "Vertex")) runProfileCmd(); else runProfileCmdWithSubs(); - } - // for additive and subtractive pipes allow the user to preselect the spines - if (which.compare("AdditivePipe") == 0 || which.compare("SubtractivePipe") == 0) { + // for additive and subtractive pipes allow the user to preselect the spines std::vector selection = cmd->getSelection().getSelectionEx(); if (selection.size() == 2) { //treat additional selected object as spine std::vector subnames = selection[1].getSubNames(); @@ -1074,6 +1082,12 @@ void prepareProfileBased(PartDesign::Body *pcActiveBody, Gui::Command* cmd, cons } } } + else { + if (feature->isDerivedFrom(Part::Part2DObject::getClassTypeId()) || subs.empty()) + runProfileCmd(); + else + runProfileCmdWithSubs(); + } func(static_cast(feature), Feat); }; @@ -1909,15 +1923,15 @@ void finishDressupFeature(const Gui::Command* cmd, const std::string& which, } str << "])"; - std::string FeatName = cmd->getUniqueObjectName(which.c_str(),base); + std::string FeatName = cmd->getUniqueObjectName(which.c_str(), base); - auto body = PartDesignGui::getBodyFor(base,false); + auto body = PartDesignGui::getBodyFor(base, false); if (!body) return; cmd->openCommand((std::string("Make ") + which).c_str()); FCMD_OBJ_CMD(body,"newObject('PartDesign::"<getDocument()->getObject(FeatName.c_str()); FCMD_OBJ_CMD(Feat,"Base = " << str.str()); - cmd->doCommand(cmd->Gui,"Gui.Selection.clearSelection()"); + cmd->doCommand(cmd->Gui, "Gui.Selection.clearSelection()"); finishFeature(cmd, Feat, base); App::DocumentObject* baseFeature = static_cast(Feat)->Base.getValue(); @@ -2090,7 +2104,7 @@ void CmdPartDesignThickness::activated(int iMsg) { std::string aSubName = static_cast(SubNames.at(i)); - if (aSubName.size() > 4 && aSubName.substr(0,4) != "Face") { + if (aSubName.size() > 4 && aSubName.substr(0, 4) != "Face") { // empty name or any other sub-element SubNames.erase(SubNames.begin()+i); }