Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.
",
- ),
+ "Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.",
+ )
+ + "",
+ "CmdType": "ForEdit",
+ }
+
+ def IsActive(self):
+ return UtilsAssembly.activeAssembly() is not None
+
+ def Activated(self):
+ panel = TaskAssemblyCreateJoint(7)
+ Gui.Control.showDialog(panel)
+
+
+class CommandToggleGrounded:
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+
+ return {
+ "Pixmap": "Assembly_ToggleGrounded",
+ "MenuText": QT_TRANSLATE_NOOP("Assembly_ToggleGrounded", "Toggle grounded"),
+ "Accel": "F",
+ "ToolTip": "
"
+ + QT_TRANSLATE_NOOP(
+ "Assembly_ToggleGrounded",
+ "Toggle the grounded state of a part. Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part per assembly.",
+ )
+ + "
",
"CmdType": "ForEdit",
}
@@ -277,224 +286,40 @@ class CommandCreateJointTangent:
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
- view = Gui.activeDocument().activeView()
- self.panel = TaskAssemblyCreateJoint(assembly, view, 7)
- Gui.Control.showDialog(self.panel)
+ joint_group = UtilsAssembly.getJointGroup(assembly)
+ selection = Gui.Selection.getSelectionEx("*", 0)
+ if not selection:
+ return
-class MakeJointSelGate:
- def __init__(self, taskbox, assembly):
- self.taskbox = taskbox
- self.assembly = assembly
+ App.setActiveTransaction("Toggle grounded")
+ for sel in selection:
+ # If you select 2 solids (bodies for example) within an assembly.
+ # There'll be a single sel but 2 SubElementNames.
+ for sub in sel.SubElementNames:
- def allow(self, doc, obj, sub):
- if not sub:
- return False
+ full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub)
+ obj = UtilsAssembly.getObject(full_element_name)
- objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
-
- if self.assembly.Name not in objs_names or element_name == "":
- # Only objects within the assembly. And not whole objects, only elements.
- return False
-
- if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
- # If it's to deselect then it's ok
- return True
-
- if len(self.taskbox.current_selection) >= 2:
- # No more than 2 elements can be selected for basic joints.
- return False
-
- full_obj_name = ".".join(objs_names)
- for selection_dict in self.taskbox.current_selection:
- if selection_dict["full_obj_name"] == full_obj_name:
- # Can't join a solid to itself. So the user need to select 2 different parts.
- return False
-
- return True
-
-
-class TaskAssemblyCreateJoint(QtCore.QObject):
- def __init__(self, assembly, view, jointTypeIndex):
- super().__init__()
-
- self.assembly = assembly
- self.view = view
- self.doc = App.ActiveDocument
-
- self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
-
- self.form.jointType.addItems(JointObject.JointTypes)
- self.form.jointType.setCurrentIndex(jointTypeIndex)
+ # Check if part is grounded and if so delete the joint.
+ for joint in joint_group.Group:
+ if hasattr(joint, "ObjectToGround") and joint.ObjectToGround == obj:
+ doc = App.ActiveDocument
+ doc.removeObject(joint.Name)
+ doc.recompute()
+ return
+ # Create groundedJoint.
+ ground = joint_group.newObject("App::FeaturePython", "GroundedJoint")
+ JointObject.GroundedJoint(ground, obj)
+ JointObject.ViewProviderGroundedJoint(ground.ViewObject)
Gui.Selection.clearSelection()
- Gui.Selection.addSelectionGate(
- MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
- )
- Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
- Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
- self.current_selection = []
- self.preselection_dict = None
-
- self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
- self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
-
- App.setActiveTransaction("Create joint")
- self.createJointObject()
-
- def accept(self):
- if len(self.current_selection) != 2:
- App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
- return False
- self.deactivate()
App.closeActiveTransaction()
- return True
-
- def reject(self):
- self.deactivate()
- App.closeActiveTransaction(True)
- return True
-
- def deactivate(self):
- Gui.Selection.removeSelectionGate()
- Gui.Selection.removeObserver(self)
- Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection)
- Gui.Selection.clearSelection()
- self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
- self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
- if Gui.Control.activeDialog():
- Gui.Control.closeDialog()
-
- def createJointObject(self):
- type_index = self.form.jointType.currentIndex()
-
- joint_group = self.assembly.getObject("Joints")
-
- if not joint_group:
- joint_group = self.assembly.newObject("App::DocumentObjectGroup", "Joints")
-
- self.joint = joint_group.newObject("App::FeaturePython", "Joint")
- JointObject.Joint(self.joint, type_index)
- JointObject.ViewProviderJoint(self.joint.ViewObject, self.joint)
-
- def updateJoint(self):
- # First we build the listwidget
- self.form.featureList.clear()
- simplified_names = []
- for sel in self.current_selection:
- # TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12
- sname = sel["full_element_name"].split(self.assembly.Name + ".", 1)[-1]
- simplified_names.append(sname)
- self.form.featureList.addItems(simplified_names)
-
- # Then we pass the new list to the join object
- self.joint.Proxy.setJointConnectors(self.current_selection)
-
- def moveMouse(self, info):
- if len(self.current_selection) >= 2 or (
- len(self.current_selection) == 1
- and self.current_selection[0]["full_element_name"]
- == self.preselection_dict["full_element_name"]
- ):
- self.joint.ViewObject.Proxy.showPreviewJCS(False)
- return
-
- cursor_pos = self.view.getCursorPos()
- cursor_info = self.view.getObjectInfo(cursor_pos)
- # cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': , 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'}
-
- if (
- not cursor_info
- or not self.preselection_dict
- or cursor_info["SubName"] != self.preselection_dict["sub_name"]
- ):
- self.joint.ViewObject.Proxy.showPreviewJCS(False)
- return
-
- # newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane
-
- newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
- self.preselection_dict["mouse_pos"] = newPos
-
- self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
- self.preselection_dict
- )
-
- placement = self.joint.Proxy.findPlacement(
- self.preselection_dict["object"],
- self.preselection_dict["element_name"],
- self.preselection_dict["vertex_name"],
- )
- self.joint.ViewObject.Proxy.showPreviewJCS(True, placement)
- self.previewJCSVisible = True
-
- # 3D view keyboard handler
- def KeyboardEvent(self, info):
- if info["State"] == "UP" and info["Key"] == "ESCAPE":
- self.reject()
-
- if info["State"] == "UP" and info["Key"] == "RETURN":
- self.accept()
-
- # selectionObserver stuff
- def addSelection(self, doc_name, obj_name, sub_name, mousePos):
- full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
- full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
- selected_object = UtilsAssembly.getObject(full_element_name)
- element_name = UtilsAssembly.getElementName(full_element_name)
-
- selection_dict = {
- "object": selected_object,
- "element_name": element_name,
- "full_element_name": full_element_name,
- "full_obj_name": full_obj_name,
- "mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]),
- }
- selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
-
- self.current_selection.append(selection_dict)
- self.updateJoint()
-
- def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None):
- full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
-
- # Find and remove the corresponding dictionary from the combined list
- selection_dict_to_remove = None
- for selection_dict in self.current_selection:
- if selection_dict["full_element_name"] == full_element_name:
- selection_dict_to_remove = selection_dict
- break
-
- if selection_dict_to_remove is not None:
- self.current_selection.remove(selection_dict_to_remove)
-
- self.updateJoint()
-
- def setPreselection(self, doc_name, obj_name, sub_name):
- if not sub_name:
- self.preselection_dict = None
- return
-
- full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
- full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
- selected_object = UtilsAssembly.getObject(full_element_name)
- element_name = UtilsAssembly.getElementName(full_element_name)
-
- self.preselection_dict = {
- "object": selected_object,
- "sub_name": sub_name,
- "element_name": element_name,
- "full_element_name": full_element_name,
- "full_obj_name": full_obj_name,
- }
-
- def clearSelection(self, doc_name):
- self.current_selection.clear()
- self.updateJoint()
if App.GuiUp:
+ Gui.addCommand("Assembly_ToggleGrounded", CommandToggleGrounded())
Gui.addCommand("Assembly_CreateJointFixed", CommandCreateJointFixed())
Gui.addCommand("Assembly_CreateJointRevolute", CommandCreateJointRevolute())
Gui.addCommand("Assembly_CreateJointCylindrical", CommandCreateJointCylindrical())
diff --git a/src/Mod/Assembly/CommandExportASMT.py b/src/Mod/Assembly/CommandExportASMT.py
new file mode 100644
index 0000000000..438c669629
--- /dev/null
+++ b/src/Mod/Assembly/CommandExportASMT.py
@@ -0,0 +1,82 @@
+# 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 as App
+import UtilsAssembly
+
+from PySide.QtCore import QT_TRANSLATE_NOOP
+from PySide.QtWidgets import QFileDialog
+
+if App.GuiUp:
+ import FreeCADGui as Gui
+
+# translate = App.Qt.translate
+
+__title__ = "Assembly Command Create Assembly"
+__author__ = "Ondsel"
+__url__ = "https://www.freecad.org"
+
+
+class CommandExportASMT:
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+ return {
+ "Pixmap": "Assembly_ExportASMT",
+ "MenuText": QT_TRANSLATE_NOOP("Assembly_ExportASMT", "Export ASMT File"),
+ "Accel": "E",
+ "ToolTip": QT_TRANSLATE_NOOP(
+ "Assembly_ExportASMT",
+ "Export currently active assembly as a ASMT file.",
+ ),
+ "CmdType": "ForEdit",
+ }
+
+ def IsActive(self):
+ return UtilsAssembly.activeAssembly() is not None
+
+ def Activated(self):
+ document = App.ActiveDocument
+ if not document:
+ return
+
+ assembly = UtilsAssembly.activeAssembly()
+ if not assembly:
+ return
+
+ # Prompt the user for a file location and name
+ defaultFileName = document.Name + ".asmt"
+ filePath, _ = QFileDialog.getSaveFileName(
+ None,
+ "Save ASMT File",
+ defaultFileName,
+ "ASMT Files (*.asmt);;All Files (*)",
+ )
+
+ if filePath:
+ assembly.exportAsASMT(filePath)
+
+
+if App.GuiUp:
+ Gui.addCommand("Assembly_ExportASMT", CommandExportASMT())
diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py
index 0c5dd143b9..5f7ddd3126 100644
--- a/src/Mod/Assembly/CommandInsertLink.py
+++ b/src/Mod/Assembly/CommandInsertLink.py
@@ -110,6 +110,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
def deactivated(self):
if self.partMoving:
self.endMove()
+ self.doc.removeObject(self.createdLink.Name)
def buildPartList(self):
self.allParts.clear()
diff --git a/src/Mod/Assembly/CommandSolveAssembly.py b/src/Mod/Assembly/CommandSolveAssembly.py
new file mode 100644
index 0000000000..95ec98f0db
--- /dev/null
+++ b/src/Mod/Assembly/CommandSolveAssembly.py
@@ -0,0 +1,76 @@
+# 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
+import FreeCAD as App
+
+from PySide.QtCore import QT_TRANSLATE_NOOP
+
+if App.GuiUp:
+ import FreeCADGui as Gui
+ from PySide import QtCore, QtGui, QtWidgets
+
+import UtilsAssembly
+import Assembly_rc
+
+# translate = App.Qt.translate
+
+__title__ = "Assembly Command to Solve Assembly"
+__author__ = "Ondsel"
+__url__ = "https://www.freecad.org"
+
+
+class CommandSolveAssembly:
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+
+ return {
+ "Pixmap": "Assembly_SolveAssembly",
+ "MenuText": QT_TRANSLATE_NOOP("Assembly_SolveAssembly", "Solve Assembly"),
+ "Accel": "F",
+ "ToolTip": "
"
+ + QT_TRANSLATE_NOOP(
+ "Assembly_SolveAssembly",
+ "Solve the currently active assembly.",
+ )
+ + "
",
+ "CmdType": "ForEdit",
+ }
+
+ def IsActive(self):
+ return UtilsAssembly.activeAssembly() is not None
+
+ def Activated(self):
+ assembly = UtilsAssembly.activeAssembly()
+ if not assembly:
+ return
+
+ App.setActiveTransaction("Solve assembly")
+ assembly.solve()
+ App.closeActiveTransaction()
+
+
+if App.GuiUp:
+ Gui.addCommand("Assembly_SolveAssembly", CommandSolveAssembly())
diff --git a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
new file mode 100644
index 0000000000..c0a2e8439b
--- /dev/null
+++ b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
@@ -0,0 +1,53 @@
+// 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 "PreCompiled.h"
+
+#include
+#include
+
+#include "ViewProviderAssembly.h"
+#include "ViewProviderJointGroup.h"
+
+
+namespace AssemblyGui
+{
+extern PyObject* initModule();
+}
+
+/* Python entry */
+PyMOD_INIT_FUNC(AssemblyGui)
+{
+ PyObject* mod = AssemblyGui::initModule();
+ Base::Console().Log("Loading AssemblyGui module... done\n");
+
+
+ // NOTE: To finish the initialization of our own type objects we must
+ // call PyType_Ready, otherwise we run into a segmentation fault, later on.
+ // This function is responsible for adding inherited slots from a type's base class.
+
+ AssemblyGui::ViewProviderAssembly ::init();
+ AssemblyGui::ViewProviderJointGroup::init();
+
+ PyMOD_Return(mod);
+}
diff --git a/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp b/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp
new file mode 100644
index 0000000000..dd6f0fdf22
--- /dev/null
+++ b/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp
@@ -0,0 +1,46 @@
+// 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 "PreCompiled.h"
+
+#include
+
+
+namespace AssemblyGui
+{
+class Module: public Py::ExtensionModule
+{
+public:
+ Module()
+ : Py::ExtensionModule("AssemblyGui")
+ {
+ initialize("This module is the Assembly module."); // register with Python
+ }
+};
+
+PyObject* initModule()
+{
+ return Base::Interpreter().addModule(new Module);
+}
+
+} // namespace AssemblyGui
diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt
index 565597d318..6dcc4acb25 100644
--- a/src/Mod/Assembly/Gui/CMakeLists.txt
+++ b/src/Mod/Assembly/Gui/CMakeLists.txt
@@ -1,9 +1,13 @@
include_directories(
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}
+ ${OCC_INCLUDE_DIR}
)
set(AssemblyGui_LIBS
+ Assembly
+ PartDesign
+ PartGui
FreeCADGui
)
@@ -17,8 +21,23 @@ qt_add_resources(AssemblyResource_SRCS Resources/Assembly.qrc ${Assembly_TR_QRC}
SOURCE_GROUP("Resources" FILES ${AssemblyResource_SRCS})
+generate_from_xml(ViewProviderAssemblyPy)
+
+SET(Python_SRCS
+ ViewProviderAssemblyPy.xml
+ ViewProviderAssemblyPyImp.cpp
+)
+SOURCE_GROUP("Python" FILES ${Python_SRCS})
SET(AssemblyGui_SRCS_Module
+ AppAssemblyGui.cpp
+ AppAssemblyGuiPy.cpp
+ PreCompiled.cpp
+ PreCompiled.h
+ ViewProviderAssembly.cpp
+ ViewProviderAssembly.h
+ ViewProviderJointGroup.cpp
+ ViewProviderJointGroup.h
${Assembly_QRC_SRCS}
)
@@ -29,8 +48,14 @@ SET(AssemblyGui_SRCS
${AssemblyResource_SRCS}
${AssemblyGui_UIC_HDRS}
${AssemblyGui_SRCS_Module}
+ ${Python_SRCS}
)
+if(FREECAD_USE_PCH)
+ add_definitions(-D_PreComp_)
+ GET_MSVC_PRECOMPILED_SOURCE("PreCompiled.cpp" PCH_SRCS ${AssemblyGui_SRCS})
+ ADD_MSVC_PRECOMPILED_HEADER(PathGui PreCompiled.h PreCompiled.cpp PCH_SRCS)
+endif(FREECAD_USE_PCH)
SET(AssemblyGuiIcon_SVG
Resources/icons/AssemblyWorkbench.svg
diff --git a/src/Mod/Assembly/Gui/PreCompiled.cpp b/src/Mod/Assembly/Gui/PreCompiled.cpp
new file mode 100644
index 0000000000..ed7cfc6869
--- /dev/null
+++ b/src/Mod/Assembly/Gui/PreCompiled.cpp
@@ -0,0 +1,25 @@
+// 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 "PreCompiled.h"
diff --git a/src/Mod/Assembly/Gui/PreCompiled.h b/src/Mod/Assembly/Gui/PreCompiled.h
new file mode 100644
index 0000000000..6c1fc5583b
--- /dev/null
+++ b/src/Mod/Assembly/Gui/PreCompiled.h
@@ -0,0 +1,43 @@
+// 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 *
+ * . *
+ * *
+ ***************************************************************************/
+
+#ifndef POINTSGUI_PRECOMPILED_H
+#define POINTSGUI_PRECOMPILED_H
+
+#include
+
+#ifdef _PreComp_
+
+// STL
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+#endif //_PreComp_
+
+#endif // POINTSGUI_PRECOMPILED_H
diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc
index 8ec25f10ba..6ace2e5c34 100644
--- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc
+++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc
@@ -2,6 +2,7 @@
icons/Assembly_InsertLink.svgicons/preferences-assembly.svg
+ icons/Assembly_ToggleGrounded.svgicons/Assembly_CreateJointBall.svgicons/Assembly_CreateJointCylindrical.svgicons/Assembly_CreateJointFixed.svg
@@ -10,6 +11,7 @@
icons/Assembly_CreateJointRevolute.svgicons/Assembly_CreateJointSlider.svgicons/Assembly_CreateJointTangent.svg
+ icons/Assembly_ExportASMT.svgpanels/TaskAssemblyCreateJoint.uipanels/TaskAssemblyInsertLink.uipreferences/Assembly.ui
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg
index 84f889e6cf..33611918f7 100644
--- a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg
@@ -7,7 +7,7 @@
id="svg2821"
sodipodi:version="0.32"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
- sodipodi:docname="Assembly_CreateJointFixed.svg"
+ sodipodi:docname="Assembly_CreateJointFixedNew.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
@@ -175,6 +175,17 @@
x2="85"
y2="35"
spreadMethod="reflect" />
+
+
+
+
+ inkscape:deskcolor="#d1d1d1"
+ showguides="true">
+
+
+
@@ -261,46 +294,51 @@
transform="translate(3.6192085e-6,-0.89630564)">
+ sodipodi:nodetypes="ccczccc" />
+ cy="31.700123"
+ ry="7.9999995"
+ rx="24" />
+
+ sodipodi:nodetypes="scczcccccs" />
+ sodipodi:nodetypes="scczcccccs" />
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg
new file mode 100644
index 0000000000..50b1905bd0
--- /dev/null
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg
@@ -0,0 +1,944 @@
+
+
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg
new file mode 100644
index 0000000000..07c83f3a85
--- /dev/null
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg
@@ -0,0 +1,441 @@
+
+
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
new file mode 100644
index 0000000000..97c3ab4935
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
@@ -0,0 +1,398 @@
+// 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 "PreCompiled.h"
+
+#ifndef _PreComp_
+#include
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "ViewProviderAssembly.h"
+#include "ViewProviderAssemblyPy.h"
+
+
+using namespace Assembly;
+using namespace AssemblyGui;
+
+PROPERTY_SOURCE(AssemblyGui::ViewProviderAssembly, Gui::ViewProviderPart)
+
+ViewProviderAssembly::ViewProviderAssembly()
+ : SelectionObserver(true)
+ , canStartDragging(false)
+ , partMoving(false)
+ , enableMovement(true)
+ , docsToMove({})
+{}
+
+ViewProviderAssembly::~ViewProviderAssembly() = default;
+
+QIcon ViewProviderAssembly::getIcon() const
+{
+ return Gui::BitmapFactory().pixmap("Geoassembly.svg");
+}
+
+bool ViewProviderAssembly::doubleClicked()
+{
+ if (isInEditMode()) {
+ // Part is already 'Active' so we exit edit mode.
+ Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()");
+ }
+ else {
+ // Part is not 'Active' so we enter edit mode to make it so.
+ Gui::Application::Instance->activeDocument()->setEdit(this);
+ }
+
+ return true;
+}
+
+
+bool ViewProviderAssembly::setEdit(int ModNum)
+{
+ // Set the part as 'Activated' ie bold in the tree.
+ Gui::Command::doCommand(Gui::Command::Gui,
+ "Gui.ActiveDocument.ActiveView.setActiveObject('%s', "
+ "App.getDocument('%s').getObject('%s'))",
+ PARTKEY,
+ this->getObject()->getDocument()->getName(),
+ this->getObject()->getNameInDocument());
+
+ return true;
+}
+
+void ViewProviderAssembly::unsetEdit(int ModNum)
+{
+ Q_UNUSED(ModNum);
+
+ canStartDragging = false;
+ partMoving = false;
+ docsToMove = {};
+
+ // Set the part as not 'Activated' ie not bold in the tree.
+ Gui::Command::doCommand(Gui::Command::Gui,
+ "Gui.ActiveDocument.ActiveView.setActiveObject('%s', None)",
+ PARTKEY);
+}
+
+bool ViewProviderAssembly::isInEditMode()
+{
+ App::DocumentObject* activePart = getActivePart();
+ if (!activePart) {
+ return false;
+ }
+
+ return activePart == this->getObject();
+}
+
+App::DocumentObject* ViewProviderAssembly::getActivePart()
+{
+ App::DocumentObject* activePart = nullptr;
+ auto activeDoc = Gui::Application::Instance->activeDocument();
+ if (!activeDoc) {
+ activeDoc = getDocument();
+ }
+ auto activeView = activeDoc->setActiveView(this);
+ if (!activeView) {
+ return nullptr;
+ }
+
+ activePart = activeView->getActiveObject(PARTKEY);
+ return activePart;
+}
+
+bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer)
+{
+ // Base::Console().Warning("Mouse move\n");
+
+ // Initialize or end the dragging of parts
+ if (canStartDragging) {
+ canStartDragging = false;
+
+ if (enableMovement && getSelectedObjectsWithinAssembly()) {
+ SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos);
+ Base::Vector3d mousePosition = Base::Vector3d(vec[0], vec[1], vec[2]);
+
+ initMove(mousePosition);
+ }
+ }
+
+ // Do the dragging of parts
+ if (partMoving) {
+ SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos);
+ Base::Vector3d mousePosition = Base::Vector3d(vec[0], vec[1], vec[2]);
+ for (auto& pair : docsToMove) {
+ App::DocumentObject* obj = pair.first;
+ auto* propPlacement =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ if (propPlacement) {
+ Base::Placement plc = propPlacement->getValue();
+ // Base::Console().Warning("transl %f %f %f\n", pair.second.x, pair.second.y,
+ // pair.second.z);
+ Base::Vector3d pos = mousePosition + pair.second;
+ Base::Placement newPlacement = Base::Placement(pos, plc.getRotation());
+ propPlacement->setValue(newPlacement);
+ }
+ }
+
+ auto* assemblyPart = static_cast(getObject());
+ assemblyPart->solve();
+ }
+ return false;
+}
+
+bool ViewProviderAssembly::mouseButtonPressed(int Button,
+ bool pressed,
+ const SbVec2s& cursorPos,
+ const Gui::View3DInventorViewer* viewer)
+{
+ // Left Mouse button ****************************************************
+ if (Button == 1) {
+ if (pressed) {
+ canStartDragging = true;
+ }
+ else { // Button 1 released
+ // release event is not received when user click on a part for selection.
+ // So we use SelectionObserver to know if something got selected.
+
+ canStartDragging = false;
+ if (partMoving) {
+ endMove();
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool ViewProviderAssembly::getSelectedObjectsWithinAssembly()
+{
+ // check the current selection, and check if any of the selected objects are within this
+ // App::Part
+ // If any, put them into the vector docsToMove and return true.
+ // Get the document
+ Gui::Document* doc = Gui::Application::Instance->activeDocument();
+
+ if (!doc) {
+ return false;
+ }
+
+ // Get the assembly object for this ViewProvider
+ AssemblyObject* assemblyPart = static_cast(getObject());
+
+ if (!assemblyPart) {
+ return false;
+ }
+
+ for (auto& selObj : Gui::Selection().getSelectionEx("",
+ App::DocumentObject::getClassTypeId(),
+ Gui::ResolveMode::NoResolve)) {
+ // getSubNames() returns ["Body001.Pad.Face14", "Body002.Pad.Face7"]
+ // if you have several objects within the same assembly selected.
+
+ std::vector objsSubNames = selObj.getSubNames();
+ for (auto& subNamesStr : objsSubNames) {
+ std::vector subNames = parseSubNames(subNamesStr);
+
+ App::DocumentObject* obj = getObjectFromSubNames(subNames);
+ if (!obj) {
+ continue;
+ }
+
+ // Check if the selected object is a child of the assembly
+ if (assemblyPart->hasObject(obj, true)) {
+ auto* propPlacement =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ if (propPlacement) {
+ Base::Placement plc = propPlacement->getValue();
+ Base::Vector3d pos = plc.getPosition();
+ docsToMove.emplace_back(obj, pos);
+ }
+ }
+ }
+ }
+
+ // This function is called before the selection is updated. So if a user click and drag a part
+ // it is not selected at that point. So we need to get the preselection too.
+ if (Gui::Selection().hasPreselection()) {
+
+ // Base::Console().Warning("Gui::Selection().getPreselection().pSubName %s\n",
+ // Gui::Selection().getPreselection().pSubName);
+
+ std::string subNamesStr = Gui::Selection().getPreselection().pSubName;
+ std::vector subNames = parseSubNames(subNamesStr);
+
+ App::DocumentObject* preselectedObj = getObjectFromSubNames(subNames);
+ if (preselectedObj) {
+ if (assemblyPart->hasObject(preselectedObj, true)) {
+ bool alreadyIn = false;
+ for (auto& pair : docsToMove) {
+ App::DocumentObject* obj = pair.first;
+ if (obj == preselectedObj) {
+ alreadyIn = true;
+ break;
+ }
+ }
+
+ if (!alreadyIn) {
+ auto* propPlacement = dynamic_cast(
+ preselectedObj->getPropertyByName("Placement"));
+ if (propPlacement) {
+ Base::Placement plc = propPlacement->getValue();
+ Base::Vector3d pos = plc.getPosition();
+ docsToMove.emplace_back(preselectedObj, pos);
+ }
+ }
+ }
+ }
+ }
+
+ return !docsToMove.empty();
+}
+
+std::vector ViewProviderAssembly::parseSubNames(std::string& subNamesStr)
+{
+ std::vector subNames;
+ std::string subName;
+ std::istringstream subNameStream(subNamesStr);
+ while (std::getline(subNameStream, subName, '.')) {
+ subNames.push_back(subName);
+ }
+ return subNames;
+}
+
+App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vector& subNames)
+{
+ App::Document* appDoc = App::GetApplication().getActiveDocument();
+
+ std::string objName;
+ if (subNames.size() < 2) {
+ return nullptr;
+ }
+ else if (subNames.size() == 2) {
+ // If two subnames then it can't be a body and the object we want is the first one
+ // For example we want box in "box.face1"
+ return appDoc->getObject(subNames[0].c_str());
+ }
+ else {
+ objName = subNames[subNames.size() - 3];
+
+ App::DocumentObject* obj = appDoc->getObject(objName.c_str());
+ if (!obj) {
+ return nullptr;
+ }
+ if (obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
+ return obj;
+ }
+ else if (obj->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) {
+
+ App::Link* link = dynamic_cast(obj);
+
+ App::DocumentObject* linkedObj = link->getLinkedObject(true);
+
+ if (linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
+ return obj;
+ }
+ }
+
+ // then its neither a body or a link to a body.
+ objName = subNames[subNames.size() - 2];
+ return appDoc->getObject(objName.c_str());
+ }
+}
+
+void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition)
+{
+ partMoving = true;
+
+ // prevent selection while moving
+ auto* view = dynamic_cast(
+ Gui::Application::Instance->editDocument()->getActiveView());
+ if (view) {
+ Gui::View3DInventorViewer* viewerNotConst;
+ viewerNotConst = static_cast(view)->getViewer();
+ viewerNotConst->setSelectionEnabled(false);
+ }
+
+ objectMasses.clear();
+
+ for (auto& pair : docsToMove) {
+ pair.second = pair.second - mousePosition;
+ objectMasses.push_back({pair.first, 10.0});
+ }
+
+ auto* assemblyPart = static_cast(getObject());
+ assemblyPart->setObjMasses(objectMasses);
+}
+
+void ViewProviderAssembly::endMove()
+{
+ docsToMove = {};
+ partMoving = false;
+ canStartDragging = false;
+
+ // enable selection after the move
+ auto* view = dynamic_cast(
+ Gui::Application::Instance->editDocument()->getActiveView());
+ if (view) {
+ Gui::View3DInventorViewer* viewerNotConst;
+ viewerNotConst = static_cast(view)->getViewer();
+ viewerNotConst->setSelectionEnabled(true);
+ }
+
+ auto* assemblyPart = static_cast(getObject());
+ assemblyPart->setObjMasses({});
+}
+
+
+void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg)
+{
+ if (msg.Type == Gui::SelectionChanges::AddSelection
+ || msg.Type == Gui::SelectionChanges::ClrSelection
+ || msg.Type == Gui::SelectionChanges::RmvSelection) {
+ canStartDragging = false;
+ }
+}
+
+PyObject* ViewProviderAssembly::getPyObject()
+{
+ if (!pyViewObject) {
+ pyViewObject = new ViewProviderAssemblyPy(this);
+ }
+ pyViewObject->IncRef();
+ return pyViewObject;
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h
new file mode 100644
index 0000000000..dd0378710b
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h
@@ -0,0 +1,106 @@
+// 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 *
+ * . *
+ * *
+ ***************************************************************************/
+
+#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
+#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
+
+#include
+
+#include
+#include
+
+namespace Gui
+{
+class View3DInventorViewer;
+}
+
+namespace AssemblyGui
+{
+
+class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart,
+ public Gui::SelectionObserver
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly);
+
+public:
+ ViewProviderAssembly();
+ ~ViewProviderAssembly() override;
+
+ /// deliver the icon shown in the tree view. Override from ViewProvider.h
+ QIcon getIcon() const override;
+
+ bool doubleClicked() override;
+
+ /** @name enter/exit edit mode */
+ //@{
+ bool setEdit(int ModNum) override;
+ void unsetEdit(int ModNum) override;
+ bool isInEditMode();
+
+ App::DocumentObject* getActivePart();
+
+ /// is called when the provider is in edit and the mouse is moved
+ bool mouseMove(const SbVec2s& pos, Gui::View3DInventorViewer* viewer) override;
+ /// is called when the Provider is in edit and the mouse is clicked
+ bool mouseButtonPressed(int Button,
+ bool pressed,
+ const SbVec2s& cursorPos,
+ const Gui::View3DInventorViewer* viewer) override;
+
+ void initMove(Base::Vector3d& mousePosition);
+ void endMove();
+
+ bool getSelectedObjectsWithinAssembly();
+ App::DocumentObject* getObjectFromSubNames(std::vector& subNames);
+ std::vector parseSubNames(std::string& subNamesStr);
+
+ /// Get the python wrapper for that ViewProvider
+ PyObject* getPyObject() override;
+
+ virtual void setEnableMovement(bool enable = true)
+ {
+ enableMovement = enable;
+ }
+ virtual bool getEnableMovement() const
+ {
+ return enableMovement;
+ }
+
+ // protected:
+ /// get called by the container whenever a property has been changed
+ // void onChanged(const App::Property* prop) override;
+
+ void onSelectionChanged(const Gui::SelectionChanges& msg) override;
+
+ bool canStartDragging;
+ bool partMoving;
+ bool enableMovement;
+ int numberOfSel;
+
+ std::vector> objectMasses;
+ std::vector> docsToMove;
+};
+
+} // namespace AssemblyGui
+
+#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml b/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml
new file mode 100644
index 0000000000..503bf213ab
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ This is the ViewProviderAssembly class
+
+
+
+ Enable moving the parts by clicking and dragging.
+
+
+
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp
new file mode 100644
index 0000000000..31a4090a65
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp
@@ -0,0 +1,59 @@
+/***************************************************************************
+ * Copyright (c) 2008 Werner Mayer *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Library General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2 of the License, or (at your option) any later version. *
+ * *
+ * This library 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 library; see the file COPYING.LIB. If not, *
+ * write to the Free Software Foundation, Inc., 59 Temple Place, *
+ * Suite 330, Boston, MA 02111-1307, USA *
+ * *
+ ***************************************************************************/
+
+#include "PreCompiled.h"
+
+// inclusion of the generated files (generated out of ViewProviderAssemblyPy.xml)
+#include "ViewProviderAssemblyPy.h"
+#include "ViewProviderAssemblyPy.cpp"
+
+
+using namespace Gui;
+
+// returns a string which represents the object e.g. when printed in python
+std::string ViewProviderAssemblyPy::representation() const
+{
+ std::stringstream str;
+ str << "";
+
+ return str.str();
+}
+
+Py::Boolean ViewProviderAssemblyPy::getEnableMovement() const
+{
+ return {getViewProviderAssemblyPtr()->getEnableMovement()};
+}
+
+void ViewProviderAssemblyPy::setEnableMovement(Py::Boolean arg)
+{
+ getViewProviderAssemblyPtr()->setEnableMovement(arg);
+}
+
+PyObject* ViewProviderAssemblyPy::getCustomAttributes(const char* /*attr*/) const
+{
+ return nullptr;
+}
+
+int ViewProviderAssemblyPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
+{
+ return 0;
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
new file mode 100644
index 0000000000..abfa1db8c0
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
@@ -0,0 +1,49 @@
+// 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 "PreCompiled.h"
+
+#ifndef _PreComp_
+#endif
+
+#include
+#include
+#include
+#include
+
+#include "ViewProviderJointGroup.h"
+
+
+using namespace AssemblyGui;
+
+PROPERTY_SOURCE(AssemblyGui::ViewProviderJointGroup, Gui::ViewProviderDocumentObjectGroup)
+
+ViewProviderJointGroup::ViewProviderJointGroup()
+{}
+
+ViewProviderJointGroup::~ViewProviderJointGroup() = default;
+
+QIcon ViewProviderJointGroup::getIcon() const
+{
+ return Gui::BitmapFactory().pixmap("Assembly_CreateJointFixed.svg");
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
new file mode 100644
index 0000000000..fb965e9c2a
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
@@ -0,0 +1,53 @@
+// 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 *
+ * . *
+ * *
+ ***************************************************************************/
+
+#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H
+#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H
+
+#include
+
+#include
+
+
+namespace AssemblyGui
+{
+
+class AssemblyGuiExport ViewProviderJointGroup: public Gui::ViewProviderDocumentObjectGroup
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderJointGroup);
+
+public:
+ ViewProviderJointGroup();
+ ~ViewProviderJointGroup() override;
+
+ /// deliver the icon shown in the tree view. Override from ViewProvider.h
+ QIcon getIcon() const override;
+
+ // protected:
+ /// get called by the container whenever a property has been changed
+ // void onChanged(const App::Property* prop) override;
+};
+
+} // namespace AssemblyGui
+
+#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H
diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py
index 7df9fb15d4..c45bae3a9d 100644
--- a/src/Mod/Assembly/InitGui.py
+++ b/src/Mod/Assembly/InitGui.py
@@ -49,7 +49,6 @@ class AssemblyWorkbench(Workbench):
"Assembly workbench"
def __init__(self):
- print("Loading Assembly workbench...")
self.__class__.Icon = (
FreeCAD.getResourceDir() + "Mod/Assembly/Resources/icons/AssemblyWorkbench.svg"
)
@@ -65,7 +64,7 @@ class AssemblyWorkbench(Workbench):
# load the builtin modules
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
- import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint
+ import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint, CommandSolveAssembly, CommandExportASMT
from Preferences import PreferencesPage
# from Preferences import preferences
@@ -76,8 +75,16 @@ class AssemblyWorkbench(Workbench):
FreeCADGui.addPreferencePage(PreferencesPage, QT_TRANSLATE_NOOP("QObject", "Assembly"))
# build commands list
- cmdlist = ["Assembly_CreateAssembly", "Assembly_InsertLink"]
+ cmdlist = [
+ "Assembly_CreateAssembly",
+ "Assembly_InsertLink",
+ "Assembly_SolveAssembly",
+ "Assembly_ExportASMT",
+ ]
+
cmdListJoints = [
+ "Assembly_ToggleGrounded",
+ "Separator",
"Assembly_CreateJointFixed",
"Assembly_CreateJointRevolute",
"Assembly_CreateJointCylindrical",
diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py
index 92aebfb10c..b4eaffd432 100644
--- a/src/Mod/Assembly/JointObject.py
+++ b/src/Mod/Assembly/JointObject.py
@@ -26,6 +26,7 @@ import math
import FreeCAD as App
import Part
+from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
@@ -54,8 +55,9 @@ JointTypes = [
class Joint:
def __init__(self, joint, type_index):
+ self.Type = "Joint"
+
joint.Proxy = self
- self.joint = joint
joint.addProperty(
"App::PropertyEnumeration",
@@ -130,7 +132,18 @@ class Joint:
),
)
- self.setJointConnectors([])
+ self.setJointConnectors(joint, [])
+
+ def __getstate__(self):
+ return self.Type
+
+ def __setstate__(self, state):
+ if state:
+ self.Type = state
+
+ def setJointType(self, joint, jointType):
+ joint.JointType = jointType
+ joint.Label = jointType.replace(" ", "")
def onChanged(self, fp, prop):
"""Do something when a property has changed"""
@@ -142,34 +155,34 @@ class Joint:
# App.Console.PrintMessage("Recompute Python Box feature\n")
pass
- def setJointConnectors(self, current_selection):
+ def setJointConnectors(self, joint, current_selection):
# current selection is a vector of strings like "Assembly.Assembly1.Assembly2.Body.Pad.Edge16" including both what selection return as obj_name and obj_sub
if len(current_selection) >= 1:
- self.joint.Object1 = current_selection[0]["object"]
- self.joint.Element1 = current_selection[0]["element_name"]
- self.joint.Vertex1 = current_selection[0]["vertex_name"]
- self.joint.Placement1 = self.findPlacement(
- self.joint.Object1, self.joint.Element1, self.joint.Vertex1
- )
+ joint.Object1 = current_selection[0]["object"]
+ joint.Element1 = current_selection[0]["element_name"]
+ joint.Vertex1 = current_selection[0]["vertex_name"]
+ joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
else:
- self.joint.Object1 = None
- self.joint.Element1 = ""
- self.joint.Vertex1 = ""
- self.joint.Placement1 = UtilsAssembly.activeAssembly().Placement
+ joint.Object1 = None
+ joint.Element1 = ""
+ joint.Vertex1 = ""
+ joint.Placement1 = UtilsAssembly.activeAssembly().Placement
if len(current_selection) >= 2:
- self.joint.Object2 = current_selection[1]["object"]
- self.joint.Element2 = current_selection[1]["element_name"]
- self.joint.Vertex2 = current_selection[1]["vertex_name"]
- self.joint.Placement2 = self.findPlacement(
- self.joint.Object2, self.joint.Element2, self.joint.Vertex2
- )
+ joint.Object2 = current_selection[1]["object"]
+ joint.Element2 = current_selection[1]["element_name"]
+ joint.Vertex2 = current_selection[1]["vertex_name"]
+ joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
else:
- self.joint.Object2 = None
- self.joint.Element2 = ""
- self.joint.Vertex2 = ""
- self.joint.Placement2 = UtilsAssembly.activeAssembly().Placement
+ joint.Object2 = None
+ joint.Element2 = ""
+ joint.Vertex2 = ""
+ joint.Placement2 = UtilsAssembly.activeAssembly().Placement
+
+ def updateJCSPlacements(self, joint):
+ joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
+ joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
@@ -182,7 +195,11 @@ class Joint:
"""
def findPlacement(self, obj, elt, vtx):
- plc = App.Placement(obj.Placement)
+ plc = App.Placement()
+
+ if not obj or not elt or not vtx:
+ return App.Placement()
+
elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
@@ -234,12 +251,18 @@ class Joint:
if surface.TypeId == "Part::GeomPlane":
plc.Rotation = App.Rotation(surface.Rotation)
+ # Now plc is the placement in the doc. But we need the placement relative to the solid origin.
return plc
class ViewProviderJoint:
- def __init__(self, obj, app_obj):
+ def __init__(self, vobj):
"""Set this object to the proxy object of the actual view provider"""
+
+ vobj.Proxy = self
+
+ def attach(self, vobj):
+ """Setup the scene sub-graph of the view provider, this method is mandatory"""
self.axis_thickness = 3
view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
@@ -258,11 +281,8 @@ class ViewProviderJoint:
self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera)
self.cameraSensor.attach(camera.height)
- self.app_obj = app_obj
- obj.Proxy = self
+ self.app_obj = vobj.Object
- def attach(self, obj):
- """Setup the scene sub-graph of the view provider, this method is mandatory"""
self.transform1 = coin.SoTransform()
self.transform2 = coin.SoTransform()
self.transform3 = coin.SoTransform()
@@ -275,21 +295,21 @@ class ViewProviderJoint:
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = self.axis_thickness
- self.switch_JCS1 = self.JCS_sep(obj, self.transform1)
- self.switch_JCS2 = self.JCS_sep(obj, self.transform2)
- self.switch_JCS_preview = self.JCS_sep(obj, self.transform3)
+ self.switch_JCS1 = self.JCS_sep(self.transform1)
+ self.switch_JCS2 = self.JCS_sep(self.transform2)
+ self.switch_JCS_preview = self.JCS_sep(self.transform3)
self.display_mode = coin.SoGroup()
self.display_mode.addChild(self.switch_JCS1)
self.display_mode.addChild(self.switch_JCS2)
self.display_mode.addChild(self.switch_JCS_preview)
- obj.addDisplayMode(self.display_mode, "Wireframe")
+ vobj.addDisplayMode(self.display_mode, "Wireframe")
def camera_callback(self, *args):
scaleF = self.get_JCS_size()
self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
- def JCS_sep(self, obj, soTransform):
+ def JCS_sep(self, soTransform):
pick = coin.SoPickStyle()
pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
@@ -424,21 +444,21 @@ class ViewProviderJoint:
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
def getIcon(self):
- if self.app_obj.getPropertyByName("JointType") == "Fixed":
+ if self.app_obj.JointType == "Fixed":
return ":/icons/Assembly_CreateJointFixed.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Revolute":
+ elif self.app_obj.JointType == "Revolute":
return ":/icons/Assembly_CreateJointRevolute.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Cylindrical":
+ elif self.app_obj.JointType == "Cylindrical":
return ":/icons/Assembly_CreateJointCylindrical.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Slider":
+ elif self.app_obj.JointType == "Slider":
return ":/icons/Assembly_CreateJointSlider.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Ball":
+ elif self.app_obj.JointType == "Ball":
return ":/icons/Assembly_CreateJointBall.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Planar":
+ elif self.app_obj.JointType == "Planar":
return ":/icons/Assembly_CreateJointPlanar.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Parallel":
+ elif self.app_obj.JointType == "Parallel":
return ":/icons/Assembly_CreateJointParallel.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Tangent":
+ elif self.app_obj.JointType == "Tangent":
return ":/icons/Assembly_CreateJointTangent.svg"
return ":/icons/Assembly_CreateJoint.svg"
@@ -453,3 +473,374 @@ class ViewProviderJoint:
"""When restoring the serialized object from document we have the chance to set some internals here.\
Since no data were serialized nothing needs to be done here."""
return None
+
+ def doubleClicked(self, vobj):
+ panel = TaskAssemblyCreateJoint(0, vobj.Object)
+ Gui.Control.showDialog(panel)
+
+
+################ Grounded Joint object #################
+
+
+class GroundedJoint:
+ def __init__(self, joint, obj_to_ground):
+ self.Type = "GoundedJoint"
+ joint.Proxy = self
+ self.joint = joint
+
+ joint.addProperty(
+ "App::PropertyLink",
+ "ObjectToGround",
+ "Ground",
+ QT_TRANSLATE_NOOP("App::Property", "The object to ground"),
+ )
+
+ joint.ObjectToGround = obj_to_ground
+
+ joint.addProperty(
+ "App::PropertyPlacement",
+ "Placement",
+ "Ground",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "This is where the part is grounded.",
+ ),
+ )
+
+ joint.Placement = obj_to_ground.Placement
+
+ def __getstate__(self):
+ return self.Type
+
+ def __setstate__(self, state):
+ if state:
+ self.Type = state
+
+ def onChanged(self, fp, prop):
+ """Do something when a property has changed"""
+ # App.Console.PrintMessage("Change property: " + str(prop) + "\n")
+ pass
+
+ def execute(self, fp):
+ """Do something when doing a recomputation, this method is mandatory"""
+ # App.Console.PrintMessage("Recompute Python Box feature\n")
+ pass
+
+
+class ViewProviderGroundedJoint:
+ def __init__(self, obj):
+ """Set this object to the proxy object of the actual view provider"""
+ obj.Proxy = self
+
+ def attach(self, obj):
+ """Setup the scene sub-graph of the view provider, this method is mandatory"""
+ pass
+
+ def updateData(self, fp, prop):
+ """If a property of the handled feature has changed we have the chance to handle this here"""
+ # fp is the handled feature, prop is the name of the property that has changed
+ pass
+
+ def getDisplayModes(self, obj):
+ """Return a list of display modes."""
+ modes = ["Wireframe"]
+ return modes
+
+ def getDefaultDisplayMode(self):
+ """Return the name of the default display mode. It must be defined in getDisplayModes."""
+ return "Wireframe"
+
+ def onChanged(self, vp, prop):
+ """Here we can do something when a single property got changed"""
+ # App.Console.PrintMessage("Change property: " + str(prop) + "\n")
+ pass
+
+ def getIcon(self):
+ return ":/icons/Assembly_ToggleGrounded.svg"
+
+
+class MakeJointSelGate:
+ def __init__(self, taskbox, assembly):
+ self.taskbox = taskbox
+ self.assembly = assembly
+
+ def allow(self, doc, obj, sub):
+ if not sub:
+ return False
+
+ objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
+
+ if self.assembly.Name not in objs_names or element_name == "":
+ # Only objects within the assembly. And not whole objects, only elements.
+ return False
+
+ if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
+ # If it's to deselect then it's ok
+ return True
+
+ if len(self.taskbox.current_selection) >= 2:
+ # No more than 2 elements can be selected for basic joints.
+ return False
+
+ full_obj_name = ".".join(objs_names)
+ full_element_name = full_obj_name + "." + element_name
+ selected_object = UtilsAssembly.getObject(full_element_name)
+
+ for selection_dict in self.taskbox.current_selection:
+ if selection_dict["object"] == selected_object:
+ # Can't join a solid to itself. So the user need to select 2 different parts.
+ return False
+
+ return True
+
+
+class TaskAssemblyCreateJoint(QtCore.QObject):
+ def __init__(self, jointTypeIndex, jointObj=None):
+ super().__init__()
+
+ self.assembly = UtilsAssembly.activeAssembly()
+ self.view = Gui.activeDocument().activeView()
+ self.doc = App.ActiveDocument
+
+ if not self.assembly or not self.view or not self.doc:
+ return
+
+ self.assembly.ViewObject.EnableMovement = False
+
+ self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
+
+ self.form.jointType.addItems(JointTypes)
+ self.form.jointType.setCurrentIndex(jointTypeIndex)
+ self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged)
+
+ Gui.Selection.clearSelection()
+
+ if jointObj:
+ self.joint = jointObj
+ self.jointName = jointObj.Label
+ App.setActiveTransaction("Edit " + self.jointName + " Joint")
+
+ self.updateTaskboxFromJoint()
+
+ else:
+ self.jointName = self.form.jointType.currentText().replace(" ", "")
+ App.setActiveTransaction("Create " + self.jointName + " Joint")
+
+ self.current_selection = []
+ self.preselection_dict = None
+
+ self.createJointObject()
+
+ Gui.Selection.addSelectionGate(
+ MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
+ )
+ Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
+ Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
+
+ self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
+ self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
+
+ def accept(self):
+ if len(self.current_selection) != 2:
+ App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
+ return False
+
+ # Hide JSC's when joint is created and enable selection highlighting
+ # self.joint.ViewObject.Visibility = False
+ # self.joint.ViewObject.OnTopWhenSelected = "Enabled"
+
+ self.deactivate()
+
+ self.assembly.solve()
+
+ App.closeActiveTransaction()
+ return True
+
+ def reject(self):
+ self.deactivate()
+ App.closeActiveTransaction(True)
+ return True
+
+ def deactivate(self):
+ self.assembly.ViewObject.EnableMovement = True
+ Gui.Selection.removeSelectionGate()
+ Gui.Selection.removeObserver(self)
+ Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection)
+ Gui.Selection.clearSelection()
+ self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
+ self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
+ if Gui.Control.activeDialog():
+ Gui.Control.closeDialog()
+
+ def createJointObject(self):
+ type_index = self.form.jointType.currentIndex()
+
+ joint_group = UtilsAssembly.getJointGroup(self.assembly)
+
+ self.joint = joint_group.newObject("App::FeaturePython", self.jointName)
+ Joint(self.joint, type_index)
+ ViewProviderJoint(self.joint.ViewObject)
+
+ def onJointTypeChanged(self, index):
+ self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText())
+
+ def updateTaskboxFromJoint(self):
+ self.current_selection = []
+ self.preselection_dict = None
+
+ selection_dict1 = {
+ "object": self.joint.Object1,
+ "element_name": self.joint.Element1,
+ "vertex_name": self.joint.Vertex1,
+ }
+
+ selection_dict2 = {
+ "object": self.joint.Object2,
+ "element_name": self.joint.Element2,
+ "vertex_name": self.joint.Vertex2,
+ }
+
+ self.current_selection.append(selection_dict1)
+ self.current_selection.append(selection_dict2)
+
+ elName = self.getObjSubNameFromObj(self.joint.Object1, self.joint.Element1)
+ """print(
+ f"Gui.Selection.addSelection('{self.doc.Name}', '{self.joint.Object1.Name}', '{elName}')"
+ )"""
+ Gui.Selection.addSelection(self.doc.Name, self.joint.Object1.Name, elName)
+
+ elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2)
+ Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName)
+
+ self.updateJointList()
+
+ def getObjSubNameFromObj(self, obj, elName):
+ if obj.TypeId == "PartDesign::Body":
+ return obj.Tip.Name + "." + elName
+ elif obj.TypeId == "App::Link":
+ linked_obj = obj.getLinkedObject()
+ if linked_obj.TypeId == "PartDesign::Body":
+ return linked_obj.Tip.Name + "." + elName
+ else:
+ return elName
+ else:
+ return elName
+
+ def updateJoint(self):
+ # First we build the listwidget
+ self.updateJointList()
+
+ # Then we pass the new list to the join object
+ self.joint.Proxy.setJointConnectors(self.joint, self.current_selection)
+
+ def updateJointList(self):
+ self.form.featureList.clear()
+ simplified_names = []
+ for sel in self.current_selection:
+ # TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12
+ sname = sel["object"].Label + "." + sel["element_name"]
+ simplified_names.append(sname)
+ self.form.featureList.addItems(simplified_names)
+
+ def moveMouse(self, info):
+ if len(self.current_selection) >= 2 or (
+ len(self.current_selection) == 1
+ and self.current_selection[0]["object"] == self.preselection_dict["object"]
+ ):
+ self.joint.ViewObject.Proxy.showPreviewJCS(False)
+ return
+
+ cursor_pos = self.view.getCursorPos()
+ cursor_info = self.view.getObjectInfo(cursor_pos)
+ # cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': , 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'}
+
+ if (
+ not cursor_info
+ or not self.preselection_dict
+ or cursor_info["SubName"] != self.preselection_dict["sub_name"]
+ ):
+ self.joint.ViewObject.Proxy.showPreviewJCS(False)
+ return
+
+ # newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane
+
+ newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
+ self.preselection_dict["mouse_pos"] = newPos
+
+ self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
+ self.preselection_dict
+ )
+
+ placement = self.joint.Proxy.findPlacement(
+ self.preselection_dict["object"],
+ self.preselection_dict["element_name"],
+ self.preselection_dict["vertex_name"],
+ )
+ self.joint.ViewObject.Proxy.showPreviewJCS(True, placement)
+ self.previewJCSVisible = True
+
+ # 3D view keyboard handler
+ def KeyboardEvent(self, info):
+ if info["State"] == "UP" and info["Key"] == "ESCAPE":
+ self.reject()
+
+ if info["State"] == "UP" and info["Key"] == "RETURN":
+ self.accept()
+
+ # selectionObserver stuff
+ def addSelection(self, doc_name, obj_name, sub_name, mousePos):
+ full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
+ full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
+ selected_object = UtilsAssembly.getObject(full_element_name)
+ element_name = UtilsAssembly.getElementName(full_element_name)
+
+ selection_dict = {
+ "object": selected_object,
+ "element_name": element_name,
+ "full_element_name": full_element_name,
+ "full_obj_name": full_obj_name,
+ "mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]),
+ }
+ selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
+
+ self.current_selection.append(selection_dict)
+ self.updateJoint()
+
+ def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None):
+ full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
+ selected_object = UtilsAssembly.getObject(full_element_name)
+ element_name = UtilsAssembly.getElementName(full_element_name)
+
+ # Find and remove the corresponding dictionary from the combined list
+ selection_dict_to_remove = None
+ for selection_dict in self.current_selection:
+ if selection_dict["object"] == selected_object:
+ selection_dict_to_remove = selection_dict
+ break
+
+ if selection_dict_to_remove is not None:
+ self.current_selection.remove(selection_dict_to_remove)
+
+ self.updateJoint()
+
+ def setPreselection(self, doc_name, obj_name, sub_name):
+ if not sub_name:
+ self.preselection_dict = None
+ return
+
+ full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
+ full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
+ selected_object = UtilsAssembly.getObject(full_element_name)
+ element_name = UtilsAssembly.getElementName(full_element_name)
+
+ self.preselection_dict = {
+ "object": selected_object,
+ "sub_name": sub_name,
+ "element_name": element_name,
+ "full_element_name": full_element_name,
+ "full_obj_name": full_obj_name,
+ }
+
+ def clearSelection(self, doc_name):
+ self.current_selection.clear()
+ self.updateJoint()
diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py
index c8bc90359e..3feed5390b 100644
--- a/src/Mod/Assembly/UtilsAssembly.py
+++ b/src/Mod/Assembly/UtilsAssembly.py
@@ -244,3 +244,11 @@ def color_from_unsigned(c):
float(int((c >> 16) & 0xFF) / 255),
float(int((c >> 8) & 0xFF) / 255),
]
+
+
+def getJointGroup(assembly):
+ joint_group = assembly.getObject("Joints")
+
+ if not joint_group:
+ joint_group = assembly.newObject("Assembly::JointGroup", "Joints")
+ return joint_group