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/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt
index 65de263288..13439df9b4 100644
--- a/src/Mod/Assembly/CMakeLists.txt
+++ b/src/Mod/Assembly/CMakeLists.txt
@@ -17,6 +17,7 @@ set(Assembly_Scripts
JointObject.py
Preferences.py
AssemblyImport.py
+ SoSwitchMarker.py
UtilsAssembly.py
)
diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py
index 414bff1543..7d65179837 100644
--- a/src/Mod/Assembly/CommandInsertLink.py
+++ b/src/Mod/Assembly/CommandInsertLink.py
@@ -99,6 +99,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)
@@ -166,6 +167,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):
@@ -357,7 +359,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/TaskAssemblyCreateJoint.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui
index 4353934916..2f60f15fb1 100644
--- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui
+++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui
@@ -82,52 +82,121 @@
-
-
-
-
-
-
- Offset
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- mm
-
-
-
-
-
- -
-
-
-
-
-
- Rotation
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- deg
-
-
-
-
+
+
+ Attachement offsets
+
+
+ -
+
+
+
+ Simple
+
+
+
-
+
+
-
+
+
+ Offset
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ mm
+
+
+
+
+
+ -
+
+
-
+
+
+ Rotation
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ deg
+
+
+
+
+
+
+
+
+
+ Advanced
+
+
+ -
+
+
-
+
+
+ Offset1
+
+
+
+ -
+
+
+ By clicking this button, you can set the attachement offset of the first marker (coordinate system) of the joint.
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Offset2
+
+
+
+ -
+
+
+ By clicking this button, you can set the attachement offset of the second marker (coordinate system) of the joint.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
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/JointObject.py b/src/Mod/Assembly/JointObject.py
index 4a76bf1b59..0f0029cd02 100644
--- a/src/Mod/Assembly/JointObject.py
+++ b/src/Mod/Assembly/JointObject.py
@@ -40,6 +40,8 @@ from pivy import coin
import UtilsAssembly
import Preferences
+from SoSwitchMarker import SoSwitchMarker
+
translate = App.Qt.translate
TranslatedJointTypes = [
@@ -800,134 +802,18 @@ class ViewProviderJoint:
def attach(self, vobj):
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
- self.axis_thickness = 3
- self.scaleFactor = 20
-
- view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
- param_x_axis_color = view_params.GetUnsigned("AxisXColor", 0xCC333300)
- param_y_axis_color = view_params.GetUnsigned("AxisYColor", 0x33CC3300)
- param_z_axis_color = view_params.GetUnsigned("AxisZColor", 0x3333CC00)
-
- self.x_axis_so_color = coin.SoBaseColor()
- self.x_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_x_axis_color))
- self.y_axis_so_color = coin.SoBaseColor()
- self.y_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_y_axis_color))
- self.z_axis_so_color = coin.SoBaseColor()
- self.z_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_z_axis_color))
-
self.app_obj = vobj.Object
- app_doc = self.app_obj.Document
- self.gui_doc = Gui.getDocument(app_doc)
- self.transform1 = coin.SoTransform()
- self.transform2 = coin.SoTransform()
- self.transform3 = coin.SoTransform()
-
- self.draw_style = coin.SoDrawStyle()
- self.draw_style.style = coin.SoDrawStyle.LINES
- self.draw_style.lineWidth = self.axis_thickness
-
- self.switch_JCS1 = self.JCS_sep(self.transform1)
- self.switch_JCS2 = self.JCS_sep(self.transform2)
- self.switch_JCS_preview = self.JCS_sep(self.transform3)
-
- self.pick = coin.SoPickStyle()
- self.setPickableState(True)
+ self.switch_JCS1 = SoSwitchMarker(vobj)
+ self.switch_JCS2 = SoSwitchMarker(vobj)
+ self.switch_JCS_preview = SoSwitchMarker(vobj)
self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance()
- self.display_mode.addChild(self.pick)
self.display_mode.addChild(self.switch_JCS1)
self.display_mode.addChild(self.switch_JCS2)
self.display_mode.addChild(self.switch_JCS_preview)
vobj.addDisplayMode(self.display_mode, "Wireframe")
- def JCS_sep(self, soTransform):
- JCS = coin.SoAnnotation()
- JCS.addChild(soTransform)
-
- base_plane_sep = self.plane_sep(0.4, 15)
- X_axis_sep = self.line_sep([0.5, 0, 0], [1, 0, 0], self.x_axis_so_color)
- Y_axis_sep = self.line_sep([0, 0.5, 0], [0, 1, 0], self.y_axis_so_color)
- Z_axis_sep = self.line_sep([0, 0, 0], [0, 0, 1], self.z_axis_so_color)
-
- JCS.addChild(base_plane_sep)
- JCS.addChild(X_axis_sep)
- JCS.addChild(Y_axis_sep)
- JCS.addChild(Z_axis_sep)
-
- switch_JCS = coin.SoSwitch()
- switch_JCS.addChild(JCS)
- switch_JCS.whichChild = coin.SO_SWITCH_NONE
-
- return switch_JCS
-
- def line_sep(self, startPoint, endPoint, soColor):
- line = coin.SoLineSet()
- line.numVertices.setValue(2)
- coords = coin.SoCoordinate3()
- coords.point.setValues(0, [startPoint, endPoint])
-
- axis_sep = coin.SoAnnotation()
- axis_sep.addChild(self.draw_style)
- axis_sep.addChild(soColor)
- axis_sep.addChild(coords)
- axis_sep.addChild(line)
-
- scale = coin.SoType.fromName("SoShapeScale").createInstance()
- scale.setPart("shape", axis_sep)
- scale.scaleFactor = self.scaleFactor
-
- return scale
-
- def plane_sep(self, size, num_vertices):
- coords = coin.SoCoordinate3()
-
- for i in range(num_vertices):
- angle = float(i) / num_vertices * 2.0 * math.pi
- x = math.cos(angle) * size
- y = math.sin(angle) * size
- coords.point.set1Value(i, x, y, 0)
-
- face = coin.SoFaceSet()
- face.numVertices.setValue(num_vertices)
-
- transform = coin.SoTransform()
- transform.translation.setValue(0, 0, 0)
-
- draw_style = coin.SoDrawStyle()
- draw_style.style = coin.SoDrawStyle.FILLED
-
- material = coin.SoMaterial()
- material.diffuseColor.setValue([0.5, 0.5, 0.5])
- material.ambientColor.setValue([0.5, 0.5, 0.5])
- material.specularColor.setValue([0.5, 0.5, 0.5])
- material.emissiveColor.setValue([0.5, 0.5, 0.5])
- material.transparency.setValue(0.3)
-
- face_sep = coin.SoAnnotation()
- face_sep.addChild(transform)
- face_sep.addChild(draw_style)
- face_sep.addChild(material)
- face_sep.addChild(coords)
- face_sep.addChild(face)
-
- scale = coin.SoType.fromName("SoShapeScale").createInstance()
- scale.setPart("shape", face_sep)
- scale.scaleFactor = self.scaleFactor
-
- return scale
-
- def set_JCS_placement(self, soTransform, placement, ref):
- # change plc to be relative to the origin of the document.
- global_plc = UtilsAssembly.getGlobalPlacement(ref)
- placement = global_plc * placement
-
- t = placement.Base
- soTransform.translation.setValue(t.x, t.y, t.z)
-
- r = placement.Rotation.Q
- soTransform.rotation.setValue(r[0], r[1], r[2], r[3])
-
def updateData(self, joint, prop):
"""If a property of the handled feature has changed we have the chance to handle this here"""
# joint is the handled feature, prop is the name of the property that has changed
@@ -936,7 +822,7 @@ class ViewProviderJoint:
plc = joint.Placement1
self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL
- self.set_JCS_placement(self.transform1, plc, joint.Reference1)
+ self.switch_JCS1.set_marker_placement(plc, joint.Reference1)
else:
self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE
@@ -945,23 +831,22 @@ class ViewProviderJoint:
plc = joint.Placement2
self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
- self.set_JCS_placement(self.transform2, plc, joint.Reference2)
+ self.switch_JCS2.set_marker_placement(plc, joint.Reference2)
else:
self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
def showPreviewJCS(self, visible, placement=None, ref=None):
if visible:
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
- self.set_JCS_placement(self.transform3, placement, ref)
+ self.switch_JCS_preview.set_marker_placement(placement, ref)
else:
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE
def setPickableState(self, state: bool):
"""Set JCS selectable or unselectable in 3D view"""
- if not state:
- self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
- else:
- self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP)
+ self.switch_JCS1.setPickableState(state)
+ self.switch_JCS2.setPickableState(state)
+ self.switch_JCS_preview.setPickableState(state)
def getDisplayModes(self, obj):
"""Return a list of display modes."""
@@ -976,15 +861,10 @@ class ViewProviderJoint:
def onChanged(self, vp, prop):
"""Here we can do something when a single property got changed"""
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
- if prop == "color_X_axis":
- c = vp.getPropertyByName("color_X_axis")
- self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
- if prop == "color_Y_axis":
- c = vp.getPropertyByName("color_Y_axis")
- self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
- if prop == "color_Z_axis":
- c = vp.getPropertyByName("color_Z_axis")
- self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
+ if prop == "color_X_axis" or prop == "color_Y_axis" or prop == "color_Z_axis":
+ self.switch_JCS1.onChanged(vp, prop)
+ self.switch_JCS2.onChanged(vp, prop)
+ self.switch_JCS_preview.onChanged(vp, prop)
def getIcon(self):
if self.app_obj.JointType == "Fixed":
@@ -1041,7 +921,10 @@ class ViewProviderJoint:
self.gui_doc.setEdit(assembly)
panel = TaskAssemblyCreateJoint(0, vobj.Object)
- Gui.Control.showDialog(panel)
+ dialog = Gui.Control.showDialog(panel)
+ if dialog is not None:
+ dialog.setAutoCloseOnTransactionChange(True)
+ dialog.setDocumentName(App.ActiveDocument.Name)
return True
@@ -1296,6 +1179,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
global activeTask
activeTask = self
+ self.blockOffsetRotation = False
self.assembly = UtilsAssembly.activeAssembly()
if not self.assembly:
@@ -1333,7 +1217,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.form.distanceSpinbox2.valueChanged.connect(self.onDistance2Changed)
self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged)
self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged)
- self.form.PushButtonReverse.clicked.connect(self.onReverseClicked)
+ self.form.offset1Button.clicked.connect(self.onOffset1Clicked)
+ self.form.offset2Button.clicked.connect(self.onOffset2Clicked)
self.form.limitCheckbox1.stateChanged.connect(self.adaptUi)
self.form.limitCheckbox2.stateChanged.connect(self.adaptUi)
@@ -1347,6 +1232,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.form.reverseRotCheckbox.setChecked(self.jType == "Gears")
self.form.reverseRotCheckbox.stateChanged.connect(self.reverseRotToggled)
+ self.form.offsetTabs.currentChanged.connect(self.on_offset_tab_changed)
+
if jointObj:
Gui.Selection.clearSelection()
self.creating = False
@@ -1380,7 +1267,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
# before handleInitialSelection tries to solve.
self.handleInitialSelection()
- self.setJointsPickableState(False)
+ UtilsAssembly.setJointsPickableState(self.doc, False)
Gui.Selection.addSelectionGate(
MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
@@ -1441,7 +1328,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
Gui.Selection.clearSelection()
self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
- self.setJointsPickableState(True)
+ UtilsAssembly.setJointsPickableState(self.doc, True)
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
@@ -1509,9 +1396,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.joint.Distance2 = self.form.distanceSpinbox2.property("rawValue")
def onOffsetChanged(self, quantity):
+ if self.blockOffsetRotation:
+ return
+
self.joint.Offset2.Base = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue"))
def onRotationChanged(self, quantity):
+ if self.blockOffsetRotation:
+ return
+
yaw = self.form.rotationSpinbox.property("rawValue")
ypr = self.joint.Offset2.Rotation.getYawPitchRoll()
self.joint.Offset2.Rotation.setYawPitchRoll(yaw, ypr[1], ypr[2])
@@ -1652,6 +1545,34 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
else:
self.form.groupBox_limits.hide()
+ self.updateOffsetWidgets()
+
+ def updateOffsetWidgets(self):
+ # Makes sure the values in both the simplified and advanced tabs are sync.
+ pos = self.joint.Offset1.Base
+ self.form.offset1Button.setText(f"({pos.x}, {pos.y}, {pos.z})")
+
+ pos = self.joint.Offset2.Base
+ self.form.offset2Button.setText(f"({pos.x}, {pos.y}, {pos.z})")
+
+ self.blockOffsetRotation = True
+ self.form.offsetSpinbox.setProperty("rawValue", pos.z)
+ self.form.rotationSpinbox.setProperty(
+ "rawValue", self.joint.Offset2.Rotation.getYawPitchRoll()[0]
+ )
+ self.blockOffsetRotation = False
+
+ def on_offset_tab_changed(self):
+ self.updateOffsetWidgets()
+
+ def onOffset1Clicked(self):
+ UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset1")
+ self.updateOffsetWidgets()
+
+ def onOffset2Clicked(self):
+ UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset2")
+ self.updateOffsetWidgets()
+
def updateTaskboxFromJoint(self):
self.refs = []
self.presel_ref = None
@@ -1752,7 +1673,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
# newPos = self.view.getPoint(*info["Position"]) is not OK: it's not pos on the object but on the focal plane
newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
- vertex_name = UtilsAssembly.findElementClosestVertex(self.assembly, ref, newPos)
+ vertex_name = UtilsAssembly.findElementClosestVertex(ref, newPos)
ref = UtilsAssembly.addVertexToReference(ref, vertex_name)
@@ -1828,7 +1749,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
# Selection is acceptable so add it
mousePos = App.Vector(mousePos[0], mousePos[1], mousePos[2])
- vertex_name = UtilsAssembly.findElementClosestVertex(self.assembly, ref, mousePos)
+ vertex_name = UtilsAssembly.findElementClosestVertex(ref, mousePos)
# add the vertex name to the reference
ref = UtilsAssembly.addVertexToReference(ref, vertex_name)
@@ -1866,15 +1787,3 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
def clearSelection(self, doc_name):
self.refs.clear()
self.updateJoint()
-
- def setJointsPickableState(self, state: bool):
- """Make all joints in assembly selectable (True) or unselectable (False) in 3D view"""
- if self.activeType == "Assembly":
- jointGroup = UtilsAssembly.getJointGroup(self.assembly)
- for joint in jointGroup.Group:
- if hasattr(joint, "JointType"):
- joint.ViewObject.Proxy.setPickableState(state)
- else:
- for obj in self.assembly.OutList:
- if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"):
- obj.ViewObject.Proxy.setPickableState(state)
diff --git a/src/Mod/Assembly/SoSwitchMarker.py b/src/Mod/Assembly/SoSwitchMarker.py
new file mode 100644
index 0000000000..0b7e3a9bf5
--- /dev/null
+++ b/src/Mod/Assembly/SoSwitchMarker.py
@@ -0,0 +1,181 @@
+# 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 *
+# . *
+# *
+# **************************************************************************/
+
+import math
+
+import FreeCAD as App
+
+
+if App.GuiUp:
+ import FreeCADGui as Gui
+
+__title__ = "Assembly Marker Inventor object"
+__author__ = "Ondsel"
+__url__ = "https://www.freecad.org"
+
+from pivy import coin
+import UtilsAssembly
+import Preferences
+
+
+class SoSwitchMarker(coin.SoSwitch):
+ def __init__(self, vobj):
+ super().__init__() # Initialize the SoSwitch base class
+
+ self.axis_thickness = 3
+ self.scaleFactor = 20
+
+ view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
+ param_x_axis_color = view_params.GetUnsigned("AxisXColor", 0xCC333300)
+ param_y_axis_color = view_params.GetUnsigned("AxisYColor", 0x33CC3300)
+ param_z_axis_color = view_params.GetUnsigned("AxisZColor", 0x3333CC00)
+
+ self.x_axis_so_color = coin.SoBaseColor()
+ self.x_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_x_axis_color))
+ self.y_axis_so_color = coin.SoBaseColor()
+ self.y_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_y_axis_color))
+ self.z_axis_so_color = coin.SoBaseColor()
+ self.z_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_z_axis_color))
+
+ self.app_obj = vobj.Object
+ app_doc = self.app_obj.Document
+ self.gui_doc = Gui.getDocument(app_doc)
+
+ self.transform = coin.SoTransform()
+
+ self.draw_style = coin.SoDrawStyle()
+ self.draw_style.style = coin.SoDrawStyle.LINES
+ self.draw_style.lineWidth = self.axis_thickness
+
+ self.pick = coin.SoPickStyle()
+ self.setPickableState(True)
+
+ JCS = coin.SoAnnotation()
+ JCS.addChild(self.transform)
+ JCS.addChild(self.pick)
+
+ base_plane_sep = self.plane_sep(0.4, 15)
+ X_axis_sep = self.line_sep([0.5, 0, 0], [1, 0, 0], self.x_axis_so_color)
+ Y_axis_sep = self.line_sep([0, 0.5, 0], [0, 1, 0], self.y_axis_so_color)
+ Z_axis_sep = self.line_sep([0, 0, 0], [0, 0, 1], self.z_axis_so_color)
+
+ JCS.addChild(base_plane_sep)
+ JCS.addChild(X_axis_sep)
+ JCS.addChild(Y_axis_sep)
+ JCS.addChild(Z_axis_sep)
+
+ switch_JCS = coin.SoSwitch()
+ self.addChild(JCS)
+ self.whichChild = coin.SO_SWITCH_NONE
+
+ def line_sep(self, startPoint, endPoint, soColor):
+ line = coin.SoLineSet()
+ line.numVertices.setValue(2)
+ coords = coin.SoCoordinate3()
+ coords.point.setValues(0, [startPoint, endPoint])
+
+ axis_sep = coin.SoAnnotation()
+ axis_sep.addChild(self.draw_style)
+ axis_sep.addChild(soColor)
+ axis_sep.addChild(coords)
+ axis_sep.addChild(line)
+
+ scale = coin.SoType.fromName("SoShapeScale").createInstance()
+ scale.setPart("shape", axis_sep)
+ scale.scaleFactor = self.scaleFactor
+
+ return scale
+
+ def plane_sep(self, size, num_vertices):
+ coords = coin.SoCoordinate3()
+
+ for i in range(num_vertices):
+ angle = float(i) / num_vertices * 2.0 * math.pi
+ x = math.cos(angle) * size
+ y = math.sin(angle) * size
+ coords.point.set1Value(i, x, y, 0)
+
+ face = coin.SoFaceSet()
+ face.numVertices.setValue(num_vertices)
+
+ transform = coin.SoTransform()
+ transform.translation.setValue(0, 0, 0)
+
+ draw_style = coin.SoDrawStyle()
+ draw_style.style = coin.SoDrawStyle.FILLED
+
+ material = coin.SoMaterial()
+ material.diffuseColor.setValue([0.5, 0.5, 0.5])
+ material.ambientColor.setValue([0.5, 0.5, 0.5])
+ material.specularColor.setValue([0.5, 0.5, 0.5])
+ material.emissiveColor.setValue([0.5, 0.5, 0.5])
+ material.transparency.setValue(0.3)
+
+ face_sep = coin.SoAnnotation()
+ face_sep.addChild(transform)
+ face_sep.addChild(draw_style)
+ face_sep.addChild(material)
+ face_sep.addChild(coords)
+ face_sep.addChild(face)
+
+ scale = coin.SoType.fromName("SoShapeScale").createInstance()
+ scale.setPart("shape", face_sep)
+ scale.scaleFactor = self.scaleFactor
+
+ return scale
+
+ def set_marker_placement(self, placement, ref):
+ # change plc to be relative to the origin of the document.
+ global_plc = UtilsAssembly.getGlobalPlacement(ref)
+ placement = global_plc * placement
+
+ t = placement.Base
+ self.transform.translation.setValue(t.x, t.y, t.z)
+
+ r = placement.Rotation.Q
+ self.transform.rotation.setValue(r[0], r[1], r[2], r[3])
+
+ def setPickableState(self, state: bool):
+ """Set JCS selectable or unselectable in 3D view"""
+ if not state:
+ self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
+ else:
+ self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP)
+
+ def show_marker(self, visible, placement=None, ref=None):
+ if visible:
+ self.whichChild = coin.SO_SWITCH_ALL
+ self.set_marker_placement(placement, ref)
+ else:
+ self.whichChild = coin.SO_SWITCH_NONE
+
+ def onChanged(self, vp, prop):
+ if prop == "color_X_axis":
+ c = vp.getPropertyByName("color_X_axis")
+ self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
+ if prop == "color_Y_axis":
+ c = vp.getPropertyByName("color_Y_axis")
+ self.y_axis_so_color.rgb.setValue(c[0], c[1], c[2])
+ if prop == "color_Z_axis":
+ c = vp.getPropertyByName("color_Z_axis")
+ self.z_axis_so_color.rgb.setValue(c[0], c[1], c[2])
diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py
index dea95a358b..da07bf0eaf 100644
--- a/src/Mod/Assembly/UtilsAssembly.py
+++ b/src/Mod/Assembly/UtilsAssembly.py
@@ -28,12 +28,10 @@ import Part
if App.GuiUp:
import FreeCADGui as Gui
-
-import PySide.QtCore as QtCore
-import PySide.QtGui as QtGui
+ from PySide import QtCore, QtGui, QtWidgets
-# translate = App.Qt.translate
+translate = App.Qt.translate
__title__ = "Assembly utilitary functions"
__author__ = "Ondsel"
@@ -392,7 +390,7 @@ def extract_type_and_number(element_name):
return None, None
-def findElementClosestVertex(assembly, ref, mousePos):
+def findElementClosestVertex(ref, mousePos):
element_name = getElementName(ref[1][0])
if element_name == "":
return ""
@@ -788,6 +786,36 @@ def findCylindersIntersection(obj, surface, edge, elt_index):
return surface.Center
+def openEditingPlacementDialog(obj, propName):
+ task_placement = Gui.TaskPlacement()
+ dialog = task_placement.form
+
+ # Connect to the placement property
+ task_placement.setPlacement(getattr(obj, propName))
+ task_placement.setSelection([obj])
+ task_placement.setPropertyName(propName)
+ task_placement.bindObject()
+ task_placement.setIgnoreTransactions(True)
+
+ dialog.findChild(QtWidgets.QPushButton, "selectedVertex").hide()
+ dialog.exec_()
+
+
+def setPickableState(obj, state: bool):
+ vobj = obj.ViewObject
+ if hasattr(vobj, "Proxy"):
+ proxy = vobj.Proxy
+ if hasattr(proxy, "setPickableState"):
+ proxy.setPickableState(state)
+
+
+def setJointsPickableState(doc, state: bool):
+ """Make all joints in document selectable (True) or unselectable (False) in 3D view"""
+ for obj in doc.Objects:
+ if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"):
+ setPickableState(obj, state)
+
+
def applyOffsetToPlacement(plc, offset):
plc.Base = plc.Base + plc.Rotation.multVec(offset)
return plc
@@ -820,6 +848,23 @@ def arePlacementZParallel(plc1, plc2):
return zAxis1.cross(zAxis2).Length < 1e-06
+def removeTNPFromSubname(doc_name, obj_name, sub_name):
+ rootObj = App.getDocument(doc_name).getObject(obj_name)
+ resolved = rootObj.resolveSubElement(sub_name)
+ element_name_TNP = resolved[1]
+ element_name = resolved[2]
+
+ # Preprocess the sub_name to remove the TNP string
+ # We do this because after we need to add the vertex_name as well.
+ # And the names will be resolved anyway after.
+ if len(element_name_TNP.split(".")) == 2:
+ names = sub_name.split(".")
+ names.pop(-2) # remove the TNP string
+ sub_name = ".".join(names)
+
+ return sub_name
+
+
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
- obj is usually a App::Link to a PartDesign::Body, or primitive, fasteners. But can also be directly the object.1
@@ -1101,6 +1146,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