diff --git a/src/App/DocumentObjectPy.xml b/src/App/DocumentObjectPy.xml index ac0f7cea1e..ead1d5573c 100644 --- a/src/App/DocumentObjectPy.xml +++ b/src/App/DocumentObjectPy.xml @@ -182,6 +182,15 @@ Return -1 if element visibility is not supported or element not found, 0 if invi in a single group, hence only a single return value. + + + Returns the group the object is in or None if it is not part of a group. + Note that an object can only be in a single group, hence only a single return + value. + The parent can be a simple group as with getParentGroup() or a + GeoFeature group as with getParentGeoFeatureGroup(). + + Get all paths from this object to another object following the OutList. diff --git a/src/App/DocumentObjectPyImp.cpp b/src/App/DocumentObjectPyImp.cpp index 810c0f33e0..b561983bfe 100644 --- a/src/App/DocumentObjectPyImp.cpp +++ b/src/App/DocumentObjectPyImp.cpp @@ -673,6 +673,24 @@ PyObject* DocumentObjectPy::getParentGeoFeatureGroup(PyObject *args) } } +PyObject* DocumentObjectPy::getParent(PyObject *args) +{ + if (!PyArg_ParseTuple(args, "")) + return nullptr; + + try { + auto grp = getDocumentObjectPtr()->getFirstParent(); + if(!grp) { + Py_INCREF(Py_None); + return Py_None; + } + return grp->getPyObject(); + } + catch (const Base::Exception& e) { + throw Py::RuntimeError(e.what()); + } +} + Py::Boolean DocumentObjectPy::getMustExecute() const { try { diff --git a/src/Base/Tools.cpp b/src/Base/Tools.cpp index f3385132c5..0a360cc5bb 100644 --- a/src/Base/Tools.cpp +++ b/src/Base/Tools.cpp @@ -291,6 +291,30 @@ std::string Base::Tools::escapeEncodeFilename(const std::string& s) return result; } +std::string Base::Tools::quoted(const char* name) +{ + std::stringstream str; + str << "\"" << name << "\""; + return str.str(); +} + +std::string Base::Tools::quoted(const std::string& name) +{ + std::stringstream str; + str << "\"" << name << "\""; + return str.str(); +} + +std::string Base::Tools::joinList(const std::vector& vec, + const std::string& sep) +{ + std::stringstream str; + for (const auto& it : vec) { + str << it << sep; + } + return str.str(); +} + // ---------------------------------------------------------------------------- using namespace Base; diff --git a/src/Base/Tools.h b/src/Base/Tools.h index 93291e218b..76a67f9e98 100644 --- a/src/Base/Tools.h +++ b/src/Base/Tools.h @@ -274,6 +274,29 @@ struct BaseExport Tools static inline QString fromStdString(const std::string & s) { return QString::fromUtf8(s.c_str(), static_cast(s.size())); } + + /** + * @brief quoted Creates a quoted string. + * @param String to be quoted. + * @return A quoted std::string. + */ + static std::string quoted(const char*); + /** + * @brief quoted Creates a quoted string. + * @param String to be quoted. + * @return A quoted std::string. + */ + static std::string quoted(const std::string&); + + /** + * @brief joinList + * Join the vector of strings \a vec using the separator \a sep + * @param vec + * @param sep + * @return + */ + static std::string joinList(const std::vector& vec, + const std::string& sep = ", "); }; diff --git a/src/Mod/Part/BOPTools/BOPFeatures.py b/src/Mod/Part/BOPTools/BOPFeatures.py new file mode 100644 index 0000000000..55d6dd424a --- /dev/null +++ b/src/Mod/Part/BOPTools/BOPFeatures.py @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2023 Werner Mayer * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD 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 * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +__title__ = "BOPTools.BOPFeatures module" +__author__ = "Werner Mayer" +__url__ = "http://www.freecad.org" +__doc__ = "Helper class to create the features for Boolean operations." + +import FreeCAD +import Part + +class BOPFeatures: + def __init__(self, doc): + self.doc = doc + + def make_section(self, inputNames): + obj = self.doc.addObject("Part::Section", "Section") + obj.Base = self.doc.getObject(inputNames[0]) + obj.Tool = self.doc.getObject(inputNames[1]) + self.copy_visual_attributes(obj, obj.Base) + target = self.move_input_objects([obj.Base, obj.Tool]) + if target: + target.addObject(obj) + return obj + + def make_cut(self, inputNames): + obj = self.doc.addObject("Part::Cut", "Cut") + obj.Base = self.doc.getObject(inputNames[0]) + obj.Tool = self.doc.getObject(inputNames[1]) + self.copy_visual_attributes(obj, obj.Base) + target = self.move_input_objects([obj.Base, obj.Tool]) + if target: + target.addObject(obj) + return obj + + def make_common(self, inputNames): + obj = self.doc.addObject("Part::Common", "Common") + obj.Base = self.doc.getObject(inputNames[0]) + obj.Tool = self.doc.getObject(inputNames[1]) + self.copy_visual_attributes(obj, obj.Base) + target = self.move_input_objects([obj.Base, obj.Tool]) + if target: + target.addObject(obj) + return obj + + def make_multi_common(self, inputNames): + obj = self.doc.addObject("Part::MultiCommon", "Common") + obj.Shapes = [self.doc.getObject(name) for name in inputNames] + self.copy_visual_attributes(obj, obj.Shapes[0]) + target = self.move_input_objects(obj.Shapes) + if target: + target.addObject(obj) + return obj + + def make_fuse(self, inputNames): + obj = self.doc.addObject("Part::Fuse", "Fusion") + obj.Base = self.doc.getObject(inputNames[0]) + obj.Tool = self.doc.getObject(inputNames[1]) + self.copy_visual_attributes(obj, obj.Base) + target = self.move_input_objects([obj.Base, obj.Tool]) + if target: + target.addObject(obj) + return obj + + def make_multi_fuse(self, inputNames): + obj = self.doc.addObject("Part::MultiFuse", "Fusion") + obj.Shapes = [self.doc.getObject(name) for name in inputNames] + self.copy_visual_attributes(obj, obj.Shapes[0]) + target = self.move_input_objects(obj.Shapes) + if target: + target.addObject(obj) + return obj + + def move_input_objects(self, objects): + targetGroup = None + for obj in objects: + obj.Visibility = False + parent = obj.getParent() + if parent: + parent.removeObject(obj) + targetGroup = parent + return targetGroup + + def copy_visual_attributes(self, target, source): + if target.ViewObject: + target.ViewObject.ShapeColor = source.ViewObject.ShapeColor + target.ViewObject.DisplayMode = source.ViewObject.DisplayMode diff --git a/src/Mod/Part/CMakeLists.txt b/src/Mod/Part/CMakeLists.txt index a2cf87b329..bc28469487 100644 --- a/src/Mod/Part/CMakeLists.txt +++ b/src/Mod/Part/CMakeLists.txt @@ -44,6 +44,7 @@ endif(BUILD_GUI) set(BOPTools_Scripts BOPTools/__init__.py + BOPTools/BOPFeatures.py BOPTools/GeneralFuseResult.py BOPTools/JoinAPI.py BOPTools/JoinFeatures.py diff --git a/src/Mod/Part/Gui/Command.cpp b/src/Mod/Part/Gui/Command.cpp index fdf24e9161..b057ef4a81 100644 --- a/src/Mod/Part/Gui/Command.cpp +++ b/src/Mod/Part/Gui/Command.cpp @@ -316,6 +316,7 @@ void CmdPartCut::activated(int iMsg) } bool askUser = false; + std::vector names; for (std::vector::iterator it = Sel.begin(); it != Sel.end(); ++it) { App::DocumentObject* obj = it->getObject(); const TopoDS_Shape& shape = Part::Feature::getShape(obj); @@ -327,34 +328,14 @@ void CmdPartCut::activated(int iMsg) return; askUser = true; } - } - std::string FeatName = getUniqueObjectName("Cut"); + names.push_back(Base::Tools::quoted(it->getFeatName())); + } openCommand(QT_TRANSLATE_NOOP("Command", "Part Cut")); - doCommand(Doc,"App.activeDocument().addObject(\"Part::Cut\",\"%s\")",FeatName.c_str()); - doCommand(Doc,"App.activeDocument().%s.Base = App.activeDocument().%s",FeatName.c_str(),Sel[0].getFeatName()); - doCommand(Doc,"App.activeDocument().%s.Tool = App.activeDocument().%s",FeatName.c_str(),Sel[1].getFeatName()); - - // hide the input objects and remove them from the parent group - App::DocumentObjectGroup* targetGroup = nullptr; - for (std::vector::iterator it = Sel.begin(); it != Sel.end(); ++it) { - doCommand(Gui,"Gui.activeDocument().%s.Visibility=False",it->getFeatName()); - App::DocumentObjectGroup* group = it->getObject()->getGroup(); - if (group) { - targetGroup = group; - doCommand(Doc, "App.activeDocument().%s.removeObject(App.activeDocument().%s)", - group->getNameInDocument(), it->getFeatName()); - } - } - - if (targetGroup) { - doCommand(Doc, "App.activeDocument().%s.addObject(App.activeDocument().%s)", - targetGroup->getNameInDocument(), FeatName.c_str()); - } - - copyVisual(FeatName.c_str(), "ShapeColor", Sel[0].getFeatName()); - copyVisual(FeatName.c_str(), "DisplayMode", Sel[0].getFeatName()); + doCommand(Doc, "from BOPTools import BOPFeatures"); + doCommand(Doc, "bp = BOPFeatures.BOPFeatures(App.activeDocument())"); + doCommand(Doc, "bp.make_cut([%s])", Base::Tools::joinList(names).c_str()); updateActive(); commitCommand(); } @@ -411,11 +392,7 @@ void CmdPartCommon::activated(int iMsg) } bool askUser = false; - std::string FeatName = getUniqueObjectName("Common"); - std::stringstream str; - std::vector partObjects; - - str << "App.activeDocument()." << FeatName << ".Shapes = ["; + std::vector names; for (std::vector::iterator it = Sel.begin(); it != Sel.end(); ++it) { App::DocumentObject* obj = it->getObject(); const TopoDS_Shape& shape = Part::Feature::getShape(obj); @@ -427,34 +404,14 @@ void CmdPartCommon::activated(int iMsg) return; askUser = true; } - str << "App.activeDocument()." << it->getFeatName() << ","; - partObjects.push_back(*it); + + names.push_back(Base::Tools::quoted(it->getFeatName())); } - str << "]"; openCommand(QT_TRANSLATE_NOOP("Command", "Common")); - doCommand(Doc,"App.activeDocument().addObject(\"Part::MultiCommon\",\"%s\")",FeatName.c_str()); - runCommand(Doc,str.str().c_str()); - - // hide the input objects and remove them from the parent group - App::DocumentObjectGroup* targetGroup = nullptr; - for (std::vector::iterator it = partObjects.begin(); it != partObjects.end(); ++it) { - doCommand(Gui,"Gui.activeDocument().%s.Visibility=False",it->getFeatName()); - App::DocumentObjectGroup* group = it->getObject()->getGroup(); - if (group) { - targetGroup = group; - doCommand(Doc, "App.activeDocument().%s.removeObject(App.activeDocument().%s)", - group->getNameInDocument(), it->getFeatName()); - } - } - - if (targetGroup) { - doCommand(Doc, "App.activeDocument().%s.addObject(App.activeDocument().%s)", - targetGroup->getNameInDocument(), FeatName.c_str()); - } - - copyVisual(FeatName.c_str(), "ShapeColor", partObjects.front().getFeatName()); - copyVisual(FeatName.c_str(), "DisplayMode", partObjects.front().getFeatName()); + doCommand(Doc, "from BOPTools import BOPFeatures"); + doCommand(Doc, "bp = BOPFeatures.BOPFeatures(App.activeDocument())"); + doCommand(Doc, "bp.make_multi_common([%s])", Base::Tools::joinList(names).c_str()); updateActive(); commitCommand(); } @@ -511,11 +468,7 @@ void CmdPartFuse::activated(int iMsg) } bool askUser = false; - std::string FeatName = getUniqueObjectName("Fusion"); - std::stringstream str; - std::vector partObjects; - - str << "App.activeDocument()." << FeatName << ".Shapes = ["; + std::vector names; for (std::vector::iterator it = Sel.begin(); it != Sel.end(); ++it) { App::DocumentObject* obj = it->getObject(); const TopoDS_Shape& shape = Part::Feature::getShape(obj); @@ -527,34 +480,14 @@ void CmdPartFuse::activated(int iMsg) return; askUser = true; } - str << "App.activeDocument()." << it->getFeatName() << ","; - partObjects.push_back(*it); + + names.push_back(Base::Tools::quoted(it->getFeatName())); } - str << "]"; openCommand(QT_TRANSLATE_NOOP("Command", "Fusion")); - doCommand(Doc,"App.activeDocument().addObject(\"Part::MultiFuse\",\"%s\")",FeatName.c_str()); - runCommand(Doc,str.str().c_str()); - - // hide the input objects and remove them from the parent group - App::DocumentObjectGroup* targetGroup = nullptr; - for (std::vector::iterator it = partObjects.begin(); it != partObjects.end(); ++it) { - doCommand(Gui,"Gui.activeDocument().%s.Visibility=False",it->getFeatName()); - App::DocumentObjectGroup* group = it->getObject()->getGroup(); - if (group) { - targetGroup = group; - doCommand(Doc, "App.activeDocument().%s.removeObject(App.activeDocument().%s)", - group->getNameInDocument(), it->getFeatName()); - } - } - - if (targetGroup) { - doCommand(Doc, "App.activeDocument().%s.addObject(App.activeDocument().%s)", - targetGroup->getNameInDocument(), FeatName.c_str()); - } - - copyVisual(FeatName.c_str(), "ShapeColor", partObjects.front().getFeatName()); - copyVisual(FeatName.c_str(), "DisplayMode", partObjects.front().getFeatName()); + doCommand(Doc, "from BOPTools import BOPFeatures"); + doCommand(Doc, "bp = BOPFeatures.BOPFeatures(App.activeDocument())"); + doCommand(Doc, "bp.make_multi_fuse([%s])", Base::Tools::joinList(names).c_str()); updateActive(); commitCommand(); } diff --git a/src/Mod/Part/Gui/DlgBooleanOperation.cpp b/src/Mod/Part/Gui/DlgBooleanOperation.cpp index d84e08bf7c..cca0f7128c 100644 --- a/src/Mod/Part/Gui/DlgBooleanOperation.cpp +++ b/src/Mod/Part/Gui/DlgBooleanOperation.cpp @@ -29,6 +29,7 @@ #endif #include +#include #include #include #include @@ -405,7 +406,7 @@ void DlgBooleanOperation::accept() return; } - std::string type, objName; + std::string method; App::DocumentObject* obj1 = activeDoc->getObject(shapeOne.c_str()); App::DocumentObject* obj2 = activeDoc->getObject(shapeTwo.c_str()); if (!obj1 || !obj2) { @@ -421,8 +422,7 @@ void DlgBooleanOperation::accept() tr("Performing union on non-solids is not possible")); return; } - type = "Part::Fuse"; - objName = activeDoc->getUniqueObjectName("Fusion"); + method = "make_fuse"; } else if (ui->interButton->isChecked()) { if (!hasSolids(obj1) || !hasSolids(obj2)) { @@ -430,8 +430,7 @@ void DlgBooleanOperation::accept() tr("Performing intersection on non-solids is not possible")); return; } - type = "Part::Common"; - objName = activeDoc->getUniqueObjectName("Common"); + method = "make_common"; } else if (ui->diffButton->isChecked()) { if (!hasSolids(obj1) || !hasSolids(obj2)) { @@ -439,55 +438,24 @@ void DlgBooleanOperation::accept() tr("Performing difference on non-solids is not possible")); return; } - type = "Part::Cut"; - objName = activeDoc->getUniqueObjectName("Cut"); + method = "make_cut"; } else if (ui->sectionButton->isChecked()) { - type = "Part::Section"; - objName = activeDoc->getUniqueObjectName("Section"); + method = "make_section"; } try { Gui::WaitCursor wc; activeDoc->openTransaction("Boolean operation"); + std::vector names; + names.push_back(Base::Tools::quoted(shapeOne.c_str())); + names.push_back(Base::Tools::quoted(shapeTwo.c_str())); Gui::Command::doCommand(Gui::Command::Doc, - "App.activeDocument().addObject(\"%s\",\"%s\")", - type.c_str(), objName.c_str()); + "from BOPTools import BOPFeatures"); Gui::Command::doCommand(Gui::Command::Doc, - "App.activeDocument().%s.Base = App.activeDocument().%s", - objName.c_str(),shapeOne.c_str()); + "bp = BOPFeatures.BOPFeatures(App.activeDocument())"); Gui::Command::doCommand(Gui::Command::Doc, - "App.activeDocument().%s.Tool = App.activeDocument().%s", - objName.c_str(),shapeTwo.c_str()); - Gui::Command::doCommand(Gui::Command::Gui, - "Gui.activeDocument().hide(\"%s\")",shapeOne.c_str()); - Gui::Command::doCommand(Gui::Command::Gui, - "Gui.activeDocument().hide(\"%s\")",shapeTwo.c_str()); - - // add/remove fromgroup if needed - App::DocumentObjectGroup* targetGroup = nullptr; - - App::DocumentObjectGroup* group1 = obj1->getGroup(); - if (group1) { - targetGroup = group1; - Gui::Command::doCommand(Gui::Command::Doc, "App.activeDocument().%s.removeObject(App.activeDocument().%s)", - group1->getNameInDocument(), obj1->getNameInDocument()); - } - - App::DocumentObjectGroup* group2 = obj2->getGroup(); - if (group2) { - targetGroup = group2; - Gui::Command::doCommand(Gui::Command::Doc, "App.activeDocument().%s.removeObject(App.activeDocument().%s)", - group2->getNameInDocument(), obj2->getNameInDocument()); - } - - if (targetGroup) { - Gui::Command::doCommand(Gui::Command::Doc, "App.activeDocument().%s.addObject(App.activeDocument().%s)", - targetGroup->getNameInDocument(), objName.c_str()); - } - - Gui::Command::copyVisual(objName.c_str(), "ShapeColor", shapeOne.c_str()); - Gui::Command::copyVisual(objName.c_str(), "DisplayMode", shapeOne.c_str()); + "bp.%s([%s])", method.c_str(), Base::Tools::joinList(names).c_str()); activeDoc->commitTransaction(); activeDoc->recompute(); } diff --git a/src/Mod/Part/TestPartApp.py b/src/Mod/Part/TestPartApp.py index d77d3cd7ee..55e91375b2 100644 --- a/src/Mod/Part/TestPartApp.py +++ b/src/Mod/Part/TestPartApp.py @@ -831,3 +831,43 @@ class PartTestShapeFix(unittest.TestCase): fix.fixGap3d(1, False) fix.fixGap2d(1, False) fix.fixTails() + +class PartBOPTestContainer(unittest.TestCase): + def setUp(self): + self.Doc = FreeCAD.newDocument() + + def testMakeFuse(self): + box = self.Doc.addObject("Part::Box", "Box") + cyl = self.Doc.addObject("Part::Cylinder", "Cylinder") + part = self.Doc.addObject("App::Part", "Part") + part.addObject(box) + part.addObject(cyl) + from BOPTools import BOPFeatures + bp = BOPFeatures.BOPFeatures(self.Doc) + fuse = bp.make_multi_fuse([cyl.Name, box.Name]) + self.assertEqual(part, fuse.getParent()) + + def testMakeCut(self): + box = self.Doc.addObject("Part::Box", "Box") + cyl = self.Doc.addObject("Part::Cylinder", "Cylinder") + part = self.Doc.addObject("App::Part", "Part") + part.addObject(box) + part.addObject(cyl) + from BOPTools import BOPFeatures + bp = BOPFeatures.BOPFeatures(self.Doc) + fuse = bp.make_cut([cyl.Name, box.Name]) + self.assertEqual(part, fuse.getParent()) + + def testMakeCommon(self): + box = self.Doc.addObject("Part::Box", "Box") + cyl = self.Doc.addObject("Part::Cylinder", "Cylinder") + part = self.Doc.addObject("App::Part", "Part") + part.addObject(box) + part.addObject(cyl) + from BOPTools import BOPFeatures + bp = BOPFeatures.BOPFeatures(self.Doc) + fuse = bp.make_multi_common([cyl.Name, box.Name]) + self.assertEqual(part, fuse.getParent()) + + def tearDown(self): + FreeCAD.closeDocument(self.Doc.Name) diff --git a/tests/src/Base/tst_Tools.cpp b/tests/src/Base/tst_Tools.cpp index 4f2fa6051d..033aff61d7 100644 --- a/tests/src/Base/tst_Tools.cpp +++ b/tests/src/Base/tst_Tools.cpp @@ -40,3 +40,13 @@ TEST(BaseToolsSuite, TestUniqueName8) { EXPECT_EQ(Base::Tools::getUniqueName("Body12345", {"Body"}, 3), "Body12346"); } + +TEST(BaseToolsSuite, TestQuote) +{ + EXPECT_EQ(Base::Tools::quoted("Test"), "\"Test\""); +} + +TEST(BaseToolsSuite, TestJoinList) +{ + EXPECT_EQ(Base::Tools::joinList({"AB", "CD"}), "AB, CD, "); +}