// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2024 Ondsel * * * * This file is part of FreeCAD. * * * * FreeCAD is free software: you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 2.1 of the * * License, or (at your option) any later version. * * * * FreeCAD is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with FreeCAD. If not, see * * . * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "AssemblyObject.h" #include "AssemblyUtils.h" #include "JointGroup.h" #include "AssemblyLink.h" #include "AssemblyLinkPy.h" namespace PartApp = Part; using namespace Assembly; // ================================ Assembly Object ============================ PROPERTY_SOURCE(Assembly::AssemblyLink, App::Part) AssemblyLink::AssemblyLink() { ADD_PROPERTY_TYPE( Rigid, (true), "General", (App::PropertyType)(App::Prop_None), "If the sub-assembly is set to Rigid, it will act " "as a rigid body. Else its joints will be taken into account." ); ADD_PROPERTY_TYPE( LinkedObject, (nullptr), "General", (App::PropertyType)(App::Prop_None), "The linked assembly." ); } AssemblyLink::~AssemblyLink() = default; PyObject* AssemblyLink::getPyObject() { if (PythonObject.is(Py::_None())) { // ref counter is set to 1 PythonObject = Py::Object(new AssemblyLinkPy(this), true); } return Py::new_reference_to(PythonObject); } App::DocumentObjectExecReturn* AssemblyLink::execute() { updateContents(); return App::Part::execute(); } void AssemblyLink::onChanged(const App::Property* prop) { if (App::GetApplication().isRestoring()) { App::Part::onChanged(prop); return; } if (prop == &Rigid) { Base::Placement movePlc; // A flexible sub-assembly cannot be grounded. // If a rigid sub-assembly has an object that is grounded, we also remove it. auto groundedJoints = getParentAssembly()->getGroundedJoints(); for (auto* joint : groundedJoints) { auto* propObj = dynamic_cast( joint->getPropertyByName("ObjectToGround") ); if (!propObj) { continue; } auto* groundedObj = propObj->getValue(); if (auto* linkElt = dynamic_cast(groundedObj)) { // hasObject does not handle link groups so we must handle it manually. groundedObj = linkElt->getLinkGroup(); } if (Rigid.getValue() ? hasObject(groundedObj) : groundedObj == this) { getDocument()->removeObject(joint->getNameInDocument()); } } if (Rigid.getValue()) { // movePlc needs to be computed before updateContents. App::DocumentObject* firstLink = nullptr; for (auto* obj : Group.getValues()) { if (obj && (obj->isDerivedFrom() || obj->isDerivedFrom())) { firstLink = obj; break; } } if (firstLink) { App::DocumentObject* sourceObj = nullptr; if (auto* link = dynamic_cast(firstLink)) { sourceObj = link->getLinkedObject(false); // Get non-recursive linked object } else if (auto* asmLink = dynamic_cast(firstLink)) { sourceObj = asmLink->getLinkedAssembly(); } if (sourceObj) { auto* propSource = dynamic_cast( sourceObj->getPropertyByName("Placement") ); auto* propLink = dynamic_cast( firstLink->getPropertyByName("Placement") ); if (propSource && propLink) { movePlc = propLink->getValue() * propSource->getValue().inverse(); } } } } updateContents(); auto* propPlc = dynamic_cast(getPropertyByName("Placement")); if (!propPlc) { return; } if (!Rigid.getValue()) { // when the assemblyLink becomes flexible, we need to make sure its placement is // identity or it's going to mess up moving parts placement within. Base::Placement plc = propPlc->getValue(); if (!plc.isIdentity()) { propPlc->setValue(Base::Placement()); // We need to apply the placement of the assembly link to the children or they will // move. std::vector group = Group.getValues(); for (auto* obj : group) { if (!obj->isDerivedFrom() && !obj->isDerivedFrom() && !obj->isDerivedFrom()) { continue; } if (obj->isLinkGroup()) { auto* srcLink = static_cast(obj); const std::vector srcElements = srcLink->ElementList.getValues(); for (auto elt : srcElements) { if (!elt) { continue; } auto* prop = dynamic_cast( elt->getPropertyByName("Placement") ); if (prop) { prop->setValue(plc * prop->getValue()); } } } else { auto* prop = dynamic_cast( obj->getPropertyByName("Placement") ); if (prop) { prop->setValue(plc * prop->getValue()); } } } AssemblyObject::redrawJointPlacements(getJoints()); } } else { // For the assemblylink not to move to origin, we need to update its placement. if (!movePlc.isIdentity()) { propPlc->setValue(movePlc); } } return; } App::Part::onChanged(prop); } void AssemblyLink::updateContents() { synchronizeComponents(); if (isRigid()) { ensureNoJointGroup(); } else { synchronizeJoints(); } purgeTouched(); } void AssemblyLink::synchronizeComponents() { App::Document* doc = getDocument(); AssemblyObject* assembly = getLinkedAssembly(); if (!assembly) { return; } objLinkMap.clear(); std::vector assemblyGroup = assembly->Group.getValues(); std::vector assemblyLinkGroup = Group.getValues(); // Filter out child objects from Part-workbench features to get only top-level components. // An object is considered a child if it's referenced by another object's 'Base', 'Tool', // or 'Shapes' property within the same group. std::set children; for (auto* obj : assemblyGroup) { if (auto* partFeat = dynamic_cast(obj)) { if (auto* prop = dynamic_cast(partFeat->getPropertyByName("Base"))) { if (prop->getValue()) { children.insert(prop->getValue()); } } if (auto* prop = dynamic_cast(partFeat->getPropertyByName("Tool"))) { if (prop->getValue()) { children.insert(prop->getValue()); } } if (auto* prop = dynamic_cast(partFeat->getPropertyByName("Shapes"))) { for (auto* shapeObj : prop->getValues()) { children.insert(shapeObj); } } } } std::vector topLevelComponents; std::copy_if( assemblyGroup.begin(), assemblyGroup.end(), std::back_inserter(topLevelComponents), [&children](App::DocumentObject* obj) { return children.find(obj) == children.end(); } ); // We check if a component needs to be added to the AssemblyLink for (auto* obj : topLevelComponents) { if (!obj->isDerivedFrom() && !obj->isDerivedFrom() && !obj->isDerivedFrom()) { continue; } // Note, the user can have nested sub-assemblies. // In which case we need to add an AssemblyLink and not a Link. App::DocumentObject* link = nullptr; bool found = false; std::set linkGroupsAdded; for (auto* obj2 : assemblyLinkGroup) { App::DocumentObject* linkedObj; auto* subAsmLink = freecad_cast(obj2); auto* link2 = dynamic_cast(obj2); if (subAsmLink) { linkedObj = subAsmLink->getLinkedObject2(false); // not recursive } else if (link2) { if (obj->isLinkGroup() && link2->isLinkGroup()) { auto* srcLink = static_cast(obj); if ((srcLink->getTrueLinkedObject(false) == link2->getTrueLinkedObject(false)) && link2->ElementCount.getValue() == srcLink->ElementCount.getValue() && linkGroupsAdded.find(srcLink) == linkGroupsAdded.end()) { found = true; link = obj2; // In case where there are more than 2 link groups with the // same number of elements. linkGroupsAdded.insert(srcLink); const std::vector srcElements = srcLink->ElementList.getValues(); const std::vector newElements = link2->ElementList.getValues(); for (size_t i = 0; i < srcElements.size(); ++i) { objLinkMap[srcElements[i]] = newElements[i]; } break; } } else if (obj->isLinkGroup() && !link2->isLinkGroup()) { continue; // make sure we migrate sub assemblies that had link to linkgroups } linkedObj = link2->getLinkedObject(false); // not recursive } else { // We consider only Links and AssemblyLinks continue; } if (linkedObj == obj) { found = true; link = obj2; break; } } if (!found) { // Add a link or a AssemblyLink to it in the AssemblyLink. if (obj->isDerivedFrom()) { auto* asmLink = static_cast(obj); App::DocumentObject* newObj = doc->addObject("Assembly::AssemblyLink", obj->getNameInDocument()); auto* subAsmLink = static_cast(newObj); subAsmLink->LinkedObject.setValue(obj); subAsmLink->Rigid.setValue(asmLink->Rigid.getValue()); subAsmLink->Label.setValue(obj->Label.getValue()); addObject(subAsmLink); link = subAsmLink; } else if (obj->isDerivedFrom() && obj->isLinkGroup()) { auto* srcLink = static_cast(obj); auto* newLink = static_cast( doc->addObject("App::Link", obj->getNameInDocument()) ); newLink->LinkedObject.setValue(srcLink->getTrueLinkedObject(false)); newLink->Label.setValue(obj->Label.getValue()); addObject(newLink); newLink->ElementCount.setValue(srcLink->ElementCount.getValue()); const std::vector srcElements = srcLink->ElementList.getValues(); const std::vector newElements = newLink->ElementList.getValues(); for (size_t i = 0; i < srcElements.size(); ++i) { auto* newObj = newElements[i]; auto* srcObj = srcElements[i]; if (newObj && srcObj) { syncPlacements(srcObj, newObj); } objLinkMap[srcObj] = newObj; } link = newLink; } else { App::DocumentObject* newObj = doc->addObject("App::Link", obj->getNameInDocument()); auto* newLink = static_cast(newObj); newLink->LinkedObject.setValue(obj); newLink->Label.setValue(obj->Label.getValue()); addObject(newLink); link = newLink; } } objLinkMap[obj] = link; } // If the assemblyLink is rigid, then we keep all placements synchronized. if (isRigid()) { for (const auto& [sourceObj, linkObj] : objLinkMap) { syncPlacements(sourceObj, linkObj); } } // We check if a component needs to be removed from the AssemblyLink // NOTE: this is not being executed when a src link is deleted, because the link // is then in error, and so AssemblyLink::execute() does not get called. std::set validLinks; for (const auto& pair : objLinkMap) { validLinks.insert(pair.second); } for (auto* obj : assemblyLinkGroup) { // We don't need to update assemblyLinkGroup after the addition since we're not removing // something we just added. if (!obj->isDerivedFrom() && !obj->isDerivedFrom() && !obj->isDerivedFrom()) { continue; } if (validLinks.find(obj) == validLinks.end()) { doc->removeObject(obj->getNameInDocument()); } } } namespace { template void copyPropertyIfDifferent( App::DocumentObject* source, App::DocumentObject* target, const char* propertyName ) { auto sourceProp = freecad_cast(source->getPropertyByName(propertyName)); auto targetProp = freecad_cast(target->getPropertyByName(propertyName)); if (sourceProp && targetProp && sourceProp->getValue() != targetProp->getValue()) { targetProp->setValue(sourceProp->getValue()); } } std::string removeUpToName(const std::string& sub, const std::string& name) { size_t pos = sub.find(name); if (pos != std::string::npos) { // Move the position to the character after the found substring and the following '.' pos += name.length() + 1; if (pos < sub.length()) { return sub.substr(pos); } } // If s2 is not found in s1, return the original string return sub; } std::string replaceLastOccurrence( const std::string& str, const std::string& oldStr, const std::string& newStr ) { size_t pos = str.rfind(oldStr); if (pos != std::string::npos) { std::string result = str; result.replace(pos, oldStr.length(), newStr); return result; } return str; } }; // namespace void AssemblyLink::synchronizeJoints() { App::Document* doc = getDocument(); AssemblyObject* assembly = getLinkedAssembly(); if (!assembly) { return; } JointGroup* jGroup = ensureJointGroup(); std::vector assemblyJoints = assembly->getJoints(assembly->isTouched(), false, false); std::vector assemblyLinkJoints = getJoints(); // We delete the excess of joints if any for (size_t i = assemblyJoints.size(); i < assemblyLinkJoints.size(); ++i) { doc->removeObject(assemblyLinkJoints[i]->getNameInDocument()); } // We make sure the joints match. for (size_t i = 0; i < assemblyJoints.size(); ++i) { App::DocumentObject* joint = assemblyJoints[i]; App::DocumentObject* lJoint; if (i < assemblyLinkJoints.size()) { lJoint = assemblyLinkJoints[i]; } else { auto ret = doc->copyObject({joint}); if (ret.size() != 1) { continue; } lJoint = ret[0]; jGroup->addObject(lJoint); } // Then we have to check the properties one by one. copyPropertyIfDifferent(joint, lJoint, "Suppressed"); copyPropertyIfDifferent(joint, lJoint, "Distance"); copyPropertyIfDifferent(joint, lJoint, "Distance2"); copyPropertyIfDifferent(joint, lJoint, "JointType"); copyPropertyIfDifferent(joint, lJoint, "Offset1"); copyPropertyIfDifferent(joint, lJoint, "Offset2"); copyPropertyIfDifferent(joint, lJoint, "Detach1"); copyPropertyIfDifferent(joint, lJoint, "Detach2"); copyPropertyIfDifferent(joint, lJoint, "AngleMax"); copyPropertyIfDifferent(joint, lJoint, "AngleMin"); copyPropertyIfDifferent(joint, lJoint, "LengthMax"); copyPropertyIfDifferent(joint, lJoint, "LengthMin"); copyPropertyIfDifferent(joint, lJoint, "EnableAngleMax"); copyPropertyIfDifferent(joint, lJoint, "EnableAngleMin"); copyPropertyIfDifferent(joint, lJoint, "EnableLengthMax"); copyPropertyIfDifferent(joint, lJoint, "EnableLengthMin"); // The reference needs to be handled specifically handleJointReference(joint, lJoint, "Reference1"); handleJointReference(joint, lJoint, "Reference2"); } assemblyLinkJoints = getJoints(); AssemblyObject::recomputeJointPlacements(assemblyLinkJoints); for (auto* joint : assemblyLinkJoints) { joint->purgeTouched(); } } void AssemblyLink::handleJointReference( App::DocumentObject* joint, App::DocumentObject* lJoint, const char* refName ) { AssemblyObject* assembly = getLinkedAssembly(); auto prop1 = dynamic_cast(joint->getPropertyByName(refName)); auto prop2 = dynamic_cast(lJoint->getPropertyByName(refName)); if (!prop1 || !prop2) { return; } App::DocumentObject* obj1 = nullptr; App::DocumentObject* obj2 = prop2->getValue(); std::vector subs1 = prop1->getSubValues(); std::vector subs2 = prop2->getSubValues(); if (subs1.empty()) { return; } // Example : // Obj1 = docA-Asm1 Subs1 = ["part1.body.pad.face0", "part1.body.pad.vertex1"] // Obj1 = docA-Part Subs1 = ["Asm1.part1.body.pad.face0", "Asm1.part1.body.pad.vertex1"] // some // user may put the assembly inside a part... should become : Obj2 = docB-Asm2 Subs2 = // ["Asm1Link.part1.linkTobody.pad.face0", "Asm1Link.part1.linkTobody.pad.vertex1"] Obj2 = // docB-Part Sub2 = ["Asm2.Asm1Link.part1.linkTobody.pad.face0", // "Asm2.Asm1Link.part1.linkTobody.pad.vertex1"] std::string asmLink = getNameInDocument(); for (auto& sub : subs1) { // First let's remove 'Asm1' name and everything before if any. sub = removeUpToName(sub, assembly->getNameInDocument()); // Then we add the assembly link name. sub = asmLink + "." + sub; // Then the question is, is there more to prepend? Because the parent assembly may have some // parents So we check assemblyLink parents and prepend necessary parents. bool first = true; std::vector inList = getInList(); int limit = 0; while (!inList.empty() && limit < 20) { ++limit; bool found = false; for (auto* obj : inList) { if (obj->isDerivedFrom()) { found = true; if (first) { first = false; } else { std::string obj1Name = obj1->getNameInDocument(); sub = obj1Name + "." + sub; } obj1 = obj; break; } } if (found) { inList = obj1->getInList(); } else { inList = {}; } } // Lastly we need to replace the object name by its link name. auto* obj = getObjFromRef(prop1); auto* link = objLinkMap[obj]; if (!obj || !link) { return; } std::string objName = obj->getNameInDocument(); std::string linkName = link->getNameInDocument(); sub = replaceLastOccurrence(sub, objName, linkName); } // Now obj1 and the subs1 are what should be in obj2 and subs2 if the joint did not changed if (obj1 != obj2) { prop2->setValue(obj1); } bool changed = false; for (size_t i = 0; i < subs1.size(); ++i) { if (i >= subs2.size() || subs1[i] != subs2[i]) { changed = true; break; } } if (changed) { prop2->setSubValues(std::move(subs1)); } } void AssemblyLink::ensureNoJointGroup() { // Make sure there is no joint group JointGroup* jGroup = getJointGroup(this); if (jGroup) { // If there is a joint group, we delete it and its content. jGroup->removeObjectsFromDocument(); getDocument()->removeObject(jGroup->getNameInDocument()); } } JointGroup* AssemblyLink::ensureJointGroup() { // Make sure there is a jointGroup JointGroup* jGroup = getJointGroup(this); if (!jGroup) { jGroup = new JointGroup(); getDocument()->addObject(jGroup, tr("Joints").toStdString().c_str()); std::vector grp = Group.getValues(); grp.insert(grp.begin(), jGroup); Group.setValues(grp); } return jGroup; } App::DocumentObject* AssemblyLink::getLinkedObject2(bool recursive) const { auto* obj = LinkedObject.getValue(); auto* assembly = freecad_cast(obj); if (assembly) { return assembly; } else { auto* assemblyLink = freecad_cast(obj); if (assemblyLink) { if (recursive) { return assemblyLink->getLinkedObject2(recursive); } else { return assemblyLink; } } } return nullptr; } AssemblyObject* AssemblyLink::getLinkedAssembly() const { return freecad_cast(getLinkedObject2()); } AssemblyObject* AssemblyLink::getParentAssembly() const { std::vector inList = getInList(); for (auto* obj : inList) { auto* assembly = freecad_cast(obj); if (assembly) { return assembly; } } return nullptr; } bool AssemblyLink::isRigid() const { auto* prop = dynamic_cast(getPropertyByName("Rigid")); if (!prop) { return true; } return prop->getValue(); } std::vector AssemblyLink::getJoints() { JointGroup* jointGroup = getJointGroup(this); if (!jointGroup) { return {}; } return jointGroup->getJoints(); } bool AssemblyLink::allowDuplicateLabel() const { return true; } int AssemblyLink::numberOfComponents() const { return isRigid() ? 1 : getLinkedAssembly()->numberOfComponents(); } bool AssemblyLink::isEmpty() const { return numberOfComponents() == 0; }