From d77cd7acf5f7431d26e74a252b922bea770bbc8f Mon Sep 17 00:00:00 2001 From: Paddle Date: Wed, 20 Sep 2023 18:45:47 +0200 Subject: [PATCH] Assembly: Introduce core functionality of assembly workbench. --- .../SetupLibOndselSolver.cmake | 4 + src/Mod/Assembly/App/AppAssembly.cpp | 63 ++ src/Mod/Assembly/App/AppAssemblyPy.cpp | 47 + src/Mod/Assembly/App/AssemblyObject.cpp | 516 ++++++++++ src/Mod/Assembly/App/AssemblyObject.h | 119 +++ src/Mod/Assembly/App/AssemblyObjectPy.xml | 50 + src/Mod/Assembly/App/AssemblyObjectPyImp.cpp | 75 ++ src/Mod/Assembly/App/CMakeLists.txt | 59 ++ src/Mod/Assembly/App/JointGroup.cpp | 55 + src/Mod/Assembly/App/JointGroup.h | 58 ++ src/Mod/Assembly/App/JointGroupPy.xml | 19 + src/Mod/Assembly/App/JointGroupPyImp.cpp | 46 + src/Mod/Assembly/App/PreCompiled.cpp | 25 + src/Mod/Assembly/App/PreCompiled.h | 46 + src/Mod/Assembly/Assembly/__init__.py | 1 + src/Mod/Assembly/CMakeLists.txt | 2 + src/Mod/Assembly/CommandCreateAssembly.py | 4 +- src/Mod/Assembly/CommandCreateJoint.py | 385 ++----- src/Mod/Assembly/CommandExportASMT.py | 82 ++ src/Mod/Assembly/CommandInsertLink.py | 1 + src/Mod/Assembly/CommandSolveAssembly.py | 76 ++ src/Mod/Assembly/Gui/AppAssemblyGui.cpp | 53 + src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp | 46 + src/Mod/Assembly/Gui/CMakeLists.txt | 25 + src/Mod/Assembly/Gui/PreCompiled.cpp | 25 + src/Mod/Assembly/Gui/PreCompiled.h | 43 + src/Mod/Assembly/Gui/Resources/Assembly.qrc | 2 + .../icons/Assembly_CreateJointFixed.svg | 124 ++- .../Resources/icons/Assembly_ExportASMT.svg | 944 ++++++++++++++++++ .../icons/Assembly_ToggleGrounded.svg | 441 ++++++++ src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 398 ++++++++ src/Mod/Assembly/Gui/ViewProviderAssembly.h | 106 ++ .../Assembly/Gui/ViewProviderAssemblyPy.xml | 23 + .../Gui/ViewProviderAssemblyPyImp.cpp | 59 ++ .../Assembly/Gui/ViewProviderJointGroup.cpp | 49 + src/Mod/Assembly/Gui/ViewProviderJointGroup.h | 53 + src/Mod/Assembly/InitGui.py | 13 +- src/Mod/Assembly/JointObject.py | 475 ++++++++- src/Mod/Assembly/UtilsAssembly.py | 8 + 39 files changed, 4229 insertions(+), 391 deletions(-) create mode 100644 cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake create mode 100644 src/Mod/Assembly/App/AppAssembly.cpp create mode 100644 src/Mod/Assembly/App/AppAssemblyPy.cpp create mode 100644 src/Mod/Assembly/App/AssemblyObject.cpp create mode 100644 src/Mod/Assembly/App/AssemblyObject.h create mode 100644 src/Mod/Assembly/App/AssemblyObjectPy.xml create mode 100644 src/Mod/Assembly/App/AssemblyObjectPyImp.cpp create mode 100644 src/Mod/Assembly/App/JointGroup.cpp create mode 100644 src/Mod/Assembly/App/JointGroup.h create mode 100644 src/Mod/Assembly/App/JointGroupPy.xml create mode 100644 src/Mod/Assembly/App/JointGroupPyImp.cpp create mode 100644 src/Mod/Assembly/App/PreCompiled.cpp create mode 100644 src/Mod/Assembly/App/PreCompiled.h create mode 100644 src/Mod/Assembly/CommandExportASMT.py create mode 100644 src/Mod/Assembly/CommandSolveAssembly.py create mode 100644 src/Mod/Assembly/Gui/AppAssemblyGui.cpp create mode 100644 src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp create mode 100644 src/Mod/Assembly/Gui/PreCompiled.cpp create mode 100644 src/Mod/Assembly/Gui/PreCompiled.h create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg create mode 100644 src/Mod/Assembly/Gui/ViewProviderAssembly.cpp create mode 100644 src/Mod/Assembly/Gui/ViewProviderAssembly.h create mode 100644 src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml create mode 100644 src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp create mode 100644 src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp create mode 100644 src/Mod/Assembly/Gui/ViewProviderJointGroup.h 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 +#include +#include +#include +#include + +#endif // _PreComp_ +#endif // ASSEMBLY_PRECOMPILED_H diff --git a/src/Mod/Assembly/Assembly/__init__.py b/src/Mod/Assembly/Assembly/__init__.py index e69de29bb2..efdb3c261e 100644 --- a/src/Mod/Assembly/Assembly/__init__.py +++ b/src/Mod/Assembly/Assembly/__init__.py @@ -0,0 +1 @@ +import AssemblyApp diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt index a6a9262fd0..713b231aeb 100644 --- a/src/Mod/Assembly/CMakeLists.txt +++ b/src/Mod/Assembly/CMakeLists.txt @@ -8,7 +8,9 @@ set(Assembly_Scripts Init.py CommandCreateAssembly.py CommandInsertLink.py + CommandSolveAssembly.py CommandCreateJoint.py + CommandExportASMT.py TestAssemblyWorkbench.py JointObject.py Preferences.py diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index 980199d43a..ac8d7d3e35 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -56,10 +56,10 @@ class CommandCreateAssembly: def Activated(self): App.setActiveTransaction("Create assembly") - assembly = App.ActiveDocument.addObject("App::Part", "Assembly") + assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") assembly.Type = "Assembly" Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) - assembly.newObject("App::DocumentObjectGroup", "Joints") + assembly.newObject("Assembly::JointGroup", "Joints") App.closeActiveTransaction() diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index cdac1611cb..aa649448cf 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -31,6 +31,7 @@ if App.GuiUp: from PySide import QtCore, QtGui, QtWidgets import JointObject +from JointObject import TaskAssemblyCreateJoint import UtilsAssembly import Assembly_rc @@ -51,10 +52,12 @@ class CommandCreateJointFixed: "Pixmap": "Assembly_CreateJointFixed", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"), "Accel": "F", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", - "

