diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index cbbdf29365..ff83f8066f 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -246,7 +246,6 @@ def createGroundedJoint(obj): joint_group = UtilsAssembly.getJointGroup(assembly) - obj.Label = obj.Label + " 🔒" ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") JointObject.GroundedJoint(ground, obj) JointObject.ViewProviderGroundedJoint(ground.ViewObject) @@ -310,9 +309,6 @@ class CommandToggleGrounded: hasattr(joint, "ObjectToGround") and joint.ObjectToGround == part_containing_obj ): - # Remove grounded tag. - if part_containing_obj.Label.endswith(" 🔒"): - part_containing_obj.Label = part_containing_obj.Label[:-2] doc = App.ActiveDocument doc.removeObject(joint.Name) doc.recompute() diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index b7540b8e23..31e350d60e 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -173,14 +173,6 @@ bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const } Gui::Command::commitCommand(); - // Remove grounded tag if any. (as it is not done in jointObject.py onDelete) - std::string label = obj->Label.getValue(); - - if (label.size() >= 4 && label.substr(label.size() - 2) == " 🔒") { - label = label.substr(0, label.size() - 2); - obj->Label.setValue(label.c_str()); - } - return true; } @@ -512,9 +504,17 @@ bool ViewProviderAssembly::getSelectedObjectsWithinAssembly(bool addPreselection if (assemblyPart->hasObject(obj, true)) { auto* propPlacement = dynamic_cast(obj->getPropertyByName("Placement")); - if (propPlacement) { - docsToMove.emplace_back(obj, propPlacement->getValue()); + if (!propPlacement) { + continue; } + // We have to exclude Grounded joints as they happen to have a Placement prop + auto* propLink = + dynamic_cast(obj->getPropertyByName("ObjectToGround")); + if (propLink) { + continue; + } + + docsToMove.emplace_back(obj, propPlacement->getValue()); } } } @@ -544,7 +544,12 @@ bool ViewProviderAssembly::getSelectedObjectsWithinAssembly(bool addPreselection auto* propPlacement = dynamic_cast( preselectedObj->getPropertyByName("Placement")); if (propPlacement) { - docsToMove.emplace_back(preselectedObj, propPlacement->getValue()); + // We have to exclude Grounded joints as they happen to have a Placement prop + auto* propLink = dynamic_cast( + preselectedObj->getPropertyByName("ObjectToGround")); + if (!propLink) { + docsToMove.emplace_back(preselectedObj, propPlacement->getValue()); + } } } } @@ -607,7 +612,7 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vectorgetTypeId().isDerivedFrom(App::Part::getClassTypeId()) - || obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + || obj->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { return obj; } else if (obj->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { @@ -619,16 +624,13 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vectorgetTypeId().isDerivedFrom(App::Part::getClassTypeId()) - || linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + || linkedObj->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { return obj; } } } - // then its neither a part or body or a link to a part or body. So it is something like - // assembly.box.face1 - objName = subNames[subNames.size() - 2]; - return appDoc->getObject(objName.c_str()); + return nullptr; } ViewProviderAssembly::DragMode ViewProviderAssembly::findDragMode() diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 968ed334db..9b7874b0b2 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -92,6 +92,19 @@ def solveIfAllowed(assembly, storePrev=False): assembly.solve(storePrev) +def get_camera_height(gui_doc): + camera = gui_doc.ActiveView.getCameraNode() + + # Check if the camera is a perspective camera + if isinstance(camera, coin.SoPerspectiveCamera): + return camera.focalDistance.getValue() + elif isinstance(camera, coin.SoOrthographicCamera): + return camera.height.getValue() + else: + # Default value if camera type is unknown + return 200 + + # The joint object consists of 2 JCS (joint coordinate systems) and a Joint Type. # A JCS is a placement that is computed (unless it is detached) from : # - An Object name: this is the name of the solid. It can be any Part::Feature solid. @@ -436,8 +449,14 @@ class Joint: # in the current closest direction, ie either matched or flipped. sameDir = self.areJcsSameDir(joint) assembly = self.getAssembly(joint) - part1ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Part1") - part2ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Part2") + isAssembly = assembly.Type == "Assembly" + if isAssembly: + part1ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Part1") + part2ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Part2") + else: + part1ConnectedByJoint = False + part2ConnectedByJoint = True + if part2ConnectedByJoint: if savePlc: self.partMovedByPresolved = joint.Part2 @@ -509,15 +528,16 @@ class ViewProviderJoint: self.z_axis_so_color = coin.SoBaseColor() self.z_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_z_axis_color)) - camera = Gui.ActiveDocument.ActiveView.getCameraNode() + self.app_obj = vobj.Object + app_doc = self.app_obj.Document + self.gui_doc = Gui.getDocument(app_doc) + camera = self.gui_doc.ActiveView.getCameraNode() self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera) if isinstance(camera, coin.SoPerspectiveCamera): self.cameraSensor.attach(camera.focalDistance) elif isinstance(camera, coin.SoOrthographicCamera): self.cameraSensor.attach(camera.height) - self.app_obj = vobj.Object - self.transform1 = coin.SoTransform() self.transform2 = coin.SoTransform() self.transform3 = coin.SoTransform() @@ -616,18 +636,7 @@ class ViewProviderJoint: return face_sep def get_JCS_size(self): - camera = Gui.ActiveDocument.ActiveView.getCameraNode() - - # Check if the camera is a perspective camera - if isinstance(camera, coin.SoPerspectiveCamera): - return camera.focalDistance.getValue() / 20 - elif isinstance(camera, coin.SoOrthographicCamera): - return camera.height.getValue() / 20 - else: - # Default value if camera type is unknown - return 10 - - return camera.height.getValue() / 20 + return get_camera_height(self.gui_doc) / 20 def set_JCS_placement(self, soTransform, placement, objName, part): # change plc to be relative to the origin of the document. @@ -735,7 +744,7 @@ class ViewProviderJoint: assembly = vobj.Object.InList[0] if UtilsAssembly.activeAssembly() != assembly: - Gui.ActiveDocument.setEdit(assembly) + self.gui_doc.setEdit(assembly) panel = TaskAssemblyCreateJoint(0, vobj.Object) Gui.Control.showDialog(panel) @@ -792,14 +801,127 @@ class ViewProviderGroundedJoint: """Set this object to the proxy object of the actual view provider""" obj.Proxy = self - def attach(self, obj): + def attach(self, vobj): """Setup the scene sub-graph of the view provider, this method is mandatory""" - pass + app_obj = vobj.Object + if app_obj is None: + return + groundedObj = app_obj.ObjectToGround + if groundedObj is None: + return + + 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) + camera = self.gui_doc.ActiveView.getCameraNode() + self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera) + if isinstance(camera, coin.SoPerspectiveCamera): + self.cameraSensor.attach(camera.focalDistance) + elif isinstance(camera, coin.SoOrthographicCamera): + self.cameraSensor.attach(camera.height) + + self.cameraSensorRot = coin.SoFieldSensor(self.camera_callback_rotation, camera) + self.cameraSensorRot.attach(camera.orientation) + + factor = self.get_lock_factor() + self.scale = coin.SoScale() + self.scale.scaleFactor.setValue(factor, factor, factor) + + self.draw_style = coin.SoDrawStyle() + self.draw_style.lineWidth = 5 + + # Create transformation (position and orientation) + self.transform = coin.SoTransform() + self.set_lock_position(groundedObj) + self.set_lock_rotation() + + # Create the 2D components of the lockpad: a square and two arcs + # Creating a square + squareCoords = [ + (-5, -4, 0), + (5, -4, 0), + (5, 4, 0), + (-5, 4, 0), + ] # Simple square, adjust size as needed + self.square = coin.SoAnnotation() + squareVertices = coin.SoCoordinate3() + squareVertices.point.setValues(0, 4, squareCoords) + squareFace = coin.SoFaceSet() + squareFace.numVertices.setValue(4) + self.square.addChild(squareVertices) + self.square.addChild(squareFace) + + # Creating the arcs (approximated with line segments) + self.arc = self.create_arc(0, 4, 3.5, 0, 180) + + self.pick = coin.SoPickStyle() + self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP) + + # Assemble the parts into a scenegraph + self.lockpadSeparator = coin.SoAnnotation() + self.lockpadSeparator.addChild(self.pick) + self.lockpadSeparator.addChild(self.transform) + self.lockpadSeparator.addChild(self.scale) + self.lockpadSeparator.addChild(self.lockpadColor) + self.lockpadSeparator.addChild(self.square) + self.lockpadSeparator.addChild(self.arc) + + # Attach the scenegraph to the view provider + vobj.addDisplayMode(self.lockpadSeparator, "Wireframe") + + def create_arc(self, centerX, centerY, radius, startAngle, endAngle): + arc = coin.SoAnnotation() + coords = coin.SoCoordinate3() + points = [] + for angle in range(startAngle, endAngle + 1): # Increment can be adjusted for smoother arcs + rad = math.radians(angle) + x = centerX + math.cos(rad) * radius + y = centerY + math.sin(rad) * radius + points.append((x, y, 0)) + coords.point.setValues(0, len(points), points) + line = coin.SoLineSet() + line.numVertices.setValue(len(points)) + arc.addChild(coords) + arc.addChild(self.draw_style) + arc.addChild(line) + return arc + + def camera_callback(self, *args): + factor = self.get_lock_factor() + self.scale.scaleFactor.setValue(factor, factor, factor) + + def camera_callback_rotation(self, *args): + self.set_lock_rotation() + + def set_lock_rotation(self): + camera = self.gui_doc.ActiveView.getCameraNode() + rotation = camera.orientation.getValue() + + q = rotation.getValue() + self.transform.rotation.setValue(q[0], q[1], q[2], q[3]) + + def get_lock_factor(self): + return get_camera_height(self.gui_doc) / 300 + + 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""" # fp is the handled feature, prop is the name of the property that has changed - pass + + if prop == "Placement" and fp.ObjectToGround: + self.set_lock_position(fp.ObjectToGround) def getDisplayModes(self, obj): """Return a list of display modes.""" @@ -815,18 +937,20 @@ class ViewProviderGroundedJoint: # App.Console.PrintMessage("Change property: " + str(prop) + "\n") pass - def onDelete(self, feature, subelements): # subelements is a tuple of strings - # Remove grounded tag. - if hasattr(feature.Object, "ObjectToGround"): - obj = feature.Object.ObjectToGround - if obj is not None and obj.Label.endswith(" 🔒"): - obj.Label = obj.Label[:-2] - - return True # If False is returned the object won't be deleted - 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 + class MakeJointSelGate: def __init__(self, taskbox, assembly): @@ -859,7 +983,13 @@ class MakeJointSelGate: selected_object.isDerivedFrom("Part::Feature") or selected_object.isDerivedFrom("App::Part") ): - return False + if selected_object.isDerivedFrom("App::Link"): + linked = selected_object.getLinkedObject() + + if not (linked.isDerivedFrom("Part::Feature") or linked.isDerivedFrom("App::Part")): + return False + else: + return False part_containing_selected_object = UtilsAssembly.getContainingPart( full_element_name, selected_object, self.assembly @@ -890,8 +1020,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): else: self.activeType = "Assembly" - self.view = Gui.activeDocument().activeView() - self.doc = App.ActiveDocument + self.doc = self.assembly.Document + self.gui_doc = Gui.getDocument(doc) + + self.view = self.gui_doc.activeView() if not self.assembly or not self.view or not self.doc: return