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, ");
+}