diff --git a/src/Mod/Assembly/App/AppAssembly.cpp b/src/Mod/Assembly/App/AppAssembly.cpp
index e9152d4dae..a2679171a7 100644
--- a/src/Mod/Assembly/App/AppAssembly.cpp
+++ b/src/Mod/Assembly/App/AppAssembly.cpp
@@ -28,6 +28,7 @@
#include
#include "AssemblyObject.h"
+#include "AssemblyLink.h"
#include "BomObject.h"
#include "BomGroup.h"
#include "JointGroup.h"
@@ -61,6 +62,7 @@ PyMOD_INIT_FUNC(AssemblyApp)
// This function is responsible for adding inherited slots from a type's base class.
Assembly::AssemblyObject ::init();
+ Assembly::AssemblyLink ::init();
Assembly::BomObject ::init();
Assembly::BomGroup ::init();
diff --git a/src/Mod/Assembly/App/AssemblyLink.cpp b/src/Mod/Assembly/App/AssemblyLink.cpp
new file mode 100644
index 0000000000..f8df4f771e
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyLink.cpp
@@ -0,0 +1,582 @@
+// 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 "PreCompiled.h"
+#ifndef _PreComp_
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "AssemblyObject.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;
+
+ if (Rigid.getValue()) {
+ // movePlc needs to be computed before updateContents.
+ if (!objLinkMap.empty()) {
+ auto firstElement = *objLinkMap.begin();
+
+ App::DocumentObject* obj = firstElement.first;
+ App::DocumentObject* link = firstElement.second;
+ auto* prop =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ auto* prop2 =
+ dynamic_cast(link->getPropertyByName("Placement"));
+ if (prop && prop2) {
+ movePlc = prop2->getValue() * prop->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;
+ }
+
+ 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();
+
+ // We check if a component needs to be added to the AssemblyLink
+ for (auto* obj : assemblyGroup) {
+ 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;
+ for (auto* obj2 : assemblyLinkGroup) {
+ App::DocumentObject* linkedObj;
+
+ auto* subAsmLink = dynamic_cast(obj2);
+ auto* link2 = dynamic_cast(obj2);
+ if (subAsmLink) {
+ linkedObj = subAsmLink->getLinkedObject2(false); // not recursive
+ }
+ else if (link2) {
+ linkedObj = link2->getLinkedObject(false); // not recursive
+ }
+ else {
+ // We consider only Links and AssemblyLinks in the AssemblyLink.
+ 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);
+ auto* subAsmLink = new AssemblyLink();
+ doc->addObject(subAsmLink, obj->getNameInDocument());
+ subAsmLink->LinkedObject.setValue(obj);
+ subAsmLink->Rigid.setValue(asmLink->Rigid.getValue());
+ subAsmLink->Label.setValue(obj->Label.getValue());
+ addObject(subAsmLink);
+ link = subAsmLink;
+ }
+ else {
+ auto* appLink = new App::Link();
+ doc->addObject(appLink, obj->getNameInDocument());
+ appLink->LinkedObject.setValue(obj);
+ appLink->Label.setValue(obj->Label.getValue());
+ addObject(appLink);
+ link = appLink;
+ }
+ }
+
+ objLinkMap[obj] = link;
+ // If the assemblyLink is rigid, then we keep the placement synchronized.
+ if (isRigid()) {
+ auto* plcProp =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ auto* plcProp2 =
+ dynamic_cast(link->getPropertyByName("Placement"));
+ if (plcProp && plcProp2) {
+ if (!plcProp->getValue().isSame(plcProp2->getValue())) {
+ plcProp2->setValue(plcProp->getValue());
+ }
+ }
+ }
+ }
+
+ // We check if a component needs to be removed from the AssemblyLink
+ for (auto* link : assemblyLinkGroup) {
+ // We don't need to update assemblyLinkGroup after the addition since we're not removing
+ // something we just added.
+
+ if (objLinkMap.find(link) != objLinkMap.end()) {
+ doc->removeObject(link->getNameInDocument());
+ }
+
+ /*if (!link->isDerivedFrom() && !link->isDerivedFrom()) {
+ // AssemblyLink should contain only Links or assembly links.
+ continue;
+ }
+
+ auto* linkedObj = link->getLinkedObject(false); // not recursive
+
+ bool found = false;
+ for (auto* obj2 : assemblyGroup) {
+ if (obj2 == linkedObj) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ doc->removeObject(link->getNameInDocument());
+ }*/
+ }
+}
+
+namespace
+{
+template
+void copyPropertyIfDifferent(App::DocumentObject* source,
+ App::DocumentObject* target,
+ const char* propertyName)
+{
+ auto sourceProp = dynamic_cast(source->getPropertyByName(propertyName));
+ auto targetProp = dynamic_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, "Activated");
+ 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 = AssemblyObject::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 = AssemblyObject::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 = AssemblyObject::getJointGroup(this);
+ if (!jGroup) {
+ jGroup = new JointGroup();
+ getDocument()->addObject(jGroup, tr("Joints").toStdString().c_str());
+
+ // we want to add jgroup at the start, so we don't use
+ // addObject(jGroup);
+ 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 = dynamic_cast(obj);
+ if (assembly) {
+ return assembly;
+ }
+ else {
+ auto* assemblyLink = dynamic_cast(obj);
+ if (assemblyLink) {
+ if (recursive) {
+ return assemblyLink->getLinkedObject2(recursive);
+ }
+ else {
+ return assemblyLink;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+AssemblyObject* AssemblyLink::getLinkedAssembly() const
+{
+ return dynamic_cast(getLinkedObject2());
+}
+
+AssemblyObject* AssemblyLink::getParentAssembly() const
+{
+ std::vector inList = getInList();
+ for (auto* obj : inList) {
+ auto* assembly = dynamic_cast(obj);
+ if (assembly) {
+ return assembly;
+ }
+ }
+
+ return nullptr;
+}
+
+bool AssemblyLink::isRigid()
+{
+ auto* prop = dynamic_cast(getPropertyByName("Rigid"));
+ if (!prop) {
+ return true;
+ }
+
+ return prop->getValue();
+}
+
+std::vector AssemblyLink::getJoints()
+{
+ JointGroup* jointGroup = AssemblyObject::getJointGroup(this);
+
+ if (!jointGroup) {
+ return {};
+ }
+
+ return jointGroup->getJoints();
+}
diff --git a/src/Mod/Assembly/App/AssemblyLink.h b/src/Mod/Assembly/App/AssemblyLink.h
new file mode 100644
index 0000000000..5bbcfefa40
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyLink.h
@@ -0,0 +1,96 @@
+// 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 *
+ * . *
+ * *
+ ***************************************************************************/
+
+
+#ifndef ASSEMBLY_AssemblyLink_H
+#define ASSEMBLY_AssemblyLink_H
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+
+namespace Assembly
+{
+class AssemblyObject;
+class JointGroup;
+
+class AssemblyExport AssemblyLink: public App::Part
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(Assembly::AssemblyLink);
+
+public:
+ AssemblyLink();
+ ~AssemblyLink() override;
+
+ PyObject* getPyObject() override;
+
+ /// returns the type name of the ViewProvider
+ const char* getViewProviderName() const override
+ {
+ return "AssemblyGui::ViewProviderAssemblyLink";
+ }
+
+ App::DocumentObjectExecReturn* execute() override;
+
+ // The linked assembly is the AssemblyObject that this AssemblyLink pseudo-links to recursively.
+ AssemblyObject* getLinkedAssembly() const;
+ // The parent assembly is the main assembly in which the linked assembly is contained
+ AssemblyObject* getParentAssembly() const;
+
+ // Overriding DocumentObject::getLinkedObject is giving bugs
+ // This function returns the linked object, either an AssemblyObject or an AssemblyLink
+ App::DocumentObject* getLinkedObject2(bool recurse = true) const;
+
+ bool isRigid();
+
+ void updateContents();
+
+ void synchronizeComponents();
+ void synchronizeJoints();
+ void handleJointReference(App::DocumentObject* joint,
+ App::DocumentObject* lJoint,
+ const char* refName);
+ void ensureNoJointGroup();
+ JointGroup* ensureJointGroup();
+ std::vector getJoints();
+
+ App::PropertyXLink LinkedObject;
+ App::PropertyBool Rigid;
+
+ std::unordered_map objLinkMap;
+
+protected:
+ /// get called by the container whenever a property has been changed
+ void onChanged(const App::Property* prop) override;
+};
+
+
+} // namespace Assembly
+
+
+#endif // ASSEMBLY_AssemblyLink_H
diff --git a/src/Mod/Assembly/App/AssemblyLinkPy.xml b/src/Mod/Assembly/App/AssemblyLinkPy.xml
new file mode 100644
index 0000000000..2b72b7af31
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyLinkPy.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ This class handles document objects in Assembly
+
+
+
+
+
diff --git a/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp b/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp
new file mode 100644
index 0000000000..2a7123a213
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp
@@ -0,0 +1,47 @@
+// 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 "PreCompiled.h"
+
+// inclusion of the generated files (generated out of AssemblyLink.xml)
+#include "AssemblyLinkPy.h"
+#include "AssemblyLinkPy.cpp"
+
+using namespace Assembly;
+
+// returns a string which represents the object e.g. when printed in python
+std::string AssemblyLinkPy::representation() const
+{
+ return {""};
+}
+
+PyObject* AssemblyLinkPy::getCustomAttributes(const char* /*attr*/) const
+{
+ return nullptr;
+}
+
+int AssemblyLinkPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
+{
+ return 0;
+}
diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp
index aba23b402b..cf74c3f830 100644
--- a/src/Mod/Assembly/App/AssemblyObject.cpp
+++ b/src/Mod/Assembly/App/AssemblyObject.cpp
@@ -82,6 +82,7 @@
#include
#include
+#include "AssemblyLink.h"
#include "AssemblyObject.h"
#include "AssemblyObjectPy.h"
#include "JointGroup.h"
@@ -445,6 +446,7 @@ void AssemblyObject::redrawJointPlacement(App::DocumentObject* joint)
void AssemblyObject::recomputeJointPlacements(std::vector joints)
{
// The Placement1 and Placement2 of each joint needs to be updated as the parts moved.
+ Base::PyGILStateLocker lock;
for (auto* joint : joints) {
if (!joint) {
continue;
@@ -1773,17 +1775,17 @@ void AssemblyObject::setObjMasses(std::vector AssemblyObject::getSubAssemblies()
+std::vector AssemblyObject::getSubAssemblies()
{
- std::vector subAssemblies = {};
+ std::vector subAssemblies = {};
App::Document* doc = getDocument();
std::vector assemblies =
- doc->getObjectsOfType(Assembly::AssemblyObject::getClassTypeId());
+ doc->getObjectsOfType(Assembly::AssemblyLink::getClassTypeId());
for (auto assembly : assemblies) {
if (hasObject(assembly)) {
- subAssemblies.push_back(dynamic_cast(assembly));
+ subAssemblies.push_back(dynamic_cast(assembly));
}
}
@@ -2465,6 +2467,14 @@ App::DocumentObject* AssemblyObject::getMovingPartFromRef(App::DocumentObject* o
continue;
}
+ // We ignore dynamic sub-assemblies.
+ if (obj->isDerivedFrom()) {
+ auto* pRigid = dynamic_cast(obj->getPropertyByName("Rigid"));
+ if (pRigid && !pRigid->getValue()) {
+ continue;
+ }
+ }
+
return obj;
}
diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h
index 1ead41f729..b2a083767f 100644
--- a/src/Mod/Assembly/App/AssemblyObject.h
+++ b/src/Mod/Assembly/App/AssemblyObject.h
@@ -59,6 +59,7 @@ class Rotation;
namespace Assembly
{
+class AssemblyLink;
class JointGroup;
class ViewGroup;
@@ -241,7 +242,7 @@ public:
double getObjMass(App::DocumentObject* obj);
void setObjMasses(std::vector> objectMasses);
- std::vector getSubAssemblies();
+ std::vector getSubAssemblies();
void updateGroundedJointsPlacements();
private:
diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt
index afbf1c1870..70063733c0 100644
--- a/src/Mod/Assembly/App/CMakeLists.txt
+++ b/src/Mod/Assembly/App/CMakeLists.txt
@@ -19,6 +19,7 @@ set(Assembly_LIBS
)
generate_from_xml(AssemblyObjectPy)
+generate_from_xml(AssemblyLinkPy)
generate_from_xml(BomObjectPy)
generate_from_xml(BomGroupPy)
generate_from_xml(JointGroupPy)
@@ -27,6 +28,8 @@ generate_from_xml(ViewGroupPy)
SET(Python_SRCS
AssemblyObjectPy.xml
AssemblyObjectPyImp.cpp
+ AssemblyLinkPy.xml
+ AssemblyLinkPyImp.cpp
BomObjectPy.xml
BomObjectPyImp.cpp
BomGroupPy.xml
@@ -49,6 +52,8 @@ SOURCE_GROUP("Module" FILES ${Module_SRCS})
SET(Assembly_SRCS
AssemblyObject.cpp
AssemblyObject.h
+ AssemblyLink.cpp
+ AssemblyLink.h
BomObject.cpp
BomObject.h
BomGroup.cpp
diff --git a/src/Mod/Assembly/App/JointGroup.cpp b/src/Mod/Assembly/App/JointGroup.cpp
index faccfd4de7..c38a4f16d2 100644
--- a/src/Mod/Assembly/App/JointGroup.cpp
+++ b/src/Mod/Assembly/App/JointGroup.cpp
@@ -53,3 +53,31 @@ PyObject* JointGroup::getPyObject()
}
return Py::new_reference_to(PythonObject);
}
+
+
+std::vector JointGroup::getJoints()
+{
+ std::vector joints = {};
+
+ Base::PyGILStateLocker lock;
+ for (auto joint : getObjects()) {
+ if (!joint) {
+ continue;
+ }
+
+ auto* prop = dynamic_cast(joint->getPropertyByName("Activated"));
+ if (!prop || !prop->getValue()) {
+ // Filter grounded joints and deactivated joints.
+ continue;
+ }
+
+ auto proxy = dynamic_cast(joint->getPropertyByName("Proxy"));
+ if (proxy) {
+ if (proxy->getValue().hasAttr("setJointConnectors")) {
+ joints.push_back(joint);
+ }
+ }
+ }
+
+ return joints;
+}
diff --git a/src/Mod/Assembly/App/JointGroup.h b/src/Mod/Assembly/App/JointGroup.h
index 17c328d2ba..8f4611815c 100644
--- a/src/Mod/Assembly/App/JointGroup.h
+++ b/src/Mod/Assembly/App/JointGroup.h
@@ -49,6 +49,8 @@ public:
{
return "AssemblyGui::ViewProviderJointGroup";
}
+
+ std::vector getJoints();
};
diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py
index a5312339b6..a9a1b74dcd 100644
--- a/src/Mod/Assembly/CommandInsertLink.py
+++ b/src/Mod/Assembly/CommandInsertLink.py
@@ -98,6 +98,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
pref = Preferences.preferences()
self.form.CheckBox_ShowOnlyParts.setChecked(pref.GetBool("InsertShowOnlyParts", False))
+ self.form.CheckBox_RigidSubAsm.setChecked(pref.GetBool("InsertRigidSubAssemblies", True))
# Actions
self.form.openFileButton.clicked.connect(self.openFiles)
@@ -165,6 +166,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
def deactivated(self):
pref = Preferences.preferences()
pref.SetBool("InsertShowOnlyParts", self.form.CheckBox_ShowOnlyParts.isChecked())
+ pref.SetBool("InsertRigidSubAssemblies", self.form.CheckBox_RigidSubAsm.isChecked())
Gui.Selection.clearSelection()
def buildPartList(self):
@@ -353,7 +355,16 @@ class TaskAssemblyInsertLink(QtCore.QObject):
print(selectedPart.Document.Name)
documentItem.setText(0, f"{newDocName}.FCStd")"""
- addedObject = self.assembly.newObject("App::Link", selectedPart.Label)
+ if selectedPart.isDerivedFrom("Assembly::AssemblyObject"):
+ objType = "Assembly::AssemblyLink"
+ else:
+ objType = "App::Link"
+
+ addedObject = self.assembly.newObject(objType, selectedPart.Label)
+
+ if selectedPart.isDerivedFrom("Assembly::AssemblyObject"):
+ addedObject.Rigid = self.form.CheckBox_RigidSubAsm.isChecked()
+
# set placement of the added object to the center of the screen.
view = Gui.activeView()
x, y = view.getSize()
diff --git a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
index c76bfd1a8a..6b4ec250e9 100644
--- a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
+++ b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
@@ -28,6 +28,7 @@
#include
#include "ViewProviderAssembly.h"
+#include "ViewProviderAssemblyLink.h"
#include "ViewProviderBom.h"
#include "ViewProviderBomGroup.h"
#include "ViewProviderJointGroup.h"
@@ -60,6 +61,7 @@ PyMOD_INIT_FUNC(AssemblyGui)
// This function is responsible for adding inherited slots from a type's base class.
AssemblyGui::ViewProviderAssembly::init();
+ AssemblyGui::ViewProviderAssemblyLink::init();
AssemblyGui::ViewProviderBom::init();
AssemblyGui::ViewProviderBomGroup::init();
AssemblyGui::ViewProviderJointGroup::init();
diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt
index 13bf42af35..7dda6251f0 100644
--- a/src/Mod/Assembly/Gui/CMakeLists.txt
+++ b/src/Mod/Assembly/Gui/CMakeLists.txt
@@ -40,6 +40,8 @@ SET(AssemblyGui_SRCS_Module
PreCompiled.h
ViewProviderAssembly.cpp
ViewProviderAssembly.h
+ ViewProviderAssemblyLink.cpp
+ ViewProviderAssemblyLink.h
ViewProviderBom.cpp
ViewProviderBom.h
ViewProviderBomGroup.cpp
diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc
index c16225e37d..22b93155a1 100644
--- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc
+++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc
@@ -1,5 +1,7 @@
+ icons/Assembly_AssemblyLink.svg
+ icons/Assembly_AssemblyLinkRigid.svg
icons/Assembly_InsertLink.svg
icons/preferences-assembly.svg
icons/Assembly_ToggleGrounded.svg
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_AssemblyLink.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_AssemblyLink.svg
new file mode 100644
index 0000000000..c40a4f8a9a
--- /dev/null
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_AssemblyLink.svg
@@ -0,0 +1,359 @@
+
+
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_AssemblyLinkRigid.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_AssemblyLinkRigid.svg
new file mode 100644
index 0000000000..f9e6c2924f
--- /dev/null
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_AssemblyLinkRigid.svg
@@ -0,0 +1,354 @@
+
+
diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui
index 4c485ffc75..640f9a93c1 100644
--- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui
+++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui
@@ -68,6 +68,28 @@
+ -
+
+
+ If checked, the inserted sub-assemblies will not be flexible.
+Rigid means that the sub-assembly will be considered as a solid.
+Flexible means that the sub-assembly joints will be taken into account in the main assembly.
+You can change this property of sub-assemblies at any time by right clicking them.
+
+
+ Rigid sub-assemblies
+
+
+ true
+
+
+ InsertRigidSubAssemblies
+
+
+ Mod/Assembly
+
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
index a3e35fcfd4..22e878c4b6 100644
--- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
+++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
@@ -63,6 +63,7 @@
#include
#include
#include
+#include
#include
#include "ViewProviderAssembly.h"
@@ -1024,7 +1025,7 @@ bool ViewProviderAssembly::onDelete(const std::vector& subNames)
for (auto obj : getObject()->getOutList()) {
if (obj->getTypeId() == Assembly::JointGroup::getClassTypeId()
|| obj->getTypeId() == Assembly::ViewGroup::getClassTypeId()
- /* || obj->getTypeId() == Assembly::BomGroup::getClassTypeId()*/) {
+ || obj->getTypeId() == Assembly::BomGroup::getClassTypeId()) {
// Delete the group content first.
Gui::Command::doCommand(Gui::Command::Doc,
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyLink.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyLink.cpp
new file mode 100644
index 0000000000..37c1c89fb8
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyLink.cpp
@@ -0,0 +1,150 @@
+// 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 "PreCompiled.h"
+
+#ifndef _PreComp_
+#include
+#include
+#include
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "ViewProviderAssembly.h"
+#include "ViewProviderAssemblyLink.h"
+
+
+using namespace Assembly;
+using namespace AssemblyGui;
+
+
+PROPERTY_SOURCE(AssemblyGui::ViewProviderAssemblyLink, Gui::ViewProviderPart)
+
+ViewProviderAssemblyLink::ViewProviderAssemblyLink()
+{}
+
+ViewProviderAssemblyLink::~ViewProviderAssemblyLink() = default;
+
+QIcon ViewProviderAssemblyLink::getIcon() const
+{
+ auto* assembly = dynamic_cast(getObject());
+ if (assembly->isRigid()) {
+ return Gui::BitmapFactory().pixmap("Assembly_AssemblyLinkRigid.svg");
+ }
+ else {
+ return Gui::BitmapFactory().pixmap("Assembly_AssemblyLink.svg");
+ }
+}
+
+bool ViewProviderAssemblyLink::setEdit(int mode)
+{
+ auto* assemblyLink = dynamic_cast(getObject());
+
+ if (!assemblyLink->isRigid() && mode == (int)ViewProvider::Transform) {
+ Base::Console().UserTranslatedNotification(
+ "Flexible sub-assemblies cannot be transformed.");
+ return true;
+ }
+
+ return ViewProviderPart::setEdit(mode);
+}
+
+bool ViewProviderAssemblyLink::doubleClicked()
+{
+ auto* link = dynamic_cast(getObject());
+
+ if (!link) {
+ return true;
+ }
+ auto* assembly = link->getLinkedAssembly();
+
+ auto* vpa =
+ dynamic_cast(Gui::Application::Instance->getViewProvider(assembly));
+ if (!vpa) {
+ return true;
+ }
+
+ return vpa->doubleClicked();
+}
+
+bool ViewProviderAssemblyLink::onDelete(const std::vector& subNames)
+{
+ Q_UNUSED(subNames)
+
+ Base::Console().Warning("onDelete\n");
+
+ Gui::Command::doCommand(Gui::Command::Doc,
+ "App.getDocument(\"%s\").getObject(\"%s\").removeObjectsFromDocument()",
+ getObject()->getDocument()->getName(),
+ getObject()->getNameInDocument());
+
+ // getObject()->purgeTouched();
+
+ return ViewProviderPart::onDelete(subNames);
+}
+
+void ViewProviderAssemblyLink::setupContextMenu(QMenu* menu, QObject* receiver, const char* member)
+{
+ auto func = new Gui::ActionFunction(menu);
+ QAction* act;
+ auto* assemblyLink = dynamic_cast(getObject());
+ if (assemblyLink->isRigid()) {
+ act = menu->addAction(QObject::tr("Turn flexible"));
+ act->setToolTip(QObject::tr(
+ "Your sub-assembly is currently rigid. This will make it flexible instead."));
+ }
+ else {
+ act = menu->addAction(QObject::tr("Turn rigid"));
+ act->setToolTip(QObject::tr(
+ "Your sub-assembly is currently flexible. This will make it rigid instead."));
+ }
+
+ func->trigger(act, [this]() {
+ auto* assemblyLink = dynamic_cast(getObject());
+ Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Toggle Rigid"));
+ Gui::cmdAppObjectArgs(assemblyLink,
+ "Rigid = %s",
+ assemblyLink->Rigid.getValue() ? "False" : "True");
+
+ Gui::Command::commitCommand();
+ Gui::Selection().clearSelection();
+ });
+
+ Q_UNUSED(receiver)
+ Q_UNUSED(member)
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyLink.h b/src/Mod/Assembly/Gui/ViewProviderAssemblyLink.h
new file mode 100644
index 0000000000..190deb32b4
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyLink.h
@@ -0,0 +1,81 @@
+// 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 *
+ * . *
+ * *
+ ***************************************************************************/
+
+#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssemblyLink_H
+#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssemblyLink_H
+
+#include
+
+#include
+
+#include
+
+
+namespace AssemblyGui
+{
+
+class AssemblyGuiExport ViewProviderAssemblyLink: public Gui::ViewProviderPart
+{
+ Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssemblyLink)
+ PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssemblyLink);
+
+public:
+ ViewProviderAssemblyLink();
+ ~ViewProviderAssemblyLink() override;
+
+ /// deliver the icon shown in the tree view. Override from ViewProvider.h
+ QIcon getIcon() const override;
+
+ bool setEdit(int ModNum) override;
+
+ bool doubleClicked() override;
+
+ // When the assembly link is deleted, we delete all its content as well.
+ bool onDelete(const std::vector& subNames) override;
+
+ // Prevent deletion of the link assembly's content.
+ bool canDelete(App::DocumentObject*) const override
+ {
+ return false;
+ };
+
+ // Prevent drag/drop of objects within the assembly link.
+ bool canDragObjects() const override
+ {
+ return false;
+ };
+ bool canDropObjects() const override
+ {
+ return false;
+ };
+ bool canDragAndDropObject(App::DocumentObject*) const override
+ {
+ return false;
+ };
+
+ void setupContextMenu(QMenu*, QObject*, const char*) override;
+};
+
+} // namespace AssemblyGui
+
+#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssemblyLink_H
diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
index 9f598654ec..f56dbde95e 100644
--- a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
+++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
@@ -47,10 +47,3 @@ QIcon ViewProviderJointGroup::getIcon() const
{
return Gui::BitmapFactory().pixmap("Assembly_JointGroup.svg");
}
-
-// Make the joint group impossible to delete.
-bool ViewProviderJointGroup::onDelete(const std::vector& subNames)
-{
- Q_UNUSED(subNames);
- return false;
-}
diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
index 76ca1b6230..96ccbc4850 100644
--- a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
+++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
@@ -57,7 +57,11 @@ public:
return false;
};
- bool onDelete(const std::vector& subNames) override;
+ // Make the joint group impossible to delete.
+ bool onDelete(const std::vector&) override
+ {
+ return false;
+ };
// protected:
/// get called by the container whenever a property has been changed
diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py
index 9b7743a3d7..279c6d2868 100644
--- a/src/Mod/Assembly/UtilsAssembly.py
+++ b/src/Mod/Assembly/UtilsAssembly.py
@@ -1112,6 +1112,10 @@ def getMovingPart(assembly, ref):
if obj.TypeId == "App::DocumentObjectGroup":
continue # we ignore groups.
+ # We ignore dynamic sub-assemblies.
+ if obj.isDerivedFrom("Assembly::AssemblyLink") and obj.Rigid == False:
+ continue
+
# If it is a LinkGroup then we skip it
if isLinkGroup(obj):
continue