diff --git a/cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake b/cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake
new file mode 100644
index 0000000000..9b2dda28c8
--- /dev/null
+++ b/cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake
@@ -0,0 +1,4 @@
+macro(SetupOndselSolverCpp)
+ # -------------------------------- OndselSolver --------------------------------
+ find_package(OndselSolver REQUIRED)
+endmacro(SetupOndselSolverCpp)
diff --git a/src/Mod/Assembly/App/AppAssembly.cpp b/src/Mod/Assembly/App/AppAssembly.cpp
new file mode 100644
index 0000000000..c73899dd84
--- /dev/null
+++ b/src/Mod/Assembly/App/AppAssembly.cpp
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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"
+
+#include
+#include
+#include
+
+#include "AssemblyObject.h"
+#include "JointGroup.h"
+
+
+namespace Assembly
+{
+extern PyObject* initModule();
+}
+
+/* Python entry */
+PyMOD_INIT_FUNC(AssemblyApp)
+{
+ // load dependent module
+ try {
+ Base::Interpreter().runString("import Part");
+ }
+ catch (const Base::Exception& e) {
+ PyErr_SetString(PyExc_ImportError, e.what());
+ PyMOD_Return(nullptr);
+ }
+
+ PyObject* mod = Assembly::initModule();
+ Base::Console().Log("Loading Assembly module... done\n");
+
+
+ // NOTE: To finish the initialization of our own type objects we must
+ // call PyType_Ready, otherwise we run into a segmentation fault, later on.
+ // This function is responsible for adding inherited slots from a type's base class.
+
+ Assembly::AssemblyObject ::init();
+ Assembly::JointGroup ::init();
+
+ PyMOD_Return(mod);
+}
diff --git a/src/Mod/Assembly/App/AppAssemblyPy.cpp b/src/Mod/Assembly/App/AppAssemblyPy.cpp
new file mode 100644
index 0000000000..f62cf30c8f
--- /dev/null
+++ b/src/Mod/Assembly/App/AppAssemblyPy.cpp
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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"
+
+#include
+#include
+
+
+namespace Assembly
+{
+class Module: public Py::ExtensionModule
+{
+public:
+ Module()
+ : Py::ExtensionModule("AssemblyApp")
+ {
+ initialize("This module is the Assembly module."); // register with Python
+ }
+};
+
+PyObject* initModule()
+{
+ return Base::Interpreter().addModule(new Module);
+}
+
+} // namespace Assembly
diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp
new file mode 100644
index 0000000000..18d56fe947
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyObject.cpp
@@ -0,0 +1,516 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "AssemblyObject.h"
+#include "AssemblyObjectPy.h"
+#include "JointGroup.h"
+
+using namespace App;
+using namespace Assembly;
+using namespace MbD;
+
+
+PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part)
+
+AssemblyObject::AssemblyObject()
+ : mbdAssembly(std::make_shared())
+{}
+
+AssemblyObject::~AssemblyObject() = default;
+
+PyObject* AssemblyObject::getPyObject()
+{
+ if (PythonObject.is(Py::_None())) {
+ // ref counter is set to 1
+ PythonObject = Py::Object(new AssemblyObjectPy(this), true);
+ }
+ return Py::new_reference_to(PythonObject);
+}
+
+std::vector AssemblyObject::getJoints()
+{
+ std::vector joints = {};
+
+ App::Document* doc = getDocument();
+
+ std::vector jointGroups =
+ doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId());
+
+ Base::PyGILStateLocker lock;
+ if (jointGroups.size() > 0) {
+ for (auto* obj : static_cast(jointGroups[0])->getObjects()) {
+ App::PropertyPythonObject* proxy = obj
+ ? dynamic_cast(obj->getPropertyByName("Proxy"))
+ : nullptr;
+ if (proxy) {
+ Py::Object joint = proxy->getValue();
+ if (joint.hasAttr("setJointConnectors")) {
+ joints.push_back(obj);
+ }
+ }
+ }
+ }
+
+ // Make sure the joints are up to date.
+ recomputeJointPlacements(joints);
+
+ return joints;
+}
+
+bool AssemblyObject::fixGroundedParts()
+{
+ App::Document* doc = getDocument();
+ App::DocumentObject* jointsGroup = doc->getObject("Joints");
+
+ bool onePartFixed = false;
+
+ Base::PyGILStateLocker lock;
+ if (jointsGroup && jointsGroup->isDerivedFrom(App::DocumentObjectGroup::getClassTypeId())) {
+ for (auto* obj : static_cast(jointsGroup)->getObjects()) {
+ auto* propObj =
+ dynamic_cast(obj->getPropertyByName("ObjectToGround"));
+ if (propObj) {
+ App::DocumentObject* objToGround = propObj->getValue();
+
+ Base::Placement plc = getPlacementFromProp(obj, "Placement");
+ std::string str = obj->getFullName();
+ fixGroundedPart(objToGround, plc, str);
+ onePartFixed = true;
+ }
+ }
+ }
+ return onePartFixed;
+}
+
+void AssemblyObject::fixGroundedPart(App::DocumentObject* obj,
+ Base::Placement& plc,
+ std::string& name)
+{
+ std::string markerName1 = "marker-" + obj->getFullName();
+ auto mbdMarker1 = makeMbdMarker(markerName1, plc);
+ mbdAssembly->addMarker(mbdMarker1);
+
+ std::shared_ptr mbdPart = getMbDPart(obj);
+
+ std::string markerName2 = "FixingMarker";
+ auto mbdMarker2 = makeMbdMarker(markerName2, plc);
+ mbdPart->addMarker(mbdMarker2);
+
+ markerName1 = "/OndselAssembly/" + mbdMarker1->name;
+ markerName2 = "/OndselAssembly/" + mbdPart->name + "/" + mbdMarker2->name;
+
+ auto mbdJoint = CREATE::With();
+ mbdJoint->setName(name);
+ mbdJoint->setMarkerI(markerName1);
+ mbdJoint->setMarkerJ(markerName2);
+
+ mbdAssembly->addJoint(mbdJoint);
+}
+
+void AssemblyObject::jointParts(std::vector joints)
+{
+ for (auto* joint : joints) {
+ std::shared_ptr mbdJoint = makeMbdJoint(joint);
+ mbdAssembly->addJoint(mbdJoint);
+ }
+}
+
+Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName)
+{
+ Base::Placement plc = Base::Placement();
+ auto* propPlacement = dynamic_cast(obj->getPropertyByName(propName));
+ if (propPlacement) {
+ plc = propPlacement->getValue();
+ }
+ return plc;
+}
+
+int AssemblyObject::solve()
+{
+ // Base::Console().Warning("solve\n");
+ mbdAssembly = makeMbdAssembly();
+ objectPartMap.clear();
+
+ if (!fixGroundedParts()) {
+ // If no part fixed we can't solve.
+ return -6;
+ }
+
+ std::vector joints = getJoints();
+
+ jointParts(joints);
+
+ try {
+ mbdAssembly->solve();
+ }
+ catch (...) {
+ Base::Console().Error("Solve failed\n");
+ return -1;
+ }
+
+ setNewPlacements();
+
+ // The Placement1 and Placement2 of each joint needs to be updated as the parts moved.
+ // Note calling only recomputeJointPlacements makes a weird illegal storage access
+ // When solving while moving part. Happens in Py::Callable(attr).apply();
+ // it apparantly can't access the JointObject 'updateJCSPlacements' function.
+ getJoints();
+
+ return 0;
+}
+
+void AssemblyObject::exportAsASMT(std::string fileName)
+{
+ Base::Console().Warning("hello 1\n");
+ mbdAssembly = makeMbdAssembly();
+ objectPartMap.clear();
+ Base::Console().Warning("hello 2\n");
+ fixGroundedParts();
+
+ std::vector joints = getJoints();
+
+ Base::Console().Warning("hello 3\n");
+ jointParts(joints);
+
+ Base::Console().Warning("hello 4\n");
+ Base::Console().Warning("%s\n", fileName.c_str());
+ mbdAssembly->outputFile(fileName);
+ Base::Console().Warning("hello 5\n");
+}
+
+std::shared_ptr AssemblyObject::makeMbdJointOfType(JointType jointType)
+{
+ std::shared_ptr mbdJoint;
+
+ if (jointType == JointType::Fixed) {
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Revolute) {
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Cylindrical) {
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Slider) {
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Ball) {
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Planar) {
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Parallel) {
+ // TODO
+ mbdJoint = CREATE::With();
+ }
+ else if (jointType == JointType::Tangent) {
+ // TODO
+ mbdJoint = CREATE::With();
+ }
+
+ return mbdJoint;
+}
+
+std::shared_ptr AssemblyObject::makeMbdJoint(App::DocumentObject* joint)
+{
+ JointType jointType = JointType::Fixed;
+
+ auto* prop = joint
+ ? dynamic_cast(joint->getPropertyByName("JointType"))
+ : nullptr;
+ if (prop) {
+ jointType = static_cast(prop->getValue());
+ }
+
+ std::shared_ptr mbdJoint = makeMbdJointOfType(jointType);
+
+ std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Object1", "Placement1");
+ std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Object2", "Placement2");
+
+ mbdJoint->setMarkerI(fullMarkerName1);
+ mbdJoint->setMarkerJ(fullMarkerName2);
+
+ return mbdJoint;
+}
+
+std::shared_ptr AssemblyObject::getMbDPart(App::DocumentObject* obj)
+{
+ std::shared_ptr mbdPart;
+
+ Base::Placement plc = getPlacementFromProp(obj, "Placement");
+
+ auto it = objectPartMap.find(obj);
+ if (it != objectPartMap.end()) {
+ // obj has been associated with an ASMTPart before
+ mbdPart = it->second;
+ }
+ else {
+ // obj has not been associated with an ASMTPart before
+ std::string str = obj->getFullName();
+ mbdPart = makeMbdPart(str, plc);
+ mbdAssembly->addPart(mbdPart);
+ objectPartMap[obj] = mbdPart; // Store the association
+ }
+
+ return mbdPart;
+}
+
+std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint,
+ const char* propLinkName,
+ const char* propPlcName)
+{
+ auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName));
+ if (!propObj) {
+ return nullptr;
+ }
+ App::DocumentObject* obj = propObj->getValue();
+
+ std::shared_ptr mbdPart = getMbDPart(obj);
+ Base::Placement objPlc = getPlacementFromProp(obj, "Placement");
+ Base::Placement plc = getPlacementFromProp(joint, propPlcName);
+ // Now we have plc which is the JCS placement, but its relative to the doc origin, not to the
+ // obj.
+
+ plc = objPlc.inverse() * plc;
+
+ std::string markerName = joint->getFullName();
+ auto mbdMarker = makeMbdMarker(markerName, plc);
+ mbdPart->addMarker(mbdMarker);
+
+ return "/OndselAssembly/" + mbdPart->name + "/" + markerName;
+}
+
+std::shared_ptr AssemblyObject::makeMbdMarker(std::string& name, Base::Placement& plc)
+{
+ auto mbdMarker = CREATE::With();
+ mbdMarker->setName(name);
+
+ Base::Vector3d pos = plc.getPosition();
+ mbdMarker->setPosition3D(pos.x, pos.y, pos.z);
+
+ // TODO : replace with quaternion to simplify
+ Base::Rotation rot = plc.getRotation();
+ Base::Matrix4D mat;
+ rot.getValue(mat);
+ Base::Vector3d r0 = mat.getRow(0);
+ Base::Vector3d r1 = mat.getRow(1);
+ Base::Vector3d r2 = mat.getRow(2);
+ mbdMarker->setRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z);
+ /*double q0, q1, q2, q3;
+ rot.getValue(q0, q1, q2, q3);
+ mbdMarker->setQuarternions(q0, q1, q2, q3);*/
+ return mbdMarker;
+}
+
+std::shared_ptr
+AssemblyObject::makeMbdPart(std::string& name, Base::Placement plc, double mass)
+{
+ auto mdbPart = CREATE::With();
+ mdbPart->setName(name);
+
+ auto massMarker = CREATE::With();
+ massMarker->setMass(mass);
+ massMarker->setDensity(1.0);
+ massMarker->setMomentOfInertias(1.0, 1.0, 1.0);
+ mdbPart->setPrincipalMassMarker(massMarker);
+
+ Base::Vector3d pos = plc.getPosition();
+ mdbPart->setPosition3D(pos.x, pos.y, pos.z);
+ // Base::Console().Warning("MbD Part placement : (%f, %f, %f)\n", pos.x, pos.y, pos.z);
+
+ // TODO : replace with quaternion to simplify
+ Base::Rotation rot = plc.getRotation();
+ Base::Matrix4D mat;
+ rot.getValue(mat);
+ Base::Vector3d r0 = mat.getRow(0);
+ Base::Vector3d r1 = mat.getRow(1);
+ Base::Vector3d r2 = mat.getRow(2);
+ mdbPart->setRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z);
+ /*double q0, q1, q2, q3;
+ rot.getValue(q0, q1, q2, q3);
+ mdbPart->setQuarternions(q0, q1, q2, q3);*/
+
+ return mdbPart;
+}
+
+std::shared_ptr AssemblyObject::makeMbdAssembly()
+{
+ auto assembly = CREATE::With();
+ assembly->setName("OndselAssembly");
+
+ return assembly;
+}
+
+void AssemblyObject::setNewPlacements()
+{
+ for (auto& pair : objectPartMap) {
+ App::DocumentObject* obj = pair.first;
+ std::shared_ptr mbdPart = pair.second;
+
+ if (!obj || !mbdPart) {
+ continue;
+ }
+
+ // Check if the object has a "Placement" property
+ auto* propPlacement =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ if (propPlacement) {
+
+ double x, y, z;
+ mbdPart->getPosition3D(x, y, z);
+ // Base::Console().Warning("in set placement : (%f, %f, %f)\n", x, y, z);
+ Base::Vector3d pos = Base::Vector3d(x, y, z);
+
+ // TODO : replace with quaternion to simplify
+ auto& r0 = mbdPart->rotationMatrix->at(0);
+ auto& r1 = mbdPart->rotationMatrix->at(1);
+ auto& r2 = mbdPart->rotationMatrix->at(2);
+ Base::Vector3d row0 = Base::Vector3d(r0->at(0), r0->at(1), r0->at(2));
+ Base::Vector3d row1 = Base::Vector3d(r1->at(0), r1->at(1), r1->at(2));
+ Base::Vector3d row2 = Base::Vector3d(r2->at(0), r2->at(1), r2->at(2));
+ Base::Matrix4D mat;
+ mat.setRow(0, row0);
+ mat.setRow(1, row1);
+ mat.setRow(2, row2);
+ Base::Rotation rot = Base::Rotation(mat);
+
+ /*double q0, q1, q2, q3;
+ mbdPart->getQuarternions(q0, q1, q2, q3);
+ Base::Rotation rot = Base::Rotation(q0, q1, q2, q3);*/
+
+ Base::Placement newPlacement = Base::Placement(pos, rot);
+
+ propPlacement->setValue(newPlacement);
+ }
+ }
+}
+
+void AssemblyObject::recomputeJointPlacements(std::vector joints)
+{
+ // The Placement1 and Placement2 of each joint needs to be updated as the parts moved.
+ for (auto* joint : joints) {
+
+ App::PropertyPythonObject* proxy = joint
+ ? dynamic_cast(joint->getPropertyByName("Proxy"))
+ : nullptr;
+
+ if (!proxy) {
+ continue;
+ }
+
+ Py::Object jointPy = proxy->getValue();
+
+ if (!jointPy.hasAttr("updateJCSPlacements")) {
+ continue;
+ }
+
+ Py::Object attr = jointPy.getAttr("updateJCSPlacements");
+ if (attr.ptr() && attr.isCallable()) {
+ Py::Tuple args(1);
+ args.setItem(0, Py::asObject(joint->getPyObject()));
+ Py::Callable(attr).apply(args);
+ }
+ }
+}
+
+double AssemblyObject::getObjMass(App::DocumentObject* obj)
+{
+ for (auto& pair : objMasses) {
+ if (pair.first == obj) {
+ return pair.second;
+ }
+ }
+ return 1.0;
+}
+
+void AssemblyObject::setObjMasses(std::vector> objectMasses)
+{
+ objMasses = objectMasses;
+}
+
+/*void Part::handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName, App::Property*
+prop)
+{
+ App::Part::handleChangedPropertyType(reader, TypeName, prop);
+}*/
+
+/* Apparantly not necessary as App::Part doesn't have this.
+// Python Assembly feature ---------------------------------------------------------
+
+namespace App
+{
+ /// @cond DOXERR
+ PROPERTY_SOURCE_TEMPLATE(Assembly::AssemblyObjectPython, Assembly::AssemblyObject)
+ template<>
+ const char* Assembly::AssemblyObjectPython::getViewProviderName() const
+ {
+ return "AssemblyGui::ViewProviderAssembly";
+ }
+ template<>
+ PyObject* Assembly::AssemblyObjectPython::getPyObject()
+ {
+ if (PythonObject.is(Py::_None())) {
+ // ref counter is set to 1
+ PythonObject = Py::Object(new FeaturePythonPyT(this), true);
+ }
+ return Py::new_reference_to(PythonObject);
+ }
+ /// @endcond
+
+ // explicit template instantiation
+ template class AssemblyExport FeaturePythonT;
+}// namespace App*/
diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h
new file mode 100644
index 0000000000..8fe6b09c84
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyObject.h
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_AssemblyObject_H
+#define ASSEMBLY_AssemblyObject_H
+
+#include
+
+#include
+#include
+#include
+
+namespace MbD
+{
+class ASMTPart;
+class ASMTAssembly;
+class ASMTJoint;
+class ASMTMarker;
+class ASMTPart;
+} // namespace MbD
+
+namespace Base
+{
+class Placement;
+class Rotation;
+} // namespace Base
+
+namespace Assembly
+{
+
+// This enum has to be the same as the one in JointObject.py
+enum class JointType
+{
+ Fixed,
+ Revolute,
+ Cylindrical,
+ Slider,
+ Ball,
+ Planar,
+ Parallel,
+ Tangent
+};
+
+class AssemblyExport AssemblyObject: public App::Part
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(Assembly::AssemblyObject);
+
+public:
+ AssemblyObject();
+ ~AssemblyObject() override;
+
+ PyObject* getPyObject() override;
+
+ /// returns the type name of the ViewProvider
+ const char* getViewProviderName() const override
+ {
+ return "AssemblyGui::ViewProviderAssembly";
+ }
+
+ int solve();
+ void exportAsASMT(std::string fileName);
+ std::shared_ptr makeMbdAssembly();
+ std::shared_ptr
+ makeMbdPart(std::string& name, Base::Placement plc = Base::Placement(), double mass = 1.0);
+ std::shared_ptr getMbDPart(App::DocumentObject* obj);
+ std::shared_ptr makeMbdMarker(std::string& name, Base::Placement& plc);
+ std::shared_ptr makeMbdJoint(App::DocumentObject* joint);
+ std::shared_ptr makeMbdJointOfType(JointType jointType);
+ std::string handleOneSideOfJoint(App::DocumentObject* joint,
+ const char* propObjLinkName,
+ const char* propPlcName);
+ void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName);
+ bool fixGroundedParts();
+ void jointParts(std::vector joints);
+ std::vector getJoints();
+ Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName);
+ void setNewPlacements();
+ void recomputeJointPlacements(std::vector joints);
+
+ double getObjMass(App::DocumentObject* obj);
+ void setObjMasses(std::vector> objectMasses);
+
+private:
+ std::shared_ptr mbdAssembly;
+
+ std::unordered_map> objectPartMap;
+ std::vector> objMasses;
+
+ // void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property
+ // *prop) override;
+};
+
+// using AssemblyObjectPython = App::FeaturePythonT;
+
+} // namespace Assembly
+
+
+#endif // ASSEMBLY_AssemblyObject_H
diff --git a/src/Mod/Assembly/App/AssemblyObjectPy.xml b/src/Mod/Assembly/App/AssemblyObjectPy.xml
new file mode 100644
index 0000000000..7b38bd92be
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyObjectPy.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ This class handles document objects in Assembly
+
+
+
+
+ Solve the assembly and update part placements.
+
+ solve()
+
+ Returns:
+ 0 in case of success, otherwise the following codes in this order of
+ priority:
+ -6 if no parts are fixed.
+ -4 if over-constrained,
+ -3 if conflicting constraints,
+ -5 if malformed constraints
+ -1 if solver error,
+ -2 if redundant constraints.
+
+
+
+
+
+
+ Export the assembly in a text format called ASMT.
+
+ exportAsASMT(fileName:str)
+
+ Args:
+ fileName: The name of the file where the ASMT will be exported.
+
+
+
+
+
+
+
diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp
new file mode 100644
index 0000000000..bfb50e346c
--- /dev/null
+++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp
@@ -0,0 +1,75 @@
+/***************************************************************************
+ * Copyright (c) 2014 Jürgen Riegel *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Library General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2 of the License, or (at your option) any later version. *
+ * *
+ * This library 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 Library General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Library General Public *
+ * License along with this library; see the file COPYING.LIB. If not, *
+ * write to the Free Software Foundation, Inc., 59 Temple Place, *
+ * Suite 330, Boston, MA 02111-1307, USA *
+ * *
+ ***************************************************************************/
+
+
+#include "PreCompiled.h"
+
+// inclusion of the generated files (generated out of AssemblyObject.xml)
+#include "AssemblyObjectPy.h"
+#include "AssemblyObjectPy.cpp"
+
+using namespace Assembly;
+
+// returns a string which represents the object e.g. when printed in python
+std::string AssemblyObjectPy::representation() const
+{
+ return {""};
+}
+
+PyObject* AssemblyObjectPy::getCustomAttributes(const char* /*attr*/) const
+{
+ return nullptr;
+}
+
+int AssemblyObjectPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
+{
+ return 0;
+}
+
+PyObject* AssemblyObjectPy::solve(PyObject* args)
+{
+ if (!PyArg_ParseTuple(args, "")) {
+ return nullptr;
+ }
+ int ret = this->getAssemblyObjectPtr()->solve();
+ return Py_BuildValue("i", ret);
+}
+
+PyObject* AssemblyObjectPy::exportAsASMT(PyObject* args)
+{
+ char* utf8Name;
+ if (!PyArg_ParseTuple(args, "et", "utf-8", &utf8Name)) {
+ return nullptr;
+ }
+
+ std::string fileName = utf8Name;
+ PyMem_Free(utf8Name);
+
+ if (fileName.empty()) {
+ PyErr_SetString(PyExc_ValueError, "Passed string is empty");
+ return nullptr;
+ }
+
+ this->getAssemblyObjectPtr()->exportAsASMT(fileName);
+
+ Py_Return;
+}
diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt
index e69de29bb2..fc314577af 100644
--- a/src/Mod/Assembly/App/CMakeLists.txt
+++ b/src/Mod/Assembly/App/CMakeLists.txt
@@ -0,0 +1,59 @@
+
+include_directories(
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src/3rdParty/OndselSolver
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${OCC_INCLUDE_DIR}
+ ${PYTHON_INCLUDE_DIRS}
+)
+link_directories(${OCC_LIBRARY_DIR})
+
+set(Assembly_LIBS
+ Part
+ FreeCADApp
+ OndselSolver
+)
+
+generate_from_xml(AssemblyObjectPy)
+generate_from_xml(JointGroupPy)
+
+SET(Python_SRCS
+ AssemblyObjectPy.xml
+ AssemblyObjectPyImp.cpp
+ JointGroupPy.xml
+ JointGroupPyImp.cpp
+)
+SOURCE_GROUP("Python" FILES ${Python_SRCS})
+
+SET(Module_SRCS
+ AppAssembly.cpp
+ AppAssemblyPy.cpp
+ PreCompiled.cpp
+ PreCompiled.h
+)
+SOURCE_GROUP("Module" FILES ${Module_SRCS})
+
+SET(Assembly_SRCS
+ AssemblyObject.cpp
+ AssemblyObject.h
+ JointGroup.cpp
+ JointGroup.h
+ ${Module_SRCS}
+ ${Python_SRCS}
+)
+
+add_library(Assembly SHARED ${Assembly_SRCS})
+target_link_libraries(Assembly ${Assembly_LIBS})
+
+if(FREECAD_USE_PCH)
+ add_definitions(-D_PreComp_)
+ GET_MSVC_PRECOMPILED_SOURCE("PreCompiled.cpp" PCH_SRCS ${Assembly_SRCS})
+ ADD_MSVC_PRECOMPILED_HEADER(Assembly PreCompiled.h PreCompiled.cpp PCH_SRCS)
+endif(FREECAD_USE_PCH)
+
+SET_BIN_DIR(Assembly AssemblyApp /Mod/Assembly)
+SET_PYTHON_PREFIX_SUFFIX(Assembly)
+
+INSTALL(TARGETS Assembly DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/src/Mod/Assembly/App/JointGroup.cpp b/src/Mod/Assembly/App/JointGroup.cpp
new file mode 100644
index 0000000000..faccfd4de7
--- /dev/null
+++ b/src/Mod/Assembly/App/JointGroup.cpp
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "JointGroup.h"
+#include "JointGroupPy.h"
+
+using namespace Assembly;
+
+
+PROPERTY_SOURCE(Assembly::JointGroup, App::DocumentObjectGroup)
+
+JointGroup::JointGroup()
+{}
+
+JointGroup::~JointGroup() = default;
+
+PyObject* JointGroup::getPyObject()
+{
+ if (PythonObject.is(Py::_None())) {
+ // ref counter is set to 1
+ PythonObject = Py::Object(new JointGroupPy(this), true);
+ }
+ return Py::new_reference_to(PythonObject);
+}
diff --git a/src/Mod/Assembly/App/JointGroup.h b/src/Mod/Assembly/App/JointGroup.h
new file mode 100644
index 0000000000..17c328d2ba
--- /dev/null
+++ b/src/Mod/Assembly/App/JointGroup.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_JointGroup_H
+#define ASSEMBLY_JointGroup_H
+
+#include
+
+#include
+#include
+
+
+namespace Assembly
+{
+
+class AssemblyExport JointGroup: public App::DocumentObjectGroup
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(Assembly::JointGroup);
+
+public:
+ JointGroup();
+ ~JointGroup() override;
+
+ PyObject* getPyObject() override;
+
+ /// returns the type name of the ViewProvider
+ const char* getViewProviderName() const override
+ {
+ return "AssemblyGui::ViewProviderJointGroup";
+ }
+};
+
+
+} // namespace Assembly
+
+
+#endif // ASSEMBLY_JointGroup_H
diff --git a/src/Mod/Assembly/App/JointGroupPy.xml b/src/Mod/Assembly/App/JointGroupPy.xml
new file mode 100644
index 0000000000..d3b8555461
--- /dev/null
+++ b/src/Mod/Assembly/App/JointGroupPy.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ This class is a group subclass for joints.
+
+
+
+
+
diff --git a/src/Mod/Assembly/App/JointGroupPyImp.cpp b/src/Mod/Assembly/App/JointGroupPyImp.cpp
new file mode 100644
index 0000000000..a403ca4d9a
--- /dev/null
+++ b/src/Mod/Assembly/App/JointGroupPyImp.cpp
@@ -0,0 +1,46 @@
+/***************************************************************************
+ * Copyright (c) 2014 Jürgen Riegel *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Library General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2 of the License, or (at your option) any later version. *
+ * *
+ * This library 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 Library General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Library General Public *
+ * License along with this library; see the file COPYING.LIB. If not, *
+ * write to the Free Software Foundation, Inc., 59 Temple Place, *
+ * Suite 330, Boston, MA 02111-1307, USA *
+ * *
+ ***************************************************************************/
+
+
+#include "PreCompiled.h"
+
+// inclusion of the generated files (generated out of JointGroup.xml)
+#include "JointGroupPy.h"
+#include "JointGroupPy.cpp"
+
+using namespace Assembly;
+
+// returns a string which represents the object e.g. when printed in python
+std::string JointGroupPy::representation() const
+{
+ return {""};
+}
+
+PyObject* JointGroupPy::getCustomAttributes(const char* /*attr*/) const
+{
+ return nullptr;
+}
+
+int JointGroupPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
+{
+ return 0;
+}
diff --git a/src/Mod/Assembly/App/PreCompiled.cpp b/src/Mod/Assembly/App/PreCompiled.cpp
new file mode 100644
index 0000000000..ed7cfc6869
--- /dev/null
+++ b/src/Mod/Assembly/App/PreCompiled.cpp
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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"
diff --git a/src/Mod/Assembly/App/PreCompiled.h b/src/Mod/Assembly/App/PreCompiled.h
new file mode 100644
index 0000000000..4e087e3c91
--- /dev/null
+++ b/src/Mod/Assembly/App/PreCompiled.h
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_PRECOMPILED_H
+#define ASSEMBLY_PRECOMPILED_H
+
+#include
+
+#ifdef _MSC_VER
+#pragma warning(disable : 5208)
+#endif
+
+#ifdef _PreComp_
+
+// standard
+#include
+#include
+#include
+#include
",
"CmdType": "ForEdit",
}
@@ -62,13 +65,8 @@ class CommandCreateJointFixed:
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
- assembly = UtilsAssembly.activeAssembly()
- if not assembly:
- return
- view = Gui.activeDocument().activeView()
-
- self.panel = TaskAssemblyCreateJoint(assembly, view, 0)
- Gui.Control.showDialog(self.panel)
+ panel = TaskAssemblyCreateJoint(0)
+ Gui.Control.showDialog(panel)
class CommandCreateJointRevolute:
@@ -81,10 +79,12 @@ class CommandCreateJointRevolute:
"Pixmap": "Assembly_CreateJointRevolute",
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointRevolute", "Create Revolute Joint"),
"Accel": "R",
- "ToolTip": QT_TRANSLATE_NOOP(
+ "ToolTip": "
Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.
",
- ),
+ "Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.",
+ )
+ + "",
+ "CmdType": "ForEdit",
+ }
+
+ def IsActive(self):
+ return UtilsAssembly.activeAssembly() is not None
+
+ def Activated(self):
+ panel = TaskAssemblyCreateJoint(7)
+ Gui.Control.showDialog(panel)
+
+
+class CommandToggleGrounded:
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+
+ return {
+ "Pixmap": "Assembly_ToggleGrounded",
+ "MenuText": QT_TRANSLATE_NOOP("Assembly_ToggleGrounded", "Toggle grounded"),
+ "Accel": "F",
+ "ToolTip": "
"
+ + QT_TRANSLATE_NOOP(
+ "Assembly_ToggleGrounded",
+ "Toggle the grounded state of a part. Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part per assembly.",
+ )
+ + "
",
"CmdType": "ForEdit",
}
@@ -277,224 +286,40 @@ class CommandCreateJointTangent:
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
- view = Gui.activeDocument().activeView()
- self.panel = TaskAssemblyCreateJoint(assembly, view, 7)
- Gui.Control.showDialog(self.panel)
+ joint_group = UtilsAssembly.getJointGroup(assembly)
+ selection = Gui.Selection.getSelectionEx("*", 0)
+ if not selection:
+ return
-class MakeJointSelGate:
- def __init__(self, taskbox, assembly):
- self.taskbox = taskbox
- self.assembly = assembly
+ App.setActiveTransaction("Toggle grounded")
+ for sel in selection:
+ # If you select 2 solids (bodies for example) within an assembly.
+ # There'll be a single sel but 2 SubElementNames.
+ for sub in sel.SubElementNames:
- def allow(self, doc, obj, sub):
- if not sub:
- return False
+ full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub)
+ obj = UtilsAssembly.getObject(full_element_name)
- objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
-
- if self.assembly.Name not in objs_names or element_name == "":
- # Only objects within the assembly. And not whole objects, only elements.
- return False
-
- if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
- # If it's to deselect then it's ok
- return True
-
- if len(self.taskbox.current_selection) >= 2:
- # No more than 2 elements can be selected for basic joints.
- return False
-
- full_obj_name = ".".join(objs_names)
- for selection_dict in self.taskbox.current_selection:
- if selection_dict["full_obj_name"] == full_obj_name:
- # Can't join a solid to itself. So the user need to select 2 different parts.
- return False
-
- return True
-
-
-class TaskAssemblyCreateJoint(QtCore.QObject):
- def __init__(self, assembly, view, jointTypeIndex):
- super().__init__()
-
- self.assembly = assembly
- self.view = view
- self.doc = App.ActiveDocument
-
- self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
-
- self.form.jointType.addItems(JointObject.JointTypes)
- self.form.jointType.setCurrentIndex(jointTypeIndex)
+ # Check if part is grounded and if so delete the joint.
+ for joint in joint_group.Group:
+ if hasattr(joint, "ObjectToGround") and joint.ObjectToGround == obj:
+ doc = App.ActiveDocument
+ doc.removeObject(joint.Name)
+ doc.recompute()
+ return
+ # Create groundedJoint.
+ ground = joint_group.newObject("App::FeaturePython", "GroundedJoint")
+ JointObject.GroundedJoint(ground, obj)
+ JointObject.ViewProviderGroundedJoint(ground.ViewObject)
Gui.Selection.clearSelection()
- Gui.Selection.addSelectionGate(
- MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
- )
- Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
- Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
- self.current_selection = []
- self.preselection_dict = None
-
- self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
- self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
-
- App.setActiveTransaction("Create joint")
- self.createJointObject()
-
- def accept(self):
- if len(self.current_selection) != 2:
- App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
- return False
- self.deactivate()
App.closeActiveTransaction()
- return True
-
- def reject(self):
- self.deactivate()
- App.closeActiveTransaction(True)
- return True
-
- def deactivate(self):
- Gui.Selection.removeSelectionGate()
- Gui.Selection.removeObserver(self)
- Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection)
- Gui.Selection.clearSelection()
- self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
- self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
- if Gui.Control.activeDialog():
- Gui.Control.closeDialog()
-
- def createJointObject(self):
- type_index = self.form.jointType.currentIndex()
-
- joint_group = self.assembly.getObject("Joints")
-
- if not joint_group:
- joint_group = self.assembly.newObject("App::DocumentObjectGroup", "Joints")
-
- self.joint = joint_group.newObject("App::FeaturePython", "Joint")
- JointObject.Joint(self.joint, type_index)
- JointObject.ViewProviderJoint(self.joint.ViewObject, self.joint)
-
- def updateJoint(self):
- # First we build the listwidget
- self.form.featureList.clear()
- simplified_names = []
- for sel in self.current_selection:
- # TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12
- sname = sel["full_element_name"].split(self.assembly.Name + ".", 1)[-1]
- simplified_names.append(sname)
- self.form.featureList.addItems(simplified_names)
-
- # Then we pass the new list to the join object
- self.joint.Proxy.setJointConnectors(self.current_selection)
-
- def moveMouse(self, info):
- if len(self.current_selection) >= 2 or (
- len(self.current_selection) == 1
- and self.current_selection[0]["full_element_name"]
- == self.preselection_dict["full_element_name"]
- ):
- self.joint.ViewObject.Proxy.showPreviewJCS(False)
- return
-
- cursor_pos = self.view.getCursorPos()
- cursor_info = self.view.getObjectInfo(cursor_pos)
- # cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': , 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'}
-
- if (
- not cursor_info
- or not self.preselection_dict
- or cursor_info["SubName"] != self.preselection_dict["sub_name"]
- ):
- self.joint.ViewObject.Proxy.showPreviewJCS(False)
- return
-
- # newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane
-
- newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
- self.preselection_dict["mouse_pos"] = newPos
-
- self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
- self.preselection_dict
- )
-
- placement = self.joint.Proxy.findPlacement(
- self.preselection_dict["object"],
- self.preselection_dict["element_name"],
- self.preselection_dict["vertex_name"],
- )
- self.joint.ViewObject.Proxy.showPreviewJCS(True, placement)
- self.previewJCSVisible = True
-
- # 3D view keyboard handler
- def KeyboardEvent(self, info):
- if info["State"] == "UP" and info["Key"] == "ESCAPE":
- self.reject()
-
- if info["State"] == "UP" and info["Key"] == "RETURN":
- self.accept()
-
- # selectionObserver stuff
- def addSelection(self, doc_name, obj_name, sub_name, mousePos):
- full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
- full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
- selected_object = UtilsAssembly.getObject(full_element_name)
- element_name = UtilsAssembly.getElementName(full_element_name)
-
- selection_dict = {
- "object": selected_object,
- "element_name": element_name,
- "full_element_name": full_element_name,
- "full_obj_name": full_obj_name,
- "mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]),
- }
- selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
-
- self.current_selection.append(selection_dict)
- self.updateJoint()
-
- def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None):
- full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
-
- # Find and remove the corresponding dictionary from the combined list
- selection_dict_to_remove = None
- for selection_dict in self.current_selection:
- if selection_dict["full_element_name"] == full_element_name:
- selection_dict_to_remove = selection_dict
- break
-
- if selection_dict_to_remove is not None:
- self.current_selection.remove(selection_dict_to_remove)
-
- self.updateJoint()
-
- def setPreselection(self, doc_name, obj_name, sub_name):
- if not sub_name:
- self.preselection_dict = None
- return
-
- full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
- full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
- selected_object = UtilsAssembly.getObject(full_element_name)
- element_name = UtilsAssembly.getElementName(full_element_name)
-
- self.preselection_dict = {
- "object": selected_object,
- "sub_name": sub_name,
- "element_name": element_name,
- "full_element_name": full_element_name,
- "full_obj_name": full_obj_name,
- }
-
- def clearSelection(self, doc_name):
- self.current_selection.clear()
- self.updateJoint()
if App.GuiUp:
+ Gui.addCommand("Assembly_ToggleGrounded", CommandToggleGrounded())
Gui.addCommand("Assembly_CreateJointFixed", CommandCreateJointFixed())
Gui.addCommand("Assembly_CreateJointRevolute", CommandCreateJointRevolute())
Gui.addCommand("Assembly_CreateJointCylindrical", CommandCreateJointCylindrical())
diff --git a/src/Mod/Assembly/CommandExportASMT.py b/src/Mod/Assembly/CommandExportASMT.py
new file mode 100644
index 0000000000..438c669629
--- /dev/null
+++ b/src/Mod/Assembly/CommandExportASMT.py
@@ -0,0 +1,82 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# /****************************************************************************
+# *
+# Copyright (c) 2023 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 FreeCAD as App
+import UtilsAssembly
+
+from PySide.QtCore import QT_TRANSLATE_NOOP
+from PySide.QtWidgets import QFileDialog
+
+if App.GuiUp:
+ import FreeCADGui as Gui
+
+# translate = App.Qt.translate
+
+__title__ = "Assembly Command Create Assembly"
+__author__ = "Ondsel"
+__url__ = "https://www.freecad.org"
+
+
+class CommandExportASMT:
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+ return {
+ "Pixmap": "Assembly_ExportASMT",
+ "MenuText": QT_TRANSLATE_NOOP("Assembly_ExportASMT", "Export ASMT File"),
+ "Accel": "E",
+ "ToolTip": QT_TRANSLATE_NOOP(
+ "Assembly_ExportASMT",
+ "Export currently active assembly as a ASMT file.",
+ ),
+ "CmdType": "ForEdit",
+ }
+
+ def IsActive(self):
+ return UtilsAssembly.activeAssembly() is not None
+
+ def Activated(self):
+ document = App.ActiveDocument
+ if not document:
+ return
+
+ assembly = UtilsAssembly.activeAssembly()
+ if not assembly:
+ return
+
+ # Prompt the user for a file location and name
+ defaultFileName = document.Name + ".asmt"
+ filePath, _ = QFileDialog.getSaveFileName(
+ None,
+ "Save ASMT File",
+ defaultFileName,
+ "ASMT Files (*.asmt);;All Files (*)",
+ )
+
+ if filePath:
+ assembly.exportAsASMT(filePath)
+
+
+if App.GuiUp:
+ Gui.addCommand("Assembly_ExportASMT", CommandExportASMT())
diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py
index 0c5dd143b9..5f7ddd3126 100644
--- a/src/Mod/Assembly/CommandInsertLink.py
+++ b/src/Mod/Assembly/CommandInsertLink.py
@@ -110,6 +110,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
def deactivated(self):
if self.partMoving:
self.endMove()
+ self.doc.removeObject(self.createdLink.Name)
def buildPartList(self):
self.allParts.clear()
diff --git a/src/Mod/Assembly/CommandSolveAssembly.py b/src/Mod/Assembly/CommandSolveAssembly.py
new file mode 100644
index 0000000000..95ec98f0db
--- /dev/null
+++ b/src/Mod/Assembly/CommandSolveAssembly.py
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# /****************************************************************************
+# *
+# Copyright (c) 2023 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 os
+import FreeCAD as App
+
+from PySide.QtCore import QT_TRANSLATE_NOOP
+
+if App.GuiUp:
+ import FreeCADGui as Gui
+ from PySide import QtCore, QtGui, QtWidgets
+
+import UtilsAssembly
+import Assembly_rc
+
+# translate = App.Qt.translate
+
+__title__ = "Assembly Command to Solve Assembly"
+__author__ = "Ondsel"
+__url__ = "https://www.freecad.org"
+
+
+class CommandSolveAssembly:
+ def __init__(self):
+ pass
+
+ def GetResources(self):
+
+ return {
+ "Pixmap": "Assembly_SolveAssembly",
+ "MenuText": QT_TRANSLATE_NOOP("Assembly_SolveAssembly", "Solve Assembly"),
+ "Accel": "F",
+ "ToolTip": "
"
+ + QT_TRANSLATE_NOOP(
+ "Assembly_SolveAssembly",
+ "Solve the currently active assembly.",
+ )
+ + "
",
+ "CmdType": "ForEdit",
+ }
+
+ def IsActive(self):
+ return UtilsAssembly.activeAssembly() is not None
+
+ def Activated(self):
+ assembly = UtilsAssembly.activeAssembly()
+ if not assembly:
+ return
+
+ App.setActiveTransaction("Solve assembly")
+ assembly.solve()
+ App.closeActiveTransaction()
+
+
+if App.GuiUp:
+ Gui.addCommand("Assembly_SolveAssembly", CommandSolveAssembly())
diff --git a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
new file mode 100644
index 0000000000..c0a2e8439b
--- /dev/null
+++ b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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"
+
+#include
+#include
+
+#include "ViewProviderAssembly.h"
+#include "ViewProviderJointGroup.h"
+
+
+namespace AssemblyGui
+{
+extern PyObject* initModule();
+}
+
+/* Python entry */
+PyMOD_INIT_FUNC(AssemblyGui)
+{
+ PyObject* mod = AssemblyGui::initModule();
+ Base::Console().Log("Loading AssemblyGui module... done\n");
+
+
+ // NOTE: To finish the initialization of our own type objects we must
+ // call PyType_Ready, otherwise we run into a segmentation fault, later on.
+ // This function is responsible for adding inherited slots from a type's base class.
+
+ AssemblyGui::ViewProviderAssembly ::init();
+ AssemblyGui::ViewProviderJointGroup::init();
+
+ PyMOD_Return(mod);
+}
diff --git a/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp b/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp
new file mode 100644
index 0000000000..dd6f0fdf22
--- /dev/null
+++ b/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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"
+
+#include
+
+
+namespace AssemblyGui
+{
+class Module: public Py::ExtensionModule
+{
+public:
+ Module()
+ : Py::ExtensionModule("AssemblyGui")
+ {
+ initialize("This module is the Assembly module."); // register with Python
+ }
+};
+
+PyObject* initModule()
+{
+ return Base::Interpreter().addModule(new Module);
+}
+
+} // namespace AssemblyGui
diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt
index 565597d318..6dcc4acb25 100644
--- a/src/Mod/Assembly/Gui/CMakeLists.txt
+++ b/src/Mod/Assembly/Gui/CMakeLists.txt
@@ -1,9 +1,13 @@
include_directories(
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}
+ ${OCC_INCLUDE_DIR}
)
set(AssemblyGui_LIBS
+ Assembly
+ PartDesign
+ PartGui
FreeCADGui
)
@@ -17,8 +21,23 @@ qt_add_resources(AssemblyResource_SRCS Resources/Assembly.qrc ${Assembly_TR_QRC}
SOURCE_GROUP("Resources" FILES ${AssemblyResource_SRCS})
+generate_from_xml(ViewProviderAssemblyPy)
+
+SET(Python_SRCS
+ ViewProviderAssemblyPy.xml
+ ViewProviderAssemblyPyImp.cpp
+)
+SOURCE_GROUP("Python" FILES ${Python_SRCS})
SET(AssemblyGui_SRCS_Module
+ AppAssemblyGui.cpp
+ AppAssemblyGuiPy.cpp
+ PreCompiled.cpp
+ PreCompiled.h
+ ViewProviderAssembly.cpp
+ ViewProviderAssembly.h
+ ViewProviderJointGroup.cpp
+ ViewProviderJointGroup.h
${Assembly_QRC_SRCS}
)
@@ -29,8 +48,14 @@ SET(AssemblyGui_SRCS
${AssemblyResource_SRCS}
${AssemblyGui_UIC_HDRS}
${AssemblyGui_SRCS_Module}
+ ${Python_SRCS}
)
+if(FREECAD_USE_PCH)
+ add_definitions(-D_PreComp_)
+ GET_MSVC_PRECOMPILED_SOURCE("PreCompiled.cpp" PCH_SRCS ${AssemblyGui_SRCS})
+ ADD_MSVC_PRECOMPILED_HEADER(PathGui PreCompiled.h PreCompiled.cpp PCH_SRCS)
+endif(FREECAD_USE_PCH)
SET(AssemblyGuiIcon_SVG
Resources/icons/AssemblyWorkbench.svg
diff --git a/src/Mod/Assembly/Gui/PreCompiled.cpp b/src/Mod/Assembly/Gui/PreCompiled.cpp
new file mode 100644
index 0000000000..ed7cfc6869
--- /dev/null
+++ b/src/Mod/Assembly/Gui/PreCompiled.cpp
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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"
diff --git a/src/Mod/Assembly/Gui/PreCompiled.h b/src/Mod/Assembly/Gui/PreCompiled.h
new file mode 100644
index 0000000000..6c1fc5583b
--- /dev/null
+++ b/src/Mod/Assembly/Gui/PreCompiled.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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 POINTSGUI_PRECOMPILED_H
+#define POINTSGUI_PRECOMPILED_H
+
+#include
+
+#ifdef _PreComp_
+
+// STL
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+#endif //_PreComp_
+
+#endif // POINTSGUI_PRECOMPILED_H
diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc
index 8ec25f10ba..6ace2e5c34 100644
--- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc
+++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc
@@ -2,6 +2,7 @@
icons/Assembly_InsertLink.svgicons/preferences-assembly.svg
+ icons/Assembly_ToggleGrounded.svgicons/Assembly_CreateJointBall.svgicons/Assembly_CreateJointCylindrical.svgicons/Assembly_CreateJointFixed.svg
@@ -10,6 +11,7 @@
icons/Assembly_CreateJointRevolute.svgicons/Assembly_CreateJointSlider.svgicons/Assembly_CreateJointTangent.svg
+ icons/Assembly_ExportASMT.svgpanels/TaskAssemblyCreateJoint.uipanels/TaskAssemblyInsertLink.uipreferences/Assembly.ui
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg
index 84f889e6cf..33611918f7 100644
--- a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg
@@ -7,7 +7,7 @@
id="svg2821"
sodipodi:version="0.32"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
- sodipodi:docname="Assembly_CreateJointFixed.svg"
+ sodipodi:docname="Assembly_CreateJointFixedNew.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
@@ -175,6 +175,17 @@
x2="85"
y2="35"
spreadMethod="reflect" />
+
+
+
+
+ inkscape:deskcolor="#d1d1d1"
+ showguides="true">
+
+
+
@@ -261,46 +294,51 @@
transform="translate(3.6192085e-6,-0.89630564)">
+ sodipodi:nodetypes="ccczccc" />
+ cy="31.700123"
+ ry="7.9999995"
+ rx="24" />
+
+ sodipodi:nodetypes="scczcccccs" />
+ sodipodi:nodetypes="scczcccccs" />
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg
new file mode 100644
index 0000000000..50b1905bd0
--- /dev/null
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg
@@ -0,0 +1,944 @@
+
+
diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg
new file mode 100644
index 0000000000..07c83f3a85
--- /dev/null
+++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg
@@ -0,0 +1,441 @@
+
+
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
new file mode 100644
index 0000000000..97c3ab4935
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp
@@ -0,0 +1,398 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "ViewProviderAssembly.h"
+#include "ViewProviderAssemblyPy.h"
+
+
+using namespace Assembly;
+using namespace AssemblyGui;
+
+PROPERTY_SOURCE(AssemblyGui::ViewProviderAssembly, Gui::ViewProviderPart)
+
+ViewProviderAssembly::ViewProviderAssembly()
+ : SelectionObserver(true)
+ , canStartDragging(false)
+ , partMoving(false)
+ , enableMovement(true)
+ , docsToMove({})
+{}
+
+ViewProviderAssembly::~ViewProviderAssembly() = default;
+
+QIcon ViewProviderAssembly::getIcon() const
+{
+ return Gui::BitmapFactory().pixmap("Geoassembly.svg");
+}
+
+bool ViewProviderAssembly::doubleClicked()
+{
+ if (isInEditMode()) {
+ // Part is already 'Active' so we exit edit mode.
+ Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()");
+ }
+ else {
+ // Part is not 'Active' so we enter edit mode to make it so.
+ Gui::Application::Instance->activeDocument()->setEdit(this);
+ }
+
+ return true;
+}
+
+
+bool ViewProviderAssembly::setEdit(int ModNum)
+{
+ // Set the part as 'Activated' ie bold in the tree.
+ Gui::Command::doCommand(Gui::Command::Gui,
+ "Gui.ActiveDocument.ActiveView.setActiveObject('%s', "
+ "App.getDocument('%s').getObject('%s'))",
+ PARTKEY,
+ this->getObject()->getDocument()->getName(),
+ this->getObject()->getNameInDocument());
+
+ return true;
+}
+
+void ViewProviderAssembly::unsetEdit(int ModNum)
+{
+ Q_UNUSED(ModNum);
+
+ canStartDragging = false;
+ partMoving = false;
+ docsToMove = {};
+
+ // Set the part as not 'Activated' ie not bold in the tree.
+ Gui::Command::doCommand(Gui::Command::Gui,
+ "Gui.ActiveDocument.ActiveView.setActiveObject('%s', None)",
+ PARTKEY);
+}
+
+bool ViewProviderAssembly::isInEditMode()
+{
+ App::DocumentObject* activePart = getActivePart();
+ if (!activePart) {
+ return false;
+ }
+
+ return activePart == this->getObject();
+}
+
+App::DocumentObject* ViewProviderAssembly::getActivePart()
+{
+ App::DocumentObject* activePart = nullptr;
+ auto activeDoc = Gui::Application::Instance->activeDocument();
+ if (!activeDoc) {
+ activeDoc = getDocument();
+ }
+ auto activeView = activeDoc->setActiveView(this);
+ if (!activeView) {
+ return nullptr;
+ }
+
+ activePart = activeView->getActiveObject(PARTKEY);
+ return activePart;
+}
+
+bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer)
+{
+ // Base::Console().Warning("Mouse move\n");
+
+ // Initialize or end the dragging of parts
+ if (canStartDragging) {
+ canStartDragging = false;
+
+ if (enableMovement && getSelectedObjectsWithinAssembly()) {
+ SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos);
+ Base::Vector3d mousePosition = Base::Vector3d(vec[0], vec[1], vec[2]);
+
+ initMove(mousePosition);
+ }
+ }
+
+ // Do the dragging of parts
+ if (partMoving) {
+ SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos);
+ Base::Vector3d mousePosition = Base::Vector3d(vec[0], vec[1], vec[2]);
+ for (auto& pair : docsToMove) {
+ App::DocumentObject* obj = pair.first;
+ auto* propPlacement =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ if (propPlacement) {
+ Base::Placement plc = propPlacement->getValue();
+ // Base::Console().Warning("transl %f %f %f\n", pair.second.x, pair.second.y,
+ // pair.second.z);
+ Base::Vector3d pos = mousePosition + pair.second;
+ Base::Placement newPlacement = Base::Placement(pos, plc.getRotation());
+ propPlacement->setValue(newPlacement);
+ }
+ }
+
+ auto* assemblyPart = static_cast(getObject());
+ assemblyPart->solve();
+ }
+ return false;
+}
+
+bool ViewProviderAssembly::mouseButtonPressed(int Button,
+ bool pressed,
+ const SbVec2s& cursorPos,
+ const Gui::View3DInventorViewer* viewer)
+{
+ // Left Mouse button ****************************************************
+ if (Button == 1) {
+ if (pressed) {
+ canStartDragging = true;
+ }
+ else { // Button 1 released
+ // release event is not received when user click on a part for selection.
+ // So we use SelectionObserver to know if something got selected.
+
+ canStartDragging = false;
+ if (partMoving) {
+ endMove();
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool ViewProviderAssembly::getSelectedObjectsWithinAssembly()
+{
+ // check the current selection, and check if any of the selected objects are within this
+ // App::Part
+ // If any, put them into the vector docsToMove and return true.
+ // Get the document
+ Gui::Document* doc = Gui::Application::Instance->activeDocument();
+
+ if (!doc) {
+ return false;
+ }
+
+ // Get the assembly object for this ViewProvider
+ AssemblyObject* assemblyPart = static_cast(getObject());
+
+ if (!assemblyPart) {
+ return false;
+ }
+
+ for (auto& selObj : Gui::Selection().getSelectionEx("",
+ App::DocumentObject::getClassTypeId(),
+ Gui::ResolveMode::NoResolve)) {
+ // getSubNames() returns ["Body001.Pad.Face14", "Body002.Pad.Face7"]
+ // if you have several objects within the same assembly selected.
+
+ std::vector objsSubNames = selObj.getSubNames();
+ for (auto& subNamesStr : objsSubNames) {
+ std::vector subNames = parseSubNames(subNamesStr);
+
+ App::DocumentObject* obj = getObjectFromSubNames(subNames);
+ if (!obj) {
+ continue;
+ }
+
+ // Check if the selected object is a child of the assembly
+ if (assemblyPart->hasObject(obj, true)) {
+ auto* propPlacement =
+ dynamic_cast(obj->getPropertyByName("Placement"));
+ if (propPlacement) {
+ Base::Placement plc = propPlacement->getValue();
+ Base::Vector3d pos = plc.getPosition();
+ docsToMove.emplace_back(obj, pos);
+ }
+ }
+ }
+ }
+
+ // This function is called before the selection is updated. So if a user click and drag a part
+ // it is not selected at that point. So we need to get the preselection too.
+ if (Gui::Selection().hasPreselection()) {
+
+ // Base::Console().Warning("Gui::Selection().getPreselection().pSubName %s\n",
+ // Gui::Selection().getPreselection().pSubName);
+
+ std::string subNamesStr = Gui::Selection().getPreselection().pSubName;
+ std::vector subNames = parseSubNames(subNamesStr);
+
+ App::DocumentObject* preselectedObj = getObjectFromSubNames(subNames);
+ if (preselectedObj) {
+ if (assemblyPart->hasObject(preselectedObj, true)) {
+ bool alreadyIn = false;
+ for (auto& pair : docsToMove) {
+ App::DocumentObject* obj = pair.first;
+ if (obj == preselectedObj) {
+ alreadyIn = true;
+ break;
+ }
+ }
+
+ if (!alreadyIn) {
+ auto* propPlacement = dynamic_cast(
+ preselectedObj->getPropertyByName("Placement"));
+ if (propPlacement) {
+ Base::Placement plc = propPlacement->getValue();
+ Base::Vector3d pos = plc.getPosition();
+ docsToMove.emplace_back(preselectedObj, pos);
+ }
+ }
+ }
+ }
+ }
+
+ return !docsToMove.empty();
+}
+
+std::vector ViewProviderAssembly::parseSubNames(std::string& subNamesStr)
+{
+ std::vector subNames;
+ std::string subName;
+ std::istringstream subNameStream(subNamesStr);
+ while (std::getline(subNameStream, subName, '.')) {
+ subNames.push_back(subName);
+ }
+ return subNames;
+}
+
+App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vector& subNames)
+{
+ App::Document* appDoc = App::GetApplication().getActiveDocument();
+
+ std::string objName;
+ if (subNames.size() < 2) {
+ return nullptr;
+ }
+ else if (subNames.size() == 2) {
+ // If two subnames then it can't be a body and the object we want is the first one
+ // For example we want box in "box.face1"
+ return appDoc->getObject(subNames[0].c_str());
+ }
+ else {
+ objName = subNames[subNames.size() - 3];
+
+ App::DocumentObject* obj = appDoc->getObject(objName.c_str());
+ if (!obj) {
+ return nullptr;
+ }
+ if (obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
+ return obj;
+ }
+ else if (obj->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) {
+
+ App::Link* link = dynamic_cast(obj);
+
+ App::DocumentObject* linkedObj = link->getLinkedObject(true);
+
+ if (linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
+ return obj;
+ }
+ }
+
+ // then its neither a body or a link to a body.
+ objName = subNames[subNames.size() - 2];
+ return appDoc->getObject(objName.c_str());
+ }
+}
+
+void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition)
+{
+ partMoving = true;
+
+ // prevent selection while moving
+ auto* view = dynamic_cast(
+ Gui::Application::Instance->editDocument()->getActiveView());
+ if (view) {
+ Gui::View3DInventorViewer* viewerNotConst;
+ viewerNotConst = static_cast(view)->getViewer();
+ viewerNotConst->setSelectionEnabled(false);
+ }
+
+ objectMasses.clear();
+
+ for (auto& pair : docsToMove) {
+ pair.second = pair.second - mousePosition;
+ objectMasses.push_back({pair.first, 10.0});
+ }
+
+ auto* assemblyPart = static_cast(getObject());
+ assemblyPart->setObjMasses(objectMasses);
+}
+
+void ViewProviderAssembly::endMove()
+{
+ docsToMove = {};
+ partMoving = false;
+ canStartDragging = false;
+
+ // enable selection after the move
+ auto* view = dynamic_cast(
+ Gui::Application::Instance->editDocument()->getActiveView());
+ if (view) {
+ Gui::View3DInventorViewer* viewerNotConst;
+ viewerNotConst = static_cast(view)->getViewer();
+ viewerNotConst->setSelectionEnabled(true);
+ }
+
+ auto* assemblyPart = static_cast(getObject());
+ assemblyPart->setObjMasses({});
+}
+
+
+void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg)
+{
+ if (msg.Type == Gui::SelectionChanges::AddSelection
+ || msg.Type == Gui::SelectionChanges::ClrSelection
+ || msg.Type == Gui::SelectionChanges::RmvSelection) {
+ canStartDragging = false;
+ }
+}
+
+PyObject* ViewProviderAssembly::getPyObject()
+{
+ if (!pyViewObject) {
+ pyViewObject = new ViewProviderAssemblyPy(this);
+ }
+ pyViewObject->IncRef();
+ return pyViewObject;
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h
new file mode 100644
index 0000000000..dd0378710b
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_ViewProviderAssembly_H
+#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
+
+#include
+
+#include
+#include
+
+namespace Gui
+{
+class View3DInventorViewer;
+}
+
+namespace AssemblyGui
+{
+
+class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart,
+ public Gui::SelectionObserver
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly);
+
+public:
+ ViewProviderAssembly();
+ ~ViewProviderAssembly() override;
+
+ /// deliver the icon shown in the tree view. Override from ViewProvider.h
+ QIcon getIcon() const override;
+
+ bool doubleClicked() override;
+
+ /** @name enter/exit edit mode */
+ //@{
+ bool setEdit(int ModNum) override;
+ void unsetEdit(int ModNum) override;
+ bool isInEditMode();
+
+ App::DocumentObject* getActivePart();
+
+ /// is called when the provider is in edit and the mouse is moved
+ bool mouseMove(const SbVec2s& pos, Gui::View3DInventorViewer* viewer) override;
+ /// is called when the Provider is in edit and the mouse is clicked
+ bool mouseButtonPressed(int Button,
+ bool pressed,
+ const SbVec2s& cursorPos,
+ const Gui::View3DInventorViewer* viewer) override;
+
+ void initMove(Base::Vector3d& mousePosition);
+ void endMove();
+
+ bool getSelectedObjectsWithinAssembly();
+ App::DocumentObject* getObjectFromSubNames(std::vector& subNames);
+ std::vector parseSubNames(std::string& subNamesStr);
+
+ /// Get the python wrapper for that ViewProvider
+ PyObject* getPyObject() override;
+
+ virtual void setEnableMovement(bool enable = true)
+ {
+ enableMovement = enable;
+ }
+ virtual bool getEnableMovement() const
+ {
+ return enableMovement;
+ }
+
+ // protected:
+ /// get called by the container whenever a property has been changed
+ // void onChanged(const App::Property* prop) override;
+
+ void onSelectionChanged(const Gui::SelectionChanges& msg) override;
+
+ bool canStartDragging;
+ bool partMoving;
+ bool enableMovement;
+ int numberOfSel;
+
+ std::vector> objectMasses;
+ std::vector> docsToMove;
+};
+
+} // namespace AssemblyGui
+
+#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml b/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml
new file mode 100644
index 0000000000..503bf213ab
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ This is the ViewProviderAssembly class
+
+
+
+ Enable moving the parts by clicking and dragging.
+
+
+
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp
new file mode 100644
index 0000000000..31a4090a65
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp
@@ -0,0 +1,59 @@
+/***************************************************************************
+ * Copyright (c) 2008 Werner Mayer *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Library General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2 of the License, or (at your option) any later version. *
+ * *
+ * This library 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 Library General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Library General Public *
+ * License along with this library; see the file COPYING.LIB. If not, *
+ * write to the Free Software Foundation, Inc., 59 Temple Place, *
+ * Suite 330, Boston, MA 02111-1307, USA *
+ * *
+ ***************************************************************************/
+
+#include "PreCompiled.h"
+
+// inclusion of the generated files (generated out of ViewProviderAssemblyPy.xml)
+#include "ViewProviderAssemblyPy.h"
+#include "ViewProviderAssemblyPy.cpp"
+
+
+using namespace Gui;
+
+// returns a string which represents the object e.g. when printed in python
+std::string ViewProviderAssemblyPy::representation() const
+{
+ std::stringstream str;
+ str << "";
+
+ return str.str();
+}
+
+Py::Boolean ViewProviderAssemblyPy::getEnableMovement() const
+{
+ return {getViewProviderAssemblyPtr()->getEnableMovement()};
+}
+
+void ViewProviderAssemblyPy::setEnableMovement(Py::Boolean arg)
+{
+ getViewProviderAssemblyPtr()->setEnableMovement(arg);
+}
+
+PyObject* ViewProviderAssemblyPy::getCustomAttributes(const char* /*attr*/) const
+{
+ return nullptr;
+}
+
+int ViewProviderAssemblyPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
+{
+ return 0;
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
new file mode 100644
index 0000000000..abfa1db8c0
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_
+#endif
+
+#include
+#include
+#include
+#include
+
+#include "ViewProviderJointGroup.h"
+
+
+using namespace AssemblyGui;
+
+PROPERTY_SOURCE(AssemblyGui::ViewProviderJointGroup, Gui::ViewProviderDocumentObjectGroup)
+
+ViewProviderJointGroup::ViewProviderJointGroup()
+{}
+
+ViewProviderJointGroup::~ViewProviderJointGroup() = default;
+
+QIcon ViewProviderJointGroup::getIcon() const
+{
+ return Gui::BitmapFactory().pixmap("Assembly_CreateJointFixed.svg");
+}
diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
new file mode 100644
index 0000000000..fb965e9c2a
--- /dev/null
+++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/****************************************************************************
+ * *
+ * Copyright (c) 2023 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_ViewProviderJointGroup_H
+#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H
+
+#include
+
+#include
+
+
+namespace AssemblyGui
+{
+
+class AssemblyGuiExport ViewProviderJointGroup: public Gui::ViewProviderDocumentObjectGroup
+{
+ PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderJointGroup);
+
+public:
+ ViewProviderJointGroup();
+ ~ViewProviderJointGroup() override;
+
+ /// deliver the icon shown in the tree view. Override from ViewProvider.h
+ QIcon getIcon() const override;
+
+ // protected:
+ /// get called by the container whenever a property has been changed
+ // void onChanged(const App::Property* prop) override;
+};
+
+} // namespace AssemblyGui
+
+#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H
diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py
index 7df9fb15d4..c45bae3a9d 100644
--- a/src/Mod/Assembly/InitGui.py
+++ b/src/Mod/Assembly/InitGui.py
@@ -49,7 +49,6 @@ class AssemblyWorkbench(Workbench):
"Assembly workbench"
def __init__(self):
- print("Loading Assembly workbench...")
self.__class__.Icon = (
FreeCAD.getResourceDir() + "Mod/Assembly/Resources/icons/AssemblyWorkbench.svg"
)
@@ -65,7 +64,7 @@ class AssemblyWorkbench(Workbench):
# load the builtin modules
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
- import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint
+ import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint, CommandSolveAssembly, CommandExportASMT
from Preferences import PreferencesPage
# from Preferences import preferences
@@ -76,8 +75,16 @@ class AssemblyWorkbench(Workbench):
FreeCADGui.addPreferencePage(PreferencesPage, QT_TRANSLATE_NOOP("QObject", "Assembly"))
# build commands list
- cmdlist = ["Assembly_CreateAssembly", "Assembly_InsertLink"]
+ cmdlist = [
+ "Assembly_CreateAssembly",
+ "Assembly_InsertLink",
+ "Assembly_SolveAssembly",
+ "Assembly_ExportASMT",
+ ]
+
cmdListJoints = [
+ "Assembly_ToggleGrounded",
+ "Separator",
"Assembly_CreateJointFixed",
"Assembly_CreateJointRevolute",
"Assembly_CreateJointCylindrical",
diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py
index 92aebfb10c..b4eaffd432 100644
--- a/src/Mod/Assembly/JointObject.py
+++ b/src/Mod/Assembly/JointObject.py
@@ -26,6 +26,7 @@ import math
import FreeCAD as App
import Part
+from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
@@ -54,8 +55,9 @@ JointTypes = [
class Joint:
def __init__(self, joint, type_index):
+ self.Type = "Joint"
+
joint.Proxy = self
- self.joint = joint
joint.addProperty(
"App::PropertyEnumeration",
@@ -130,7 +132,18 @@ class Joint:
),
)
- self.setJointConnectors([])
+ self.setJointConnectors(joint, [])
+
+ def __getstate__(self):
+ return self.Type
+
+ def __setstate__(self, state):
+ if state:
+ self.Type = state
+
+ def setJointType(self, joint, jointType):
+ joint.JointType = jointType
+ joint.Label = jointType.replace(" ", "")
def onChanged(self, fp, prop):
"""Do something when a property has changed"""
@@ -142,34 +155,34 @@ class Joint:
# App.Console.PrintMessage("Recompute Python Box feature\n")
pass
- def setJointConnectors(self, current_selection):
+ def setJointConnectors(self, joint, current_selection):
# current selection is a vector of strings like "Assembly.Assembly1.Assembly2.Body.Pad.Edge16" including both what selection return as obj_name and obj_sub
if len(current_selection) >= 1:
- self.joint.Object1 = current_selection[0]["object"]
- self.joint.Element1 = current_selection[0]["element_name"]
- self.joint.Vertex1 = current_selection[0]["vertex_name"]
- self.joint.Placement1 = self.findPlacement(
- self.joint.Object1, self.joint.Element1, self.joint.Vertex1
- )
+ joint.Object1 = current_selection[0]["object"]
+ joint.Element1 = current_selection[0]["element_name"]
+ joint.Vertex1 = current_selection[0]["vertex_name"]
+ joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
else:
- self.joint.Object1 = None
- self.joint.Element1 = ""
- self.joint.Vertex1 = ""
- self.joint.Placement1 = UtilsAssembly.activeAssembly().Placement
+ joint.Object1 = None
+ joint.Element1 = ""
+ joint.Vertex1 = ""
+ joint.Placement1 = UtilsAssembly.activeAssembly().Placement
if len(current_selection) >= 2:
- self.joint.Object2 = current_selection[1]["object"]
- self.joint.Element2 = current_selection[1]["element_name"]
- self.joint.Vertex2 = current_selection[1]["vertex_name"]
- self.joint.Placement2 = self.findPlacement(
- self.joint.Object2, self.joint.Element2, self.joint.Vertex2
- )
+ joint.Object2 = current_selection[1]["object"]
+ joint.Element2 = current_selection[1]["element_name"]
+ joint.Vertex2 = current_selection[1]["vertex_name"]
+ joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
else:
- self.joint.Object2 = None
- self.joint.Element2 = ""
- self.joint.Vertex2 = ""
- self.joint.Placement2 = UtilsAssembly.activeAssembly().Placement
+ joint.Object2 = None
+ joint.Element2 = ""
+ joint.Vertex2 = ""
+ joint.Placement2 = UtilsAssembly.activeAssembly().Placement
+
+ def updateJCSPlacements(self, joint):
+ joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
+ joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
@@ -182,7 +195,11 @@ class Joint:
"""
def findPlacement(self, obj, elt, vtx):
- plc = App.Placement(obj.Placement)
+ plc = App.Placement()
+
+ if not obj or not elt or not vtx:
+ return App.Placement()
+
elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
@@ -234,12 +251,18 @@ class Joint:
if surface.TypeId == "Part::GeomPlane":
plc.Rotation = App.Rotation(surface.Rotation)
+ # Now plc is the placement in the doc. But we need the placement relative to the solid origin.
return plc
class ViewProviderJoint:
- def __init__(self, obj, app_obj):
+ def __init__(self, vobj):
"""Set this object to the proxy object of the actual view provider"""
+
+ vobj.Proxy = self
+
+ def attach(self, vobj):
+ """Setup the scene sub-graph of the view provider, this method is mandatory"""
self.axis_thickness = 3
view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
@@ -258,11 +281,8 @@ class ViewProviderJoint:
self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera)
self.cameraSensor.attach(camera.height)
- self.app_obj = app_obj
- obj.Proxy = self
+ self.app_obj = vobj.Object
- def attach(self, obj):
- """Setup the scene sub-graph of the view provider, this method is mandatory"""
self.transform1 = coin.SoTransform()
self.transform2 = coin.SoTransform()
self.transform3 = coin.SoTransform()
@@ -275,21 +295,21 @@ class ViewProviderJoint:
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = self.axis_thickness
- self.switch_JCS1 = self.JCS_sep(obj, self.transform1)
- self.switch_JCS2 = self.JCS_sep(obj, self.transform2)
- self.switch_JCS_preview = self.JCS_sep(obj, self.transform3)
+ 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.display_mode = coin.SoGroup()
self.display_mode.addChild(self.switch_JCS1)
self.display_mode.addChild(self.switch_JCS2)
self.display_mode.addChild(self.switch_JCS_preview)
- obj.addDisplayMode(self.display_mode, "Wireframe")
+ vobj.addDisplayMode(self.display_mode, "Wireframe")
def camera_callback(self, *args):
scaleF = self.get_JCS_size()
self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
- def JCS_sep(self, obj, soTransform):
+ def JCS_sep(self, soTransform):
pick = coin.SoPickStyle()
pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
@@ -424,21 +444,21 @@ class ViewProviderJoint:
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
def getIcon(self):
- if self.app_obj.getPropertyByName("JointType") == "Fixed":
+ if self.app_obj.JointType == "Fixed":
return ":/icons/Assembly_CreateJointFixed.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Revolute":
+ elif self.app_obj.JointType == "Revolute":
return ":/icons/Assembly_CreateJointRevolute.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Cylindrical":
+ elif self.app_obj.JointType == "Cylindrical":
return ":/icons/Assembly_CreateJointCylindrical.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Slider":
+ elif self.app_obj.JointType == "Slider":
return ":/icons/Assembly_CreateJointSlider.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Ball":
+ elif self.app_obj.JointType == "Ball":
return ":/icons/Assembly_CreateJointBall.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Planar":
+ elif self.app_obj.JointType == "Planar":
return ":/icons/Assembly_CreateJointPlanar.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Parallel":
+ elif self.app_obj.JointType == "Parallel":
return ":/icons/Assembly_CreateJointParallel.svg"
- elif self.app_obj.getPropertyByName("JointType") == "Tangent":
+ elif self.app_obj.JointType == "Tangent":
return ":/icons/Assembly_CreateJointTangent.svg"
return ":/icons/Assembly_CreateJoint.svg"
@@ -453,3 +473,374 @@ class ViewProviderJoint:
"""When restoring the serialized object from document we have the chance to set some internals here.\
Since no data were serialized nothing needs to be done here."""
return None
+
+ def doubleClicked(self, vobj):
+ panel = TaskAssemblyCreateJoint(0, vobj.Object)
+ Gui.Control.showDialog(panel)
+
+
+################ Grounded Joint object #################
+
+
+class GroundedJoint:
+ def __init__(self, joint, obj_to_ground):
+ self.Type = "GoundedJoint"
+ joint.Proxy = self
+ self.joint = joint
+
+ joint.addProperty(
+ "App::PropertyLink",
+ "ObjectToGround",
+ "Ground",
+ QT_TRANSLATE_NOOP("App::Property", "The object to ground"),
+ )
+
+ joint.ObjectToGround = obj_to_ground
+
+ joint.addProperty(
+ "App::PropertyPlacement",
+ "Placement",
+ "Ground",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "This is where the part is grounded.",
+ ),
+ )
+
+ joint.Placement = obj_to_ground.Placement
+
+ def __getstate__(self):
+ return self.Type
+
+ def __setstate__(self, state):
+ if state:
+ self.Type = state
+
+ def onChanged(self, fp, prop):
+ """Do something when a property has changed"""
+ # App.Console.PrintMessage("Change property: " + str(prop) + "\n")
+ pass
+
+ def execute(self, fp):
+ """Do something when doing a recomputation, this method is mandatory"""
+ # App.Console.PrintMessage("Recompute Python Box feature\n")
+ pass
+
+
+class ViewProviderGroundedJoint:
+ def __init__(self, obj):
+ """Set this object to the proxy object of the actual view provider"""
+ obj.Proxy = self
+
+ def attach(self, obj):
+ """Setup the scene sub-graph of the view provider, this method is mandatory"""
+ pass
+
+ def updateData(self, fp, prop):
+ """If a property of the handled feature has changed we have the chance to handle this here"""
+ # fp is the handled feature, prop is the name of the property that has changed
+ pass
+
+ def getDisplayModes(self, obj):
+ """Return a list of display modes."""
+ modes = ["Wireframe"]
+ return modes
+
+ def getDefaultDisplayMode(self):
+ """Return the name of the default display mode. It must be defined in getDisplayModes."""
+ return "Wireframe"
+
+ def onChanged(self, vp, prop):
+ """Here we can do something when a single property got changed"""
+ # App.Console.PrintMessage("Change property: " + str(prop) + "\n")
+ pass
+
+ def getIcon(self):
+ return ":/icons/Assembly_ToggleGrounded.svg"
+
+
+class MakeJointSelGate:
+ def __init__(self, taskbox, assembly):
+ self.taskbox = taskbox
+ self.assembly = assembly
+
+ def allow(self, doc, obj, sub):
+ if not sub:
+ return False
+
+ objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
+
+ if self.assembly.Name not in objs_names or element_name == "":
+ # Only objects within the assembly. And not whole objects, only elements.
+ return False
+
+ if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
+ # If it's to deselect then it's ok
+ return True
+
+ if len(self.taskbox.current_selection) >= 2:
+ # No more than 2 elements can be selected for basic joints.
+ return False
+
+ full_obj_name = ".".join(objs_names)
+ full_element_name = full_obj_name + "." + element_name
+ selected_object = UtilsAssembly.getObject(full_element_name)
+
+ for selection_dict in self.taskbox.current_selection:
+ if selection_dict["object"] == selected_object:
+ # Can't join a solid to itself. So the user need to select 2 different parts.
+ return False
+
+ return True
+
+
+class TaskAssemblyCreateJoint(QtCore.QObject):
+ def __init__(self, jointTypeIndex, jointObj=None):
+ super().__init__()
+
+ self.assembly = UtilsAssembly.activeAssembly()
+ self.view = Gui.activeDocument().activeView()
+ self.doc = App.ActiveDocument
+
+ if not self.assembly or not self.view or not self.doc:
+ return
+
+ self.assembly.ViewObject.EnableMovement = False
+
+ self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
+
+ self.form.jointType.addItems(JointTypes)
+ self.form.jointType.setCurrentIndex(jointTypeIndex)
+ self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged)
+
+ Gui.Selection.clearSelection()
+
+ if jointObj:
+ self.joint = jointObj
+ self.jointName = jointObj.Label
+ App.setActiveTransaction("Edit " + self.jointName + " Joint")
+
+ self.updateTaskboxFromJoint()
+
+ else:
+ self.jointName = self.form.jointType.currentText().replace(" ", "")
+ App.setActiveTransaction("Create " + self.jointName + " Joint")
+
+ self.current_selection = []
+ self.preselection_dict = None
+
+ self.createJointObject()
+
+ Gui.Selection.addSelectionGate(
+ MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
+ )
+ Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
+ Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
+
+ self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
+ self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
+
+ def accept(self):
+ if len(self.current_selection) != 2:
+ App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
+ return False
+
+ # Hide JSC's when joint is created and enable selection highlighting
+ # self.joint.ViewObject.Visibility = False
+ # self.joint.ViewObject.OnTopWhenSelected = "Enabled"
+
+ self.deactivate()
+
+ self.assembly.solve()
+
+ App.closeActiveTransaction()
+ return True
+
+ def reject(self):
+ self.deactivate()
+ App.closeActiveTransaction(True)
+ return True
+
+ def deactivate(self):
+ self.assembly.ViewObject.EnableMovement = True
+ Gui.Selection.removeSelectionGate()
+ Gui.Selection.removeObserver(self)
+ Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection)
+ Gui.Selection.clearSelection()
+ self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
+ self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
+ if Gui.Control.activeDialog():
+ Gui.Control.closeDialog()
+
+ def createJointObject(self):
+ type_index = self.form.jointType.currentIndex()
+
+ joint_group = UtilsAssembly.getJointGroup(self.assembly)
+
+ self.joint = joint_group.newObject("App::FeaturePython", self.jointName)
+ Joint(self.joint, type_index)
+ ViewProviderJoint(self.joint.ViewObject)
+
+ def onJointTypeChanged(self, index):
+ self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText())
+
+ def updateTaskboxFromJoint(self):
+ self.current_selection = []
+ self.preselection_dict = None
+
+ selection_dict1 = {
+ "object": self.joint.Object1,
+ "element_name": self.joint.Element1,
+ "vertex_name": self.joint.Vertex1,
+ }
+
+ selection_dict2 = {
+ "object": self.joint.Object2,
+ "element_name": self.joint.Element2,
+ "vertex_name": self.joint.Vertex2,
+ }
+
+ self.current_selection.append(selection_dict1)
+ self.current_selection.append(selection_dict2)
+
+ elName = self.getObjSubNameFromObj(self.joint.Object1, self.joint.Element1)
+ """print(
+ f"Gui.Selection.addSelection('{self.doc.Name}', '{self.joint.Object1.Name}', '{elName}')"
+ )"""
+ Gui.Selection.addSelection(self.doc.Name, self.joint.Object1.Name, elName)
+
+ elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2)
+ Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName)
+
+ self.updateJointList()
+
+ def getObjSubNameFromObj(self, obj, elName):
+ if obj.TypeId == "PartDesign::Body":
+ return obj.Tip.Name + "." + elName
+ elif obj.TypeId == "App::Link":
+ linked_obj = obj.getLinkedObject()
+ if linked_obj.TypeId == "PartDesign::Body":
+ return linked_obj.Tip.Name + "." + elName
+ else:
+ return elName
+ else:
+ return elName
+
+ def updateJoint(self):
+ # First we build the listwidget
+ self.updateJointList()
+
+ # Then we pass the new list to the join object
+ self.joint.Proxy.setJointConnectors(self.joint, self.current_selection)
+
+ def updateJointList(self):
+ self.form.featureList.clear()
+ simplified_names = []
+ for sel in self.current_selection:
+ # TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12
+ sname = sel["object"].Label + "." + sel["element_name"]
+ simplified_names.append(sname)
+ self.form.featureList.addItems(simplified_names)
+
+ def moveMouse(self, info):
+ if len(self.current_selection) >= 2 or (
+ len(self.current_selection) == 1
+ and self.current_selection[0]["object"] == self.preselection_dict["object"]
+ ):
+ self.joint.ViewObject.Proxy.showPreviewJCS(False)
+ return
+
+ cursor_pos = self.view.getCursorPos()
+ cursor_info = self.view.getObjectInfo(cursor_pos)
+ # cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': , 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'}
+
+ if (
+ not cursor_info
+ or not self.preselection_dict
+ or cursor_info["SubName"] != self.preselection_dict["sub_name"]
+ ):
+ self.joint.ViewObject.Proxy.showPreviewJCS(False)
+ return
+
+ # newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane
+
+ newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
+ self.preselection_dict["mouse_pos"] = newPos
+
+ self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
+ self.preselection_dict
+ )
+
+ placement = self.joint.Proxy.findPlacement(
+ self.preselection_dict["object"],
+ self.preselection_dict["element_name"],
+ self.preselection_dict["vertex_name"],
+ )
+ self.joint.ViewObject.Proxy.showPreviewJCS(True, placement)
+ self.previewJCSVisible = True
+
+ # 3D view keyboard handler
+ def KeyboardEvent(self, info):
+ if info["State"] == "UP" and info["Key"] == "ESCAPE":
+ self.reject()
+
+ if info["State"] == "UP" and info["Key"] == "RETURN":
+ self.accept()
+
+ # selectionObserver stuff
+ def addSelection(self, doc_name, obj_name, sub_name, mousePos):
+ full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
+ full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
+ selected_object = UtilsAssembly.getObject(full_element_name)
+ element_name = UtilsAssembly.getElementName(full_element_name)
+
+ selection_dict = {
+ "object": selected_object,
+ "element_name": element_name,
+ "full_element_name": full_element_name,
+ "full_obj_name": full_obj_name,
+ "mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]),
+ }
+ selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
+
+ self.current_selection.append(selection_dict)
+ self.updateJoint()
+
+ def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None):
+ full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
+ selected_object = UtilsAssembly.getObject(full_element_name)
+ element_name = UtilsAssembly.getElementName(full_element_name)
+
+ # Find and remove the corresponding dictionary from the combined list
+ selection_dict_to_remove = None
+ for selection_dict in self.current_selection:
+ if selection_dict["object"] == selected_object:
+ selection_dict_to_remove = selection_dict
+ break
+
+ if selection_dict_to_remove is not None:
+ self.current_selection.remove(selection_dict_to_remove)
+
+ self.updateJoint()
+
+ def setPreselection(self, doc_name, obj_name, sub_name):
+ if not sub_name:
+ self.preselection_dict = None
+ return
+
+ full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
+ full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
+ selected_object = UtilsAssembly.getObject(full_element_name)
+ element_name = UtilsAssembly.getElementName(full_element_name)
+
+ self.preselection_dict = {
+ "object": selected_object,
+ "sub_name": sub_name,
+ "element_name": element_name,
+ "full_element_name": full_element_name,
+ "full_obj_name": full_obj_name,
+ }
+
+ def clearSelection(self, doc_name):
+ self.current_selection.clear()
+ self.updateJoint()
diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py
index c8bc90359e..3feed5390b 100644
--- a/src/Mod/Assembly/UtilsAssembly.py
+++ b/src/Mod/Assembly/UtilsAssembly.py
@@ -244,3 +244,11 @@ def color_from_unsigned(c):
float(int((c >> 16) & 0xFF) / 255),
float(int((c >> 8) & 0xFF) / 255),
]
+
+
+def getJointGroup(assembly):
+ joint_group = assembly.getObject("Joints")
+
+ if not joint_group:
+ joint_group = assembly.newObject("Assembly::JointGroup", "Joints")
+ return joint_group