[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.
This commit is contained in:
@@ -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<TopoDS_Wire> wires;
|
||||
try {
|
||||
wires = getProfileWires();
|
||||
} catch (const Base::Exception& e) {
|
||||
return new App::DocumentObjectExecReturn(e.what());
|
||||
}
|
||||
auto getSectionShape =
|
||||
[](App::DocumentObject* feature, const std::vector<std::string> &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<Part::Part2DObject*>(feature)->Shape.getValue();
|
||||
else {
|
||||
if(subName.empty())
|
||||
throw Base::ValueError("Pipe: No valid subelement linked in Part::Feature");
|
||||
return static_cast<Part::Feature*>(feature)->Shape.getShape().getSubShape(subName.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
auto addWiresToWireSections =
|
||||
[](TopoDS_Shape& section,
|
||||
std::vector<std::vector<TopoDS_Shape>>& 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<TopoDS_Wire> 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<std::string> 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<std::vector<TopoDS_Wire>> 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<std::vector<TopoDS_Shape>> 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<Part::Part2DObject*>(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<Part::Feature*>(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 (i<wiresections.size())
|
||||
if (!isLastSectionVertex && nWiresAdded < wiresections.size())
|
||||
return new App::DocumentObjectExecReturn("Multisections need to have the same amount of inner wires as the base section");
|
||||
}
|
||||
}
|
||||
@@ -223,7 +266,7 @@ App::DocumentObjectExecReturn *Pipe::execute(void)
|
||||
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);
|
||||
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<TopoDS_Shape> shells;
|
||||
std::vector<TopoDS_Wire> frontwires, backwires;
|
||||
for (std::vector<TopoDS_Wire>& wires : wiresections) {
|
||||
|
||||
TopoDS_Shape copyProfilePoint(profilePoint);
|
||||
if (!profilePoint.IsNull())
|
||||
copyProfilePoint.Move(invObjLoc);
|
||||
|
||||
std::vector<TopoDS_Wire> 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<std::string>(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);
|
||||
|
||||
|
||||
@@ -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<Gui::SelectionObject> selection = cmd->getSelection().getSelectionEx();
|
||||
if (selection.size() == 2) { //treat additional selected object as spine
|
||||
std::vector <string> 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<Part::Feature*>(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::"<<which<<"','"<<FeatName<<"')");
|
||||
auto Feat = body->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<PartDesign::DressUp*>(Feat)->Base.getValue();
|
||||
@@ -2090,7 +2104,7 @@ void CmdPartDesignThickness::activated(int iMsg)
|
||||
{
|
||||
std::string aSubName = static_cast<std::string>(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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user