Create a Fixed Joint: Permanently locks two parts together, preventing any movement or rotation.

", - ), + "Create a Fixed Joint: Permanently locks two parts together, preventing any movement or rotation.", + ) + + "

", "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": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointRevolute", - "

Create a Revolute Joint: Allows rotation around a single axis between selected parts.

", - ), + "Create a Revolute Joint: Allows rotation around a single axis between selected parts.", + ) + + "

", "CmdType": "ForEdit", } @@ -92,13 +92,8 @@ class CommandCreateJointRevolute: 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, 1) - Gui.Control.showDialog(self.panel) + panel = TaskAssemblyCreateJoint(1) + Gui.Control.showDialog(panel) class CommandCreateJointCylindrical: @@ -113,10 +108,12 @@ class CommandCreateJointCylindrical: "Assembly_CreateJointCylindrical", "Create Cylindrical Joint" ), "Accel": "C", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointCylindrical", - "

Create a Cylindrical Joint: Enables rotation along one axis while permitting movement along the same axis between assembled parts.

", - ), + "Create a Cylindrical Joint: Enables rotation along one axis while permitting movement along the same axis between assembled parts.", + ) + + "

", "CmdType": "ForEdit", } @@ -124,13 +121,8 @@ class CommandCreateJointCylindrical: 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, 2) - Gui.Control.showDialog(self.panel) + panel = TaskAssemblyCreateJoint(2) + Gui.Control.showDialog(panel) class CommandCreateJointSlider: @@ -143,10 +135,12 @@ class CommandCreateJointSlider: "Pixmap": "Assembly_CreateJointSlider", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointSlider", "Create Slider Joint"), "Accel": "S", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointSlider", - "

