From f306515b2891a47d3f89d92d04cfa53dcbd03463 Mon Sep 17 00:00:00 2001 From: Paddle Date: Tue, 19 Dec 2023 09:21:46 +0100 Subject: [PATCH] Assembly: Add support to external objects. (And various fixes) --- src/Mod/Assembly/App/AssemblyObject.cpp | 73 ++++++++++---- src/Mod/Assembly/App/AssemblyObject.h | 4 +- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 13 ++- src/Mod/Assembly/JointObject.py | 97 +++++++++++++------ src/Mod/Assembly/UtilsAssembly.py | 93 ++++++++++++++---- 5 files changed, 213 insertions(+), 67 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 490987e085..3e98a95299 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -444,8 +448,8 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeEdge(App::Doc { const char* elt1 = getElementFromProp(joint, "Element1"); const char* elt2 = getElementFromProp(joint, "Element2"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); - auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); if (isEdgeType(obj1, elt1, GeomAbs_Line) || isEdgeType(obj2, elt2, GeomAbs_Line)) { if (!isEdgeType(obj1, elt1, GeomAbs_Line)) { @@ -492,8 +496,8 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceFace(App::Doc { const char* elt1 = getElementFromProp(joint, "Element1"); const char* elt2 = getElementFromProp(joint, "Element2"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); - auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); if (isFaceType(obj1, elt1, GeomAbs_Plane) || isFaceType(obj2, elt2, GeomAbs_Plane)) { if (!isFaceType(obj1, elt1, GeomAbs_Plane)) { @@ -623,7 +627,7 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceVertex(App::DocumentObject* joint) { const char* elt1 = getElementFromProp(joint, "Element1"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); if (isFaceType(obj1, elt1, GeomAbs_Plane)) { auto mbdJoint = CREATE::With(); @@ -654,7 +658,7 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint) { const char* elt1 = getElementFromProp(joint, "Element1"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); if (isEdgeType(obj1, elt1, GeomAbs_Line)) { // Point on line joint. auto mbdJoint = CREATE::With(); @@ -676,7 +680,7 @@ AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint) std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceEdge(App::DocumentObject* joint) { const char* elt2 = getElementFromProp(joint, "Element2"); - auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); if (isEdgeType(obj2, elt2, GeomAbs_Line)) { // Make line in plane joint. @@ -705,16 +709,16 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) return {}; } - std::string fullMarkerName1 = handleOneSideOfJoint(joint, jointType, "Part1", "Placement1"); - std::string fullMarkerName2 = handleOneSideOfJoint(joint, jointType, "Part2", "Placement2"); + std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Part1", "Placement1"); + std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Part2", "Placement2"); mbdJoint->setMarkerI(fullMarkerName1); mbdJoint->setMarkerJ(fullMarkerName2); return {mbdJoint}; } + std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, - JointType jointType, const char* propLinkName, const char* propPlcName) { @@ -843,10 +847,10 @@ void AssemblyObject::swapJCS(App::DocumentObject* joint) propPlacement1->setValue(propPlacement2->getValue()); propPlacement2->setValue(temp); } - auto propObject1 = dynamic_cast(joint->getPropertyByName("Object1")); - auto propObject2 = dynamic_cast(joint->getPropertyByName("Object2")); + auto propObject1 = dynamic_cast(joint->getPropertyByName("Object1")); + auto propObject2 = dynamic_cast(joint->getPropertyByName("Object2")); if (propObject1 && propObject2) { - auto temp = propObject1->getValue(); + auto temp = std::string(propObject1->getValue()); propObject1->setValue(propObject2->getValue()); propObject2->setValue(temp); } @@ -885,6 +889,10 @@ void AssemblyObject::savePlacementsForUndo() void AssemblyObject::undoSolve() { + if (previousPositions.size() == 0) { + return; + } + for (auto& pair : previousPositions) { App::DocumentObject* obj = pair.first; if (!obj) { @@ -1004,7 +1012,7 @@ bool AssemblyObject::isFaceType(App::DocumentObject* obj, GeomAbs_SurfaceType type) { auto base = static_cast(obj); - const PartApp::TopoShape& TopShape = base->Shape.getShape(); + PartApp::TopoShape TopShape = base->Shape.getShape(); // Check for valid face types TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elName)); @@ -1127,10 +1135,41 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi return propObj->getValue(); } -App::DocumentObject* AssemblyObject::getLinkedObjFromProp(App::DocumentObject* joint, - const char* propLinkName) +App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) { - return getLinkObjFromProp(joint, propLinkName)->getLinkedObject(true); + auto* propObjName = dynamic_cast(joint->getPropertyByName(pObjName)); + if (!propObjName) { + return nullptr; + } + std::string objName = std::string(propObjName->getValue()); + + App::DocumentObject* containingPart = getLinkObjFromProp(joint, pPart); + if (!containingPart) { + return nullptr; + } + + if (objName == containingPart->getNameInDocument()) { + return containingPart->getLinkedObject(true); + } + + if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { + App::Link* link = dynamic_cast(containingPart); + + containingPart = link->getLinkedObject(true); + if (!containingPart) { + return nullptr; + } + } + + for (auto obj : containingPart->getOutList()) { + if (objName == obj->getNameInDocument()) { + return obj->getLinkedObject(true); + } + } + + return nullptr; } Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 49a4e8765d..53b550ceda 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -103,7 +103,6 @@ public: std::shared_ptr makeMbdJointDistanceFaceFace(App::DocumentObject* joint); std::string handleOneSideOfJoint(App::DocumentObject* joint, - JointType jointType, const char* propObjLinkName, const char* propPlcName); void jointParts(std::vector joints); @@ -144,7 +143,8 @@ public: std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); - App::DocumentObject* getLinkedObjFromProp(App::DocumentObject* joint, const char* propName); + App::DocumentObject* + getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); private: std::shared_ptr mbdAssembly; diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 865dc51364..9c32514b13 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -72,7 +72,8 @@ bool ViewProviderAssembly::doubleClicked() { if (isInEditMode()) { // Part is already 'Active' so we exit edit mode. - Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()"); + // Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()"); + Gui::Application::Instance->activeDocument()->resetEdit(); } else { // Part is not 'Active' so we enter edit mode to make it so. @@ -160,6 +161,16 @@ void ViewProviderAssembly::unsetEdit(int ModNum) partMoving = false; docsToMove = {}; + // Check if the view is still active before trying to deactivate the assembly. + auto activeDoc = Gui::Application::Instance->activeDocument(); + if (!activeDoc) { + return; + } + auto activeView = activeDoc->getActiveView(); + if (!activeView) { + return; + } + // Set the part as not 'Activated' ie not bold in the tree. Gui::Command::doCommand(Gui::Command::Gui, "Gui.ActiveDocument.ActiveView.setActiveObject('%s', None)", diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index eac706658e..52514de94a 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -40,6 +40,7 @@ __url__ = "https://www.freecad.org" from pivy import coin import UtilsAssembly +import Preferences JointTypes = [ QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), @@ -73,6 +74,11 @@ JointUsingReverse = [ ] +def solveIfAllowed(assembly, storePrev=False): + if Preferences.preferences().GetBool("SolveInJointCreation", True): + assembly.solve(storePrev) + + def flipPlacement(plc, localXAxis): flipRot = App.Rotation(localXAxis, 180) plc.Rotation = plc.Rotation.multiply(flipRot) @@ -96,10 +102,10 @@ class Joint: # First Joint Connector joint.addProperty( - "App::PropertyLink", + "App::PropertyString", # Not PropertyLink because they don't support external objects "Object1", "Joint Connector 1", - QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"), + QT_TRANSLATE_NOOP("App::Property", "The name of the first object of the joint"), ) joint.addProperty( @@ -133,12 +139,22 @@ class Joint: ), ) + joint.addProperty( + "App::PropertyBool", + "Detach1", + "Joint Connector 1", + QT_TRANSLATE_NOOP( + "App::Property", + "This prevent Placement1 from recomputing, enabling custom positioning of the placement.", + ), + ) + # Second Joint Connector joint.addProperty( - "App::PropertyLink", + "App::PropertyString", "Object2", "Joint Connector 2", - QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"), + QT_TRANSLATE_NOOP("App::Property", "The name of the second object of the joint"), ) joint.addProperty( @@ -172,6 +188,16 @@ class Joint: ), ) + joint.addProperty( + "App::PropertyBool", + "Detach2", + "Joint Connector 2", + QT_TRANSLATE_NOOP( + "App::Property", + "This prevent Placement2 from recomputing, enabling custom positioning of the placement.", + ), + ) + joint.addProperty( "App::PropertyFloat", "Distance", @@ -236,7 +262,7 @@ class Joint: if hasattr( joint, "Vertex1" ): # during loading the onchanged may be triggered before full init. - self.getAssembly(joint).solve() + solveIfAllowed(self.getAssembly(joint)) def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -251,7 +277,7 @@ class Joint: joint.Part1 = None joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"]) - joint.Object1 = current_selection[0]["object"] + joint.Object1 = current_selection[0]["object"].Name joint.Part1 = current_selection[0]["part"] joint.Element1 = current_selection[0]["element_name"] joint.Vertex1 = current_selection[0]["vertex_name"] @@ -259,24 +285,24 @@ class Joint: joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 ) else: - joint.Object1 = None + joint.Object1 = "" joint.Part1 = None joint.Element1 = "" joint.Vertex1 = "" joint.Placement1 = App.Placement() if len(current_selection) >= 2: - joint.Object2 = current_selection[1]["object"] + joint.Object2 = current_selection[1]["object"].Name joint.Part2 = current_selection[1]["part"] joint.Element2 = current_selection[1]["element_name"] joint.Vertex2 = current_selection[1]["vertex_name"] joint.Placement2 = self.findPlacement( joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) - assembly.solve(True) + solveIfAllowed(assembly, True) else: - joint.Object2 = None + joint.Object2 = "" joint.Part2 = None joint.Element2 = "" joint.Vertex2 = "" @@ -284,12 +310,15 @@ class Joint: assembly.undoSolve() def updateJCSPlacements(self, joint): - joint.Placement1 = self.findPlacement( - joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 - ) - joint.Placement2 = self.findPlacement( - joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True - ) + if not joint.Detach1: + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) + + if not joint.Detach2: + joint.Placement2 = self.findPlacement( + joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True + ) """ So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex. @@ -301,7 +330,11 @@ class Joint: - if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face. """ - def findPlacement(self, joint, obj, part, elt, vtx, isSecond=False): + def findPlacement(self, joint, objName, part, elt, vtx, isSecond=False): + if not objName or not part: + return App.Placement() + + obj = UtilsAssembly.getObjectInPart(objName, part) assembly = self.getAssembly(joint) plc = App.Placement() @@ -430,7 +463,8 @@ class Joint: plc = joint.Part1.Placement.inverse() * joint.Placement1 localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) joint.Part1.Placement = flipPlacement(joint.Part1.Placement, localXAxis) - self.getAssembly(joint).solve() + + solveIfAllowed(self.getAssembly(joint)) def findCylindersIntersection(self, obj, surface, edge, elt_index): for j, facej in enumerate(obj.Shape.Faces): @@ -887,7 +921,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.deactivate() - self.assembly.solve() + solveIfAllowed(self.assembly) App.closeActiveTransaction() return True @@ -974,15 +1008,18 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.current_selection = [] self.preselection_dict = None + obj1 = UtilsAssembly.getObjectInPart(self.joint.Object1, self.joint.Part1) + obj2 = UtilsAssembly.getObjectInPart(self.joint.Object2, self.joint.Part2) + selection_dict1 = { - "object": self.joint.Object1, + "object": obj1, "part": self.joint.Part1, "element_name": self.joint.Element1, "vertex_name": self.joint.Vertex1, } selection_dict2 = { - "object": self.joint.Object2, + "object": obj2, "part": self.joint.Part2, "element_name": self.joint.Element2, "vertex_name": self.joint.Vertex2, @@ -991,14 +1028,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.current_selection.append(selection_dict1) self.current_selection.append(selection_dict2) - elName = self.getObjSubNameFromObj(self.joint.Object1, self.joint.Element1) - """print( - f"Gui.Selection.addSelection('{self.doc.Name}', '{self.joint.Object1.Name}', '{elName}')" - )""" - Gui.Selection.addSelection(self.doc.Name, self.joint.Object1.Name, elName) + elName = self.getObjSubNameFromObj(obj1, self.joint.Element1) + if obj1 != self.joint.Part1: + elName = obj1.Name + "." + elName + Gui.Selection.addSelection(self.doc.Name, self.joint.Part1.Name, elName) - elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2) - Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName) + elName = self.getObjSubNameFromObj(obj2, self.joint.Element2) + if obj2 != self.joint.Part2: + elName = obj2.Name + "." + elName + Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.Name, elName) self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance) self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z) @@ -1033,7 +1071,6 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.featureList.clear() simplified_names = [] for sel in self.current_selection: - # TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12 sname = sel["object"].Label if sel["element_name"] != "": sname = sname + "." + sel["element_name"] @@ -1076,7 +1113,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): placement = self.joint.Proxy.findPlacement( self.joint, - self.preselection_dict["object"], + self.preselection_dict["object"].Name, self.preselection_dict["part"], self.preselection_dict["element_name"], self.preselection_dict["vertex_name"], diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index c7e5d05edd..09b906b928 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -90,43 +90,82 @@ def assembly_has_at_least_n_parts(n): def getObject(full_name): - # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" - # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" - # or "Assembly.Assembly1.LinkOrPart1.Body.Local_CS.X" - # We want either Body or Box or Local_CS. + # full_name is "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16" + # or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16" + # or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X" + # We want either LinkOrBody or LinkOrBox or Local_CS. names = full_name.split(".") doc = App.ActiveDocument + if len(names) < 3: App.Console.PrintError( "getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter" ) return None - obj = doc.getObject(names[-2]) + prevObj = None - if obj and obj.TypeId == "PartDesign::CoordinateSystem": - return doc.getObject(names[-2]) + for i, objName in enumerate(names): + if i == 0: + prevObj = doc.getObject(objName) + if prevObj.TypeId == "App::Link": + prevObj = prevObj.getLinkedObject() + continue - obj = doc.getObject(names[-3]) # So either 'Body', or 'Assembly' + obj = None + if prevObj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + for obji in prevObj.OutList: + if obji.Name == objName: + obj = obji + break - if not obj: - return None + if obj is None: + return None - if obj.TypeId == "PartDesign::Body": - return obj - elif obj.TypeId == "App::Link": - linked_obj = obj.getLinkedObject() - if linked_obj.TypeId == "PartDesign::Body": + # the last is the element name. So if we are at the last but one name, then it must be the selected + if i == len(names) - 2: return obj - # primitive, fastener, gear ... or link to primitive, fastener, gear... - return doc.getObject(names[-2]) + if obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "PartDesign::Body": + if i + 1 < len(names): + obj2 = doc.getObject(names[i + 1]) + if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem": + return obj2 + return obj + elif linked_obj.isDerivedFrom("Part::Feature"): + return obj + else: + prevObj = linked_obj + continue + + elif obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + prevObj = obj + continue + + elif obj.TypeId == "PartDesign::Body": + if i + 1 < len(names): + obj2 = doc.getObject(names[i + 1]) + if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem": + return obj2 + return obj + + elif obj.isDerivedFrom("Part::Feature"): + # primitive, fastener, gear ... + return obj + + return None def getContainingPart(full_name, selected_object): # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" # We want either Body or Box. + if selected_object is None: + App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None") + return None + names = full_name.split(".") doc = App.ActiveDocument if len(names) < 3: @@ -141,6 +180,9 @@ def getContainingPart(full_name, selected_object): if not obj: continue + if obj == selected_object: + return selected_object + if ( obj.TypeId == "PartDesign::Body" and selected_object.TypeId == "PartDesign::CoordinateSystem" @@ -156,6 +198,8 @@ def getContainingPart(full_name, selected_object): elif obj.TypeId == "App::Link": linked_obj = obj.getLinkedObject() if linked_obj.TypeId == "App::Part": + # linked_obj_doc = linked_obj.Document + # selected_obj_in_doc = doc.getObject(selected_object.Name) if linked_obj.hasObject(selected_object, True): return obj @@ -163,6 +207,21 @@ def getContainingPart(full_name, selected_object): return selected_object +def getObjectInPart(objName, part): + if part.Name == objName: + return part + + if part.TypeId == "App::Link": + part = part.getLinkedObject() + + if part.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + for obji in part.OutList: + if obji.Name == objName: + return obji + + return None + + # The container is used to support cases where the same object appears at several places # which happens when you have a link to a part. def getGlobalPlacement(targetObj, container=None):