From b7a6558c72e733ee850fedd6c6f63a87771e936a Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Thu, 31 Aug 2023 19:30:10 +0200 Subject: [PATCH] Assembly: Initial implementation (#10427) * Assembly: Initial implementation. * Disable Assembly wb as it's WIP. * Stub code for handling assembly import. Co-authored-by: sliptonic Co-authored-by: Paddle --- .../InitializeFreeCADBuildOptions.cmake | 1 + .../DlgSettingsWorkbenchesImp.cpp | 2 +- src/Mod/AddonManager/Addon.py | 4 +- src/Mod/Assembly/App/CMakeLists.txt | 0 src/Mod/Assembly/Assembly/__init__.py | 0 src/Mod/Assembly/AssemblyGlobal.h | 48 ++ src/Mod/Assembly/AssemblyImport.py | 32 ++ src/Mod/Assembly/AssemblyTests/TestCore.py | 74 +++ .../Assembly/AssemblyTests/TestTEMPLATE.py | 74 +++ src/Mod/Assembly/AssemblyTests/__init__.py | 0 src/Mod/Assembly/CMakeLists.txt | 63 +++ src/Mod/Assembly/Commands.py | 345 ++++++++++++++ src/Mod/Assembly/Gui/CMakeLists.txt | 52 +++ src/Mod/Assembly/Gui/Resources/Assembly.qrc | 8 + .../Gui/Resources/icons/AssemblyWorkbench.svg | 343 ++++++++++++++ .../icons/AssemblyWorkbench_alternate.svg | 424 ++++++++++++++++++ .../Resources/icons/Assembly_InsertLink.svg | 381 ++++++++++++++++ .../Resources/icons/preferences-assembly.svg | 343 ++++++++++++++ .../panels/TaskAssemblyInsertLink.ui | 49 ++ .../Gui/Resources/preferences/Assembly.ui | 34 ++ src/Mod/Assembly/Init.py | 37 ++ src/Mod/Assembly/InitGui.py | 104 +++++ src/Mod/Assembly/Preferences.py | 40 ++ src/Mod/Assembly/TestAssemblyWorkbench.py | 31 ++ src/Mod/Assembly/assembly.dox | 4 + src/Mod/CMakeLists.txt | 4 + 26 files changed, 2494 insertions(+), 3 deletions(-) create mode 100644 src/Mod/Assembly/App/CMakeLists.txt create mode 100644 src/Mod/Assembly/Assembly/__init__.py create mode 100644 src/Mod/Assembly/AssemblyGlobal.h create mode 100644 src/Mod/Assembly/AssemblyImport.py create mode 100644 src/Mod/Assembly/AssemblyTests/TestCore.py create mode 100644 src/Mod/Assembly/AssemblyTests/TestTEMPLATE.py create mode 100644 src/Mod/Assembly/AssemblyTests/__init__.py create mode 100644 src/Mod/Assembly/CMakeLists.txt create mode 100644 src/Mod/Assembly/Commands.py create mode 100644 src/Mod/Assembly/Gui/CMakeLists.txt create mode 100644 src/Mod/Assembly/Gui/Resources/Assembly.qrc create mode 100644 src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench.svg create mode 100644 src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench_alternate.svg create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_InsertLink.svg create mode 100644 src/Mod/Assembly/Gui/Resources/icons/preferences-assembly.svg create mode 100644 src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui create mode 100644 src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui create mode 100644 src/Mod/Assembly/Init.py create mode 100644 src/Mod/Assembly/InitGui.py create mode 100644 src/Mod/Assembly/Preferences.py create mode 100644 src/Mod/Assembly/TestAssemblyWorkbench.py create mode 100644 src/Mod/Assembly/assembly.dox diff --git a/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake b/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake index 88081f2467..15ea3f6680 100644 --- a/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake +++ b/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake @@ -118,6 +118,7 @@ macro(InitializeFreeCADBuildOptions) option(BUILD_PART "Build the FreeCAD part module" ON) option(BUILD_PART_DESIGN "Build the FreeCAD part design module" ON) option(BUILD_PATH "Build the FreeCAD path module" ON) + option(BUILD_ASSEMBLY "Build the FreeCAD Assembly module" ON) option(BUILD_PLOT "Build the FreeCAD plot module" ON) option(BUILD_POINTS "Build the FreeCAD points module" ON) option(BUILD_REVERSEENGINEERING "Build the FreeCAD reverse engineering module" ON) diff --git a/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp b/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp index 35cfc1c1cd..a45429e7e2 100644 --- a/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp +++ b/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp @@ -433,7 +433,7 @@ QStringList DlgSettingsWorkbenchesImp::getDisabledWorkbenches() ParameterGrp::handle hGrp; hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Workbenches"); - disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench")); + disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench")); #if QT_VERSION >= QT_VERSION_CHECK(5,15,0) unfiltered_disabled_wbs_list = disabled_wbs.split(QLatin1String(","), Qt::SkipEmptyParts); #else diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py index 2356845eb3..e67614de85 100644 --- a/src/Mod/AddonManager/Addon.py +++ b/src/Mod/AddonManager/Addon.py @@ -610,7 +610,7 @@ class Addon: wbName = self.get_workbench_name() # Add the wb to the list of disabled if it was not already - disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench") + disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench") # print(f"start disabling {disabled_wbs}") disabled_wbs_list = disabled_wbs.split(",") if not (wbName in disabled_wbs_list): @@ -641,7 +641,7 @@ class Addon: def remove_from_disabled_wbs(self, wbName: str): pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches") - disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench") + disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench") # print(f"start enabling : {disabled_wbs}") disabled_wbs_list = disabled_wbs.split(",") disabled_wbs = "" diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Assembly/Assembly/__init__.py b/src/Mod/Assembly/Assembly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Assembly/AssemblyGlobal.h b/src/Mod/Assembly/AssemblyGlobal.h new file mode 100644 index 0000000000..6f0eef6d39 --- /dev/null +++ b/src/Mod/Assembly/AssemblyGlobal.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include + +#ifndef ASSEMBLY_GLOBAL_H +#define ASSEMBLY_GLOBAL_H + + + // Assembly +#ifndef AssemblyExport +#ifdef Assembly_EXPORTS +# define AssemblyExport FREECAD_DECL_EXPORT +#else +# define AssemblyExport FREECAD_DECL_IMPORT +#endif +#endif + +// AssemblyGui +#ifndef AssemblyGuiExport +#ifdef AssemblyGui_EXPORTS +# define AssemblyGuiExport FREECAD_DECL_EXPORT +#else +# define AssemblyGuiExport FREECAD_DECL_IMPORT +#endif +#endif + +#endif //ASSEMBLY_GLOBAL_H diff --git a/src/Mod/Assembly/AssemblyImport.py b/src/Mod/Assembly/AssemblyImport.py new file mode 100644 index 0000000000..be4ffb8b46 --- /dev/null +++ b/src/Mod/Assembly/AssemblyImport.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + + +def open(filename): + doc = App.activeDocument() + # here you do all what is needed with filename, read, classify data, create corresponding FreeCAD objects + doc.recompute() + + +def insert(filename, docname): + print("Inserting file: " + filename + " into document: " + docname) diff --git a/src/Mod/Assembly/AssemblyTests/TestCore.py b/src/Mod/Assembly/AssemblyTests/TestCore.py new file mode 100644 index 0000000000..e2e3796733 --- /dev/null +++ b/src/Mod/Assembly/AssemblyTests/TestCore.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +import FreeCAD +import Part +import unittest + + +class TestCore(unittest.TestCase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + pass + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + pass + + # Close geometry document without saving + # FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + pass + + def test00(self): + pass + + self.assertTrue(True) diff --git a/src/Mod/Assembly/AssemblyTests/TestTEMPLATE.py b/src/Mod/Assembly/AssemblyTests/TestTEMPLATE.py new file mode 100644 index 0000000000..d69d18f9b5 --- /dev/null +++ b/src/Mod/Assembly/AssemblyTests/TestTEMPLATE.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +import FreeCAD +import Part +import unittest + + +class TestTEMPLATE(unittest.TestCase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + pass + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + pass + + # Close geometry document without saving + # FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + pass + + def test00(self): + pass + + self.assertTrue(True) diff --git a/src/Mod/Assembly/AssemblyTests/__init__.py b/src/Mod/Assembly/AssemblyTests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt new file mode 100644 index 0000000000..26c1245418 --- /dev/null +++ b/src/Mod/Assembly/CMakeLists.txt @@ -0,0 +1,63 @@ +add_subdirectory(App) + +if(BUILD_GUI) + add_subdirectory(Gui) +endif(BUILD_GUI) + +set(Assembly_Scripts + Init.py + Commands.py + TestAssemblyWorkbench.py + Preferences.py + AssemblyImport.py +) + +if(BUILD_GUI) + list (APPEND Assembly_Scripts InitGui.py) +endif(BUILD_GUI) + +INSTALL( + FILES + ${Assembly_Scripts} + DESTINATION + Mod/Assembly +) + +SET(AssemblyScripts_SRCS + Assembly/__init__.py +) + + +SET(AssemblyTests_SRCS + AssemblyTests/__init__.py + AssemblyTests/TestCore.py +) + + +SET(all_files + ${AssemblyTests_SRCS} + ${AssemblyScripts_SRCS} +) + +ADD_CUSTOM_TARGET(AssemblyScripts ALL + SOURCES ${all_files} +) + +SET(test_files + ${Assembly_Scripts} + ${AssemblyTests_SRCS} +) + +ADD_CUSTOM_TARGET(AssemblyTests ALL + SOURCES ${test_files} +) + +fc_copy_sources(AssemblyScripts "${CMAKE_BINARY_DIR}/Mod/Assembly" ${all_files}) +fc_copy_sources(AssemblyTests "${CMAKE_BINARY_DIR}/Mod/Assembly" ${test_files}) + +INSTALL( + FILES + ${AssemblyTests_SRCS} + DESTINATION + Mod/Assembly/AssemblyTests +) diff --git a/src/Mod/Assembly/Commands.py b/src/Mod/Assembly/Commands.py new file mode 100644 index 0000000000..0fd18cbf37 --- /dev/null +++ b/src/Mod/Assembly/Commands.py @@ -0,0 +1,345 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +import os, re +import FreeCAD as App + +from PySide.QtCore import QT_TRANSLATE_NOOP + +if App.GuiUp: + import FreeCADGui as Gui + from PySide import QtCore, QtGui, QtWidgets + +# translate = App.Qt.translate + +__title__ = "Assembly Commands" +__author__ = "Ondsel" +__url__ = "https://www.freecad.org" + + +def activeAssembly(): + doc = Gui.ActiveDocument + + if doc is None or doc.ActiveView is None: + return None + + active_part = doc.ActiveView.getActiveObject("part") + + if active_part is not None and active_part.Type == "Assembly": + return active_part + + return None + + +def isDocTemporary(doc): + # Guard against older versions of FreeCad which don't have the Temporary attribute + try: + docTemporary = doc.Temporary + except AttributeError: + docTemporary = False + return docTemporary + + +def labelName(obj): + if obj: + if obj.Name == obj.Label: + txt = obj.Label + else: + txt = obj.Label + " (" + obj.Name + ")" + return txt + else: + return None + + +class CommandCreateAssembly: + def __init__(self): + pass + + def GetResources(self): + return { + "Pixmap": "Geoassembly", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateAssembly", "Create Assembly"), + "Accel": "A", + "ToolTip": QT_TRANSLATE_NOOP( + "Assembly_CreateAssembly", + "Create an assembly object in the current document.", + ), + "CmdType": "ForEdit", + } + + def IsActive(self): + return App.ActiveDocument is not None + + def Activated(self): + App.setActiveTransaction("Create assembly") + assembly = App.ActiveDocument.addObject("App::Part", "Assembly") + assembly.Type = "Assembly" + Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + App.closeActiveTransaction() + + +class CommandInsertLink: + def __init__(self): + pass + + def GetResources(self): + tooltip = "

Insert a Link into the assembly. " + tooltip += ( + "This will create dynamic links to parts/bodies/primitives/assemblies, " + ) + tooltip += "which can be in this document or in another document " + tooltip += "that is open in the current session

" + tooltip += "

Press shift to add several links while clicking on the view." + + return { + "Pixmap": "Assembly_InsertLink", + "MenuText": QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert Link"), + "Accel": "I", + "ToolTip": QT_TRANSLATE_NOOP("Assembly_InsertLink", tooltip), + "CmdType": "ForEdit", + } + + def IsActive(self): + return activeAssembly() is not None + + def Activated(self): + assembly = activeAssembly() + if not assembly: + return + view = Gui.activeDocument().activeView() + + self.panel = TaskAssemblyInsertLink(assembly, view) + Gui.Control.showDialog(self.panel) + + +class TaskAssemblyInsertLink(QtCore.QObject): + def __init__(self, assembly, view): + super().__init__() + + self.assembly = assembly + self.view = view + self.doc = App.ActiveDocument + + self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyInsertLink.ui") + self.form.installEventFilter(self) + + # Actions + self.form.openFileButton.clicked.connect(self.openFile) + self.form.partList.itemClicked.connect(self.onItemClicked) + self.form.filterPartList.textChanged.connect(self.onFilterChange) + + self.allParts = [] + self.partsDoc = [] + self.numberOfAddedParts = 0 + self.translation = 0 + self.partMoving = False + + self.buildPartList() + + App.setActiveTransaction("Insert Link") + + def accept(self): + App.closeActiveTransaction() + self.deactivated() + return True + + def reject(self): + App.closeActiveTransaction(True) + self.deactivated() + return True + + def deactivated(self): + if self.partMoving: + self.endMove() + + def buildPartList(self): + self.allParts.clear() + self.partsDoc.clear() + + docList = App.listDocuments().values() + + for doc in docList: + if isDocTemporary(doc): + continue + + for obj in doc.findObjects("App::Part"): + # we don't want to link to itself + if obj != self.assembly: + self.allParts.append(obj) + self.partsDoc.append(doc) + + for obj in doc.findObjects("PartDesign::Body"): + # but only those at top level (not nested inside other containers) + if obj.getParentGeoFeatureGroup() is None: + self.allParts.append(obj) + self.partsDoc.append(doc) + + self.form.partList.clear() + for part in self.allParts: + newItem = QtGui.QListWidgetItem() + newItem.setText(part.Document.Name + "#" + labelName(part)) + newItem.setIcon(part.ViewObject.Icon) + self.form.partList.addItem(newItem) + + def onFilterChange(self): + filterStr = self.form.filterPartList.text().strip() + for x in range(self.form.partList.count()): + item = self.form.partList.item(x) + # check the items's text match the filter ignoring the case + matchStr = re.search(filterStr, item.text(), flags=re.IGNORECASE) + if filterStr and not matchStr: + item.setHidden(True) + else: + item.setHidden(False) + + def openFile(self): + filename = None + importDoc = None + importDocIsOpen = False + dialog = QtGui.QFileDialog( + QtGui.QApplication.activeWindow(), + "Select FreeCAD document to import part from", + ) + + dialog.setNameFilter( + "Supported Formats *.FCStd *.fcstd (*.FCStd *.fcstd);;All files (*.*)" + ) + if dialog.exec_(): + filename = str(dialog.selectedFiles()[0]) + # look only for filenames, not paths, as there are problems on WIN10 (Address-translation??) + requestedFile = os.path.split(filename)[1] + # see whether the file is already open + for d in App.listDocuments().values(): + recentFile = os.path.split(d.FileName)[1] + if requestedFile == recentFile: + importDocIsOpen = True + break + # if not, open it + if not importDocIsOpen: + if filename.lower().endswith(".fcstd"): + App.openDocument(filename) + App.setActiveDocument(self.doc.Name) + self.buildPartList() + return + + def onItemClicked(self, item): + for selected in self.form.partList.selectedIndexes(): + selectedPart = self.allParts[selected.row()] + if not selectedPart: + return + + if self.partMoving: + self.endMove() + + # check that the current document had been saved or that it's the same document as that of the selected part + if not self.doc.FileName != "" and not self.doc == selectedPart.Document: + print( + "The current document must be saved before inserting an external part" + ) + return + + self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name) + self.createdLink.LinkedObject = selectedPart + self.createdLink.Placement.Base = self.getTranslationVec(selectedPart) + self.createdLink.recompute() + + self.numberOfAddedParts += 1 + + # highlight the link + Gui.Selection.clearSelection() + Gui.Selection.addSelection( + self.doc.Name, self.assembly.Name, self.createdLink.Name + "." + ) + + # Start moving the part if user brings mouse on view + self.initMove() + + def initMove(self): + self.callbackMove = self.view.addEventCallback( + "SoLocation2Event", self.moveMouse + ) + self.callbackClick = self.view.addEventCallback( + "SoMouseButtonEvent", self.clickMouse + ) + self.callbackKey = self.view.addEventCallback( + "SoKeyboardEvent", self.KeyboardEvent + ) + self.partMoving = True + + # Selection filter to avoid selecting the part while it's moving + # filter = Gui.Selection.Filter('SELECT ???') + # Gui.Selection.addSelectionGate(filter) + + def endMove(self): + self.view.removeEventCallback("SoLocation2Event", self.callbackMove) + self.view.removeEventCallback("SoMouseButtonEvent", self.callbackClick) + self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey) + self.partMoving = False + self.doc.recompute() + # Gui.Selection.removeSelectionGate() + + def moveMouse(self, info): + newPos = self.view.getPoint(*info["Position"]) + self.createdLink.Placement.Base = newPos + + def clickMouse(self, info): + if info["Button"] == "BUTTON1" and info["State"] == "DOWN": + if info["ShiftDown"]: + # Create a new link and moves this one now + currentPos = self.createdLink.Placement.Base + selectedPart = self.createdLink.LinkedObject + self.createdLink = self.assembly.newObject( + "App::Link", selectedPart.Name + ) + self.createdLink.LinkedObject = selectedPart + self.createdLink.Placement.Base = currentPos + else: + self.endMove() + + # 3D view keyboard handler + def KeyboardEvent(self, info): + if info["State"] == "UP" and info["Key"] == "ESCAPE": + self.endMove() + self.doc.removeObject(self.createdLink.Name) + + # Taskbox keyboard event handler + def eventFilter(self, watched, event): + if watched == self.form and event.type() == QtCore.QEvent.KeyPress: + if event.key() == QtCore.Qt.Key_Escape and self.partMoving: + self.endMove() + self.doc.removeObject(self.createdLink.Name) + return True # Consume the event + return super().eventFilter(watched, event) + + def getTranslationVec(self, part): + bb = part.Shape.BoundBox + if bb: + self.translation += (bb.XMax + bb.YMax + bb.ZMax) * 0.15 + else: + self.translation += 10 + return App.Vector(self.translation, self.translation, self.translation) + + +if App.GuiUp: + Gui.addCommand("Assembly_CreateAssembly", CommandCreateAssembly()) + Gui.addCommand("Assembly_InsertLink", CommandInsertLink()) diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt new file mode 100644 index 0000000000..5191a0b7ce --- /dev/null +++ b/src/Mod/Assembly/Gui/CMakeLists.txt @@ -0,0 +1,52 @@ +include_directories( + ${CMAKE_BINARY_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +set(AssemblyGui_LIBS + FreeCADGui +) + +PYSIDE_WRAP_RC(Assembly_QRC_SRCS Resources/Assembly.qrc) + +set (Assembly_TR_QRC ${CMAKE_CURRENT_BINARY_DIR}/Resources/Assembly_translation.qrc) +qt_find_and_add_translation(QM_SRCS "Resources/translations/*_*.ts" + ${CMAKE_CURRENT_BINARY_DIR}/Resources/translations) +qt_create_resource_file(${Assembly_TR_QRC} ${QM_SRCS}) +qt_add_resources(AssemblyResource_SRCS Resources/Assembly.qrc ${Assembly_TR_QRC}) + +SOURCE_GROUP("Resources" FILES ${AssemblyResource_SRCS}) + + +SET(AssemblyGui_SRCS_Module + ${Assembly_QRC_SRCS} +) + +SOURCE_GROUP("Module" FILES ${AssemblyGui_SRCS_Module}) + + +SET(AssemblyGui_SRCS + ${AssemblyResource_SRCS} + ${AssemblyGui_UIC_HDRS} + ${AssemblyGui_SRCS_Module} +) + + +SET(AssemblyGuiIcon_SVG + Resources/icons/AssemblyWorkbench.svg +) + +add_library(AssemblyGui SHARED ${AssemblyGui_SRCS} ${AssemblyGuiIcon_SVG}) +target_link_libraries(AssemblyGui ${AssemblyGui_LIBS}) + +SET_BIN_DIR(AssemblyGui AssemblyGui /Mod/Assembly) +SET_PYTHON_PREFIX_SUFFIX(AssemblyGui) + +fc_copy_sources(AssemblyGui "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Mod/Assembly" ${AssemblyGuiIcon_SVG}) +fc_target_copy_resource(AssemblyGui + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_BINARY_DIR}/Mod/Assembly + Assembly_rc.py) + +INSTALL(TARGETS AssemblyGui DESTINATION ${CMAKE_INSTALL_LIBDIR}) +INSTALL(FILES ${AssemblyGuiIcon_SVG} DESTINATION "${CMAKE_INSTALL_DATADIR}/Mod/Assembly/Resources/icons") diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc new file mode 100644 index 0000000000..59bd7b8950 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -0,0 +1,8 @@ + + + icons/Assembly_InsertLink.svg + icons/preferences-assembly.svg + panels/TaskAssemblyInsertLink.ui + preferences/Assembly.ui + + diff --git a/src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench.svg b/src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench.svg new file mode 100644 index 0000000000..768a886fc4 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-Stock + 2015-07-04 + http://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench_alternate.svg b/src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench_alternate.svg new file mode 100644 index 0000000000..8b8c99fb47 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/AssemblyWorkbench_alternate.svg @@ -0,0 +1,424 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Path-Stock + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_InsertLink.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_InsertLink.svg new file mode 100644 index 0000000000..677eda4719 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_InsertLink.svg @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Path-Stock + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/icons/preferences-assembly.svg b/src/Mod/Assembly/Gui/Resources/icons/preferences-assembly.svg new file mode 100644 index 0000000000..768a886fc4 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/preferences-assembly.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-Stock + 2015-07-04 + http://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui new file mode 100644 index 0000000000..76a641643a --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui @@ -0,0 +1,49 @@ + + + TaskAssemblyInsertLink + + + + 0 + 0 + 376 + 387 + + + + Insert Link + + + + + + Search parts... + + + + + + + + + + + + Don't find your part? + + + + + + + Open file + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui b/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui new file mode 100644 index 0000000000..83a06b97c5 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui @@ -0,0 +1,34 @@ + + + AssemblyGui::DlgSettingsAssembly + + + + 0 + 0 + 487 + 691 + + + + General + + + + + + Qt::Vertical + + + + 20 + 217 + + + + + + + + + diff --git a/src/Mod/Assembly/Init.py b/src/Mod/Assembly/Init.py new file mode 100644 index 0000000000..9207b0b484 --- /dev/null +++ b/src/Mod/Assembly/Init.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +# Get the Parameter Group of this module +ParGrp = App.ParamGet("System parameter:Modules").GetGroup("Assembly") + +# Set the needed information +ParGrp.SetString("HelpIndex", "Assembly/Help/index.html") +ParGrp.SetString("WorkBenchName", "Assembly") +ParGrp.SetString("WorkBenchModule", "AssemblyWorkbench.py") + +FreeCAD.__unit_test__ += ["TestAssemblyWorkbench"] + +# This adds a custom import type to the FreeCAD import dialog. +# The correct format for assembly interoperability is a research topic. ASMT is a placeholder. +FreeCAD.addImportType("Assembly Format (*.asmt)", "AssemblyImport") +# FreeCAD.addExportType() diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py new file mode 100644 index 0000000000..aa9af54504 --- /dev/null +++ b/src/Mod/Assembly/InitGui.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +import Assembly_rc + + +class AssemblyCommandGroup: + def __init__(self, cmdlist, menu, tooltip=None): + self.cmdlist = cmdlist + self.menu = menu + if tooltip is None: + self.tooltip = menu + else: + self.tooltip = tooltip + + def GetCommands(self): + return tuple(self.cmdlist) + + def GetResources(self): + return {"MenuText": self.menu, "ToolTip": self.tooltip} + + def IsActive(self): + if FreeCAD.ActiveDocument is not None: + return True + return False + + +class AssemblyWorkbench(Workbench): + "Assembly workbench" + + def __init__(self): + print("Loading Assembly workbench...") + self.__class__.Icon = ( + FreeCAD.getResourceDir() + + "Mod/Assembly/Resources/icons/AssemblyWorkbench.svg" + ) + self.__class__.MenuText = "Assembly" + self.__class__.ToolTip = "Assembly workbench" + + def Initialize(self): + print("Initializing Assembly workbench...") + global AssemblyCommandGroup + + translate = FreeCAD.Qt.translate + + # load the builtin modules + from PySide import QtCore, QtGui + from PySide.QtCore import QT_TRANSLATE_NOOP + import Commands + from Preferences import PreferencesPage + + # from Preferences import preferences + + FreeCADGui.addLanguagePath(":/translations") + FreeCADGui.addIconPath(":/icons") + + FreeCADGui.addPreferencePage( + PreferencesPage, QT_TRANSLATE_NOOP("QObject", "Assembly") + ) + + # build commands list + cmdlist = ["Assembly_CreateAssembly", "Assembly_InsertLink"] + + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist) + + self.appendMenu( + [QT_TRANSLATE_NOOP("Workbench", "&Assembly")], + cmdlist + ["Separator"], + ) + + print("Assembly workbench loaded") + + def Activated(self): + # update the translation engine + FreeCADGui.updateLocale() + + def Deactivated(self): + pass + + def ContextMenu(self, recipient): + pass + + +Gui.addWorkbench(AssemblyWorkbench()) diff --git a/src/Mod/Assembly/Preferences.py b/src/Mod/Assembly/Preferences.py new file mode 100644 index 0000000000..f5d9b5e57d --- /dev/null +++ b/src/Mod/Assembly/Preferences.py @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +import FreeCAD +import FreeCADGui + + +def preferences(): + return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly") + + +class PreferencesPage: + def __init__(self, parent=None): + self.form = FreeCADGui.PySideUic.loadUi(":preferences/Assembly.ui") + + def saveSettings(self): + pass + + def loadSettings(self): + pass diff --git a/src/Mod/Assembly/TestAssemblyWorkbench.py b/src/Mod/Assembly/TestAssemblyWorkbench.py new file mode 100644 index 0000000000..331d645b1d --- /dev/null +++ b/src/Mod/Assembly/TestAssemblyWorkbench.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /**************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# 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 * +# . * +# * +# ***************************************************************************/ + +import TestApp + +from AssemblyTests.TestCore import TestCore + + +# dummy usage to get flake8 and lgtm quiet +False if TestCore.__name__ else True +False if TestApp.__name__ else True diff --git a/src/Mod/Assembly/assembly.dox b/src/Mod/Assembly/assembly.dox new file mode 100644 index 0000000000..4adb844add --- /dev/null +++ b/src/Mod/Assembly/assembly.dox @@ -0,0 +1,4 @@ +/** \defgroup ASSEMBLY Assembly + * \ingroup PYTHONWORKBENCHES + * \brief Tools to build assemblies + */ diff --git a/src/Mod/CMakeLists.txt b/src/Mod/CMakeLists.txt index c83b3f197d..4bef5ea396 100644 --- a/src/Mod/CMakeLists.txt +++ b/src/Mod/CMakeLists.txt @@ -6,6 +6,10 @@ if(BUILD_ARCH) add_subdirectory(Arch) endif(BUILD_ARCH) +if(BUILD_ASSEMBLY) + add_subdirectory(Assembly) +endif(BUILD_ASSEMBLY) + if(BUILD_CLOUD) add_subdirectory(Cloud) endif(BUILD_CLOUD)