Create a Slider Joint: Allows linear movement along a single axis but restricts rotation between selected parts.

", - ), + "Create a Slider Joint: Allows linear movement along a single axis but restricts rotation between selected parts.", + ) + + "

", "CmdType": "ForEdit", } @@ -154,13 +148,8 @@ class CommandCreateJointSlider: 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, 3) - Gui.Control.showDialog(self.panel) + panel = TaskAssemblyCreateJoint(3) + Gui.Control.showDialog(panel) class CommandCreateJointBall: @@ -173,10 +162,12 @@ class CommandCreateJointBall: "Pixmap": "Assembly_CreateJointBall", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointBall", "Create Ball Joint"), "Accel": "B", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointBall", - "

Create a Ball Joint: Connects parts at a point, allowing unrestricted movement as long as the connection points remain in contact.

", - ), + "Create a Ball Joint: Connects parts at a point, allowing unrestricted movement as long as the connection points remain in contact.", + ) + + "

", "CmdType": "ForEdit", } @@ -184,13 +175,8 @@ class CommandCreateJointBall: 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, 4) - Gui.Control.showDialog(self.panel) + panel = TaskAssemblyCreateJoint(4) + Gui.Control.showDialog(panel) class CommandCreateJointPlanar: @@ -203,10 +189,12 @@ class CommandCreateJointPlanar: "Pixmap": "Assembly_CreateJointPlanar", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointPlanar", "Create Planar Joint"), "Accel": "P", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointPlanar", - "

Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.

", - ), + "Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.", + ) + + "

", "CmdType": "ForEdit", } @@ -214,13 +202,8 @@ class CommandCreateJointPlanar: 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, 5) - Gui.Control.showDialog(self.panel) + panel = TaskAssemblyCreateJoint(5) + Gui.Control.showDialog(panel) class CommandCreateJointParallel: @@ -233,10 +216,12 @@ class CommandCreateJointParallel: "Pixmap": "Assembly_CreateJointParallel", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointParallel", "Create Parallel Joint"), "Accel": "L", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointParallel", - "

Create a Parallel Joint: Aligns two features to be parallel, constraining relative movement to parallel translations.

", - ), + "Create a Parallel Joint: Aligns two features to be parallel, constraining relative movement to parallel translations.", + ) + + "

", "CmdType": "ForEdit", } @@ -244,13 +229,8 @@ class CommandCreateJointParallel: 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, 6) - Gui.Control.showDialog(self.panel) + panel = TaskAssemblyCreateJoint(6) + Gui.Control.showDialog(panel) class CommandCreateJointTangent: @@ -263,10 +243,39 @@ class CommandCreateJointTangent: "Pixmap": "Assembly_CreateJointTangent", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointTangent", "Create Tangent Joint"), "Accel": "T", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointTangent", - "

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.svg icons/preferences-assembly.svg + icons/Assembly_ToggleGrounded.svg icons/Assembly_CreateJointBall.svg icons/Assembly_CreateJointCylindrical.svg icons/Assembly_CreateJointFixed.svg @@ -10,6 +11,7 @@ icons/Assembly_CreateJointRevolute.svg icons/Assembly_CreateJointSlider.svg icons/Assembly_CreateJointTangent.svg + icons/Assembly_ExportASMT.svg panels/TaskAssemblyCreateJoint.ui panels/TaskAssemblyInsertLink.ui preferences/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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + 2005-10-15 + + + Andreas Nilsson + + + + + edit + copy + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [wmayer] + + + Part_Cylinder + 2011-10-10 + http://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Cylinder.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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