From abfb92cb55efb1e0c41783adf7e6b93bf3d129be Mon Sep 17 00:00:00 2001 From: wmayer Date: Mon, 15 May 2023 23:40:51 +0200 Subject: [PATCH] Part: fixes #9549: Part Fuse not working inside Part container --- src/Mod/Part/BOPTools/BOPFeatures.py | 107 +++++++++++++++++++++++ src/Mod/Part/CMakeLists.txt | 1 + src/Mod/Part/Gui/Command.cpp | 103 ++++------------------ src/Mod/Part/Gui/DlgBooleanOperation.cpp | 56 +++--------- src/Mod/Part/TestPartApp.py | 40 +++++++++ 5 files changed, 178 insertions(+), 129 deletions(-) create mode 100644 src/Mod/Part/BOPTools/BOPFeatures.py 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 96fafdeca9..0acdfb10d1 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)