From 5fb3589f266cccec378179331ee20adbfa92c2fe Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Tue, 14 May 2024 16:18:56 +0200 Subject: [PATCH] Assembly: Add 'Angle', 'Perpendicular' and 'Parallel' joints. --- src/Mod/Assembly/App/AssemblyObject.cpp | 20 ++ src/Mod/Assembly/App/AssemblyObject.h | 3 + src/Mod/Assembly/CommandCreateJoint.py | 91 ++++++- src/Mod/Assembly/Gui/Resources/Assembly.qrc | 3 + .../icons/Assembly_CreateJointAngle.svg | 247 ++++++++++++++++++ .../icons/Assembly_CreateJointParallel.svg | 240 +++++++++++++++++ .../Assembly_CreateJointPerpendicular.svg | 237 +++++++++++++++++ src/Mod/Assembly/InitGui.py | 4 + src/Mod/Assembly/JointObject.py | 112 ++++++-- src/Mod/Assembly/UtilsAssembly.py | 6 + 10 files changed, 935 insertions(+), 28 deletions(-) create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointAngle.svg create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointPerpendicular.svg diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index fa8afb7f1a..7e0810e00d 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -58,12 +58,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -846,6 +849,23 @@ std::shared_ptr AssemblyObject::makeMbdJointOfType(App::DocumentObjec else if (type == JointType::Distance) { return makeMbdJointDistance(joint); } + else if (type == JointType::Parallel) { + return CREATE::With(); + } + else if (type == JointType::Perpendicular) { + return CREATE::With(); + } + else if (type == JointType::Angle) { + double angle = fabs(Base::toRadians(getJointDistance(joint))); + if (fmod(angle, 2 * M_PI) < Precision::Confusion()) { + return CREATE::With(); + } + else { + auto mbdJoint = CREATE::With(); + mbdJoint->theIzJz = angle; + return mbdJoint; + } + } else if (type == JointType::RackPinion) { auto mbdJoint = CREATE::With(); mbdJoint->pitchRadius = getJointDistance(joint); diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 7bac83394d..9cbc449a80 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -66,6 +66,9 @@ enum class JointType Slider, Ball, Distance, + Parallel, + Perpendicular, + Angle, RackPinion, Screw, Gears, diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 0da7bbdbd3..0bd7b142f6 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -238,6 +238,86 @@ class CommandCreateJointDistance: activateJoint(5) +class CommandCreateJointParallel: + def __init__(self): + pass + + def GetResources(self): + + return { + "Pixmap": "Assembly_CreateJointParallel", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointParallel", "Create Parallel Joint"), + "Accel": "N", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointParallel", + "Create an Parallel Joint: Make the Z axis of selected coordinate systems parallel.", + ) + + "

", + "CmdType": "ForEdit", + } + + def IsActive(self): + return isCreateJointActive() + + def Activated(self): + activateJoint(6) + + +class CommandCreateJointPerpendicular: + def __init__(self): + pass + + def GetResources(self): + + return { + "Pixmap": "Assembly_CreateJointPerpendicular", + "MenuText": QT_TRANSLATE_NOOP( + "Assembly_CreateJointPerpendicular", "Create Perpendicular Joint" + ), + "Accel": "M", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointPerpendicular", + "Create an Perpendicular Joint: Make the Z axis of selected coordinate systems perpendicular.", + ) + + "

", + "CmdType": "ForEdit", + } + + def IsActive(self): + return isCreateJointActive() + + def Activated(self): + activateJoint(7) + + +class CommandCreateJointAngle: + def __init__(self): + pass + + def GetResources(self): + + return { + "Pixmap": "Assembly_CreateJointAngle", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointAngle", "Create Angle Joint"), + "Accel": "X", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointAngle", + "Create an Angle Joint: Fix the angle between the Z axis of selected coordinate systems.", + ) + + "

