// 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::updateParentJoints() { AssemblyObject* parent = getParentAssembly(); if (!parent) { return; } bool rigid = Rigid.getValue(); // Iterate joints in the immediate parent assembly only (recursive=false) for (auto* joint : parent->getJoints(false, false, false)) { for (const char* refName : {"Reference1", "Reference2"}) { auto* prop = dynamic_cast(joint->getPropertyByName(refName)); if (!prop) { continue; } App::DocumentObject* refObj = prop->getValue(); if (!refObj) { continue; } if (rigid) { // Flexible -> Rigid if (hasObject(refObj)) { // The joint currently points to a child (refObj) inside this AssemblyLink. // We must repoint it to 'this' and prepend the child's name to the sub-elements. std::vector subs = prop->getSubValues(); std::vector newSubs; std::string prefix = refObj->getNameInDocument(); prefix += "."; for (const auto& s : subs) { newSubs.push_back(prefix + s); } prop->setValue(this); prop->setSubValues(std::move(newSubs)); } } else { // Rigid -> Flexible if (refObj == this) { // The joint currently points to 'this'. // We must extract the child's name from the sub-element, point to the child, // and strip the prefix. std::vector subs = prop->getSubValues(); if (subs.empty()) { continue; } std::vector parts = Base::Tools::splitSubName(subs[0]); if (parts.empty()) { continue; } std::string childName = parts[0]; App::DocumentObject* child = getDocument()->getObject(childName.c_str()); if (child && hasObject(child)) { std::vector newSubs; size_t prefixLen = childName.length() + 1; // "Name." for (const auto& s : subs) { if (s.length() >= prefixLen) { newSubs.push_back(s.substr(prefixLen)); } else { newSubs.push_back(s); } } prop->setValue(child); prop->setSubValues(std::move(newSubs)); } } } } if (joint->isTouched()) { joint->recomputeFeature(); } } } void AssemblyLink::updateContents() { synchronizeComponents(); if (isRigid()) { ensureNoJointGroup(); } else { synchronizeJoints(); } purgeTouched(); } // Generate an instance label for assembly components by appending a -N suffix. // All instances get a suffix (starting at -1) so that structured part numbers // like "P03-0001" are never mangled by UniqueNameManager's trailing-digit logic. static std::string makeInstanceLabel(App::Document* doc, const std::string& baseLabel) { for (int i = 1;; ++i) { std::string candidate = baseLabel + "-" + std::to_string(i); if (!doc->containsLabel(candidate)) { return candidate; } } } 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(makeInstanceLabel(doc, 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(makeInstanceLabel(doc, 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(makeInstanceLabel(doc, 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(); for (auto* joint : assemblyLinkJoints) { joint->purgeTouched(); } } void AssemblyLink::handleJointReference( App::DocumentObject* joint, App::DocumentObject* lJoint, const char* refName ) { auto prop1 = dynamic_cast(joint->getPropertyByName(refName)); auto prop2 = dynamic_cast(lJoint->getPropertyByName(refName)); if (!prop1 || !prop2) { return; } // 1. Get the external component prop1 is [ExternalPart, "Sub"] App::DocumentObject* externalComponent = prop1->getValue(); if (!externalComponent) { return; } // 2. Map to local link auto it = objLinkMap.find(externalComponent); if (it == objLinkMap.end()) { Base::Console().warning( "AssemblyLink: Could not map external component %s to a local link for joint %s\n", externalComponent->getNameInDocument(), joint->getNameInDocument() ); return; } App::DocumentObject* localLink = it->second; // 3. Set the new reference // The local joint now points to the local link [LocalLink, "Sub"] if (prop2->getValue() != localLink) { prop2->setValue(localLink); } // 4. Sync sub-elements // The sub-elements (e.g. "Body.Face1") are relative to the component. // Since the LocalLink points to the ExternalPart, the relative path is identical. std::vector subs1 = prop1->getSubValues(); std::vector subs2 = prop2->getSubValues(); bool changed = false; if (subs1.size() != subs2.size()) { changed = true; } else { for (size_t i = 0; i < subs1.size(); ++i) { if (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; }