| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import math |
| |
|
| | import FreeCAD as App |
| | import Part |
| |
|
| | from PySide import QtCore |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | from collections.abc import Sequence |
| |
|
| | if App.GuiUp: |
| | import FreeCADGui as Gui |
| | from PySide import QtGui, QtWidgets |
| |
|
| | __title__ = "Assembly Joint object" |
| | __author__ = "Ondsel" |
| | __url__ = "https://www.freecad.org" |
| |
|
| | from pivy import coin |
| | import UtilsAssembly |
| | import Preferences |
| |
|
| | from SoSwitchMarker import SoSwitchMarker |
| |
|
| | translate = App.Qt.translate |
| |
|
| | TranslatedJointTypes = [ |
| | translate("Assembly", "Fixed"), |
| | translate("Assembly", "Revolute"), |
| | translate("Assembly", "Cylindrical"), |
| | 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"), |
| | translate("Assembly", "Belt"), |
| | ] |
| |
|
| | JointTypes = [ |
| | "Fixed", |
| | "Revolute", |
| | "Cylindrical", |
| | "Slider", |
| | "Ball", |
| | "Distance", |
| | "Parallel", |
| | "Perpendicular", |
| | "Angle", |
| | "RackPinion", |
| | "Screw", |
| | "Gears", |
| | "Belt", |
| | ] |
| |
|
| | JointUsingAngle = [ |
| | "Angle", |
| | ] |
| |
|
| | JointUsingDistance = [ |
| | "Distance", |
| | "RackPinion", |
| | "Screw", |
| | "Gears", |
| | "Belt", |
| | ] |
| |
|
| | JointUsingDistance2 = [ |
| | "Gears", |
| | "Belt", |
| | ] |
| |
|
| | JointNoNegativeDistance = [ |
| | "Gears", |
| | "Belt", |
| | ] |
| |
|
| | JointUsingOffset = [ |
| | "Fixed", |
| | "Revolute", |
| | ] |
| |
|
| | JointUsingRotation = [ |
| | "Fixed", |
| | "Slider", |
| | ] |
| |
|
| | JointUsingReverse = [ |
| | "Fixed", |
| | "Revolute", |
| | "Cylindrical", |
| | "Slider", |
| | "Distance", |
| | "Parallel", |
| | ] |
| |
|
| | JointUsingLimitLength = [ |
| | "Cylindrical", |
| | "Slider", |
| | ] |
| |
|
| | JointUsingLimitAngle = [ |
| | "Revolute", |
| | "Cylindrical", |
| | ] |
| |
|
| | JointUsingPreSolve = [ |
| | "Fixed", |
| | "Revolute", |
| | "Cylindrical", |
| | "Slider", |
| | "Ball", |
| | ] |
| |
|
| | JointParallelForbidden = [ |
| | "Angle", |
| | "Perpendicular", |
| | ] |
| |
|
| |
|
| | def solveIfAllowed(assembly, storePrev=False): |
| | if assembly.Type == "Assembly" and Preferences.preferences().GetBool( |
| | "SolveInJointCreation", True |
| | ): |
| | assembly.solve(storePrev) |
| |
|
| |
|
| | def getContext(obj): |
| | """Fetch the context of an object.""" |
| | context = [] |
| | current = obj |
| |
|
| | while current: |
| | |
| | context.insert(0, current.Label if current.Label else current.Name) |
| | |
| | parents = getattr(current, "InList", []) |
| | current = parents[0] if parents else None |
| | return ".".join(context) |
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | class Joint: |
| | def __init__(self, joint, type_index): |
| | joint.Proxy = self |
| |
|
| | joint.addExtension("App::SuppressibleExtensionPython") |
| |
|
| | joint.addProperty( |
| | "App::PropertyEnumeration", |
| | "JointType", |
| | "Joint", |
| | QT_TRANSLATE_NOOP("App::Property", "The type of the joint"), |
| | locked=True, |
| | ) |
| | joint.JointType = JointTypes |
| | joint.JointType = JointTypes[type_index] |
| |
|
| | self.createProperties(joint) |
| |
|
| | self.setJointConnectors(joint, []) |
| |
|
| | def onDocumentRestored(self, joint): |
| | self.createProperties(joint) |
| |
|
| | def createProperties(self, joint): |
| | self.migrationScript(joint) |
| | self.migrationScript2(joint) |
| | self.migrationScript3(joint) |
| | self.migrationScript4(joint) |
| | self.migrationScript5(joint) |
| | self.migrationScript6(joint) |
| |
|
| | |
| | if not hasattr(joint, "Reference1"): |
| | joint.addProperty( |
| | "App::PropertyXLinkSubHidden", |
| | "Reference1", |
| | "Joint Connector 1", |
| | QT_TRANSLATE_NOOP("App::Property", "The first reference of the joint"), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(joint, "Placement1"): |
| | joint.addProperty( |
| | "App::PropertyPlacement", |
| | "Placement1", |
| | "Joint Connector 1", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the local coordinate system within Reference1's object that will be used for the joint", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(joint, "Detach1"): |
| | joint.addProperty( |
| | "App::PropertyBool", |
| | "Detach1", |
| | "Joint Connector 1", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This prevents Placement1 from recomputing, enabling custom positioning of the placement", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(joint, "Offset1"): |
| | joint.addProperty( |
| | "App::PropertyPlacement", |
| | "Offset1", |
| | "Joint Connector 1", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the attachment offset of the first connector of the joint", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | |
| | if not hasattr(joint, "Reference2"): |
| | joint.addProperty( |
| | "App::PropertyXLinkSubHidden", |
| | "Reference2", |
| | "Joint Connector 2", |
| | QT_TRANSLATE_NOOP("App::Property", "The second reference of the joint"), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(joint, "Placement2"): |
| | joint.addProperty( |
| | "App::PropertyPlacement", |
| | "Placement2", |
| | "Joint Connector 2", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the local coordinate system within Reference2's object that will be used for the joint", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(joint, "Detach2"): |
| | joint.addProperty( |
| | "App::PropertyBool", |
| | "Detach2", |
| | "Joint Connector 2", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This prevents Placement2 from recomputing, enabling custom positioning of the placement", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(joint, "Offset2"): |
| | joint.addProperty( |
| | "App::PropertyPlacement", |
| | "Offset2", |
| | "Joint Connector 2", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the attachment offset of the second connector of the joint", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | |
| | if not hasattr(joint, "Angle"): |
| | self.addAngleProperty(joint) |
| |
|
| | if not hasattr(joint, "Distance"): |
| | self.addDistanceProperty(joint) |
| |
|
| | if not hasattr(joint, "Distance2"): |
| | self.addDistance2Property(joint) |
| |
|
| | if not hasattr(joint, "EnableLengthMin"): |
| | joint.addProperty( |
| | "App::PropertyBool", |
| | "EnableLengthMin", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Enable the minimum length limit of the joint", |
| | ), |
| | locked=True, |
| | ) |
| | joint.EnableLengthMin = False |
| |
|
| | if not hasattr(joint, "EnableLengthMax"): |
| | joint.addProperty( |
| | "App::PropertyBool", |
| | "EnableLengthMax", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Enable the maximum length limit of the joint", |
| | ), |
| | locked=True, |
| | ) |
| | joint.EnableLengthMax = False |
| |
|
| | if not hasattr(joint, "EnableAngleMin"): |
| | joint.addProperty( |
| | "App::PropertyBool", |
| | "EnableAngleMin", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Enable the minimum angle limit of the joint", |
| | ), |
| | locked=True, |
| | ) |
| | joint.EnableAngleMin = False |
| |
|
| | if not hasattr(joint, "EnableAngleMax"): |
| | joint.addProperty( |
| | "App::PropertyBool", |
| | "EnableAngleMax", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Enable the maximum angle limit of the joint", |
| | ), |
| | locked=True, |
| | ) |
| | joint.EnableAngleMax = False |
| |
|
| | if not hasattr(joint, "LengthMin"): |
| | self.addLengthMinProperty(joint) |
| |
|
| | if not hasattr(joint, "LengthMax"): |
| | self.addLengthMaxProperty(joint) |
| |
|
| | if not hasattr(joint, "AngleMin"): |
| | self.addAngleMinProperty(joint) |
| |
|
| | if not hasattr(joint, "AngleMax"): |
| | self.addAngleMaxProperty(joint) |
| |
|
| | joint.setPropertyStatus("Distance", "AllowNegativeValues") |
| | joint.setPropertyStatus("Distance2", "AllowNegativeValues") |
| | joint.setPropertyStatus("LengthMin", "AllowNegativeValues") |
| | joint.setPropertyStatus("LengthMax", "AllowNegativeValues") |
| |
|
| | def addAngleProperty(self, joint): |
| | joint.addProperty( |
| | "App::PropertyAngle", |
| | "Angle", |
| | "Joint", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the angle of the joint. It is used only by the Angle joint.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def addDistanceProperty(self, joint): |
| | joint.addProperty( |
| | "App::PropertyLength", |
| | "Distance", |
| | "Joint", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the distance of the joint. It is used only by the Distance joint and Rack and Pinion (pitch radius), Screw and Gears and Belt (radius1)", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def addDistance2Property(self, joint): |
| | joint.addProperty( |
| | "App::PropertyLength", |
| | "Distance2", |
| | "Joint", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the second distance of the joint. It is used only by the gear joint to store the second radius.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def addLengthMinProperty(self, joint): |
| | joint.addProperty( |
| | "App::PropertyLength", |
| | "LengthMin", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the minimum limit for the length between both coordinate systems (along their z-axis)", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def addLengthMaxProperty(self, joint): |
| | joint.addProperty( |
| | "App::PropertyLength", |
| | "LengthMax", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the maximum limit for the length between both coordinate systems (along their z-axis)", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def addAngleMinProperty(self, joint): |
| | joint.addProperty( |
| | "App::PropertyAngle", |
| | "AngleMin", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the minimum limit for the angle between both coordinate systems (between their x-axis)", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def addAngleMaxProperty(self, joint): |
| | joint.addProperty( |
| | "App::PropertyAngle", |
| | "AngleMax", |
| | "Limits", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the maximum limit for the angle between both coordinate systems (between their x-axis)", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def migrationScript(self, joint): |
| | if hasattr(joint, "Object1") and isinstance(joint.Object1, str): |
| | objName = joint.Object1 |
| | obj1 = UtilsAssembly.getObjectInPart(objName, joint.Part1) |
| | el1 = joint.Element1 |
| | vtx1 = joint.Vertex1 |
| |
|
| | joint.removeProperty("Object1") |
| | joint.removeProperty("Element1") |
| | joint.removeProperty("Vertex1") |
| |
|
| | joint.addProperty( |
| | "App::PropertyXLinkSub", |
| | "Object1", |
| | "Joint Connector 1", |
| | QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"), |
| | locked=True, |
| | ) |
| |
|
| | joint.Object1 = [obj1, [el1, vtx1]] |
| |
|
| | if hasattr(joint, "Object2") and isinstance(joint.Object2, str): |
| | objName = joint.Object2 |
| | obj2 = UtilsAssembly.getObjectInPart(objName, joint.Part2) |
| | el2 = joint.Element2 |
| | vtx2 = joint.Vertex2 |
| |
|
| | joint.removeProperty("Object2") |
| | joint.removeProperty("Element2") |
| | joint.removeProperty("Vertex2") |
| |
|
| | joint.addProperty( |
| | "App::PropertyXLinkSub", |
| | "Object2", |
| | "Joint Connector 2", |
| | QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"), |
| | locked=True, |
| | ) |
| |
|
| | joint.Object2 = [obj2, [el2, vtx2]] |
| |
|
| | def migrationScript2(self, joint): |
| | def processObject(object_attr, reference_attr, part_attr, connector_label, order): |
| | try: |
| | if hasattr(joint, object_attr): |
| | joint.addProperty( |
| | "App::PropertyXLinkSubHidden", |
| | reference_attr, |
| | connector_label, |
| | QT_TRANSLATE_NOOP("App::Property", f"The {order} reference of the joint"), |
| | locked=True, |
| | ) |
| |
|
| | obj = getattr(joint, object_attr) |
| |
|
| | base_obj = obj[0] |
| | part = getattr(joint, part_attr) |
| | elt = obj[1][0] |
| | vtx = obj[1][1] |
| |
|
| | |
| | root_obj, path = UtilsAssembly.getRootPath(base_obj, part) |
| | base_obj = root_obj |
| | elt = path + elt |
| | vtx = path + vtx |
| |
|
| | setattr(joint, reference_attr, [base_obj, [elt, vtx]]) |
| |
|
| | joint.removeProperty(object_attr) |
| | joint.removeProperty(part_attr) |
| |
|
| | except (AttributeError, IndexError, TypeError) as e: |
| | App.Console.PrintWarning( |
| | f"{translate('Assembly', 'Assembly joint')} '{getContext(joint)}' " |
| | f"{translate('Assembly', 'has an invalid')} '{object_attr}' " |
| | f"{translate('Assembly', 'or related attributes')}. {str(e)}\n" |
| | ) |
| |
|
| | processObject("Object1", "Reference1", "Part1", "Joint Connector 1", "first") |
| | processObject("Object2", "Reference2", "Part2", "Joint Connector 2", "second") |
| |
|
| | def migrationScript3(self, joint): |
| | if hasattr(joint, "Offset"): |
| | current_offset = joint.Offset |
| | current_rotation = joint.Rotation |
| |
|
| | joint.removeProperty("Offset") |
| | joint.removeProperty("Rotation") |
| |
|
| | joint.addProperty( |
| | "App::PropertyPlacement", |
| | "Offset1", |
| | "Joint Connector 1", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the attachment offset of the first connector of the joint", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | joint.addProperty( |
| | "App::PropertyPlacement", |
| | "Offset2", |
| | "Joint Connector 2", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the attachment offset of the second connector of the joint", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | joint.Offset2 = App.Placement(current_offset, App.Rotation(current_rotation, 0, 0)) |
| |
|
| | def migrationScript4(self, joint): |
| | def processReference(reference_attr): |
| | try: |
| | if hasattr(joint, reference_attr): |
| | ref = getattr(joint, reference_attr) |
| |
|
| | doc_name = ref[0].Document.Name |
| | sub1 = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, ref[1][0]) |
| | sub2 = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, ref[1][1]) |
| |
|
| | if sub1 != ref[1][0] or sub2 != ref[1][1]: |
| | setattr(joint, reference_attr, (ref[0], [sub1, sub2])) |
| |
|
| | except (AttributeError, IndexError, TypeError) as e: |
| | App.Console.PrintWarning( |
| | f"{translate('Assembly', 'Assembly joint')} '{getContext(joint)}' " |
| | f"{translate('Assembly', 'has an invalid')} '{reference_attr}' " |
| | f"{translate('Assembly', 'or related attributes')}. {str(e)}\n" |
| | ) |
| |
|
| | processReference("Reference1") |
| | processReference("Reference2") |
| |
|
| | def migrationScript5(self, joint): |
| | if not joint.hasExtension("App::SuppressibleExtensionPython"): |
| | joint.addExtension("App::SuppressibleExtensionPython") |
| |
|
| | if App.GuiUp: |
| | if not joint.ViewObject.hasExtension("Gui::ViewProviderSuppressibleExtensionPython"): |
| | joint.ViewObject.addExtension("Gui::ViewProviderSuppressibleExtensionPython") |
| |
|
| | if hasattr(joint, "Activated"): |
| | activated = joint.Activated |
| | if not activated: |
| | print( |
| | "The 'Activated' property has been replaced by the 'Suppressed' property. The file has a deactivated joint that is being migrated. If you open this file in an older version, it will not be deactivated anymore." |
| | ) |
| | joint.removeProperty("Activated") |
| | joint.Suppressed = not activated |
| |
|
| | def migrationScript6(self, joint): |
| | """ |
| | This script migrates properties with fixed types from PropertyFloat to the |
| | correct PropertyLength or PropertyAngle type. |
| | """ |
| | if ( |
| | hasattr(joint, "Distance") |
| | and joint.getTypeIdOfProperty("Distance") == "App::PropertyFloat" |
| | ): |
| | old_value = joint.Distance |
| | joint.setPropertyStatus("Distance", "-LockDynamic") |
| | joint.removeProperty("Distance") |
| | self.addDistanceProperty(joint) |
| |
|
| | |
| | if joint.JointType == "Angle": |
| | self.addAngleProperty(joint) |
| | joint.Angle = old_value |
| | else: |
| | joint.Distance = old_value |
| |
|
| | if ( |
| | hasattr(joint, "Distance2") |
| | and joint.getTypeIdOfProperty("Distance2") == "App::PropertyFloat" |
| | ): |
| | old_value = joint.Distance2 |
| | joint.setPropertyStatus("Distance2", "-LockDynamic") |
| | joint.removeProperty("Distance2") |
| | self.addDistance2Property(joint) |
| | joint.Distance2 = old_value |
| |
|
| | if ( |
| | hasattr(joint, "LengthMin") |
| | and joint.getTypeIdOfProperty("LengthMin") == "App::PropertyFloat" |
| | ): |
| | old_value = joint.LengthMin |
| | joint.setPropertyStatus("LengthMin", "-LockDynamic") |
| | joint.removeProperty("LengthMin") |
| | self.addLengthMinProperty(joint) |
| | joint.LengthMin = old_value |
| |
|
| | if ( |
| | hasattr(joint, "LengthMax") |
| | and joint.getTypeIdOfProperty("LengthMax") == "App::PropertyFloat" |
| | ): |
| | old_value = joint.LengthMax |
| | joint.setPropertyStatus("LengthMax", "-LockDynamic") |
| | joint.removeProperty("LengthMax") |
| | self.addLengthMaxProperty(joint) |
| | joint.LengthMax = old_value |
| |
|
| | if ( |
| | hasattr(joint, "AngleMin") |
| | and joint.getTypeIdOfProperty("AngleMin") == "App::PropertyFloat" |
| | ): |
| | old_value = joint.AngleMin |
| | joint.setPropertyStatus("AngleMin", "-LockDynamic") |
| | joint.removeProperty("AngleMin") |
| | self.addAngleMinProperty(joint) |
| | joint.AngleMin = old_value |
| |
|
| | if ( |
| | hasattr(joint, "AngleMax") |
| | and joint.getTypeIdOfProperty("AngleMax") == "App::PropertyFloat" |
| | ): |
| | old_value = joint.AngleMax |
| | joint.setPropertyStatus("AngleMax", "-LockDynamic") |
| | joint.removeProperty("AngleMax") |
| | self.addAngleMaxProperty(joint) |
| | joint.AngleMax = old_value |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | return None |
| |
|
| | def getAssembly(self, joint): |
| | for obj in joint.InList: |
| | if obj.isDerivedFrom("Assembly::AssemblyObject"): |
| | return obj |
| | elif obj.isDerivedFrom("Assembly::AssemblyLink"): |
| | return self.getAssembly(obj) |
| |
|
| | return None |
| |
|
| | def setJointType(self, joint, newType): |
| | oldType = joint.JointType |
| | if newType != oldType: |
| | joint.JointType = newType |
| |
|
| | def onChanged(self, joint, prop): |
| | """Do something when a property has changed""" |
| | |
| |
|
| | |
| | if App.isRestoring(): |
| | return |
| |
|
| | if prop == "JointType": |
| | newType = joint.JointType |
| | tr_new_type = TranslatedJointTypes[JointTypes.index(newType)] |
| |
|
| | |
| | for i, old_type_name in enumerate(JointTypes): |
| | if old_type_name == newType: |
| | continue |
| | tr_old_type = TranslatedJointTypes[i] |
| | if tr_old_type in joint.Label: |
| | joint.Label = joint.Label.replace(tr_old_type, tr_new_type) |
| | break |
| |
|
| | if prop == "Reference1" or prop == "Reference2": |
| | joint.recompute() |
| |
|
| | if ( |
| | not hasattr(joint, "Reference1") |
| | or not hasattr(joint, "Reference2") |
| | or joint.Reference1 is None |
| | or joint.Reference2 is None |
| | ): |
| | return |
| |
|
| | if prop == "Offset1" or prop == "Offset2": |
| | self.updateJCSPlacements(joint) |
| |
|
| | presolved = joint.JointType in JointUsingPreSolve and self.preSolve(joint, False) |
| |
|
| | isAssembly = self.getAssembly(joint).Type == "Assembly" |
| | if isAssembly and not presolved: |
| | solveIfAllowed(self.getAssembly(joint)) |
| | else: |
| | self.updateJCSPlacements(joint) |
| |
|
| | if prop == "Distance" and joint.JointType == "Distance": |
| | solveIfAllowed(self.getAssembly(joint)) |
| |
|
| | if prop == "Angle" and joint.JointType == "Angle": |
| | if joint.Angle != 0.0: |
| | self.preventParallel(joint) |
| | solveIfAllowed(self.getAssembly(joint)) |
| |
|
| | def execute(self, joint): |
| | errStr = joint.Label + ": " + QT_TRANSLATE_NOOP("Assembly", "Broken link in: ") |
| | if ( |
| | hasattr(joint, "Reference1") |
| | and joint.Reference1 is not None |
| | and len(joint.Reference1) == 2 |
| | and len(joint.Reference1[1]) != 0 |
| | and (joint.Reference1[1][0].find("?") != -1) |
| | ): |
| | raise Exception(errStr + "Reference1") |
| |
|
| | if ( |
| | hasattr(joint, "Reference2") |
| | and joint.Reference2 is not None |
| | and len(joint.Reference2) == 2 |
| | and len(joint.Reference2[1]) != 0 |
| | and (joint.Reference2[1][0].find("?") != -1) |
| | ): |
| | raise Exception(errStr + "Reference2") |
| |
|
| | def setJointConnectors(self, joint, refs): |
| | |
| | assembly = self.getAssembly(joint) |
| | isAssembly = assembly.Type == "Assembly" |
| |
|
| | if len(refs) >= 1: |
| | joint.Reference1 = refs[0] |
| | joint.Placement1 = self.findPlacement(joint, joint.Reference1, 0) |
| | else: |
| | joint.Reference1 = None |
| | joint.Placement1 = App.Placement() |
| | self.partMovedByPresolved = None |
| |
|
| | if len(refs) >= 2: |
| | joint.Reference2 = refs[1] |
| | joint.Placement2 = self.findPlacement(joint, joint.Reference2, 1) |
| | if joint.JointType in JointUsingPreSolve: |
| | self.preSolve(joint) |
| | elif joint.JointType in JointParallelForbidden: |
| | self.preventParallel(joint) |
| |
|
| | if isAssembly: |
| | solveIfAllowed(assembly, True) |
| | else: |
| | self.updateJCSPlacements(joint) |
| |
|
| | else: |
| | joint.Reference2 = None |
| | joint.Placement2 = App.Placement() |
| | if isAssembly: |
| | assembly.undoSolve() |
| | self.undoPreSolve(joint) |
| |
|
| | def updateJCSPlacements(self, joint): |
| | if not joint.Detach1: |
| | joint.Placement1 = self.findPlacement(joint, joint.Reference1, 0) |
| |
|
| | if not joint.Detach2: |
| | joint.Placement2 = self.findPlacement(joint, joint.Reference2, 1) |
| |
|
| | """ |
| | 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 |
| | - elt can be a face, an edge or a vertex. |
| | - If elt is a vertex, then vtx = elt And placement is vtx coordinates without rotation. |
| | - if elt is an edge, then vtx = edge start/end vertex depending on which is closer. If elt is an arc or circle, vtx can also be the center. The rotation is the plane normal to the line positioned at vtx. Or for arcs/circle, the plane of the arc. |
| | - if elt is a plane face, vtx is the face vertex (to the list of vertex we need to add arc/circle centers) the closer to the mouse. The placement is the plane rotation positioned at vtx |
| | - if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face. |
| | """ |
| |
|
| | def findPlacement(self, joint, ref, index=0): |
| | ignoreVertex = joint.JointType == "Distance" |
| | plc = UtilsAssembly.findPlacement(ref, ignoreVertex) |
| |
|
| | |
| | if index == 0: |
| | plc = plc * joint.Offset1 |
| | else: |
| | plc = plc * joint.Offset2 |
| |
|
| | return plc |
| |
|
| | def flipOnePart(self, joint): |
| | self.matchJCS(joint, False, True) |
| |
|
| | def preSolve(self, joint, savePlc=True): |
| | |
| |
|
| | |
| | |
| | self.matchJCS(joint, savePlc) |
| |
|
| | def matchJCS(self, joint, savePlc=True, reverse=False): |
| | assembly = self.getAssembly(joint) |
| | sameDir = self.areJcsSameDir(joint) |
| | if reverse: |
| | sameDir = not sameDir |
| |
|
| | part1 = UtilsAssembly.getMovingPart(assembly, joint.Reference1) |
| | part2 = UtilsAssembly.getMovingPart(assembly, joint.Reference2) |
| |
|
| | if not part1 or not part2: |
| | return False |
| |
|
| | isAssembly = assembly.Type == "Assembly" |
| | if isAssembly: |
| | joint.Suppressed = True |
| | part1Connected = assembly.isPartConnected(part1) |
| | part2Connected = assembly.isPartConnected(part2) |
| | joint.Suppressed = False |
| | else: |
| | part1Connected = True |
| | part2Connected = False |
| |
|
| | moving_part = None |
| | moving_part_ref = None |
| | fixed_part_ref = None |
| | moving_placement = None |
| | fixed_placement = None |
| |
|
| | if not part1Connected and part1: |
| | moving_part = part1 |
| | moving_part_ref = joint.Reference1 |
| | fixed_part_ref = joint.Reference2 |
| | moving_placement = joint.Placement1 |
| | fixed_placement = joint.Placement2 |
| | elif not part2Connected and part2: |
| | moving_part = part2 |
| | moving_part_ref = joint.Reference2 |
| | fixed_part_ref = joint.Reference1 |
| | moving_placement = joint.Placement2 |
| | fixed_placement = joint.Placement1 |
| | else: |
| | |
| | return False |
| |
|
| | parts_to_move = [moving_part] |
| | if isAssembly: |
| | parts_to_move = parts_to_move + assembly.getDownstreamParts(moving_part, joint) |
| |
|
| | if savePlc: |
| | self.partsMovedByPresolved = {p: p.Placement for p in parts_to_move} |
| |
|
| | moving_part_global_jcs = UtilsAssembly.getJcsGlobalPlc(moving_placement, moving_part_ref) |
| | fixed_part_global_jcs = UtilsAssembly.getJcsGlobalPlc(fixed_placement, fixed_part_ref) |
| |
|
| | if not sameDir: |
| | moving_part_global_jcs = UtilsAssembly.flipPlacement(moving_part_global_jcs) |
| |
|
| | transform_plc = fixed_part_global_jcs * moving_part_global_jcs.inverse() |
| |
|
| | for part in parts_to_move: |
| | part.Placement = transform_plc * part.Placement |
| |
|
| | return True |
| |
|
| | def undoPreSolve(self, joint): |
| | if hasattr(self, "partsMovedByPresolved") and self.partsMovedByPresolved: |
| | for part, plc in self.partsMovedByPresolved.items(): |
| | if part and hasattr(part, "Placement"): |
| | part.Placement = plc |
| | self.partsMovedByPresolved = {} |
| |
|
| | joint.Placement1 = joint.Placement1 |
| |
|
| | def preventParallel(self, joint): |
| | |
| | parallel = self.areJcsZParallel(joint) |
| | if not parallel: |
| | return |
| |
|
| | assembly = self.getAssembly(joint) |
| |
|
| | part1 = UtilsAssembly.getMovingPart(assembly, joint.Reference1) |
| | part2 = UtilsAssembly.getMovingPart(assembly, joint.Reference2) |
| |
|
| | isAssembly = assembly.Type == "Assembly" |
| | if isAssembly: |
| | part1ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Reference1") |
| | part2ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Reference2") |
| | else: |
| | part1ConnectedByJoint = False |
| | part2ConnectedByJoint = True |
| |
|
| | if part2ConnectedByJoint: |
| | self.partMovedByPresolved = part2 |
| | self.presolveBackupPlc = part2.Placement |
| |
|
| | |
| | globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Reference2) |
| | |
| | rotation_axis = globalJcsPlc.Rotation.multVec(App.Vector(1, 0, 0)) |
| |
|
| | part2.Placement = UtilsAssembly.applyRotationToPlacementAlongAxis( |
| | part2.Placement, 10, rotation_axis |
| | ) |
| |
|
| | elif part1ConnectedByJoint: |
| | self.partMovedByPresolved = part1 |
| | self.presolveBackupPlc = part1.Placement |
| |
|
| | |
| | globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Reference1) |
| | |
| | rotation_axis = globalJcsPlc.Rotation.multVec(App.Vector(1, 0, 0)) |
| |
|
| | part1.Placement = UtilsAssembly.applyRotationToPlacementAlongAxis( |
| | part1.Placement, 10, rotation_axis |
| | ) |
| |
|
| | def areJcsSameDir(self, joint): |
| | globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Reference1) |
| | globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Reference2) |
| |
|
| | return UtilsAssembly.arePlacementSameDir(globalJcsPlc1, globalJcsPlc2) |
| |
|
| | def areJcsZParallel(self, joint): |
| | globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Reference1) |
| | globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Reference2) |
| |
|
| | return UtilsAssembly.arePlacementZParallel(globalJcsPlc1, globalJcsPlc2) |
| |
|
| |
|
| | class ViewProviderJoint: |
| | def __init__(self, vobj): |
| | """Set this object to the proxy object of the actual view provider""" |
| |
|
| | vobj.Proxy = self |
| |
|
| | vobj.addExtension("Gui::ViewProviderSuppressibleExtensionPython") |
| |
|
| | def attach(self, vobj): |
| | """Setup the scene sub-graph of the view provider, this method is mandatory""" |
| | self.app_obj = vobj.Object |
| |
|
| | self.switch_JCS1 = SoSwitchMarker(vobj) |
| | self.switch_JCS2 = SoSwitchMarker(vobj) |
| | self.switch_JCS_preview = SoSwitchMarker(vobj) |
| |
|
| | self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() |
| | self.display_mode.addChild(self.switch_JCS1) |
| | self.display_mode.addChild(self.switch_JCS2) |
| | self.display_mode.addChild(self.switch_JCS_preview) |
| | vobj.addDisplayMode(self.display_mode, "Wireframe") |
| |
|
| | def updateData(self, joint, prop): |
| | """If a property of the handled feature has changed we have the chance to handle this here""" |
| | |
| | if prop == "Placement1": |
| | if hasattr(joint, "Reference1") and joint.Reference1: |
| | plc = joint.Placement1 |
| | self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL |
| |
|
| | self.switch_JCS1.set_marker_placement(plc, joint.Reference1) |
| | else: |
| | self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE |
| |
|
| | if prop == "Placement2": |
| | if hasattr(joint, "Reference2") and joint.Reference2: |
| | plc = joint.Placement2 |
| | self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL |
| |
|
| | self.switch_JCS2.set_marker_placement(plc, joint.Reference2) |
| | else: |
| | self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE |
| |
|
| | def showPreviewJCS(self, visible, placement=None, ref=None): |
| | if visible: |
| | self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL |
| | self.switch_JCS_preview.set_marker_placement(placement, ref) |
| | else: |
| | self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE |
| |
|
| | def setPickableState(self, state: bool): |
| | """Set JCS selectable or unselectable in 3D view""" |
| | self.switch_JCS1.setPickableState(state) |
| | self.switch_JCS2.setPickableState(state) |
| | self.switch_JCS_preview.setPickableState(state) |
| |
|
| | def getDisplayModes(self, obj): |
| | """Return a list of display modes.""" |
| | modes = [] |
| | modes.append("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""" |
| | |
| | if prop == "color_X_axis" or prop == "color_Y_axis" or prop == "color_Z_axis": |
| | self.switch_JCS1.onChanged(vp, prop) |
| | self.switch_JCS2.onChanged(vp, prop) |
| | self.switch_JCS_preview.onChanged(vp, prop) |
| |
|
| | def getIcon(self): |
| | if self.app_obj.JointType == "Fixed": |
| | return ":/icons/Assembly_CreateJointFixed.svg" |
| | elif self.app_obj.JointType == "Revolute": |
| | return ":/icons/Assembly_CreateJointRevolute.svg" |
| | elif self.app_obj.JointType == "Cylindrical": |
| | return ":/icons/Assembly_CreateJointCylindrical.svg" |
| | elif self.app_obj.JointType == "Slider": |
| | return ":/icons/Assembly_CreateJointSlider.svg" |
| | elif self.app_obj.JointType == "Ball": |
| | 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": |
| | return ":/icons/Assembly_CreateJointScrew.svg" |
| | elif self.app_obj.JointType == "Gears": |
| | return ":/icons/Assembly_CreateJointGears.svg" |
| | elif self.app_obj.JointType == "Belt": |
| | return ":/icons/Assembly_CreateJointPulleys.svg" |
| |
|
| | return ":/icons/Assembly_CreateJoint.svg" |
| |
|
| | def getOverlayIcons(self): |
| | """ |
| | Return a dictionary of overlay icons. |
| | Keys are positions from Gui.IconPosition. |
| | Values are the icon resource names. |
| | """ |
| |
|
| | overlays = {} |
| |
|
| | assembly = self.app_obj.Proxy.getAssembly(self.app_obj) |
| | |
| | if hasattr(self.app_obj, "Reference1"): |
| | part = UtilsAssembly.getMovingPart(assembly, self.app_obj.Reference1) |
| | if part is not None and not assembly.isPartConnected(part): |
| | overlays[Gui.IconPosition.BottomLeft] = "Part_Detached" |
| |
|
| | return overlays |
| |
|
| | def dumps(self): |
| | """When saving the document this object gets stored using Python's json module.\ |
| | Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ |
| | to return a tuple of all serializable objects or None.""" |
| | return None |
| |
|
| | def loads(self, state): |
| | """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): |
| | App.closeActiveTransaction(True) |
| |
|
| | task = Gui.Control.activeTaskDialog() |
| | if task: |
| | task.reject() |
| |
|
| | assembly = vobj.Object.Proxy.getAssembly(vobj.Object) |
| |
|
| | if assembly is None: |
| | return False |
| |
|
| | if UtilsAssembly.activeAssembly() != assembly: |
| | vobj.Document.setEdit(assembly) |
| |
|
| | panel = TaskAssemblyCreateJoint(0, vobj.Object) |
| | dialog = Gui.Control.showDialog(panel) |
| | if dialog is not None: |
| | dialog.setAutoCloseOnTransactionChange(True) |
| | dialog.setAutoCloseOnDeletedDocument(True) |
| | dialog.setDocumentName(App.ActiveDocument.Name) |
| |
|
| | return True |
| |
|
| | def canDelete(self, _obj): |
| | return True |
| |
|
| |
|
| | |
| |
|
| |
|
| | class GroundedJoint: |
| | def __init__(self, joint, obj_to_ground): |
| | joint.Proxy = self |
| | self.joint = joint |
| |
|
| | joint.addProperty( |
| | "App::PropertyLink", |
| | "ObjectToGround", |
| | "Ground", |
| | QT_TRANSLATE_NOOP("App::Property", "The object to ground"), |
| | locked=True, |
| | ) |
| |
|
| | joint.ObjectToGround = obj_to_ground |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | return None |
| |
|
| | def onChanged(self, fp, prop): |
| | """Do something when a property has changed""" |
| | |
| | pass |
| |
|
| | def execute(self, fp): |
| | """Do something when doing a recomputation, this method is mandatory""" |
| | |
| | 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, vobj): |
| | """Setup the scene sub-graph of the view provider, this method is mandatory""" |
| | app_obj = vobj.Object |
| | if app_obj is None: |
| | return |
| | groundedObj = app_obj.ObjectToGround |
| | if groundedObj is None: |
| | return |
| |
|
| | self.scaleFactor = 3.0 |
| |
|
| | lockpadColorInt = Preferences.preferences().GetUnsigned("AssemblyConstraints", 0xCC333300) |
| | self.lockpadColor = coin.SoBaseColor() |
| | self.lockpadColor.rgb.setValue(UtilsAssembly.color_from_unsigned(lockpadColorInt)) |
| |
|
| | self.app_obj = vobj.Object |
| | app_doc = self.app_obj.Document |
| | self.gui_doc = Gui.getDocument(app_doc) |
| |
|
| | |
| | self.transform = coin.SoTransform() |
| | self.set_lock_position(groundedObj) |
| |
|
| | |
| | self.square = self.create_square() |
| |
|
| | |
| | self.arc = self.create_arc(0, 4, 4, 0, 180) |
| |
|
| | self.pick = coin.SoPickStyle() |
| | self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP) |
| |
|
| | |
| | self.lockpadSeparator = coin.SoSeparator() |
| | self.lockpadSeparator.addChild(self.lockpadColor) |
| | self.lockpadSeparator.addChild(self.square) |
| | self.lockpadSeparator.addChild(self.arc) |
| |
|
| | |
| | self.billboard = coin.SoVRMLBillboard() |
| | self.billboard.addChild(self.lockpadSeparator) |
| |
|
| | self.scale = coin.SoType.fromName("SoShapeScale").createInstance() |
| | self.scale.setPart("shape", self.billboard) |
| | self.scale.scaleFactor = self.scaleFactor |
| |
|
| | self.transformSeparator = coin.SoSeparator() |
| | self.transformSeparator.addChild(self.transform) |
| | self.transformSeparator.addChild(self.pick) |
| | self.transformSeparator.addChild(self.scale) |
| |
|
| | |
| | vobj.addDisplayMode(self.transformSeparator, "Wireframe") |
| |
|
| | def create_square(self): |
| | coords = [ |
| | (-5, -4, 0), |
| | (5, -4, 0), |
| | (5, 4, 0), |
| | (-5, 4, 0), |
| | ] |
| | vertices = coin.SoCoordinate3() |
| | vertices.point.setValues(0, 4, coords) |
| |
|
| | squareFace = coin.SoFaceSet() |
| | squareFace.numVertices.setValue(4) |
| |
|
| | square = coin.SoAnnotation() |
| | square.addChild(vertices) |
| | square.addChild(squareFace) |
| |
|
| | return square |
| |
|
| | def create_arc(self, centerX, centerY, radius, startAngle, endAngle): |
| | coords = [] |
| | for angle in range( |
| | startAngle, endAngle + 1, 5 |
| | ): |
| | rad = math.radians(angle) |
| | x = centerX + math.cos(rad) * radius |
| | y = centerY + math.sin(rad) * radius |
| | coords.append((x, y, 0)) |
| |
|
| | radius = radius * 0.7 |
| | for angle in range(endAngle + 1, startAngle - 1, -5): |
| | rad = math.radians(angle) |
| | x = centerX + math.cos(rad) * radius |
| | y = centerY + math.sin(rad) * radius |
| | coords.append((x, y, 0)) |
| |
|
| | vertices = coin.SoCoordinate3() |
| | vertices.point.setValues(0, len(coords), coords) |
| |
|
| | shapeHints = coin.SoShapeHints() |
| | shapeHints.faceType = coin.SoShapeHints.UNKNOWN_FACE_TYPE |
| |
|
| | line = coin.SoFaceSet() |
| | line.numVertices.setValue(len(coords)) |
| |
|
| | arc = coin.SoAnnotation() |
| | arc.addChild(shapeHints) |
| | arc.addChild(vertices) |
| | arc.addChild(line) |
| |
|
| | return arc |
| |
|
| | def set_lock_position(self, groundedObj): |
| | bBox = groundedObj.ViewObject.getBoundingBox() |
| | if bBox.isValid(): |
| | pos = bBox.Center |
| | else: |
| | pos = groundedObj.Placement.Base |
| |
|
| | self.transform.translation.setValue(pos.x, pos.y, pos.z) |
| |
|
| | def updateData(self, fp, prop): |
| | """If a property of the handled feature has changed we have the chance to handle this here""" |
| | |
| |
|
| | if prop == "Placement" and fp.ObjectToGround: |
| | self.set_lock_position(fp.ObjectToGround) |
| |
|
| | 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""" |
| | |
| | pass |
| |
|
| | def getIcon(self): |
| | return ":/icons/Assembly_ToggleGrounded.svg" |
| |
|
| | def dumps(self): |
| | """When saving the document this object gets stored using Python's json module.\ |
| | Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ |
| | to return a tuple of all serializable objects or None.""" |
| | return None |
| |
|
| | def loads(self, state): |
| | """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 canDelete(self, _obj): |
| | return True |
| |
|
| |
|
| | 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: |
| | |
| | return False |
| |
|
| | ref = [obj, [sub]] |
| | sel_obj = UtilsAssembly.getObject(ref) |
| |
|
| | if UtilsAssembly.isLink(sel_obj): |
| | linked = sel_obj.getLinkedObject() |
| | if linked == sel_obj: |
| | return True |
| | sel_obj = linked |
| |
|
| | if sel_obj.isDerivedFrom("Part::Feature") or sel_obj.isDerivedFrom("App::Part"): |
| | return True |
| |
|
| | if sel_obj.isDerivedFrom("App::LocalCoordinateSystem") or sel_obj.isDerivedFrom( |
| | "App::DatumElement" |
| | ): |
| | datum = sel_obj |
| | if datum.isDerivedFrom("App::DatumElement"): |
| | parent = datum.getParent() |
| | if parent.isDerivedFrom("App::LocalCoordinateSystem"): |
| | datum = parent |
| |
|
| | if self.assembly.hasObject(datum) and hasattr(datum, "MapMode"): |
| | |
| | return datum.MapMode == "Deactivated" |
| |
|
| | return True |
| |
|
| | return False |
| |
|
| |
|
| | activeTask = None |
| |
|
| |
|
| | class TaskAssemblyCreateJoint(QtCore.QObject): |
| | def __init__(self, jointTypeIndex, jointObj=None, subclass=False): |
| | super().__init__() |
| |
|
| | global activeTask |
| | activeTask = self |
| | self.blockOffsetRotation = False |
| |
|
| | self.assembly = UtilsAssembly.activeAssembly() |
| | if not self.assembly: |
| | self.assembly = UtilsAssembly.activePart() |
| | self.activeType = "Part" |
| | else: |
| | self.activeType = "Assembly" |
| | self.assembly.ensureIdentityPlacements() |
| |
|
| | self.doc = self.assembly.Document |
| | self.gui_doc = Gui.getDocument(self.doc) |
| |
|
| | self.view = self.gui_doc.activeView() |
| |
|
| | if not self.assembly or not self.view or not self.doc: |
| | return |
| |
|
| | if self.activeType == "Assembly": |
| | self.assembly.ViewObject.MoveOnlyPreselected = True |
| | self.assembly.ViewObject.MoveInCommand = False |
| |
|
| | |
| | self.form = QtWidgets.QWidget() |
| |
|
| | |
| | self.jForm = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui", self.form) |
| |
|
| | |
| | layout = QtWidgets.QVBoxLayout(self.form) |
| | if not subclass: |
| | layout.setContentsMargins(0, 0, 0, 0) |
| | layout.setSpacing(0) |
| | layout.addWidget(self.jForm) |
| |
|
| | self.isolate_modes = ["Transparent", "Wireframe", "Hidden", "Disabled"] |
| | self.jForm.isolateType.addItems( |
| | [translate("Assembly", mode) for mode in self.isolate_modes] |
| | ) |
| | self.jForm.isolateType.currentIndexChanged.connect(self.updateIsolation) |
| |
|
| | if self.activeType == "Part": |
| | self.jForm.setWindowTitle("Match parts") |
| | self.jForm.jointType.hide() |
| | self.jForm.isolateType.hide() |
| |
|
| | self.jForm.jointType.addItems(TranslatedJointTypes) |
| |
|
| | self.jForm.jointType.setCurrentIndex(jointTypeIndex) |
| | self.jType = JointTypes[self.jForm.jointType.currentIndex()] |
| | self.jForm.jointType.currentIndexChanged.connect(self.onJointTypeChanged) |
| |
|
| | if jointObj: |
| | Gui.Selection.clearSelection() |
| | self.creating = False |
| | self.joint = jointObj |
| | self.jointName = jointObj.Label |
| | App.setActiveTransaction("Edit " + self.jointName + " Joint") |
| |
|
| | self.updateTaskboxFromJoint() |
| | self.visibilityBackup = self.joint.Visibility |
| | self.joint.Visibility = True |
| |
|
| | else: |
| | self.creating = True |
| | self.jointName = self.jForm.jointType.currentText().replace(" ", "") |
| | if self.activeType == "Part": |
| | App.setActiveTransaction("Transform") |
| | else: |
| | App.setActiveTransaction("Create " + self.jointName + " Joint") |
| |
|
| | self.refs = [] |
| | self.presel_ref = None |
| |
|
| | self.createJointObject() |
| | self.visibilityBackup = False |
| |
|
| | self.jForm.angleSpinbox.valueChanged.connect(self.onAngleChanged) |
| | self.jForm.distanceSpinbox.valueChanged.connect(self.onDistanceChanged) |
| | self.jForm.distanceSpinbox2.valueChanged.connect(self.onDistance2Changed) |
| | self.jForm.offsetSpinbox.valueChanged.connect(self.onOffsetChanged) |
| | self.jForm.rotationSpinbox.valueChanged.connect(self.onRotationChanged) |
| | bind = Gui.ExpressionBinding(self.jForm.angleSpinbox).bind(self.joint, "Angle") |
| | bind = Gui.ExpressionBinding(self.jForm.distanceSpinbox).bind(self.joint, "Distance") |
| | bind = Gui.ExpressionBinding(self.jForm.distanceSpinbox2).bind(self.joint, "Distance2") |
| | bind = Gui.ExpressionBinding(self.jForm.offsetSpinbox).bind(self.joint, "Offset2.Base.z") |
| | bind = Gui.ExpressionBinding(self.jForm.rotationSpinbox).bind( |
| | self.joint, "Offset2.Rotation.Yaw" |
| | ) |
| |
|
| | self.jForm.reverseRotCheckbox.setChecked(self.jType == "Gears") |
| | self.jForm.reverseRotCheckbox.stateChanged.connect(self.reverseRotToggled) |
| |
|
| | self.jForm.advancedOffsetCheckbox.stateChanged.connect(self.advancedOffsetToggled) |
| |
|
| | self.jForm.offset1Button.clicked.connect(self.onOffset1Clicked) |
| | self.jForm.offset2Button.clicked.connect(self.onOffset2Clicked) |
| | self.jForm.PushButtonReverse.clicked.connect(self.onReverseClicked) |
| |
|
| | self.jForm.limitCheckbox1.stateChanged.connect(self.adaptUi) |
| | self.jForm.limitCheckbox2.stateChanged.connect(self.adaptUi) |
| | self.jForm.limitCheckbox3.stateChanged.connect(self.adaptUi) |
| | self.jForm.limitCheckbox4.stateChanged.connect(self.adaptUi) |
| |
|
| | self.jForm.limitLenMinSpinbox.valueChanged.connect(self.onLimitLenMinChanged) |
| | self.jForm.limitLenMaxSpinbox.valueChanged.connect(self.onLimitLenMaxChanged) |
| | self.jForm.limitRotMinSpinbox.valueChanged.connect(self.onLimitRotMinChanged) |
| | self.jForm.limitRotMaxSpinbox.valueChanged.connect(self.onLimitRotMaxChanged) |
| | bind = Gui.ExpressionBinding(self.jForm.limitLenMinSpinbox).bind(self.joint, "LengthMin") |
| | bind = Gui.ExpressionBinding(self.jForm.limitLenMaxSpinbox).bind(self.joint, "LengthMax") |
| | bind = Gui.ExpressionBinding(self.jForm.limitRotMinSpinbox).bind(self.joint, "AngleMin") |
| | bind = Gui.ExpressionBinding(self.jForm.limitRotMaxSpinbox).bind(self.joint, "AngleMax") |
| |
|
| | self.adaptUi() |
| |
|
| | if self.creating: |
| | |
| | |
| | |
| | self.handleInitialSelection() |
| |
|
| | UtilsAssembly.setJointsPickableState(self.doc, False) |
| |
|
| | 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) |
| |
|
| | self.jForm.featureList.installEventFilter(self) |
| |
|
| | self.createDeleteAction() |
| |
|
| | self.addition_rejected = False |
| |
|
| | def accept(self): |
| | if len(self.refs) != 2: |
| | App.Console.PrintWarning( |
| | translate("Assembly", "Select 2 elements from 2 separate parts") |
| | ) |
| | return False |
| |
|
| | self.deactivate() |
| |
|
| | solveIfAllowed(self.assembly) |
| | if self.activeType == "Assembly": |
| | self.joint.Visibility = self.visibilityBackup |
| | else: |
| | self.joint.Document.removeObject(self.joint.Name) |
| |
|
| | cmds = UtilsAssembly.generatePropertySettings(self.joint) |
| | Gui.doCommand(cmds) |
| |
|
| | App.closeActiveTransaction() |
| | return True |
| |
|
| | def reject(self): |
| | self.deactivate() |
| | App.closeActiveTransaction(True) |
| | if not self.creating: |
| | self.joint.Visibility = self.visibilityBackup |
| | return True |
| |
|
| | def autoClosedOnTransactionChange(self): |
| | self.reject() |
| |
|
| | def autoClosedOnDeletedDocument(self): |
| | global activeTask |
| | activeTask = None |
| | Gui.Selection.removeSelectionGate() |
| | Gui.Selection.removeObserver(self) |
| | Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection) |
| | App.closeActiveTransaction(True) |
| |
|
| | def deactivate(self): |
| | global activeTask |
| | activeTask = None |
| |
|
| | if self.activeType == "Assembly": |
| | self.assembly.clearUndo() |
| | self.assembly.ViewObject.MoveOnlyPreselected = False |
| | self.assembly.ViewObject.MoveInCommand = 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) |
| | UtilsAssembly.setJointsPickableState(self.doc, True) |
| | if Gui.Control.activeDialog(): |
| | Gui.Control.closeDialog() |
| |
|
| | def handleInitialSelection(self): |
| | selection = Gui.Selection.getSelectionEx("*", 0) |
| | if not selection: |
| | return |
| | for sel in selection: |
| | |
| | |
| |
|
| | if not sel.SubElementNames: |
| | |
| | Gui.Selection.removeSelection(sel.Object) |
| | continue |
| |
|
| | for sub_name in sel.SubElementNames: |
| | |
| | |
| | ref = [sel.Object, [sub_name, sub_name]] |
| | moving_part = self.getMovingPart(ref) |
| |
|
| | |
| | if moving_part is None: |
| | Gui.Selection.removeSelection(sel.Object, sub_name) |
| | continue |
| |
|
| | if len(self.refs) == 1 and moving_part == self.getMovingPart(self.refs[0]): |
| | |
| | self.refs.clear() |
| | Gui.Selection.clearSelection() |
| | return |
| |
|
| | self.refs.append(ref) |
| |
|
| | |
| | if len(self.refs) != 2: |
| | self.refs.clear() |
| | Gui.Selection.clearSelection() |
| | else: |
| | self.updateJoint() |
| |
|
| | def createJointObject(self): |
| | type_index = self.jForm.jointType.currentIndex() |
| |
|
| | if self.activeType == "Part": |
| | self.joint = self.assembly.newObject("App::FeaturePython", "Temporary joint") |
| | else: |
| | joint_group = UtilsAssembly.getJointGroup(self.assembly) |
| | self.joint = joint_group.newObject("App::FeaturePython", "Joint") |
| | self.joint.Label = self.jointName |
| |
|
| | Joint(self.joint, type_index) |
| | ViewProviderJoint(self.joint.ViewObject) |
| |
|
| | def onJointTypeChanged(self, index): |
| | self.jType = JointTypes[self.jForm.jointType.currentIndex()] |
| | self.joint.Proxy.setJointType(self.joint, self.jType) |
| | self.adaptUi() |
| |
|
| | def onAngleChanged(self, quantity): |
| | self.joint.Angle = self.jForm.angleSpinbox.property("rawValue") |
| |
|
| | def onDistanceChanged(self, quantity): |
| | self.joint.Distance = self.jForm.distanceSpinbox.property("rawValue") |
| |
|
| | def onDistance2Changed(self, quantity): |
| | self.joint.Distance2 = self.jForm.distanceSpinbox2.property("rawValue") |
| |
|
| | def onOffsetChanged(self, quantity): |
| | if self.blockOffsetRotation: |
| | return |
| |
|
| | self.joint.Offset2.Base = App.Vector(0, 0, self.jForm.offsetSpinbox.property("rawValue")) |
| |
|
| | def onRotationChanged(self, quantity): |
| | if self.blockOffsetRotation: |
| | return |
| |
|
| | yaw = self.jForm.rotationSpinbox.property("rawValue") |
| | ypr = self.joint.Offset2.Rotation.getYawPitchRoll() |
| | self.joint.Offset2.Rotation.setYawPitchRoll(yaw, ypr[1], ypr[2]) |
| |
|
| | def onLimitLenMinChanged(self, quantity): |
| | if self.jForm.limitCheckbox1.isChecked(): |
| | self.joint.LengthMin = self.jForm.limitLenMinSpinbox.property("rawValue") |
| |
|
| | def onLimitLenMaxChanged(self, quantity): |
| | if self.jForm.limitCheckbox2.isChecked(): |
| | self.joint.LengthMax = self.jForm.limitLenMaxSpinbox.property("rawValue") |
| |
|
| | def onLimitRotMinChanged(self, quantity): |
| | if self.jForm.limitCheckbox3.isChecked(): |
| | self.joint.AngleMin = self.jForm.limitRotMinSpinbox.property("rawValue") |
| |
|
| | def onLimitRotMaxChanged(self, quantity): |
| | if self.jForm.limitCheckbox4.isChecked(): |
| | self.joint.AngleMax = self.jForm.limitRotMaxSpinbox.property("rawValue") |
| |
|
| | def onReverseClicked(self): |
| | self.joint.Proxy.flipOnePart(self.joint) |
| |
|
| | def reverseRotToggled(self, val): |
| | if val: |
| | self.jForm.jointType.setCurrentIndex(JointTypes.index("Gears")) |
| | else: |
| | self.jForm.jointType.setCurrentIndex(JointTypes.index("Belt")) |
| |
|
| | def adaptUi(self): |
| | jType = self.jType |
| |
|
| | needAngle = jType in JointUsingAngle |
| | self.jForm.angleLabel.setVisible(needAngle) |
| | self.jForm.angleSpinbox.setVisible(needAngle) |
| |
|
| | needDistance = jType in JointUsingDistance |
| | self.jForm.distanceLabel.setVisible(needDistance) |
| | self.jForm.distanceSpinbox.setVisible(needDistance) |
| | if needDistance: |
| | if jType == "Distance": |
| | self.jForm.distanceLabel.setText(translate("Assembly", "Distance")) |
| | elif jType == "Gears" or jType == "Belt": |
| | self.jForm.distanceLabel.setText(translate("Assembly", "Radius 1")) |
| | elif jType == "Screw": |
| | self.jForm.distanceLabel.setText(translate("Assembly", "Thread pitch")) |
| | else: |
| | self.jForm.distanceLabel.setText(translate("Assembly", "Pitch radius")) |
| |
|
| | needDistance2 = jType in JointUsingDistance2 |
| | self.jForm.distanceLabel2.setVisible(needDistance2) |
| | self.jForm.distanceSpinbox2.setVisible(needDistance2) |
| | self.jForm.reverseRotCheckbox.setVisible(needDistance2) |
| |
|
| | if jType in JointNoNegativeDistance: |
| | |
| | self.jForm.distanceSpinbox.setProperty("minimum", 1e-7) |
| | if self.jForm.distanceSpinbox.property("rawValue") == 0.0: |
| | self.jForm.distanceSpinbox.setProperty("rawValue", 1.0) |
| |
|
| | if jType == "Gears" or jType == "Belt": |
| | self.jForm.distanceSpinbox2.setProperty("minimum", 1e-7) |
| | if self.jForm.distanceSpinbox2.property("rawValue") == 0.0: |
| | self.jForm.distanceSpinbox2.setProperty("rawValue", 1.0) |
| | else: |
| | self.jForm.distanceSpinbox.setProperty("minimum", float("-inf")) |
| | self.jForm.distanceSpinbox2.setProperty("minimum", float("-inf")) |
| |
|
| | advancedOffset = self.jForm.advancedOffsetCheckbox.isChecked() |
| | needOffset = jType in JointUsingOffset |
| | needRotation = jType in JointUsingRotation |
| | self.jForm.offset1Label.setVisible(advancedOffset) |
| | self.jForm.offset2Label.setVisible(advancedOffset) |
| | self.jForm.offset1Button.setVisible(advancedOffset) |
| | self.jForm.offset2Button.setVisible(advancedOffset) |
| | self.jForm.offsetLabel.setVisible(not advancedOffset and needOffset) |
| | self.jForm.offsetSpinbox.setVisible(not advancedOffset and needOffset) |
| | self.jForm.rotationLabel.setVisible(not advancedOffset and needRotation) |
| | self.jForm.rotationSpinbox.setVisible(not advancedOffset and needRotation) |
| |
|
| | self.jForm.PushButtonReverse.setVisible(jType in JointUsingReverse) |
| |
|
| | needLengthLimits = jType in JointUsingLimitLength |
| | needAngleLimits = jType in JointUsingLimitAngle |
| | needLimits = needLengthLimits or needAngleLimits |
| | self.jForm.groupBox_limits.setVisible(needLimits) |
| |
|
| | if needLimits: |
| | self.joint.EnableLengthMin = self.jForm.limitCheckbox1.isChecked() |
| | self.joint.EnableLengthMax = self.jForm.limitCheckbox2.isChecked() |
| | self.joint.EnableAngleMin = self.jForm.limitCheckbox3.isChecked() |
| | self.joint.EnableAngleMax = self.jForm.limitCheckbox4.isChecked() |
| |
|
| | self.jForm.limitCheckbox1.setVisible(needLengthLimits) |
| | self.jForm.limitCheckbox2.setVisible(needLengthLimits) |
| | self.jForm.limitLenMinSpinbox.setVisible(needLengthLimits) |
| | self.jForm.limitLenMaxSpinbox.setVisible(needLengthLimits) |
| |
|
| | self.jForm.limitCheckbox3.setVisible(needAngleLimits) |
| | self.jForm.limitCheckbox4.setVisible(needAngleLimits) |
| | self.jForm.limitRotMinSpinbox.setVisible(needAngleLimits) |
| | self.jForm.limitRotMaxSpinbox.setVisible(needAngleLimits) |
| |
|
| | if needLengthLimits: |
| | self.jForm.limitLenMinSpinbox.setEnabled(self.joint.EnableLengthMin) |
| | self.jForm.limitLenMaxSpinbox.setEnabled(self.joint.EnableLengthMax) |
| | self.onLimitLenMinChanged(0) |
| | self.onLimitLenMaxChanged(0) |
| |
|
| | if needAngleLimits: |
| | self.jForm.limitRotMinSpinbox.setEnabled(self.joint.EnableAngleMin) |
| | self.jForm.limitRotMaxSpinbox.setEnabled(self.joint.EnableAngleMax) |
| | self.onLimitRotMinChanged(0) |
| | self.onLimitRotMaxChanged(0) |
| |
|
| | self.updateOffsetWidgets() |
| |
|
| | def updateOffsetWidgets(self): |
| | |
| | pos = self.joint.Offset1.Base |
| | self.jForm.offset1Button.setText(f"({pos.x}, {pos.y}, {pos.z})") |
| |
|
| | pos = self.joint.Offset2.Base |
| | self.jForm.offset2Button.setText(f"({pos.x}, {pos.y}, {pos.z})") |
| |
|
| | self.blockOffsetRotation = True |
| | self.jForm.offsetSpinbox.setProperty("rawValue", pos.z) |
| | self.jForm.rotationSpinbox.setProperty( |
| | "rawValue", self.joint.Offset2.Rotation.getYawPitchRoll()[0] |
| | ) |
| | self.blockOffsetRotation = False |
| |
|
| | def advancedOffsetToggled(self, on): |
| | self.adaptUi() |
| | self.updateOffsetWidgets() |
| |
|
| | def onOffset1Clicked(self): |
| | UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset1") |
| | self.updateOffsetWidgets() |
| |
|
| | def onOffset2Clicked(self): |
| | UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset2") |
| | self.updateOffsetWidgets() |
| |
|
| | def updateIsolation(self): |
| | """Isolates the two selected components or clears isolation.""" |
| |
|
| | if self.activeType != "Assembly": |
| | return |
| |
|
| | isolate_mode = self.jForm.isolateType.currentIndex() |
| |
|
| | assembly_vobj = self.assembly.ViewObject |
| |
|
| | |
| | if isolate_mode == 3: |
| | assembly_vobj.clearIsolate() |
| | return |
| |
|
| | if len(self.refs) == 2: |
| | try: |
| | |
| | parts_to_isolate = { |
| | self.getMovingPart(self.refs[0]), |
| | self.getMovingPart(self.refs[1]), |
| | } |
| | assembly_vobj.isolateComponents(list(parts_to_isolate), isolate_mode) |
| | except Exception as e: |
| | App.Console.PrintWarning(f"Could not update isolation: {e}\n") |
| | assembly_vobj.clearIsolate() |
| | else: |
| | assembly_vobj.clearIsolate() |
| |
|
| | def updateTaskboxFromJoint(self): |
| | self.refs = [] |
| | self.presel_ref = None |
| |
|
| | ref1 = self.joint.Reference1 |
| | ref2 = self.joint.Reference2 |
| |
|
| | self.refs.append(ref1) |
| | self.refs.append(ref2) |
| |
|
| | sub1 = UtilsAssembly.addTipNameToSub(ref1) |
| | sub2 = UtilsAssembly.addTipNameToSub(ref2) |
| |
|
| | Gui.Selection.addSelection(ref1[0].Document.Name, ref1[0].Name, sub1) |
| | Gui.Selection.addSelection(ref2[0].Document.Name, ref2[0].Name, sub2) |
| |
|
| | self.jForm.angleSpinbox.setProperty("rawValue", self.joint.Angle.Value) |
| | self.jForm.distanceSpinbox.setProperty("rawValue", self.joint.Distance.Value) |
| | self.jForm.distanceSpinbox2.setProperty("rawValue", self.joint.Distance2.Value) |
| | self.jForm.offsetSpinbox.setProperty("rawValue", self.joint.Offset2.Base.z) |
| | self.jForm.rotationSpinbox.setProperty( |
| | "rawValue", self.joint.Offset2.Rotation.getYawPitchRoll()[0] |
| | ) |
| |
|
| | self.jForm.limitCheckbox1.setChecked(self.joint.EnableLengthMin) |
| | self.jForm.limitCheckbox2.setChecked(self.joint.EnableLengthMax) |
| | self.jForm.limitCheckbox3.setChecked(self.joint.EnableAngleMin) |
| | self.jForm.limitCheckbox4.setChecked(self.joint.EnableAngleMax) |
| | self.jForm.limitLenMinSpinbox.setProperty("rawValue", self.joint.LengthMin.Value) |
| | self.jForm.limitLenMaxSpinbox.setProperty("rawValue", self.joint.LengthMax.Value) |
| | self.jForm.limitRotMinSpinbox.setProperty("rawValue", self.joint.AngleMin.Value) |
| | self.jForm.limitRotMaxSpinbox.setProperty("rawValue", self.joint.AngleMax.Value) |
| |
|
| | self.jForm.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType)) |
| | self.updateJointList() |
| | self.updateIsolation() |
| |
|
| | def updateJoint(self): |
| | |
| | self.updateJointList() |
| |
|
| | |
| | self.joint.Proxy.setJointConnectors(self.joint, self.refs) |
| |
|
| | self.updateIsolation() |
| |
|
| | def updateJointList(self): |
| | self.jForm.featureList.clear() |
| | simplified_names = [] |
| | for ref in self.refs: |
| |
|
| | sname = UtilsAssembly.getObject(ref).Label |
| |
|
| | element_name = UtilsAssembly.getElementName(ref[1][0]) |
| | if element_name != "": |
| | sname = sname + "." + element_name |
| | simplified_names.append(sname) |
| | self.jForm.featureList.addItems(simplified_names) |
| |
|
| | def updateLimits(self): |
| | needLengthLimits = self.jType in JointUsingLimitLength |
| | needAngleLimits = self.jType in JointUsingLimitAngle |
| | if needLengthLimits: |
| | distance = UtilsAssembly.getJointDistance(self.joint) |
| | if ( |
| | not self.jForm.limitCheckbox1.isChecked() |
| | and self.jForm.limitLenMinSpinbox.property("expression") == "" |
| | ): |
| | self.jForm.limitLenMinSpinbox.setProperty("rawValue", distance) |
| | if ( |
| | not self.jForm.limitCheckbox2.isChecked() |
| | and self.jForm.limitLenMaxSpinbox.property("expression") == "" |
| | ): |
| | self.jForm.limitLenMaxSpinbox.setProperty("rawValue", distance) |
| |
|
| | if needAngleLimits: |
| | angle = UtilsAssembly.getJointXYAngle(self.joint) / math.pi * 180 |
| | if ( |
| | not self.jForm.limitCheckbox3.isChecked() |
| | and self.jForm.limitRotMinSpinbox.property("expression") == "" |
| | ): |
| | self.jForm.limitRotMinSpinbox.setProperty("rawValue", angle) |
| | if ( |
| | not self.jForm.limitCheckbox4.isChecked() |
| | and self.jForm.limitRotMaxSpinbox.property("expression") == "" |
| | ): |
| | self.jForm.limitRotMaxSpinbox.setProperty("rawValue", angle) |
| |
|
| | def moveMouse(self, info): |
| | if len(self.refs) >= 2 or ( |
| | len(self.refs) == 1 |
| | and ( |
| | not self.presel_ref |
| | or self.getMovingPart(self.refs[0]) == self.getMovingPart(self.presel_ref) |
| | ) |
| | ): |
| | self.joint.ViewObject.Proxy.showPreviewJCS(False) |
| | if len(self.refs) >= 2: |
| | self.updateLimits() |
| | return |
| |
|
| | cursor_pos = self.view.getCursorPos() |
| | cursor_info = self.view.getObjectInfo(cursor_pos) |
| | |
| |
|
| | if ( |
| | not cursor_info |
| | or not self.presel_ref |
| | |
| | |
| | |
| | ): |
| | self.joint.ViewObject.Proxy.showPreviewJCS(False) |
| | return |
| |
|
| | ref = self.presel_ref |
| |
|
| | |
| | newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"]) |
| | vertex_name = UtilsAssembly.findElementClosestVertex(ref, newPos) |
| |
|
| | ref = UtilsAssembly.addVertexToReference(ref, vertex_name) |
| |
|
| | placement = self.joint.Proxy.findPlacement(self.joint, ref, 0) |
| | self.joint.ViewObject.Proxy.showPreviewJCS(True, placement, ref) |
| | self.previewJCSVisible = True |
| |
|
| | |
| | def KeyboardEvent(self, info): |
| | if info["State"] == "UP" and info["Key"] == "ESCAPE": |
| | self.reject() |
| |
|
| | if info["State"] == "UP" and info["Key"] == "RETURN": |
| | self.accept() |
| |
|
| | def _removeSelectedItems(self, selected_indexes): |
| | for index in selected_indexes: |
| | row = index.row() |
| | if row < len(self.refs): |
| | ref = self.refs[row] |
| | sub = UtilsAssembly.addTipNameToSub(ref) |
| | Gui.Selection.removeSelection(ref[0], sub) |
| | else: |
| | print(f"Row {row} is out of bounds for refs (length: {len(self.refs)})") |
| |
|
| | def eventFilter(self, watched, event): |
| | if self.jForm is not None and watched == self.jForm.featureList: |
| | if event.type() == QtCore.QEvent.ShortcutOverride: |
| | if ( |
| | hasattr(self, "deleteAction") |
| | and self.deleteAction.shortcut().matches(event.key()) |
| | != QtGui.QKeySequence.NoMatch |
| | ): |
| | event.accept() |
| | return True |
| | return False |
| |
|
| | elif event.type() == QtCore.QEvent.KeyPress: |
| | if ( |
| | hasattr(self, "deleteAction") |
| | and self.deleteAction.shortcut().matches(event.key()) |
| | != QtGui.QKeySequence.NoMatch |
| | ): |
| | self.deleteAction.trigger() |
| | return True |
| |
|
| | return super().eventFilter(watched, event) |
| |
|
| | def createDeleteAction(self): |
| | """Create delete action with shortcut""" |
| | try: |
| | delete_sequence = Gui.QtTools.deleteKeySequence() |
| | except AttributeError: |
| | |
| | delete_sequence = QtGui.QKeySequence(QtCore.Qt.Key_Delete) |
| |
|
| | self.deleteAction = QtGui.QAction("Remove", self.jForm) |
| | self.deleteAction.setShortcut(delete_sequence) |
| |
|
| | self.deleteAction.setIcon( |
| | QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton) |
| | ) |
| |
|
| | self.deleteAction.setShortcutVisibleInContextMenu(True) |
| |
|
| | self.jForm.featureList.addAction(self.deleteAction) |
| | self.jForm.featureList.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) |
| |
|
| | self.deleteAction.triggered.connect(self.deleteSelectedItems) |
| |
|
| | def deleteSelectedItems(self): |
| | """Delete selected items from the feature list - same logic as Delete key in ev filter""" |
| | selected_indexes = self.jForm.featureList.selectedIndexes() |
| | self._removeSelectedItems(selected_indexes) |
| |
|
| | def getMovingPart(self, ref): |
| | return UtilsAssembly.getMovingPart(self.assembly, ref) |
| |
|
| | |
| | def addSelection(self, doc_name, obj_name, sub_name, mousePos): |
| | rootObj = App.getDocument(doc_name).getObject(obj_name) |
| |
|
| | |
| | |
| | resolved = rootObj.resolveSubElement(sub_name, True) |
| | sub_name = resolved[2] |
| |
|
| | sub_name = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, sub_name) |
| |
|
| | ref = [rootObj, [sub_name]] |
| | moving_part = self.getMovingPart(ref) |
| |
|
| | |
| | acceptable = True |
| | if len(self.refs) >= 2: |
| | |
| | acceptable = False |
| |
|
| | for reference in self.refs: |
| | sel_moving_part = self.getMovingPart(reference) |
| | if sel_moving_part == moving_part: |
| | |
| | acceptable = False |
| |
|
| | if not acceptable: |
| | self.addition_rejected = True |
| | Gui.Selection.removeSelection(doc_name, obj_name, sub_name) |
| | return |
| |
|
| | |
| |
|
| | mousePos = App.Vector(mousePos[0], mousePos[1], mousePos[2]) |
| | vertex_name = UtilsAssembly.findElementClosestVertex(ref, mousePos) |
| |
|
| | |
| | ref = UtilsAssembly.addVertexToReference(ref, vertex_name) |
| |
|
| | self.refs.append(ref) |
| | self.updateJoint() |
| |
|
| | |
| | self.joint.ViewObject.Proxy.showPreviewJCS(False) |
| |
|
| | def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None): |
| | if self.addition_rejected: |
| | self.addition_rejected = False |
| | return |
| |
|
| | rootObj = App.getDocument(doc_name).getObject(obj_name) |
| |
|
| | |
| | resolved = rootObj.resolveSubElement(sub_name, True) |
| | sub_name = resolved[2] |
| |
|
| | sub_name = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, sub_name) |
| |
|
| | for reference in self.refs[:]: |
| | ref_obj = reference[0] |
| | ref_element_name = reference[1][0] if len(reference[1]) > 0 else "" |
| |
|
| | |
| | if ref_obj == rootObj and ref_element_name == sub_name: |
| | self.refs.remove(reference) |
| | break |
| | else: |
| | print("No matching ref found for removal!") |
| |
|
| | self.updateJoint() |
| |
|
| | def setPreselection(self, doc_name, obj_name, sub_name): |
| | if not sub_name: |
| | self.presel_ref = None |
| | return |
| |
|
| | self.presel_ref = [App.getDocument(doc_name).getObject(obj_name), [sub_name]] |
| |
|
| | def clearSelection(self, doc_name): |
| | self.refs.clear() |
| | self.updateJoint() |
| |
|