diff --git a/src/Mod/Part/App/CMakeLists.txt b/src/Mod/Part/App/CMakeLists.txt index d7c1b8b1daa5..c9d9b1def224 100644 --- a/src/Mod/Part/App/CMakeLists.txt +++ b/src/Mod/Part/App/CMakeLists.txt @@ -265,6 +265,7 @@ SET(Part_Scripts Init.py TestPartApp.py MakeBottle.py + JoinFeatures.py ) add_library(Part SHARED ${Part_SRCS}) diff --git a/src/Mod/Part/CMakeLists.txt b/src/Mod/Part/CMakeLists.txt index 958502f5aae3..b08e551f5b27 100644 --- a/src/Mod/Part/CMakeLists.txt +++ b/src/Mod/Part/CMakeLists.txt @@ -11,6 +11,7 @@ INSTALL( MakeBottle.py TestPartApp.py TestPartGui.py + JoinFeatures.py DESTINATION Mod/Part ) diff --git a/src/Mod/Part/Gui/Command.cpp b/src/Mod/Part/Gui/Command.cpp index 5b900aa50750..e5234464999e 100644 --- a/src/Mod/Part/Gui/Command.cpp +++ b/src/Mod/Part/Gui/Command.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -490,6 +491,101 @@ bool CmdPartFuse::isActive(void) return getSelection().countObjectsOfType(Part::Feature::getClassTypeId())>=2; } +//=========================================================================== +// Part_CompJoinFeatures (dropdown toolbar button for Connect, Embed and Cutout) +//=========================================================================== + +DEF_STD_CMD_ACL(CmdPartCompJoinFeatures); + +CmdPartCompJoinFeatures::CmdPartCompJoinFeatures() + : Command("Part_CompJoinFeatures") +{ + sAppModule = "Part"; + sGroup = QT_TR_NOOP("Part"); + sMenuText = QT_TR_NOOP("Join objects..."); + sToolTipText = QT_TR_NOOP("Join walled objects"); + sWhatsThis = "Part_CompJoinFeatures"; + sStatusTip = sToolTipText; +} + +void CmdPartCompJoinFeatures::activated(int iMsg) +{ + Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); + if (iMsg==0) + rcCmdMgr.runCommandByName("Part_JoinConnect"); + else if (iMsg==1) + rcCmdMgr.runCommandByName("Part_JoinEmbed"); + else if (iMsg==2) + rcCmdMgr.runCommandByName("Part_JoinCutout"); + else + return; + + // Since the default icon is reset when enabing/disabling the command we have + // to explicitly set the icon of the used command. + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + QList a = pcAction->actions(); + + assert(iMsg < a.size()); + pcAction->setIcon(a[iMsg]->icon()); +} + +Gui::Action * CmdPartCompJoinFeatures::createAction(void) +{ + Gui::ActionGroup* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow()); + pcAction->setDropDownMenu(true); + applyCommandData(this->className(), pcAction); + + QAction* cmd0 = pcAction->addAction(QString()); + cmd0->setIcon(Gui::BitmapFactory().pixmap("Part_JoinConnect")); + QAction* cmd1 = pcAction->addAction(QString()); + cmd1->setIcon(Gui::BitmapFactory().pixmap("Part_JoinEmbed")); + QAction* cmd2 = pcAction->addAction(QString()); + cmd2->setIcon(Gui::BitmapFactory().pixmap("Part_JoinCutout")); + + _pcAction = pcAction; + languageChange(); + + pcAction->setIcon(cmd0->icon()); + int defaultId = 0; + pcAction->setProperty("defaultAction", QVariant(defaultId)); + + return pcAction; +} + +void CmdPartCompJoinFeatures::languageChange() +{ + Command::languageChange(); + + if (!_pcAction) + return; + + Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); + + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + QList a = pcAction->actions(); + + QAction* cmd0 = a[0]; + cmd0->setText(QApplication::translate("PartCompJoinFeatures", rcCmdMgr.getCommandByName("Part_JoinConnect")->getMenuText())); + cmd0->setToolTip(QApplication::translate("Part_JoinConnect", rcCmdMgr.getCommandByName("Part_JoinConnect")->getToolTipText())); + cmd0->setStatusTip(QApplication::translate("Part_JoinConnect", rcCmdMgr.getCommandByName("Part_JoinConnect")->getStatusTip())); + QAction* cmd1 = a[1]; + cmd1->setText(QApplication::translate("PartCompJoinFeatures", rcCmdMgr.getCommandByName("Part_JoinEmbed")->getMenuText())); + cmd1->setToolTip(QApplication::translate("Part_JoinEmbed", rcCmdMgr.getCommandByName("Part_JoinEmbed")->getToolTipText())); + cmd1->setStatusTip(QApplication::translate("Part_JoinEmbed", rcCmdMgr.getCommandByName("Part_JoinEmbed")->getStatusTip())); + QAction* cmd2 = a[2]; + cmd2->setText(QApplication::translate("PartCompJoinFeatures", rcCmdMgr.getCommandByName("Part_JoinCutout")->getMenuText())); + cmd2->setToolTip(QApplication::translate("Part_JoinCutout", rcCmdMgr.getCommandByName("Part_JoinCutout")->getToolTipText())); + cmd2->setStatusTip(QApplication::translate("Part_JoinCutout", rcCmdMgr.getCommandByName("Part_JoinCutout")->getStatusTip())); +} + +bool CmdPartCompJoinFeatures::isActive(void) +{ + if (getActiveGuiDocument()) + return true; + else + return false; +} + //=========================================================================== // Part_Compound //=========================================================================== @@ -1747,6 +1843,7 @@ void CreatePartCommands(void) rcCmdMgr.addCommand(new CmdPartCommon()); rcCmdMgr.addCommand(new CmdPartCut()); rcCmdMgr.addCommand(new CmdPartFuse()); + rcCmdMgr.addCommand(new CmdPartCompJoinFeatures()); rcCmdMgr.addCommand(new CmdPartCompound()); rcCmdMgr.addCommand(new CmdPartSection()); //rcCmdMgr.addCommand(new CmdPartBox2()); diff --git a/src/Mod/Part/Gui/Resources/Part.qrc b/src/Mod/Part/Gui/Resources/Part.qrc index 2d9c68ac0b5a..762ecba2613b 100644 --- a/src/Mod/Part/Gui/Resources/Part.qrc +++ b/src/Mod/Part/Gui/Resources/Part.qrc @@ -59,6 +59,10 @@ icons/Tree_Part_Prism.svg icons/Tree_Part_Wedge.svg icons/Part_Shape_from_Mesh.svg + icons/Part_JoinBypass.svg + icons/Part_JoinConnect.svg + icons/Part_JoinCutout.svg + icons/Part_JoinEmbed.svg translations/Part_af.qm translations/Part_de.qm translations/Part_fi.qm diff --git a/src/Mod/Part/Gui/Resources/icons/Part_JoinBypass.svg b/src/Mod/Part/Gui/Resources/icons/Part_JoinBypass.svg new file mode 100644 index 000000000000..650bf2ec0312 --- /dev/null +++ b/src/Mod/Part/Gui/Resources/icons/Part_JoinBypass.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/Mod/Part/Gui/Resources/icons/Part_JoinConnect.svg b/src/Mod/Part/Gui/Resources/icons/Part_JoinConnect.svg new file mode 100644 index 000000000000..9a4b0ffa8cf4 --- /dev/null +++ b/src/Mod/Part/Gui/Resources/icons/Part_JoinConnect.svg @@ -0,0 +1,194 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Part/Gui/Resources/icons/Part_JoinCutout.svg b/src/Mod/Part/Gui/Resources/icons/Part_JoinCutout.svg new file mode 100644 index 000000000000..8cc066dd8e83 --- /dev/null +++ b/src/Mod/Part/Gui/Resources/icons/Part_JoinCutout.svg @@ -0,0 +1,185 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Part/Gui/Resources/icons/Part_JoinEmbed.svg b/src/Mod/Part/Gui/Resources/icons/Part_JoinEmbed.svg new file mode 100644 index 000000000000..237a006c2f83 --- /dev/null +++ b/src/Mod/Part/Gui/Resources/icons/Part_JoinEmbed.svg @@ -0,0 +1,198 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Part/Gui/Workbench.cpp b/src/Mod/Part/Gui/Workbench.cpp index 123927cd36d5..0736719e7a1f 100644 --- a/src/Mod/Part/Gui/Workbench.cpp +++ b/src/Mod/Part/Gui/Workbench.cpp @@ -66,7 +66,10 @@ Gui::MenuItem* Workbench::setupMenuBar() const Gui::MenuItem* bop = new Gui::MenuItem; bop->setCommand("Boolean"); *bop << "Part_Boolean" << "Part_Cut" << "Part_Fuse" << "Part_Common"; - + + Gui::MenuItem* join = new Gui::MenuItem; + join->setCommand("Join"); + *join << "Part_JoinConnect" << "Part_JoinEmbed" << "Part_JoinCutout"; Gui::MenuItem* part = new Gui::MenuItem; root->insertItem(item, part); @@ -75,7 +78,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const *part << prim << "Part_Primitives" << "Part_Builder" << "Separator" << "Part_ShapeFromMesh" << "Part_MakeSolid" << "Part_ReverseShape" << "Part_SimpleCopy" << "Part_RefineShape" << "Part_CheckGeometry" - << "Separator" << bop << "Separator" + << "Separator" << bop << join << "Separator" << "Part_CrossSections" << "Part_Compound" << "Part_Extrude" << "Part_Revolve" << "Part_Mirror" << "Part_Fillet" << "Part_Chamfer" << "Part_RuledSurface" << "Part_Loft" << "Part_Sweep" @@ -120,7 +123,8 @@ Gui::ToolBarItem* Workbench::setupToolBars() const Gui::ToolBarItem* boolop = new Gui::ToolBarItem(root); boolop->setCommand("Boolean"); *boolop << "Part_Boolean" << "Part_Cut" << "Part_Fuse" << "Part_Common" - << "Part_CheckGeometry" << "Part_Section" << "Part_CrossSections"; + << "Part_CompJoinFeatures" << "Part_CheckGeometry" << "Part_Section" + << "Part_CrossSections"; Gui::ToolBarItem* measure = new Gui::ToolBarItem(root); measure->setCommand("Measure"); diff --git a/src/Mod/Part/InitGui.py b/src/Mod/Part/InitGui.py index 159909f8f835..1a905eb81833 100644 --- a/src/Mod/Part/InitGui.py +++ b/src/Mod/Part/InitGui.py @@ -32,8 +32,8 @@ class PartWorkbench ( Workbench ): - "Part workbench object" - Icon = """ + "Part workbench object" + Icon = """ /* XPM */ static char * part_xpm[] = { "16 16 9 1", @@ -62,15 +62,20 @@ class PartWorkbench ( Workbench ): ".@$%&****%.%.. ", " ......@##.. ", " ... "}; - """ - MenuText = "Part" - ToolTip = "Part workbench" + """ + MenuText = "Part" + ToolTip = "Part workbench" - def Initialize(self): - # load the module - import PartGui - import Part - def GetClassName(self): - return "PartGui::Workbench" + def Initialize(self): + # load the module + import PartGui + import Part + try: + import JoinFeatures + except ImportError: + print "JoinFeatures module cannot be loaded" + + def GetClassName(self): + return "PartGui::Workbench" Gui.addWorkbench(PartWorkbench()) diff --git a/src/Mod/Part/JoinFeatures.py b/src/Mod/Part/JoinFeatures.py new file mode 100644 index 000000000000..84c655a5ff00 --- /dev/null +++ b/src/Mod/Part/JoinFeatures.py @@ -0,0 +1,260 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2015 - Victor Titov (DeepSOIC) * +#* * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* This program 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +import FreeCAD, Part + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + +__title__="JoinFeatures module" +__author__ = "DeepSOIC" +__url__ = "http://www.freecadweb.org" + +#-------------------------- translation-related code ---------------------------------------- +#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!" +#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 ) +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) +#--------------------------/translation-related code ---------------------------------------- + +# -------------------------- common stuff -------------------------------------------------- +def getParamRefine(): + return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Part/Boolean").GetBool("RefineModel") + +def shapeOfMaxVol(compound): + if compound.ShapeType == 'Compound': + maxVol = 0 + cntEq = 0 + shMax = None + for sh in compound.childShapes(): + v = sh.Volume + if v > maxVol + 1e-8 : + maxVol = v + shMax = sh + cntEq = 1 + elif abs(v - maxVol) <= 1e-8 : + cntEq = cntEq + 1 + if cntEq > 1 : + raise ValueError("Equal volumes, can't figure out what to cut off!") + return shMax + else: + return compound + +def makePartJoinFeature(name, mode = 'bypass'): + '''makePartJoinFeature(name, mode = 'bypass'): makes an PartJoinFeature object.''' + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name) + _PartJoinFeature(obj) + obj.Mode = mode + obj.Refine = getParamRefine() + _ViewProviderPartJoinFeature(obj.ViewObject) + return obj + +class _PartJoinFeature: + "The PartJoinFeature object" + def __init__(self,obj): + self.Type = "PartJoinFeature" + obj.addProperty("App::PropertyEnumeration","Mode","Join","The mode of operation. bypass = make compound (fast)") + obj.Mode = ['bypass','Connect','Embed','Cutout'] + obj.addProperty("App::PropertyLink","Base","Join","First object") + obj.addProperty("App::PropertyLink","Tool","Join","Second object") + obj.addProperty("App::PropertyBool","Refine","Join","True = refine resulting shape. False = output as is.") + + obj.Proxy = self + + + def execute(self,obj): + rst = None + if obj.Mode == 'bypass': + rst = Part.makeCompound([obj.Base.Shape, obj.Tool.Shape]) + else: + cut1 = obj.Base.Shape.cut(obj.Tool.Shape) + cut1 = shapeOfMaxVol(cut1) + if obj.Mode == 'Connect': + cut2 = obj.Tool.Shape.cut(obj.Base.Shape) + cut2 = shapeOfMaxVol(cut2) + rst = cut1.multiFuse([cut2, obj.Tool.Shape.common(obj.Base.Shape)]) + elif obj.Mode == 'Embed': + rst = cut1.fuse(obj.Tool.Shape) + elif obj.Mode == 'Cutout': + rst = cut1 + if obj.Refine: + rst = rst.removeSplitter() + obj.Shape = rst + return + + +class _ViewProviderPartJoinFeature: + "A View Provider for the PartJoinFeature object" + + def __init__(self,vobj): + vobj.Proxy = self + + def getIcon(self): + if self.Object == None: + return getIconPath("Part_JoinConnect.svg") + else: + return getIconPath( { + 'bypass':"Part_JoinBypass.svg", + 'Connect':"Part_JoinConnect.svg", + 'Embed':"Part_JoinEmbed.svg", + 'Cutout':"Part_JoinCutout.svg", + }[self.Object.Mode] ) + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + + + def setEdit(self,vobj,mode): + return False + + def unsetEdit(self,vobj,mode): + return + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None + +def CreateJoinFeature(name, mode): + FreeCAD.ActiveDocument.openTransaction("Create "+mode+"ObjectsFeature") + FreeCADGui.addModule("JoinFeatures") + FreeCADGui.doCommand("j = JoinFeatures.makePartJoinFeature(name = '"+name+"', mode = '"+mode+"' )") + FreeCADGui.doCommand("j.Base = FreeCADGui.Selection.getSelection()[0]") + FreeCADGui.doCommand("j.Tool = FreeCADGui.Selection.getSelection()[1]") + FreeCADGui.doCommand("j.Proxy.execute(j)") + FreeCADGui.doCommand("j.purgeTouched()") + FreeCADGui.doCommand("j.Base.ViewObject.hide()") + FreeCADGui.doCommand("j.Tool.ViewObject.hide()") + FreeCAD.ActiveDocument.commitTransaction() + +def getIconPath(icon_dot_svg): + return ":/icons/" + icon_dot_svg + +# -------------------------- /common stuff -------------------------------------------------- + +# -------------------------- ConnectObjectsFeature -------------------------------------------------- + +class _CommandConnectFeature: + "Command to create PartJoinFeature in Connect mode" + def GetResources(self): + return {'Pixmap' : getIconPath("Part_JoinConnect.svg"), + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Connect objects"), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ConnectFeature","Fuses objects, taking care to preserve voids.")} + + def Activated(self): + if len(FreeCADGui.Selection.getSelection()) == 2 : + CreateJoinFeature(name = "Connect", mode = "Connect") + else: + mb = QtGui.QMessageBox() + mb.setIcon(mb.Icon.Warning) + mb.setText(_translate("Part_JoinFeatures", "Two solids need to be selected, first!", None)) + mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None)) + mb.exec_() + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + +FreeCADGui.addCommand('Part_JoinConnect',_CommandConnectFeature()) + +# -------------------------- /ConnectObjectsFeature -------------------------------------------------- + + +# -------------------------- EmbedFeature -------------------------------------------------- + +class _CommandEmbedFeature: + "Command to create PartJoinFeature in Embed mode" + def GetResources(self): + return {'Pixmap' : getIconPath("Part_JoinEmbed.svg"), + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Embed object"), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_EmbedFeature","Fuses one object into another, taking care to preserve voids.")} + + def Activated(self): + if len(FreeCADGui.Selection.getSelection()) == 2 : + CreateJoinFeature(name = "Embed", mode = "Embed") + else: + mb = QtGui.QMessageBox() + mb.setIcon(mb.Icon.Warning) + mb.setText(_translate("Part_JoinFeatures","Select base object, then the object to embed, and invoke this tool.", None)) + mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None)) + mb.exec_() + + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + +FreeCADGui.addCommand('Part_JoinEmbed',_CommandEmbedFeature()) + +# -------------------------- /EmbedFeature -------------------------------------------------- + + + +# -------------------------- CutoutFeature -------------------------------------------------- + +class _CommandCutoutFeature: + "Command to create PartJoinFeature in Cutout mode" + def GetResources(self): + return {'Pixmap' : getIconPath("Part_JoinCutout.svg"), + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Cutout for object"), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CutoutFeature","Makes a cutout in one object to fit another object.")} + + def Activated(self): + if len(FreeCADGui.Selection.getSelection()) == 2 : + CreateJoinFeature(name = "Cutout", mode = "Cutout") + else: + mb = QtGui.QMessageBox() + mb.setIcon(mb.Icon.Warning) + mb.setText(_translate("Part_JoinFeatures","Select the object to make a cutout in, then the object that should fit into the cutout, and invoke this tool.", None)) + mb.setWindowTitle(_translate("Part_JoinFeatures","Bad selection", None)) + mb.exec_() + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + +FreeCADGui.addCommand('Part_JoinCutout',_CommandCutoutFeature()) + +# -------------------------- /CutoutFeature --------------------------------------------------