", + "CmdType": "ForEdit", + } + + def IsActive(self): + return isCreateJointActive() + + def Activated(self): + activateJoint(8) + + class CommandCreateJointRackPinion: def __init__(self): pass @@ -268,7 +348,7 @@ class CommandCreateJointRackPinion: return isCreateJointActive() def Activated(self): - activateJoint(6) + activateJoint(9) class CommandCreateJointScrew: @@ -299,7 +379,7 @@ class CommandCreateJointScrew: return isCreateJointActive() def Activated(self): - activateJoint(7) + activateJoint(10) class CommandCreateJointGears: @@ -330,7 +410,7 @@ class CommandCreateJointGears: return isCreateJointActive() def Activated(self): - activateJoint(8) + activateJoint(11) class CommandCreateJointBelt: @@ -361,7 +441,7 @@ class CommandCreateJointBelt: return isCreateJointActive() def Activated(self): - activateJoint(9) + activateJoint(12) class CommandGroupGearBelt: @@ -483,6 +563,9 @@ if App.GuiUp: Gui.addCommand("Assembly_CreateJointSlider", CommandCreateJointSlider()) Gui.addCommand("Assembly_CreateJointBall", CommandCreateJointBall()) Gui.addCommand("Assembly_CreateJointDistance", CommandCreateJointDistance()) + Gui.addCommand("Assembly_CreateJointParallel", CommandCreateJointParallel()) + Gui.addCommand("Assembly_CreateJointPerpendicular", CommandCreateJointPerpendicular()) + Gui.addCommand("Assembly_CreateJointAngle", CommandCreateJointAngle()) Gui.addCommand("Assembly_CreateJointRackPinion", CommandCreateJointRackPinion()) Gui.addCommand("Assembly_CreateJointScrew", CommandCreateJointScrew()) Gui.addCommand("Assembly_CreateJointGears", CommandCreateJointGears()) diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc index 91529fa602..b394ccd04d 100644 --- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -3,9 +3,12 @@ icons/Assembly_InsertLink.svg icons/preferences-assembly.svg icons/Assembly_ToggleGrounded.svg + icons/Assembly_CreateJointAngle.svg icons/Assembly_CreateJointBall.svg icons/Assembly_CreateJointCylindrical.svg icons/Assembly_CreateJointFixed.svg + icons/Assembly_CreateJointParallel.svg + icons/Assembly_CreateJointPerpendicular.svg icons/Assembly_CreateJointPlanar.svg icons/Assembly_CreateJointRevolute.svg icons/Assembly_CreateJointSlider.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointAngle.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointAngle.svg new file mode 100644 index 0000000000..680b2e9a10 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointAngle.svg @@ -0,0 +1,247 @@ + +image/svg+xml[wmayer]Part_Cylinder2011-10-10https://www.freecad.org/wiki/index.php?title=ArtworkFreeCADFreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Cylinder.svgFreeCAD LGPL2+https://www.gnu.org/copyleft/lesser.html[agryson] Alexander Gryson diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg new file mode 100644 index 0000000000..0be7d0599c --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg @@ -0,0 +1,240 @@ + +image/svg+xml[wmayer]Part_Cylinder2011-10-10https://www.freecad.org/wiki/index.php?title=ArtworkFreeCADFreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Cylinder.svgFreeCAD LGPL2+https://www.gnu.org/copyleft/lesser.html[agryson] Alexander Gryson diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointPerpendicular.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointPerpendicular.svg new file mode 100644 index 0000000000..7092cf3d52 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointPerpendicular.svg @@ -0,0 +1,237 @@ + +image/svg+xml[wmayer]Part_Cylinder2011-10-10https://www.freecad.org/wiki/index.php?title=ArtworkFreeCADFreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Cylinder.svgFreeCAD LGPL2+https://www.gnu.org/copyleft/lesser.html[agryson] Alexander Gryson diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index 5ece952744..f36a773745 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -93,7 +93,11 @@ class AssemblyWorkbench(Workbench): "Assembly_CreateJointCylindrical", "Assembly_CreateJointSlider", "Assembly_CreateJointBall", + "Separator", "Assembly_CreateJointDistance", + "Assembly_CreateJointParallel", + "Assembly_CreateJointPerpendicular", + "Assembly_CreateJointAngle", "Separator", "Assembly_CreateJointRackPinion", "Assembly_CreateJointScrew", diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index ebfec7975a..f104db8a9d 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -51,6 +51,9 @@ TranslatedJointTypes = [ translate("Assembly", "Slider"), translate("Assembly", "Ball"), translate("Assembly", "Distance"), + translate("Assembly", "Parallel"), + translate("Assembly", "Perpendicular"), + translate("Assembly", "Angle"), translate("Assembly", "RackPinion"), translate("Assembly", "Screw"), translate("Assembly", "Gears"), @@ -64,6 +67,9 @@ JointTypes = [ "Slider", "Ball", "Distance", + "Parallel", + "Perpendicular", + "Angle", "RackPinion", "Screw", "Gears", @@ -72,6 +78,7 @@ JointTypes = [ JointUsingDistance = [ "Distance", + "Angle", "RackPinion", "Screw", "Gears", @@ -106,6 +113,7 @@ JointUsingReverse = [ "Cylindrical", "Slider", "Distance", + "Parallel", ] JointUsingLimitLength = [ @@ -118,6 +126,19 @@ JointUsingLimitAngle = [ "Cylindrical", ] +JointUsingPreSolve = [ + "Fixed", + "Revolute", + "Cylindrical", + "Slider", + "Ball", +] + +JointParallelForbidden = [ + "Angle", + "Perpendicular", +] + def solveIfAllowed(assembly, storePrev=False): if assembly.Type == "Assembly" and Preferences.preferences().GetBool( @@ -399,7 +420,6 @@ class Joint: return None def loads(self, state): - return None def getAssembly(self, joint): @@ -425,14 +445,7 @@ class Joint: if obj1 is None or obj2 is None: return - presolved = self.preSolve( - joint, - obj1, - joint.Part1, - obj2, - joint.Part2, - False, - ) + presolved = self.preSolve(joint, False) isAssembly = self.getAssembly(joint).Type == "Assembly" if isAssembly and not presolved: @@ -440,10 +453,13 @@ class Joint: else: self.updateJCSPlacements(joint) - if prop == "Distance" and joint.JointType == "Distance": + if prop == "Distance" and (joint.JointType == "Distance" or joint.JointType == "Angle"): # during loading the onchanged may be triggered before full init. if hasattr(joint, "Vertex1"): # so we check Vertex1 - solveIfAllowed(self.getAssembly(joint)) + if joint.Part1 and joint.Part2: + if joint.JointType == "Angle" and joint.Distance != 0.0: + self.preventParallel(joint) + solveIfAllowed(self.getAssembly(joint)) def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -479,14 +495,10 @@ class Joint: joint.Placement2 = self.findPlacement( joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) - if joint.JointType != "Distance": - self.preSolve( - joint, - current_selection[0]["object"], - joint.Part1, - current_selection[1]["object"], - joint.Part2, - ) + if joint.JointType in JointUsingPreSolve: + self.preSolve(joint) + elif joint.JointType in JointParallelForbidden: + self.preventParallel(joint) if isAssembly: solveIfAllowed(assembly, True) @@ -569,7 +581,7 @@ class Joint: solveIfAllowed(self.getAssembly(joint)) - def preSolve(self, joint, obj1, part1, obj2, part2, savePlc=True): + def preSolve(self, joint, savePlc=True): # The goal of this is to put the part in the correct position to avoid wrong placement by the solve. # we actually don't want to match perfectly the JCS, it is best to match them @@ -624,12 +636,49 @@ class Joint: joint.Placement1 = joint.Placement1 # Make sure plc1 is redrawn + def preventParallel(self, joint): + # Angle and perpendicular joints in the solver cannot handle the situation where both JCS are Parallel + parallel = self.areJcsZParallel(joint) + if not parallel: + return + + assembly = self.getAssembly(joint) + isAssembly = assembly.Type == "Assembly" + if isAssembly: + part1ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Part1") + part2ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Part2") + else: + part1ConnectedByJoint = False + part2ConnectedByJoint = True + + if part2ConnectedByJoint: + self.partMovedByPresolved = joint.Part2 + self.presolveBackupPlc = joint.Part2.Placement + + joint.Part2.Placement = UtilsAssembly.applyRotationToPlacementAlongAxis( + joint.Part2.Placement, 10, App.Vector(1, 0, 0) + ) + + elif part1ConnectedByJoint: + self.partMovedByPresolved = joint.Part1 + self.presolveBackupPlc = joint.Part1.Placement + + joint.Part1.Placement = UtilsAssembly.applyRotationToPlacementAlongAxis( + joint.Part1.Placement, 10, App.Vector(1, 0, 0) + ) + def areJcsSameDir(self, joint): globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1) globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2) return UtilsAssembly.arePlacementSameDir(globalJcsPlc1, globalJcsPlc2) + def areJcsZParallel(self, joint): + globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1) + globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2) + + return UtilsAssembly.arePlacementZParallel(globalJcsPlc1, globalJcsPlc2) + class ViewProviderJoint: def __init__(self, vobj): @@ -848,6 +897,12 @@ class ViewProviderJoint: return ":/icons/Assembly_CreateJointBall.svg" elif self.app_obj.JointType == "Distance": return ":/icons/Assembly_CreateJointDistance.svg" + elif self.app_obj.JointType == "Parallel": + return ":/icons/Assembly_CreateJointParallel.svg" + elif self.app_obj.JointType == "Perpendicular": + return ":/icons/Assembly_CreateJointPerpendicular.svg" + elif self.app_obj.JointType == "Angle": + return ":/icons/Assembly_CreateJointAngle.svg" elif self.app_obj.JointType == "RackPinion": return ":/icons/Assembly_CreateJointRackPinion.svg" elif self.app_obj.JointType == "Screw": @@ -1179,6 +1234,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.jointType.addItems(TranslatedJointTypes) self.form.jointType.setCurrentIndex(jointTypeIndex) + self.jType = JointTypes[self.form.jointType.currentIndex()] self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged) self.form.distanceSpinbox.valueChanged.connect(self.onDistanceChanged) @@ -1192,8 +1248,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.limitRotMinSpinbox.valueChanged.connect(self.onLimitRotMinChanged) self.form.limitRotMaxSpinbox.valueChanged.connect(self.onLimitRotMaxChanged) - jType = JointTypes[self.form.jointType.currentIndex()] - self.form.reverseRotCheckbox.setChecked(jType == "Gears") + self.form.reverseRotCheckbox.setChecked(self.jType == "Gears") self.form.reverseRotCheckbox.stateChanged.connect(self.reverseRotToggled) if jointObj: @@ -1355,7 +1410,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): ViewProviderJoint(self.joint.ViewObject) def onJointTypeChanged(self, index): - self.joint.Proxy.setJointType(self.joint, JointTypes[self.form.jointType.currentIndex()]) + self.jType = JointTypes[self.form.jointType.currentIndex()] + self.joint.Proxy.setJointType(self.joint, self.jType) self.adaptUi() def onDistanceChanged(self, quantity): @@ -1392,17 +1448,25 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.jointType.setCurrentIndex(9) def adaptUi(self): - jType = JointTypes[self.form.jointType.currentIndex()] + jType = self.jType if jType in JointUsingDistance: self.form.distanceLabel.show() self.form.distanceSpinbox.show() if jType == "Distance": self.form.distanceLabel.setText("Distance") + elif jType == "Angle": + self.form.distanceLabel.setText("Angle") elif jType == "Gears" or jType == "Belt": self.form.distanceLabel.setText("Radius 1") else: self.form.distanceLabel.setText("Pitch radius") + + if jType == "Angle": + self.form.distanceSpinbox.setProperty("unit", "deg") + else: + self.form.distanceSpinbox.setProperty("unit", "mm") + else: self.form.distanceLabel.hide() self.form.distanceSpinbox.hide() diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 759e0a485e..a775018369 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -850,6 +850,12 @@ def arePlacementSameDir(plc1, plc2): return zAxis1.dot(zAxis2) > 0 +def arePlacementZParallel(plc1, plc2): + zAxis1 = plc1.Rotation.multVec(App.Vector(0, 0, 1)) + zAxis2 = plc2.Rotation.multVec(App.Vector(0, 0, 1)) + return zAxis1.cross(zAxis2).Length < 1e-06 + + """ So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex. - obj is usually a App::Link to a PartDesign::Body, or primitive, fasteners. But can also be directly the object.1