From 452d4a61d2a20cec1c6b02c2b14ce182a1cc45f7 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Thu, 8 Aug 2024 17:16:43 +0200 Subject: [PATCH] Assembly: Fix case of link groups --- src/Mod/Assembly/App/AssemblyObject.cpp | 86 ++++++++++- src/Mod/Assembly/App/AssemblyObject.h | 8 +- src/Mod/Assembly/App/AssemblyObjectPy.xml | 11 ++ src/Mod/Assembly/App/AssemblyObjectPyImp.cpp | 9 ++ src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 11 ++ src/Mod/Assembly/JointObject.py | 54 ++----- src/Mod/Assembly/UtilsAssembly.py | 143 ++++++++---------- 7 files changed, 183 insertions(+), 139 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 9695a0dfdd..139af0821d 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -92,7 +92,7 @@ namespace PartApp = Part; using namespace Assembly; using namespace MbD; -void printPlacement(Base::Placement plc, const char* name) +static void printPlacement(Base::Placement plc, const char* name) { Base::Vector3d pos = plc.getPosition(); Base::Vector3d axis; @@ -111,6 +111,39 @@ void printPlacement(Base::Placement plc, const char* name) angle); } +static bool isLink(App::DocumentObject* obj) +{ + if (!obj) { + return false; + } + + auto* link = dynamic_cast(obj); + if (link) { + return link->ElementCount.getValue() == 0; + } + + auto* linkEl = dynamic_cast(obj); + if (linkEl) { + return true; + } + + return false; +} + +static bool isLinkGroup(App::DocumentObject* obj) +{ + if (!obj) { + return false; + } + + auto* link = dynamic_cast(obj); + if (link) { + return link->ElementCount.getValue() > 0; + } + + return false; +} + // ================================ Assembly Object ============================ PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) @@ -144,6 +177,8 @@ App::DocumentObjectExecReturn* AssemblyObject::execute() int AssemblyObject::solve(bool enableRedo, bool updateJCS) { + ensureIdentityPlacements(); + mbdAssembly = makeMbdAssembly(); objectPartMap.clear(); @@ -454,7 +489,7 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::Docum return nullptr; } -JointGroup* AssemblyObject::getJointGroup(App::Part* part) +JointGroup* AssemblyObject::getJointGroup(const App::Part* part) { App::Document* doc = part->getDocument(); @@ -471,12 +506,12 @@ JointGroup* AssemblyObject::getJointGroup(App::Part* part) return nullptr; } -JointGroup* AssemblyObject::getJointGroup() +JointGroup* AssemblyObject::getJointGroup() const { return getJointGroup(this); } -ViewGroup* AssemblyObject::getExplodedViewGroup() +ViewGroup* AssemblyObject::getExplodedViewGroup() const { App::Document* doc = getDocument(); @@ -1693,6 +1728,37 @@ void AssemblyObject::updateGroundedJointsPlacements() } } +void AssemblyObject::ensureIdentityPlacements() +{ + std::vector group = Group.getValues(); + for (auto* obj : group) { + // When used in assembly, link groups must have identity placements. + if (isLinkGroup(obj)) { + auto* link = dynamic_cast(obj); + auto* pPlc = dynamic_cast(obj->getPropertyByName("Placement")); + if (!pPlc || !link) { + continue; + } + + Base::Placement plc = pPlc->getValue(); + if (plc.isIdentity()) { + continue; + } + + pPlc->setValue(Base::Placement()); + obj->purgeTouched(); + + // To keep the LinkElement positions, we apply plc to their placements + std::vector elts = link->ElementList.getValues(); + for (auto* elt : elts) { + pPlc = dynamic_cast(elt->getPropertyByName("Placement")); + pPlc->setValue(plc * pPlc->getValue()); + elt->purgeTouched(); + } + } + } +} + // ======================================= Utils ====================================== void AssemblyObject::swapJCS(App::DocumentObject* joint) @@ -2055,7 +2121,7 @@ Base::Placement AssemblyObject::getGlobalPlacement(App::DocumentObject* targetOb if (obj == targetObj) { return plc; } - if (obj->isDerivedFrom()) { + if (isLink(obj)) { // Update doc in case its an external link. doc = obj->getLinkedObject()->getDocument(); } @@ -2240,7 +2306,7 @@ App::DocumentObject* AssemblyObject::getObjFromRef(App::DocumentObject* obj, std return obj; } - if (obj->isDerivedFrom()) { + if (obj->isDerivedFrom() || isLinkGroup(obj)) { continue; } else if (obj->isDerivedFrom()) { @@ -2250,7 +2316,7 @@ App::DocumentObject* AssemblyObject::getObjFromRef(App::DocumentObject* obj, std // Primitive, fastener, gear, etc. return obj; } - else if (obj->isDerivedFrom()) { + else if (isLink(obj)) { App::DocumentObject* linked_obj = obj->getLinkedObject(); if (linked_obj->isDerivedFrom()) { auto* retObj = handlePartDesignBody(linked_obj, it); @@ -2315,7 +2381,7 @@ App::DocumentObject* AssemblyObject::getMovingPartFromRef(App::DocumentObject* o continue; } - if (obj->isDerivedFrom()) { // update the document if necessary for next object + if (isLink(obj)) { // update the document if necessary for next object doc = obj->getLinkedObject()->getDocument(); } @@ -2332,6 +2398,10 @@ App::DocumentObject* AssemblyObject::getMovingPartFromRef(App::DocumentObject* o continue; // we ignore groups. } + if (isLinkGroup(obj)) { + continue; + } + return obj; } diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index bb685d76dd..1615ffae31 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -171,6 +171,8 @@ public: static void recomputeJointPlacements(std::vector joints); static void redrawJointPlacements(std::vector joints); + // This makes sure that LinkGroups or sub-assemblies have identity placements. + void ensureIdentityPlacements(); // Ondsel Solver interface std::shared_ptr makeMbdAssembly(); @@ -191,8 +193,8 @@ public: int slidingPartIndex(App::DocumentObject* joint); void jointParts(std::vector joints); - JointGroup* getJointGroup(); - ViewGroup* getExplodedViewGroup(); + JointGroup* getJointGroup() const; + ViewGroup* getExplodedViewGroup() const; std::vector getJoints(bool updateJCS = true, bool delBadJoints = false, bool subJoints = true); std::vector getGroundedJoints(); @@ -257,7 +259,7 @@ public: static DistanceType getDistanceType(App::DocumentObject* joint); - static JointGroup* getJointGroup(App::Part* part); + static JointGroup* getJointGroup(const App::Part* part); // getters to get from properties static void setJointActivated(App::DocumentObject* joint, bool val); diff --git a/src/Mod/Assembly/App/AssemblyObjectPy.xml b/src/Mod/Assembly/App/AssemblyObjectPy.xml index bcd729f7fe..35f1ba366d 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPy.xml +++ b/src/Mod/Assembly/App/AssemblyObjectPy.xml @@ -49,6 +49,17 @@ + + + + Makes sure that LinkGroups or sub-assemblies have identity placements. + + ensureIdentityPlacements() + + Returns: None + + + diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp index 6ec108729f..29b60c2f3b 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp +++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp @@ -68,6 +68,15 @@ PyObject* AssemblyObjectPy::solve(PyObject* args) return Py_BuildValue("i", ret); } +PyObject* AssemblyObjectPy::ensureIdentityPlacements(PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + this->getAssemblyObjectPtr()->ensureIdentityPlacements(); + Py_Return; +} + PyObject* AssemblyObjectPy::undoSolve(PyObject* args) { if (!PyArg_ParseTuple(args, "")) { diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 345a816b7e..834006cb84 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -544,6 +544,17 @@ bool ViewProviderAssembly::canDragObjectIn3d(App::DocumentObject* obj) const // Check if the selected object is a child of the assembly if (!assemblyPart->hasObject(obj, true)) { + // hasObject does not detect LinkElements (see + // https://github.com/FreeCAD/FreeCAD/issues/16113) the following block can be removed if + // the issue is fixed : + auto* linkEl = dynamic_cast(obj); + if (linkEl) { + auto* linkGroup = linkEl->getLinkGroup(); + if (assemblyPart->hasObject(linkGroup, true)) { + return true; + } + } + return false; } diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index e79ec3112a..6d04c7afd1 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -499,49 +499,6 @@ class Joint: joint.Reference2 = [obj, [elt, vtx]] - def getSubnameForSelection(self, obj, part, elName): - # We need the subname starting from the part. - # Example for : Assembly.Part1.LinkToPart2.Part3.Body.Tip.Face1 - # part is Part1 and obj is Body - # we should get : LinkToPart2.Part3.Body.Tip.Face1 - - if obj is None or part is None: - return elName - - if obj.TypeId == "PartDesign::Body": - elName = obj.Tip.Name + "." + elName - elif obj.TypeId == "App::Link": - linked_obj = obj.getLinkedObject() - if linked_obj.TypeId == "PartDesign::Body": - elName = linked_obj.Tip.Name + "." + elName - - if obj != part and obj in part.OutListRecursive: - bSub = "" - currentObj = part - - limit = 0 - while limit < 1000: - limit = limit + 1 - - if currentObj != part: - if bSub != "": - bSub = bSub + "." - bSub = bSub + currentObj.Name - - if currentObj == obj: - break - - if currentObj.TypeId == "App::Link": - currentObj = currentObj.getLinkedObject() - - for obji in currentObj.OutList: - if obji == obj or obj in obji.OutListRecursive: - currentObj = obji - break - - elName = bSub + "." + elName - return elName - def dumps(self): return None @@ -716,6 +673,13 @@ class Joint: ) if not sameDir: jcsPlc2 = UtilsAssembly.flipPlacement(jcsPlc2) + + # For link groups and sub-assemblies we have to take into account + # the parent placement (ie the linkgroup plc) as the linkgroup is not the moving part + # But instead of doing as follow, we rather enforce identity placement for linkgroups. + # parentPlc = UtilsAssembly.getParentPlacementIfNeeded(part2) + # part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() * parentPlc.inverse() + part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() return True @@ -730,6 +694,7 @@ class Joint: ) if not sameDir: jcsPlc1 = UtilsAssembly.flipPlacement(jcsPlc1) + part1.Placement = globalJcsPlc2 * jcsPlc1.inverse() return True return False @@ -1288,7 +1253,7 @@ class MakeJointSelGate: selected_object.isDerivedFrom("Part::Feature") or selected_object.isDerivedFrom("App::Part") ): - if selected_object.isDerivedFrom("App::Link"): + if UtilsAssembly.isLink(selected_object): linked = selected_object.getLinkedObject() if not (linked.isDerivedFrom("Part::Feature") or linked.isDerivedFrom("App::Part")): @@ -1315,6 +1280,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.activeType = "Part" else: self.activeType = "Assembly" + self.assembly.ensureIdentityPlacements() self.doc = self.assembly.Document self.gui_doc = Gui.getDocument(self.doc) diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 6d18bde59e..b529c4d09e 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -95,6 +95,16 @@ def assembly_has_at_least_n_parts(n): return False +def isLink(obj): + # If element count is not 0, then its a link group in which case the Link + # is a container and it's the LinkElement that is linking to external doc. + return (obj.TypeId == "App::Link" and obj.ElementCount == 0) or obj.TypeId == "App::LinkElement" + + +def isLinkGroup(obj): + return obj.TypeId == "App::Link" and obj.ElementCount > 0 + + def getObject(ref): if len(ref) != 2: return None @@ -113,7 +123,6 @@ def getObject(ref): return None doc = ref[0].Document - for i, obj_name in enumerate(names): obj = doc.getObject(obj_name) @@ -124,7 +133,7 @@ def getObject(ref): if i == len(names) - 2: return obj - if obj.TypeId in {"App::Part", "Assembly::AssemblyObject"}: + if obj.TypeId in {"App::Part", "Assembly::AssemblyObject"} or isLinkGroup(obj): continue elif obj.TypeId == "PartDesign::Body": @@ -142,7 +151,7 @@ def getObject(ref): # primitive, fastener, gear ... return obj - elif obj.TypeId == "App::Link": + elif isLink(obj): linked_obj = obj.getLinkedObject() if linked_obj.TypeId == "PartDesign::Body": if i + 1 < len(names): @@ -173,72 +182,7 @@ def isBodySubObject(typeId): ) -# To be deprecated. CommandCreateView needs to stop using it. -def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): - # full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1 - # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1 - # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.Sketch.Edge1" -> LinkOrPart1 - - 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: - App.Console.PrintError( - "getContainingPart() 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 - - for objName in names: - obj = doc.getObject(objName) - - if not obj: - continue - - if obj == selected_object: - return selected_object - - if obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): - if selected_object in obj.OutListRecursive: - return obj - - # Note here we may want to specify a specific behavior for Assembly::AssemblyObject. - if obj.TypeId == "App::Part": - if selected_object in obj.OutListRecursive: - if not activeAssemblyOrPart: - return obj - elif activeAssemblyOrPart in obj.OutListRecursive or obj == activeAssemblyOrPart: - # If the user put the assembly inside a Part, then we ignore it. - continue - else: - return obj - - elif obj.TypeId == "App::Link": - linked_obj = obj.getLinkedObject() - if linked_obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): - if selected_object in linked_obj.OutListRecursive: - return obj - if linked_obj.TypeId in ["App::Part", "Assembly::AssemblyObject"]: - # linked_obj_doc = linked_obj.Document - # selected_obj_in_doc = doc.getObject(selected_object.Name) - if selected_object in linked_obj.OutListRecursive: - if not activeAssemblyOrPart: - return obj - elif (linked_obj.Document == activeAssemblyOrPart.Document) and ( - activeAssemblyOrPart in linked_obj.OutListRecursive - or linked_obj == activeAssemblyOrPart - ): - continue - else: - return obj - - # no container found so we return the object itself. - return selected_object - - -# To be deprecated. Kept for migrationScript. +# Deprecated. Kept for migrationScript. def getObjectInPart(objName, part): if part is None: return None @@ -336,7 +280,7 @@ def getGlobalPlacement(ref, targetObj=None): if obj == targetObj: return plc - if obj.TypeId == "App::Link": + if isLink(obj): linked_obj = obj.getLinkedObject() doc = linked_obj.Document # in case its an external link. @@ -437,17 +381,15 @@ def findElementClosestVertex(assembly, ref, mousePos): if element_name == "": return "" - moving_part = getMovingPart(assembly, ref) obj = getObject(ref) - # We need mousePos to be relative to the part containing obj global placement - if obj != moving_part: - plc = App.Placement() - plc.Base = mousePos - global_plc = getGlobalPlacement(ref) - plc = global_plc.inverse() * plc # We make it relative to obj Origin - plc = obj.Placement * plc # Make plc in the same lcs as obj - mousePos = plc.Base + # We need mousePos to be in the same lcs as obj + plc = App.Placement() + plc.Base = mousePos + global_plc = getGlobalPlacement(ref) + plc = global_plc.inverse() * plc # We make it relative to obj Origin + plc = obj.Placement * plc # Make plc in the same lcs as obj + mousePos = plc.Base elt_type, elt_index = extract_type_and_number(element_name) @@ -693,10 +635,10 @@ def getSubMovingParts(obj, partsAsSolid): objs.append(obj) return objs - elif obj.TypeId == "App::DocumentObjectGroup": + elif isLinkGroup(obj) or obj.TypeId == "App::DocumentObjectGroup": return getMovablePartsWithin(obj) - if obj.TypeId == "App::Link": + if isLink(obj): linked_obj = obj.getLinkedObject() if linked_obj.TypeId == "App::Part" or linked_obj.isDerivedFrom("Part::Feature"): return [obj] @@ -727,7 +669,7 @@ def getCenterOfMass(parts): def getObjMassAndCom(obj, containingPart=None): link_global_plc = None - if obj.TypeId == "App::Link": + if isLink(obj): link_global_plc = getGlobalPlacement(obj, containingPart) obj = obj.getLinkedObject() @@ -754,14 +696,23 @@ def getObjMassAndCom(obj, containingPart=None): com = comPlc.Base * mass return mass, com - elif obj.isDerivedFrom("App::Part") or obj.isDerivedFrom("App::DocumentObjectGroup"): + elif ( + isLinkGroup(obj) + or obj.isDerivedFrom("App::Part") + or obj.isDerivedFrom("App::DocumentObjectGroup") + ): if containingPart is None and obj.isDerivedFrom("App::Part"): containingPart = obj total_mass = 0 total_com = App.Vector(0, 0, 0) - for subObj in obj.OutList: + if isLinkGroup(obj): + children = obj.ElementList + else: + children = obj.Group + + for subObj in children: mass, com = getObjMassAndCom(subObj, containingPart) total_mass += mass total_com += com @@ -1134,6 +1085,10 @@ def getMovingPart(assembly, ref): if obj.TypeId == "App::DocumentObjectGroup": continue # we ignore groups. + # If it is a LinkGroup then we skip it + if isLinkGroup(obj): + continue + return obj return None @@ -1195,3 +1150,23 @@ def addVertexToReference(ref, vertex_name): ref = [ref[0], subs] return ref + + +def getLinkGroup(linkElement): + if linkElement.TypeId == "App::LinkElement": + for obj in linkElement.InList: + if obj.TypeId == "App::Link": + if linkElement in obj.ElementList: + return obj + print("Link Group not found.") + + return None + + +def getParentPlacementIfNeeded(part): + if part.TypeId == "App::LinkElement": + linkGroup = getLinkGroup(part) + if linkGroup: + return linkGroup.Placement + + return Base.Placement()