From 13d4cb128a4d22b33a842a208fb63c5294515f58 Mon Sep 17 00:00:00 2001 From: Paddle Date: Fri, 8 Dec 2023 21:54:11 +0100 Subject: [PATCH 01/22] Update Ondsel Solver --- src/3rdParty/OndselSolver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/3rdParty/OndselSolver b/src/3rdParty/OndselSolver index 3952f45945..fe99ad2593 160000 --- a/src/3rdParty/OndselSolver +++ b/src/3rdParty/OndselSolver @@ -1 +1 @@ -Subproject commit 3952f459457e339a0987ad2b271f3823b67323e7 +Subproject commit fe99ad259391b8fd9390f919926aa3c8b6cde787 From d77cd7acf5f7431d26e74a252b922bea770bbc8f Mon Sep 17 00:00:00 2001 From: Paddle Date: Wed, 20 Sep 2023 18:45:47 +0200 Subject: [PATCH 02/22] 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 From 323deff46a2281297a40148d5a11f434bcb72702 Mon Sep 17 00:00:00 2001 From: Paddle Date: Tue, 14 Nov 2023 18:39:09 +0100 Subject: [PATCH 03/22] Assembly: Replace Tangent+Parallel+Planar by 'Distance'. --- src/Gui/CommandDoc.cpp | 2 +- .../DlgSettingsWorkbenchesImp.cpp | 2 +- src/Gui/ViewProviderDocumentObject.h | 2 + src/Mod/AddonManager/Addon.py | 4 +- src/Mod/Assembly/App/AssemblyObject.cpp | 991 +++++++++++++++--- src/Mod/Assembly/App/AssemblyObject.h | 65 +- src/Mod/Assembly/App/AssemblyObjectPy.xml | 43 +- src/Mod/Assembly/App/AssemblyObjectPyImp.cpp | 47 +- src/Mod/Assembly/App/PreCompiled.h | 5 + src/Mod/Assembly/CMakeLists.txt | 6 + src/Mod/Assembly/CommandCreateAssembly.py | 2 +- src/Mod/Assembly/CommandCreateJoint.py | 143 +-- src/Mod/Assembly/CommandExportASMT.py | 2 +- src/Mod/Assembly/CommandInsertLink.py | 197 +++- src/Mod/Assembly/CommandSolveAssembly.py | 4 +- src/Mod/Assembly/Gui/PreCompiled.h | 4 + src/Mod/Assembly/Gui/Resources/Assembly.qrc | 5 +- ...l.svg => Assembly_CreateJointDistance.svg} | 0 .../icons/Assembly_SolveAssembly.svg | 615 +++++++++++ .../panels/TaskAssemblyCreateJoint.ui | 99 ++ .../panels/TaskAssemblyInsertLink.ui | 26 + src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 102 +- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 11 + .../Assembly/Gui/ViewProviderJointGroup.cpp | 6 + src/Mod/Assembly/Gui/ViewProviderJointGroup.h | 16 + src/Mod/Assembly/InitGui.py | 4 +- src/Mod/Assembly/JointObject.py | 410 +++++++- src/Mod/Assembly/UtilsAssembly.py | 302 +++++- 28 files changed, 2716 insertions(+), 399 deletions(-) rename src/Mod/Assembly/Gui/Resources/icons/{Assembly_CreateJointParallel.svg => Assembly_CreateJointDistance.svg} (100%) create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg diff --git a/src/Gui/CommandDoc.cpp b/src/Gui/CommandDoc.cpp index 55d4b91d64..d8bf79af53 100644 --- a/src/Gui/CommandDoc.cpp +++ b/src/Gui/CommandDoc.cpp @@ -1341,7 +1341,7 @@ void StdCmdDelete::activated(int iMsg) ViewProviderDocumentObject *vpedit = nullptr; if(editDoc) vpedit = dynamic_cast(editDoc->getInEdit()); - if(vpedit) { + if(vpedit && !vpedit->acceptDeletionsInEdit()) { for(auto &sel : Selection().getSelectionEx(editDoc->getDocument()->getName())) { if(sel.getObject() == vpedit->getObject()) { if (!sel.getSubNames().empty()) { diff --git a/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp b/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp index 5cd157d509..c621a26cd9 100644 --- a/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp +++ b/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp @@ -465,7 +465,7 @@ QStringList DlgSettingsWorkbenchesImp::getDisabledWorkbenches() ParameterGrp::handle hGrp; hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Workbenches"); - disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench")); + disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench")); #if QT_VERSION >= QT_VERSION_CHECK(5,15,0) unfiltered_disabled_wbs_list = disabled_wbs.split(QLatin1String(","), Qt::SkipEmptyParts); #else diff --git a/src/Gui/ViewProviderDocumentObject.h b/src/Gui/ViewProviderDocumentObject.h index 14b8cd38c6..2490e6ed42 100644 --- a/src/Gui/ViewProviderDocumentObject.h +++ b/src/Gui/ViewProviderDocumentObject.h @@ -94,6 +94,8 @@ public: App::DocumentObject *getObject() const {return pcObject;} /// Asks the view provider if the given object can be deleted. bool canDelete(App::DocumentObject* obj) const override; + /// Ask the view provider if it accepts object deletions while in edit + virtual bool acceptDeletionsInEdit() { return false; } /// Get the GUI document to this ViewProvider object Gui::Document* getDocument() const; /// Get the python wrapper for that ViewProvider diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py index 5289f2752b..f6aa56ea62 100644 --- a/src/Mod/AddonManager/Addon.py +++ b/src/Mod/AddonManager/Addon.py @@ -621,7 +621,7 @@ class Addon: wbName = self.get_workbench_name() # Add the wb to the list of disabled if it was not already - disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench") + disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench") # print(f"start disabling {disabled_wbs}") disabled_wbs_list = disabled_wbs.split(",") if not (wbName in disabled_wbs_list): @@ -652,7 +652,7 @@ class Addon: def remove_from_disabled_wbs(self, wbName: str): pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches") - disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench") + disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench") # print(f"start enabling : {disabled_wbs}") disabled_wbs_list = disabled_wbs.split(",") disabled_wbs = "" diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 18d56fe947..490987e085 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -23,6 +23,10 @@ #include "PreCompiled.h" #ifndef _PreComp_ +#include +#include +#include +#include #include #include #include @@ -39,6 +43,9 @@ #include #include +#include +#include + #include #include #include @@ -51,6 +58,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include @@ -58,11 +71,11 @@ #include "AssemblyObjectPy.h" #include "JointGroup.h" -using namespace App; +namespace PartApp = Part; + using namespace Assembly; using namespace MbD; - PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) AssemblyObject::AssemblyObject() @@ -84,22 +97,22 @@ std::vector AssemblyObject::getJoints() { std::vector joints = {}; - App::Document* doc = getDocument(); - - std::vector jointGroups = - doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId()); + JointGroup* jointGroup = getJointGroup(); + if (!jointGroup) { + return {}; + } 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); - } + for (auto obj : jointGroup->getObjects()) { + if (!obj) { + continue; + } + + auto proxy = dynamic_cast(obj->getPropertyByName("Proxy")); + if (proxy) { + Py::Object joint = proxy->getValue(); + if (joint.hasAttr("setJointConnectors")) { + joints.push_back(obj); } } } @@ -110,29 +123,163 @@ std::vector AssemblyObject::getJoints() return joints; } -bool AssemblyObject::fixGroundedParts() +std::vector AssemblyObject::getGroundedJoints() { - App::Document* doc = getDocument(); - App::DocumentObject* jointsGroup = doc->getObject("Joints"); + std::vector joints = {}; - bool onePartFixed = false; + JointGroup* jointGroup = getJointGroup(); + if (!jointGroup) { + return {}; + } 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(); + for (auto obj : jointGroup->getObjects()) { + if (!obj) { + continue; + } - Base::Placement plc = getPlacementFromProp(obj, "Placement"); - std::string str = obj->getFullName(); - fixGroundedPart(objToGround, plc, str); - onePartFixed = true; - } + auto* propObj = dynamic_cast(obj->getPropertyByName("ObjectToGround")); + + if (propObj) { + joints.push_back(obj); } } - return onePartFixed; + + return joints; +} + +void AssemblyObject::removeUnconnectedJoints(std::vector& joints, + std::vector groundedObjs) +{ + std::set connectedParts; + + // Initialize connectedParts with groundedObjs + for (auto* groundedObj : groundedObjs) { + connectedParts.insert(groundedObj); + } + + // Perform a traversal from each grounded object + for (auto* groundedObj : groundedObjs) { + traverseAndMarkConnectedParts(groundedObj, connectedParts, joints); + } + + // Filter out unconnected joints + joints.erase( + std::remove_if( + joints.begin(), + joints.end(), + [&connectedParts, this](App::DocumentObject* joint) { + App::DocumentObject* obj1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* obj2 = getLinkObjFromProp(joint, "Part2"); + if ((connectedParts.find(obj1) == connectedParts.end()) + || (connectedParts.find(obj2) == connectedParts.end())) { + Base::Console().Warning( + "%s is unconnected to a grounded part so it is ignored.\n", + joint->getFullName()); + return true; // Remove joint if any connected object is not in connectedParts + } + return false; + }), + joints.end()); +} + +void AssemblyObject::traverseAndMarkConnectedParts(App::DocumentObject* currentObj, + std::set& connectedParts, + const std::vector& joints) +{ + // getConnectedParts returns the objs connected to the currentObj by any joint + auto connectedObjs = getConnectedParts(currentObj, joints); + for (auto* nextObj : connectedObjs) { + if (connectedParts.find(nextObj) == connectedParts.end()) { + connectedParts.insert(nextObj); + traverseAndMarkConnectedParts(nextObj, connectedParts, joints); + } + } +} + +std::vector +AssemblyObject::getConnectedParts(App::DocumentObject* part, + const std::vector& joints) +{ + std::vector connectedParts; + for (auto joint : joints) { + App::DocumentObject* obj1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* obj2 = getLinkObjFromProp(joint, "Part2"); + if (obj1 == part) { + connectedParts.push_back(obj2); + } + else if (obj2 == part) { + connectedParts.push_back(obj1); + } + } + return connectedParts; +} + +bool AssemblyObject::isPartConnected(App::DocumentObject* obj) +{ + std::vector groundedObjs = fixGroundedParts(); + std::vector joints = getJoints(); + + std::set connectedParts; + + // Initialize connectedParts with groundedObjs + for (auto* groundedObj : groundedObjs) { + connectedParts.insert(groundedObj); + } + + // Perform a traversal from each grounded object + for (auto* groundedObj : groundedObjs) { + traverseAndMarkConnectedParts(groundedObj, connectedParts, joints); + } + + for (auto part : connectedParts) { + if (obj == part) { + return true; + } + } + + return false; +} + +JointGroup* AssemblyObject::getJointGroup() +{ + App::Document* doc = getDocument(); + + std::vector jointGroups = + doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId()); + if (jointGroups.empty()) { + return nullptr; + } + for (auto jointGroup : jointGroups) { + if (hasObject(jointGroup)) { + return dynamic_cast(jointGroup); + } + } + return nullptr; +} + +std::vector AssemblyObject::fixGroundedParts() +{ + std::vector groundedJoints = getGroundedJoints(); + + std::vector groundedObjs; + for (auto obj : groundedJoints) { + if (!obj) { + continue; + } + + 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); + groundedObjs.push_back(objToGround); + } + } + return groundedObjs; } void AssemblyObject::fixGroundedPart(App::DocumentObject* obj, @@ -146,7 +293,8 @@ void AssemblyObject::fixGroundedPart(App::DocumentObject* obj, std::shared_ptr mbdPart = getMbDPart(obj); std::string markerName2 = "FixingMarker"; - auto mbdMarker2 = makeMbdMarker(markerName2, plc); + Base::Placement basePlc = Base::Placement(); + auto mbdMarker2 = makeMbdMarker(markerName2, basePlc); mbdPart->addMarker(mbdMarker2); markerName1 = "/OndselAssembly/" + mbdMarker1->name; @@ -163,36 +311,38 @@ void AssemblyObject::fixGroundedPart(App::DocumentObject* obj, void AssemblyObject::jointParts(std::vector joints) { for (auto* joint : joints) { - std::shared_ptr mbdJoint = makeMbdJoint(joint); - mbdAssembly->addJoint(mbdJoint); + if (!joint) { + continue; + } + + std::vector> mbdJoints = makeMbdJoint(joint); + for (auto& mbdJoint : mbdJoints) { + mbdAssembly->addJoint(mbdJoint); + } } } -Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) +int AssemblyObject::solve(bool enableRedo) { - 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()) { + std::vector groundedObjs = fixGroundedParts(); + if (groundedObjs.empty()) { // If no part fixed we can't solve. return -6; } std::vector joints = getJoints(); + removeUnconnectedJoints(joints, groundedObjs); + jointParts(joints); + if (enableRedo) { + savePlacementsForUndo(); + } + try { mbdAssembly->solve(); } @@ -206,7 +356,7 @@ int AssemblyObject::solve() // 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. + // it apparently can't access the JointObject 'updateJCSPlacements' function. getJoints(); return 0; @@ -214,77 +364,375 @@ int AssemblyObject::solve() 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 AssemblyObject::makeMbdJointOfType(App::DocumentObject* joint, + JointType type) { - std::shared_ptr mbdJoint; - - if (jointType == JointType::Fixed) { - mbdJoint = CREATE::With(); + if (type == JointType::Fixed) { + return CREATE::With(); } - else if (jointType == JointType::Revolute) { - mbdJoint = CREATE::With(); + else if (type == JointType::Revolute) { + return CREATE::With(); } - else if (jointType == JointType::Cylindrical) { - mbdJoint = CREATE::With(); + else if (type == JointType::Cylindrical) { + return CREATE::With(); } - else if (jointType == JointType::Slider) { - mbdJoint = CREATE::With(); + else if (type == JointType::Slider) { + return CREATE::With(); } - else if (jointType == JointType::Ball) { - mbdJoint = CREATE::With(); + else if (type == JointType::Ball) { + return 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(); + else if (type == JointType::Distance) { + return makeMbdJointDistance(joint); } - return mbdJoint; + return nullptr; } -std::shared_ptr AssemblyObject::makeMbdJoint(App::DocumentObject* joint) +std::shared_ptr AssemblyObject::makeMbdJointDistance(App::DocumentObject* joint) { - JointType jointType = JointType::Fixed; + // Depending on the type of element of the JCS, we apply the correct set of constraints. + std::string type1 = getElementTypeFromProp(joint, "Element1"); + std::string type2 = getElementTypeFromProp(joint, "Element2"); - auto* prop = joint - ? dynamic_cast(joint->getPropertyByName("JointType")) - : nullptr; - if (prop) { - jointType = static_cast(prop->getValue()); + if (type1 == "Vertex" && type2 == "Vertex") { + // Point to point distance, or ball joint if distance=0. + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint); + return mbdJoint; + } + else if (type1 == "Edge" && type2 == "Edge") { + return makeMbdJointDistanceEdgeEdge(joint); + } + else if (type1 == "Face" && type2 == "Face") { + return makeMbdJointDistanceFaceFace(joint); + } + else if ((type1 == "Vertex" && type2 == "Face") || (type1 == "Face" && type2 == "Vertex")) { + if (type1 == "Vertex") { // Make sure face is the first. + swapJCS(joint); + } + return makeMbdJointDistanceFaceVertex(joint); + } + else if ((type1 == "Edge" && type2 == "Face") || (type1 == "Face" && type2 == "Edge")) { + if (type1 == "Edge") { // Make sure face is the first. + swapJCS(joint); + } + return makeMbdJointDistanceFaceEdge(joint); + } + else if ((type1 == "Vertex" && type2 == "Edge") || (type1 == "Edge" && type2 == "Vertex")) { + if (type1 == "Vertex") { // Make sure edge is the first. + swapJCS(joint); + } + return makeMbdJointDistanceEdgeVertex(joint); } - std::shared_ptr mbdJoint = makeMbdJointOfType(jointType); + return nullptr; +} - std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Object1", "Placement1"); - std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Object2", "Placement2"); +std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + + if (isEdgeType(obj1, elt1, GeomAbs_Line) || isEdgeType(obj2, elt2, GeomAbs_Line)) { + if (!isEdgeType(obj1, elt1, GeomAbs_Line)) { + swapJCS(joint); // make sure that line is first if not 2 lines. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isEdgeType(obj2, elt2, GeomAbs_Line)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint); + return mbdJoint; + } + else if (isEdgeType(obj2, elt2, GeomAbs_Circle)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint) + getEdgeRadius(obj2, elt2); + return mbdJoint; + } + // TODO : other cases Ellipse, parabola, hyperbola... + } + + else if (isEdgeType(obj1, elt1, GeomAbs_Circle) || isEdgeType(obj2, elt2, GeomAbs_Circle)) { + if (!isEdgeType(obj1, elt1, GeomAbs_Circle)) { + swapJCS(joint); // make sure that circle is first if not 2 lines. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isEdgeType(obj2, elt2, GeomAbs_Circle)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getEdgeRadius(obj1, elt1) + getEdgeRadius(obj2, elt2); + return mbdJoint; + } + // TODO : other cases Ellipse, parabola, hyperbola... + } + + // TODO : other cases Ellipse, parabola, hyperbola... + + return nullptr; +} + +std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceFace(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + + if (isFaceType(obj1, elt1, GeomAbs_Plane) || isFaceType(obj2, elt2, GeomAbs_Plane)) { + if (!isFaceType(obj1, elt1, GeomAbs_Plane)) { + swapJCS(joint); // make sure plane is first if its not 2 planes. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Plane)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Cylinder)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Cylinder) || isFaceType(obj2, elt2, GeomAbs_Cylinder)) { + if (!isFaceType(obj1, elt1, GeomAbs_Cylinder)) { + swapJCS(joint); // make sure cylinder is first if its not 2 cylinders. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Cylinder)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Cone) || isFaceType(obj2, elt2, GeomAbs_Cone)) { + if (!isFaceType(obj1, elt1, GeomAbs_Cone)) { + swapJCS(joint); // make sure cone is first if its not 2 cones. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + // TODO + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Torus) || isFaceType(obj2, elt2, GeomAbs_Torus)) { + if (!isFaceType(obj1, elt1, GeomAbs_Torus)) { + swapJCS(joint); // make sure torus is first if its not 2 torus. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Sphere) || isFaceType(obj2, elt2, GeomAbs_Sphere)) { + if (!isFaceType(obj1, elt1, GeomAbs_Sphere)) { + swapJCS(joint); // make sure sphere is first if its not 2 spheres. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + } + else { + // by default we make a planar joint. + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + + return nullptr; +} + +std::shared_ptr +AssemblyObject::makeMbdJointDistanceFaceVertex(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + + if (isFaceType(obj1, elt1, GeomAbs_Plane)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else if (isFaceType(obj1, elt1, GeomAbs_Cylinder)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint) + getFaceRadius(obj1, elt1); + return mbdJoint; + } + else if (isFaceType(obj1, elt1, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint) + getFaceRadius(obj1, elt1); + return mbdJoint; + } + /*else if (isFaceType(obj1, elt1, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj1, elt1, GeomAbs_Thorus)) { + // TODO + }*/ + + return nullptr; +} + +std::shared_ptr +AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + + if (isEdgeType(obj1, elt1, GeomAbs_Line)) { // Point on line joint. + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint); + return mbdJoint; + } + else { + // For other curves we do a point in plane-of-the-curve. + // Maybe it would be best tangent / distance to the conic? + // For arcs and circles we could use ASMTRevSphJoint. But is it better than pointInPlane? + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + + return nullptr; +} + +std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceEdge(App::DocumentObject* joint) +{ + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + + if (isEdgeType(obj2, elt2, GeomAbs_Line)) { + // Make line in plane joint. + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else { + // planar joint for other edges. + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + + return nullptr; +} + + +std::vector> +AssemblyObject::makeMbdJoint(App::DocumentObject* joint) +{ + JointType jointType = getJointType(joint); + + std::shared_ptr mbdJoint = makeMbdJointOfType(joint, jointType); + if (!mbdJoint) { + return {}; + } + + std::string fullMarkerName1 = handleOneSideOfJoint(joint, jointType, "Part1", "Placement1"); + std::string fullMarkerName2 = handleOneSideOfJoint(joint, jointType, "Part2", "Placement2"); mbdJoint->setMarkerI(fullMarkerName1); mbdJoint->setMarkerJ(fullMarkerName2); - return mbdJoint; + return {mbdJoint}; +} +std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, + JointType jointType, + const char* propLinkName, + const char* propPlcName) +{ + App::DocumentObject* obj = getLinkObjFromProp(joint, propLinkName); + + 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::getMbDPart(App::DocumentObject* obj) @@ -309,53 +757,6 @@ std::shared_ptr AssemblyObject::getMbDPart(App::DocumentObject* obj) 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) { @@ -395,6 +796,121 @@ std::shared_ptr AssemblyObject::makeMbdAssembly() return assembly; } +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; +} + +void AssemblyObject::swapJCS(App::DocumentObject* joint) +{ + auto propElement1 = dynamic_cast(joint->getPropertyByName("Element1")); + auto propElement2 = dynamic_cast(joint->getPropertyByName("Element2")); + if (propElement1 && propElement2) { + auto temp = std::string(propElement1->getValue()); + propElement1->setValue(propElement2->getValue()); + propElement2->setValue(temp); + } + auto propVertex1 = dynamic_cast(joint->getPropertyByName("Vertex1")); + auto propVertex2 = dynamic_cast(joint->getPropertyByName("Vertex2")); + if (propVertex1 && propVertex2) { + auto temp = std::string(propVertex1->getValue()); + propVertex1->setValue(propVertex2->getValue()); + propVertex2->setValue(temp); + } + auto propPlacement1 = + dynamic_cast(joint->getPropertyByName("Placement1")); + auto propPlacement2 = + dynamic_cast(joint->getPropertyByName("Placement2")); + if (propPlacement1 && propPlacement2) { + auto temp = propPlacement1->getValue(); + propPlacement1->setValue(propPlacement2->getValue()); + propPlacement2->setValue(temp); + } + auto propObject1 = dynamic_cast(joint->getPropertyByName("Object1")); + auto propObject2 = dynamic_cast(joint->getPropertyByName("Object2")); + if (propObject1 && propObject2) { + auto temp = propObject1->getValue(); + propObject1->setValue(propObject2->getValue()); + propObject2->setValue(temp); + } + auto propPart1 = dynamic_cast(joint->getPropertyByName("Part1")); + auto propPart2 = dynamic_cast(joint->getPropertyByName("Part2")); + if (propPart1 && propPart2) { + auto temp = propPart1->getValue(); + propPart1->setValue(propPart2->getValue()); + propPart2->setValue(temp); + } +} + +void AssemblyObject::savePlacementsForUndo() +{ + previousPositions.clear(); + + for (auto& pair : objectPartMap) { + App::DocumentObject* obj = pair.first; + if (!obj) { + continue; + } + + std::pair savePair; + savePair.first = obj; + + // Check if the object has a "Placement" property + auto* propPlc = dynamic_cast(obj->getPropertyByName("Placement")); + if (!propPlc) { + continue; + } + savePair.second = propPlc->getValue(); + + previousPositions.push_back(savePair); + } +} + +void AssemblyObject::undoSolve() +{ + for (auto& pair : previousPositions) { + App::DocumentObject* obj = pair.first; + if (!obj) { + continue; + } + + // Check if the object has a "Placement" property + auto* propPlacement = + dynamic_cast(obj->getPropertyByName("Placement")); + if (!propPlacement) { + continue; + } + + propPlacement->setValue(pair.second); + } + previousPositions.clear(); + + // update joint placements: + getJoints(); +} + +void AssemblyObject::clearUndo() +{ + previousPositions.clear(); +} + void AssemblyObject::setNewPlacements() { for (auto& pair : objectPartMap) { @@ -408,34 +924,35 @@ void AssemblyObject::setNewPlacements() // 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); + if (!propPlacement) { + continue; } + + 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); } } @@ -482,13 +999,157 @@ void AssemblyObject::setObjMasses(std::vector(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elName)); + BRepAdaptor_Surface sf(face); + // GeomAbs_Plane GeomAbs_Cylinder GeomAbs_Cone GeomAbs_Sphere GeomAbs_Thorus + if (sf.GetType() == type) { + return true; + } + + return false; +} + +bool AssemblyObject::isEdgeType(App::DocumentObject* obj, + const char* elName, + GeomAbs_CurveType type) +{ + PartApp::Feature* base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(elName)); + BRepAdaptor_Curve sf(edge); + + if (sf.GetType() == type) { + return true; + } + + return false; +} + +double AssemblyObject::getFaceRadius(App::DocumentObject* obj, const char* elt) +{ + auto base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elt)); + BRepAdaptor_Surface sf(face); + + if (sf.GetType() == GeomAbs_Cylinder) { + return sf.Cylinder().Radius(); + } + else if (sf.GetType() == GeomAbs_Sphere) { + return sf.Sphere().Radius(); + } + + return 0.0; +} + +double AssemblyObject::getEdgeRadius(App::DocumentObject* obj, const char* elt) +{ + auto base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(elt)); + BRepAdaptor_Curve sf(edge); + + if (sf.GetType() == GeomAbs_Circle) { + return sf.Circle().Radius(); + } + + return 0.0; +} + +// getters to get from properties +double AssemblyObject::getJointDistance(App::DocumentObject* joint) +{ + double distance = 0.0; + + auto* prop = dynamic_cast(joint->getPropertyByName("Distance")); + if (prop) { + distance = prop->getValue(); + } + + return distance; +} + +JointType AssemblyObject::getJointType(App::DocumentObject* joint) +{ + JointType jointType = JointType::Fixed; + + auto* prop = dynamic_cast(joint->getPropertyByName("JointType")); + if (prop) { + jointType = static_cast(prop->getValue()); + } + + return jointType; +} + +const char* AssemblyObject::getElementFromProp(App::DocumentObject* obj, const char* propName) +{ + auto* prop = dynamic_cast(obj->getPropertyByName(propName)); + if (!prop) { + return ""; + } + + return prop->getValue(); +} + +std::string AssemblyObject::getElementTypeFromProp(App::DocumentObject* obj, const char* propName) +{ + // The prop is going to be something like 'Edge14' or 'Face7'. We need 'Edge' or 'Face' + std::string elementType; + for (char ch : std::string(getElementFromProp(obj, propName))) { + if (std::isalpha(ch)) { + elementType += ch; + } + } + return elementType; +} + +App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joint, + const char* propLinkName) +{ + auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); + if (!propObj) { + return nullptr; + } + return propObj->getValue(); +} + +App::DocumentObject* AssemblyObject::getLinkedObjFromProp(App::DocumentObject* joint, + const char* propLinkName) +{ + return getLinkObjFromProp(joint, propLinkName)->getLinkedObject(true); +} + +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; +} + /*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. +/* Apparently not necessary as App::Part doesn't have this. // Python Assembly feature --------------------------------------------------------- namespace App diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 8fe6b09c84..49a4e8765d 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -25,6 +25,10 @@ #ifndef ASSEMBLY_AssemblyObject_H #define ASSEMBLY_AssemblyObject_H + +#include +#include + #include #include @@ -49,6 +53,8 @@ class Rotation; namespace Assembly { +class JointGroup; + // This enum has to be the same as the one in JointObject.py enum class JointType { @@ -57,9 +63,7 @@ enum class JointType Cylindrical, Slider, Ball, - Planar, - Parallel, - Tangent + Distance }; class AssemblyExport AssemblyObject: public App::Part @@ -78,35 +82,78 @@ public: return "AssemblyGui::ViewProviderAssembly"; } - int solve(); + int solve(bool enableRedo = false); + void savePlacementsForUndo(); + void undoSolve(); + void clearUndo(); 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::vector> makeMbdJoint(App::DocumentObject* joint); + std::shared_ptr makeMbdJointOfType(App::DocumentObject* joint, + JointType jointType); + std::shared_ptr makeMbdJointDistance(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceFaceVertex(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceFaceEdge(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceFaceFace(App::DocumentObject* joint); + std::string handleOneSideOfJoint(App::DocumentObject* joint, + JointType jointType, 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); + std::vector getGroundedJoints(); + void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); + std::vector fixGroundedParts(); + + void removeUnconnectedJoints(std::vector& joints, + std::vector groundedObjs); + void traverseAndMarkConnectedParts(App::DocumentObject* currentPart, + std::set& connectedParts, + const std::vector& joints); + std::vector + getConnectedParts(App::DocumentObject* part, const std::vector& joints); + + JointGroup* getJointGroup(); + + void swapJCS(App::DocumentObject* joint); + void setNewPlacements(); void recomputeJointPlacements(std::vector joints); + bool isPartConnected(App::DocumentObject* obj); + double getObjMass(App::DocumentObject* obj); void setObjMasses(std::vector> objectMasses); + bool isEdgeType(App::DocumentObject* obj, const char* elName, GeomAbs_CurveType type); + bool isFaceType(App::DocumentObject* obj, const char* elName, GeomAbs_SurfaceType type); + double getFaceRadius(App::DocumentObject* obj, const char* elName); + double getEdgeRadius(App::DocumentObject* obj, const char* elName); + + // getters to get from properties + double getJointDistance(App::DocumentObject* joint); + JointType getJointType(App::DocumentObject* joint); + const char* getElementFromProp(App::DocumentObject* obj, const char* propName); + std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); + Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); + App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); + App::DocumentObject* getLinkedObjFromProp(App::DocumentObject* joint, const char* propName); + private: std::shared_ptr mbdAssembly; std::unordered_map> objectPartMap; std::vector> objMasses; + std::vector> previousPositions; + // void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property // *prop) override; }; diff --git a/src/Mod/Assembly/App/AssemblyObjectPy.xml b/src/Mod/Assembly/App/AssemblyObjectPy.xml index 7b38bd92be..7ddee537dd 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPy.xml +++ b/src/Mod/Assembly/App/AssemblyObjectPy.xml @@ -18,7 +18,13 @@ Solve the assembly and update part placements. - solve() + solve(enableRedo=False) -> int + + Args: + enableRedo: Whether the solve save the initial position of parts + to enable undoing it even without a transaction. + Defaults to `False` ie the solve cannot be undone if called + outside of a transaction. Returns: 0 in case of success, otherwise the following codes in this order of @@ -32,6 +38,41 @@ + + + + Undo the last solve of the assembly and return part placements to their initial position. + + undoSolve() + + Returns: None + + + + + + + Clear the registered undo positions. + + clearUndo() + + Returns: None + + + + + + + Check if a part is connected to the ground through joints. + + isPartConnected(obj) -> bool + + Args: document object to check. + + Returns: True if part is connected to ground + + + diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp index bfb50e346c..abe4a4428c 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp +++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp @@ -46,12 +46,55 @@ int AssemblyObjectPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj* } PyObject* AssemblyObjectPy::solve(PyObject* args) +{ + PyObject* enableUndoPy; + bool enableUndo; + + if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &enableUndoPy)) { + PyErr_Clear(); + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + else { + enableUndo = false; + } + } + else { + enableUndo = Base::asBoolean(enableUndoPy); + } + + int ret = this->getAssemblyObjectPtr()->solve(enableUndo); + return Py_BuildValue("i", ret); +} + +PyObject* AssemblyObjectPy::undoSolve(PyObject* args) { if (!PyArg_ParseTuple(args, "")) { return nullptr; } - int ret = this->getAssemblyObjectPtr()->solve(); - return Py_BuildValue("i", ret); + this->getAssemblyObjectPtr()->undoSolve(); + Py_Return; +} + +PyObject* AssemblyObjectPy::clearUndo(PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + this->getAssemblyObjectPtr()->clearUndo(); + Py_Return; +} + +PyObject* AssemblyObjectPy::isPartConnected(PyObject* args) +{ + PyObject* pyobj; + + if (!PyArg_ParseTuple(args, "O", &pyobj)) { + return nullptr; + } + auto* obj = static_cast(pyobj)->getDocumentObjectPtr(); + bool ok = this->getAssemblyObjectPtr()->isPartConnected(obj); + return Py_BuildValue("O", (ok ? Py_True : Py_False)); } PyObject* AssemblyObjectPy::exportAsASMT(PyObject* args) diff --git a/src/Mod/Assembly/App/PreCompiled.h b/src/Mod/Assembly/App/PreCompiled.h index 4e087e3c91..f70a22f5f1 100644 --- a/src/Mod/Assembly/App/PreCompiled.h +++ b/src/Mod/Assembly/App/PreCompiled.h @@ -42,5 +42,10 @@ #include #include +#include +#include +#include +#include + #endif // _PreComp_ #endif // ASSEMBLY_PRECOMPILED_H diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt index 713b231aeb..b6f964016c 100644 --- a/src/Mod/Assembly/CMakeLists.txt +++ b/src/Mod/Assembly/CMakeLists.txt @@ -67,3 +67,9 @@ INSTALL( DESTINATION Mod/Assembly/AssemblyTests ) +INSTALL( + FILES + ${AssemblyScripts_SRCS} + DESTINATION + Mod/Assembly/Assembly +) diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index ac8d7d3e35..6c6cfd702a 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -58,7 +58,7 @@ class CommandCreateAssembly: App.setActiveTransaction("Create assembly") assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") assembly.Type = "Assembly" - Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + Gui.ActiveDocument.setEdit(assembly) assembly.newObject("Assembly::JointGroup", "Joints") App.closeActiveTransaction() diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index aa649448cf..765de4c994 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -42,6 +42,18 @@ __author__ = "Ondsel" __url__ = "https://www.freecad.org" +def isCreateJointActive(): + return UtilsAssembly.isAssemblyGrounded() and UtilsAssembly.assembly_has_at_least_n_parts(2) + + +def activateJoint(index): + if JointObject.activeTask: + JointObject.activeTask.reject() + + panel = TaskAssemblyCreateJoint(index) + Gui.Control.showDialog(panel) + + class CommandCreateJointFixed: def __init__(self): pass @@ -51,7 +63,7 @@ class CommandCreateJointFixed: return { "Pixmap": "Assembly_CreateJointFixed", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"), - "Accel": "F", + "Accel": "J", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", @@ -62,11 +74,10 @@ class CommandCreateJointFixed: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(0) - Gui.Control.showDialog(panel) + activateJoint(0) class CommandCreateJointRevolute: @@ -89,11 +100,10 @@ class CommandCreateJointRevolute: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(1) - Gui.Control.showDialog(panel) + activateJoint(1) class CommandCreateJointCylindrical: @@ -118,11 +128,10 @@ class CommandCreateJointCylindrical: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(2) - Gui.Control.showDialog(panel) + activateJoint(2) class CommandCreateJointSlider: @@ -145,11 +154,10 @@ class CommandCreateJointSlider: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(3) - Gui.Control.showDialog(panel) + activateJoint(3) class CommandCreateJointBall: @@ -172,92 +180,37 @@ class CommandCreateJointBall: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(4) - Gui.Control.showDialog(panel) + activateJoint(4) -class CommandCreateJointPlanar: +class CommandCreateJointDistance: def __init__(self): pass def GetResources(self): return { - "Pixmap": "Assembly_CreateJointPlanar", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointPlanar", "Create Planar Joint"), - "Accel": "P", + "Pixmap": "Assembly_CreateJointDistance", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointDistance", "Create Distance Joint"), + "Accel": "D", "ToolTip": "

" + QT_TRANSLATE_NOOP( - "Assembly_CreateJointPlanar", - "Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.", + "Assembly_CreateJointDistance", + "Create a Distance Joint: Depending on your selection this tool will apply different constraints.", ) + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + # return False + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(5) - Gui.Control.showDialog(panel) - - -class CommandCreateJointParallel: - def __init__(self): - pass - - def GetResources(self): - - return { - "Pixmap": "Assembly_CreateJointParallel", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointParallel", "Create Parallel Joint"), - "Accel": "L", - "ToolTip": "

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

", - "CmdType": "ForEdit", - } - - def IsActive(self): - return UtilsAssembly.activeAssembly() is not None - - def Activated(self): - panel = TaskAssemblyCreateJoint(6) - Gui.Control.showDialog(panel) - - -class CommandCreateJointTangent: - def __init__(self): - pass - - def GetResources(self): - - return { - "Pixmap": "Assembly_CreateJointTangent", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointTangent", "Create Tangent Joint"), - "Accel": "T", - "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.", - ) - + "

", - "CmdType": "ForEdit", - } - - def IsActive(self): - return UtilsAssembly.activeAssembly() is not None - - def Activated(self): - panel = TaskAssemblyCreateJoint(7) - Gui.Control.showDialog(panel) + activateJoint(5) class CommandToggleGrounded: @@ -269,18 +222,21 @@ class CommandToggleGrounded: return { "Pixmap": "Assembly_ToggleGrounded", "MenuText": QT_TRANSLATE_NOOP("Assembly_ToggleGrounded", "Toggle grounded"), - "Accel": "F", + "Accel": "G", "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.", + "Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part before starting to assemble.", ) + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return ( + UtilsAssembly.isAssemblyCommandActive() + and UtilsAssembly.assembly_has_at_least_n_parts(1) + ) def Activated(self): assembly = UtilsAssembly.activeAssembly() @@ -301,20 +257,33 @@ class CommandToggleGrounded: full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub) obj = UtilsAssembly.getObject(full_element_name) + part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj) + + # Only objects within the assembly. + objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(sel.ObjectName, sub) + if assembly.Name not in objs_names: + continue # 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: + if ( + hasattr(joint, "ObjectToGround") + and joint.ObjectToGround == part_containing_obj + ): + # Remove grounded tag. + if part_containing_obj.Label.endswith(" 🔒"): + part_containing_obj.Label = part_containing_obj.Label[:-2] doc = App.ActiveDocument doc.removeObject(joint.Name) doc.recompute() return # Create groundedJoint. + + part_containing_obj.Label = part_containing_obj.Label + " 🔒" ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") - JointObject.GroundedJoint(ground, obj) + JointObject.GroundedJoint(ground, part_containing_obj) JointObject.ViewProviderGroundedJoint(ground.ViewObject) - Gui.Selection.clearSelection() App.closeActiveTransaction() @@ -325,6 +294,4 @@ if App.GuiUp: Gui.addCommand("Assembly_CreateJointCylindrical", CommandCreateJointCylindrical()) Gui.addCommand("Assembly_CreateJointSlider", CommandCreateJointSlider()) Gui.addCommand("Assembly_CreateJointBall", CommandCreateJointBall()) - Gui.addCommand("Assembly_CreateJointPlanar", CommandCreateJointPlanar()) - Gui.addCommand("Assembly_CreateJointParallel", CommandCreateJointParallel()) - Gui.addCommand("Assembly_CreateJointTangent", CommandCreateJointTangent()) + Gui.addCommand("Assembly_CreateJointDistance", CommandCreateJointDistance()) diff --git a/src/Mod/Assembly/CommandExportASMT.py b/src/Mod/Assembly/CommandExportASMT.py index 438c669629..b32734f388 100644 --- a/src/Mod/Assembly/CommandExportASMT.py +++ b/src/Mod/Assembly/CommandExportASMT.py @@ -54,7 +54,7 @@ class CommandExportASMT: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded() def Activated(self): document = App.ActiveDocument diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index 5f7ddd3126..f70cd8732b 100644 --- a/src/Mod/Assembly/CommandInsertLink.py +++ b/src/Mod/Assembly/CommandInsertLink.py @@ -21,6 +21,7 @@ # * # ***************************************************************************/ +import re import os import FreeCAD as App @@ -31,6 +32,7 @@ if App.GuiUp: from PySide import QtCore, QtGui, QtWidgets import UtilsAssembly +import Preferences # translate = App.Qt.translate @@ -44,22 +46,30 @@ class CommandInsertLink: pass def GetResources(self): - tooltip = "

Insert a Link into the assembly. " - tooltip += "This will create dynamic links to parts/bodies/primitives/assemblies." - tooltip += "To insert external objects, make sure that the file " - tooltip += "is open in the current session

" - tooltip += "

Press shift to add several links while clicking on the view." - return { "Pixmap": "Assembly_InsertLink", "MenuText": QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert Link"), "Accel": "I", - "ToolTip": QT_TRANSLATE_NOOP("Assembly_InsertLink", tooltip), + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_InsertLink", + "Insert a Link into the currently active assembly. This will create dynamic links to parts/bodies/primitives/assemblies. To insert external objects, make sure that the file is open in the current session", + ) + + "

  • " + + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert by left clicking items in the list.") + + "
  • " + + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Undo by right clicking items in the list.") + + "
  • " + + QT_TRANSLATE_NOOP( + "Assembly_InsertLink", + "Press shift to add several links while clicking on the view.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return UtilsAssembly.isAssemblyCommandActive() def Activated(self): assembly = UtilsAssembly.activeAssembly() @@ -81,6 +91,10 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyInsertLink.ui") self.form.installEventFilter(self) + self.form.partList.installEventFilter(self) + + pref = Preferences.preferences() + self.form.CheckBox_InsertInParts.setChecked(pref.GetBool("InsertInParts", True)) # Actions self.form.openFileButton.clicked.connect(self.openFiles) @@ -89,28 +103,37 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.allParts = [] self.partsDoc = [] - self.numberOfAddedParts = 0 self.translation = 0 self.partMoving = False + self.totalTranslation = App.Vector() + + self.insertionStack = [] # used to handle cancellation of insertions. self.buildPartList() App.setActiveTransaction("Insert Link") def accept(self): - App.closeActiveTransaction() self.deactivated() + + if self.partMoving: + self.endMove() + + App.closeActiveTransaction() return True def reject(self): - App.closeActiveTransaction(True) self.deactivated() + + if self.partMoving: + self.dismissPart() + + App.closeActiveTransaction(True) return True def deactivated(self): - if self.partMoving: - self.endMove() - self.doc.removeObject(self.createdLink.Name) + pref = Preferences.preferences() + pref.SetBool("InsertInParts", self.form.CheckBox_InsertInParts.isChecked()) def buildPartList(self): self.allParts.clear() @@ -136,7 +159,7 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.allParts.append(obj) self.partsDoc.append(doc) - for obj in doc.findObjects("PartDesign::Body"): + for obj in doc.findObjects("Part::Feature"): # but only those at top level (not nested inside other containers) if obj.getParentGeoFeatureGroup() is None: self.allParts.append(obj) @@ -145,7 +168,7 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.form.partList.clear() for part in self.allParts: newItem = QtGui.QListWidgetItem() - newItem.setText(part.Document.Name + " - " + part.Name) + newItem.setText(part.Label + " (" + part.Document.Name + ".FCStd)") newItem.setIcon(part.ViewObject.Icon) self.form.partList.addItem(newItem) @@ -193,23 +216,82 @@ class TaskAssemblyInsertLink(QtCore.QObject): # check that the current document had been saved or that it's the same document as that of the selected part if not self.doc.FileName != "" and not self.doc == selectedPart.Document: - print("The current document must be saved before inserting an external part") - return + msgBox = QtWidgets.QMessageBox() + msgBox.setIcon(QtWidgets.QMessageBox.Warning) + msgBox.setText("The current document must be saved before inserting external parts.") + msgBox.setWindowTitle("Save Document") + saveButton = msgBox.addButton("Save", QtWidgets.QMessageBox.AcceptRole) + cancelButton = msgBox.addButton("Cancel", QtWidgets.QMessageBox.RejectRole) - self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name) - self.createdLink.LinkedObject = selectedPart - self.createdLink.Placement.Base = self.getTranslationVec(selectedPart) - self.createdLink.recompute() + msgBox.exec_() - self.numberOfAddedParts += 1 + if not (msgBox.clickedButton() == saveButton and Gui.ActiveDocument.saveAs()): + return + + objectWhereToInsert = self.assembly + + if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part": + objectWhereToInsert = self.assembly.newObject("App::Part", "Part_" + selectedPart.Label) + + createdLink = objectWhereToInsert.newObject("App::Link", selectedPart.Label) + createdLink.LinkedObject = selectedPart + createdLink.recompute() + + addedObject = createdLink + if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part": + addedObject = objectWhereToInsert + + insertionDict = {} + insertionDict["item"] = item + insertionDict["addedObject"] = addedObject + self.insertionStack.append(insertionDict) + self.increment_counter(item) + + translation = self.getTranslationVec(addedObject) + insertionDict["translation"] = translation + self.totalTranslation += translation + addedObject.Placement.Base = self.totalTranslation # highlight the link Gui.Selection.clearSelection() - Gui.Selection.addSelection(self.doc.Name, self.assembly.Name, self.createdLink.Name + ".") + Gui.Selection.addSelection(self.doc.Name, addedObject.Name, "") # Start moving the part if user brings mouse on view self.initMove() + self.form.partList.setItemSelected(item, False) + + def increment_counter(self, item): + text = item.text() + match = re.search(r"(\d+) inserted$", text) + + if match: + # Counter exists, increment it + counter = int(match.group(1)) + 1 + new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text) + else: + # Counter does not exist, add it + new_text = f"{text} : 1 inserted" + + item.setText(new_text) + + def decrement_counter(self, item): + text = item.text() + match = re.search(r"(\d+) inserted$", text) + + if match: + counter = int(match.group(1)) - 1 + if counter > 0: + # Update the counter + new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text) + elif counter == 0: + # Remove the counter part from the text + new_text = re.sub(r" : \d+ inserted$", "", text) + else: + return + + item.setText(new_text) + def initMove(self): self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse) self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent", self.clickMouse) @@ -230,42 +312,83 @@ class TaskAssemblyInsertLink(QtCore.QObject): def moveMouse(self, info): newPos = self.view.getPoint(*info["Position"]) - self.createdLink.Placement.Base = newPos + self.insertionStack[-1]["addedObject"].Placement.Base = newPos def clickMouse(self, info): if info["Button"] == "BUTTON1" and info["State"] == "DOWN": + Gui.Selection.clearSelection() if info["ShiftDown"]: # Create a new link and moves this one now - currentPos = self.createdLink.Placement.Base - selectedPart = self.createdLink.LinkedObject - self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name) - self.createdLink.LinkedObject = selectedPart - self.createdLink.Placement.Base = currentPos + addedObject = self.insertionStack[-1]["addedObject"] + currentPos = addedObject.Placement.Base + selectedPart = addedObject + if addedObject.TypeId == "App::Link": + selectedPart = addedObject.LinkedObject + + addedObject = self.assembly.newObject("App::Link", selectedPart.Label) + addedObject.LinkedObject = selectedPart + addedObject.Placement.Base = currentPos + + insertionDict = {} + insertionDict["translation"] = App.Vector() + insertionDict["item"] = self.insertionStack[-1]["item"] + insertionDict["addedObject"] = addedObject + self.insertionStack.append(insertionDict) + else: self.endMove() + elif info["Button"] == "BUTTON2" and info["State"] == "DOWN": + self.dismissPart() + # 3D view keyboard handler def KeyboardEvent(self, info): if info["State"] == "UP" and info["Key"] == "ESCAPE": - self.endMove() - self.doc.removeObject(self.createdLink.Name) + self.dismissPart() + + def dismissPart(self): + self.endMove() + stack_item = self.insertionStack.pop() + self.totalTranslation -= stack_item["translation"] + UtilsAssembly.removeObjAndChilds(stack_item["addedObject"]) + self.decrement_counter(stack_item["item"]) # Taskbox keyboard event handler def eventFilter(self, watched, event): if watched == self.form and event.type() == QtCore.QEvent.KeyPress: if event.key() == QtCore.Qt.Key_Escape and self.partMoving: - self.endMove() - self.doc.removeObject(self.createdLink.Name) + self.dismissPart() return True # Consume the event + + if event.type() == QtCore.QEvent.ContextMenu and watched is self.form.partList: + item = watched.itemAt(event.pos()) + + if item: + # Iterate through the insertionStack in reverse + for i in reversed(range(len(self.insertionStack))): + stack_item = self.insertionStack[i] + + if stack_item["item"] == item: + if self.partMoving: + self.endMove() + + self.totalTranslation -= stack_item["translation"] + UtilsAssembly.removeObjAndChilds(stack_item["addedObject"]) + self.decrement_counter(item) + del self.insertionStack[i] + self.form.partList.setItemSelected(item, False) + + return True + return super().eventFilter(watched, event) def getTranslationVec(self, part): bb = part.Shape.BoundBox if bb: - self.translation += (bb.XMax + bb.YMax + bb.ZMax) * 0.15 + translation = (bb.XMax + bb.YMax + bb.ZMax) * 0.15 else: - self.translation += 10 - return App.Vector(self.translation, self.translation, self.translation) + translation = 10 + return App.Vector(translation, translation, translation) if App.GuiUp: diff --git a/src/Mod/Assembly/CommandSolveAssembly.py b/src/Mod/Assembly/CommandSolveAssembly.py index 95ec98f0db..b332f742d2 100644 --- a/src/Mod/Assembly/CommandSolveAssembly.py +++ b/src/Mod/Assembly/CommandSolveAssembly.py @@ -49,7 +49,7 @@ class CommandSolveAssembly: return { "Pixmap": "Assembly_SolveAssembly", "MenuText": QT_TRANSLATE_NOOP("Assembly_SolveAssembly", "Solve Assembly"), - "Accel": "F", + "Accel": "Z", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_SolveAssembly", @@ -60,7 +60,7 @@ class CommandSolveAssembly: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded() def Activated(self): assembly = UtilsAssembly.activeAssembly() diff --git a/src/Mod/Assembly/Gui/PreCompiled.h b/src/Mod/Assembly/Gui/PreCompiled.h index 6c1fc5583b..02bfff4f01 100644 --- a/src/Mod/Assembly/Gui/PreCompiled.h +++ b/src/Mod/Assembly/Gui/PreCompiled.h @@ -37,6 +37,10 @@ #include #include +// Qt +#ifndef __QtAll__ +#include +#endif #endif //_PreComp_ diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc index 6ace2e5c34..ad258eecf3 100644 --- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -1,19 +1,20 @@ - + icons/Assembly_InsertLink.svg icons/preferences-assembly.svg icons/Assembly_ToggleGrounded.svg icons/Assembly_CreateJointBall.svg icons/Assembly_CreateJointCylindrical.svg icons/Assembly_CreateJointFixed.svg - icons/Assembly_CreateJointParallel.svg icons/Assembly_CreateJointPlanar.svg icons/Assembly_CreateJointRevolute.svg icons/Assembly_CreateJointSlider.svg icons/Assembly_CreateJointTangent.svg icons/Assembly_ExportASMT.svg + icons/Assembly_SolveAssembly.svg panels/TaskAssemblyCreateJoint.ui panels/TaskAssemblyInsertLink.ui preferences/Assembly.ui + icons/Assembly_CreateJointDistance.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointDistance.svg similarity index 100% rename from src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg rename to src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointDistance.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg new file mode 100644 index 0000000000..d3044a83f6 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Path-Stock + 2015-07-04 + https://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui index ed194b75c5..be41367552 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui @@ -20,8 +20,107 @@ + + + + + + Distance + + + + + + + + 0 + 0 + + + + mm + + + + + + + + + + + Offset + + + + + + + + 0 + 0 + + + + mm + + + + + + + + + + + Rotation + + + + + + + + 0 + 0 + + + + deg + + + + + + + + + + 0 + 0 + + + + Reverse the direction of the joint. + + + Reverse + + + + :/icons/button_sort.svg:/icons/button_sort.svg + + + + + + Gui::QuantitySpinBox + QWidget +

Gui/QuantitySpinBox.h
+ + diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui index 76a641643a..214df9adfb 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui @@ -42,8 +42,34 @@ + + + + If checked, the selected object will be inserted inside a Part container, unless it is already a Part. + + + Insert as part + + + true + + + InsertInParts + + + Mod/Assembly + + + + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 97c3ab4935..865dc51364 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2023 Ondsel * @@ -24,6 +24,7 @@ #include "PreCompiled.h" #ifndef _PreComp_ +#include #include #include #include @@ -35,11 +36,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include #include "ViewProviderAssembly.h" @@ -80,6 +82,62 @@ bool ViewProviderAssembly::doubleClicked() return true; } +bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const +{ + Base::Console().Warning("ViewProviderAssembly::canDragObject\n"); + if (!obj || obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { + Base::Console().Warning("so should be false...\n"); + return false; + } + + // else if a solid is removed, remove associated joints if any. + bool prompted = false; + auto* assemblyPart = static_cast(getObject()); + std::vector joints = assemblyPart->getJoints(); + + // Combine the joints and groundedJoints vectors into one for simplicity. + std::vector allJoints = assemblyPart->getJoints(); + std::vector groundedJoints = assemblyPart->getGroundedJoints(); + allJoints.insert(allJoints.end(), groundedJoints.begin(), groundedJoints.end()); + + Gui::Command::openCommand(tr("Delete associated joints").toStdString().c_str()); + for (auto joint : allJoints) { + // Assume getLinkObjFromProp can return nullptr if the property doesn't exist. + App::DocumentObject* obj1 = assemblyPart->getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* obj2 = assemblyPart->getLinkObjFromProp(joint, "Part2"); + App::DocumentObject* obj3 = assemblyPart->getLinkObjFromProp(joint, "ObjectToGround"); + if (obj == obj1 || obj == obj2 || obj == obj3) { + if (!prompted) { + prompted = true; + QMessageBox msgBox; + msgBox.setText(tr("The object is associated to one or more joints.")); + msgBox.setInformativeText( + tr("Do you want to move the object and delete associated joints?")); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + int ret = msgBox.exec(); + + if (ret == QMessageBox::No) { + return false; + } + } + Gui::Command::doCommand(Gui::Command::Gui, + "App.activeDocument().removeObject('%s')", + joint->getNameInDocument()); + } + } + Gui::Command::commitCommand(); + + // Remove grounded tag if any. (as it is not done in jointObject.py onDelete) + std::string label = obj->Label.getValue(); + + if (label.size() >= 4 && label.substr(label.size() - 2) == " 🔒") { + label = label.substr(0, label.size() - 2); + obj->Label.setValue(label.c_str()); + } + + return true; +} bool ViewProviderAssembly::setEdit(int ModNum) { @@ -308,35 +366,53 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vectorgetObject(subNames[0].c_str()); } - else { - objName = subNames[subNames.size() - 3]; + // From here subnames is at least 3 and can be more. There are several cases to consider : + // bodyOrLink.pad.face1 -> bodyOrLink should be the moving + // entity partOrLink.bodyOrLink.pad.face1 -> partOrLink should be the + // moving entity partOrLink.box.face1 -> partOrLink should + // be the moving entity partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> partOrLink1 + // should be the moving entity assembly1.partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> + // partOrLink1 should be the moving entity assembly1.boxOrLink1.face1 -> boxOrLink1 should be + // the moving entity + + for (auto objName : subNames) { App::DocumentObject* obj = appDoc->getObject(objName.c_str()); if (!obj) { - return nullptr; + continue; } - if (obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + + if (obj->getTypeId().isDerivedFrom(AssemblyObject::getClassTypeId())) { + continue; + } + else if (obj->getTypeId().isDerivedFrom(App::Part::getClassTypeId()) + || 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) { + continue; + } - if (linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + if (linkedObj->getTypeId().isDerivedFrom(App::Part::getClassTypeId()) + || 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()); } + + // then its neither a part or body or a link to a part or body. So it is something like + // assembly.box.face1 + objName = subNames[subNames.size() - 2]; + return appDoc->getObject(objName.c_str()); } void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition) { + Gui::Command::openCommand(tr("Move part").toStdString().c_str()); partMoving = true; // prevent selection while moving @@ -376,6 +452,8 @@ void ViewProviderAssembly::endMove() auto* assemblyPart = static_cast(getObject()); assemblyPart->setObjMasses({}); + + Gui::Command::commitCommand(); } diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index dd0378710b..32b6d8f848 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -24,6 +24,8 @@ #ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H #define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H +#include + #include #include @@ -40,6 +42,7 @@ namespace AssemblyGui class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart, public Gui::SelectionObserver { + Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssembly) PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly); public: @@ -57,6 +60,14 @@ public: void unsetEdit(int ModNum) override; bool isInEditMode(); + /// Ask the view provider if it accepts object deletions while in edit + bool acceptDeletionsInEdit() override + { + return true; + } + + bool canDragObject(App::DocumentObject*) const override; + App::DocumentObject* getActivePart(); /// is called when the provider is in edit and the mouse is moved diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp index abfa1db8c0..29302adfb0 100644 --- a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp @@ -47,3 +47,9 @@ QIcon ViewProviderJointGroup::getIcon() const { return Gui::BitmapFactory().pixmap("Assembly_CreateJointFixed.svg"); } + +// Make the joint group impossible to delete. +bool ViewProviderJointGroup::onDelete(const std::vector& subNames) +{ + return false; +} diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h index fb965e9c2a..76ca1b6230 100644 --- a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h +++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h @@ -43,6 +43,22 @@ public: /// deliver the icon shown in the tree view. Override from ViewProvider.h QIcon getIcon() const override; + // Prevent dragging of the joints and dropping things inside the joint group. + bool canDragObjects() const override + { + return false; + }; + bool canDropObjects() const override + { + return false; + }; + bool canDragAndDropObject(App::DocumentObject*) const override + { + return false; + }; + + bool onDelete(const std::vector& subNames) override; + // protected: /// get called by the container whenever a property has been changed // void onChanged(const App::Property* prop) override; diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index c45bae3a9d..6968a2a350 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -90,9 +90,7 @@ class AssemblyWorkbench(Workbench): "Assembly_CreateJointCylindrical", "Assembly_CreateJointSlider", "Assembly_CreateJointBall", - "Assembly_CreateJointPlanar", - "Assembly_CreateJointParallel", - "Assembly_CreateJointTangent", + "Assembly_CreateJointDistance", ] self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index b4eaffd432..eac706658e 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -47,14 +47,40 @@ JointTypes = [ QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"), QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), QT_TRANSLATE_NOOP("AssemblyJoint", "Ball"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Planar"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Parallel"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Tangent"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), +] + +JointUsingDistance = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), +] + +JointUsingOffset = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), +] + +JointUsingRotation = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), +] + +JointUsingReverse = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), ] +def flipPlacement(plc, localXAxis): + flipRot = App.Rotation(localXAxis, 180) + plc.Rotation = plc.Rotation.multiply(flipRot) + return plc + + class Joint: - def __init__(self, joint, type_index): + def __init__(self, joint, type_index, assembly): self.Type = "Joint" joint.Proxy = self @@ -76,6 +102,13 @@ class Joint: QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"), ) + joint.addProperty( + "App::PropertyLink", + "Part1", + "Joint Connector 1", + QT_TRANSLATE_NOOP("App::Property", "The first part of the joint"), + ) + joint.addProperty( "App::PropertyString", "Element1", @@ -108,6 +141,13 @@ class Joint: QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"), ) + joint.addProperty( + "App::PropertyLink", + "Part2", + "Joint Connector 2", + QT_TRANSLATE_NOOP("App::Property", "The second part of the joint"), + ) + joint.addProperty( "App::PropertyString", "Element2", @@ -132,6 +172,46 @@ class Joint: ), ) + joint.addProperty( + "App::PropertyFloat", + "Distance", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the distance of the joint. It is used only by the distance joint.", + ), + ) + + joint.addProperty( + "App::PropertyFloat", + "Rotation", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the rotation of the joint.", + ), + ) + + joint.addProperty( + "App::PropertyVector", + "Offset", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the offset vector of the joint.", + ), + ) + + joint.addProperty( + "App::PropertyBool", + "FirstPartConnected", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This indicate if the first part was connected to ground at the time of joint creation.", + ), + ) + self.setJointConnectors(joint, []) def __getstate__(self): @@ -141,14 +221,22 @@ class Joint: if state: self.Type = state + def getAssembly(self, joint): + return joint.InList[0] + def setJointType(self, joint, jointType): joint.JointType = jointType joint.Label = jointType.replace(" ", "") - def onChanged(self, fp, prop): + def onChanged(self, joint, prop): """Do something when a property has changed""" # App.Console.PrintMessage("Change property: " + str(prop) + "\n") - pass + + if prop == "Rotation" or prop == "Offset" or prop == "Distance": + if hasattr( + joint, "Vertex1" + ): # during loading the onchanged may be triggered before full init. + self.getAssembly(joint).solve() def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -157,32 +245,51 @@ class Joint: 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 + assembly = self.getAssembly(joint) if len(current_selection) >= 1: + joint.Part1 = None + joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"]) + joint.Object1 = current_selection[0]["object"] + joint.Part1 = current_selection[0]["part"] joint.Element1 = current_selection[0]["element_name"] joint.Vertex1 = current_selection[0]["vertex_name"] - joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1) + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) else: joint.Object1 = None + joint.Part1 = None joint.Element1 = "" joint.Vertex1 = "" - joint.Placement1 = UtilsAssembly.activeAssembly().Placement + joint.Placement1 = App.Placement() if len(current_selection) >= 2: joint.Object2 = current_selection[1]["object"] + joint.Part2 = current_selection[1]["part"] joint.Element2 = current_selection[1]["element_name"] joint.Vertex2 = current_selection[1]["vertex_name"] - joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2) + joint.Placement2 = self.findPlacement( + joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True + ) + assembly.solve(True) + else: joint.Object2 = None + joint.Part2 = None joint.Element2 = "" joint.Vertex2 = "" - joint.Placement2 = UtilsAssembly.activeAssembly().Placement + joint.Placement2 = App.Placement() + assembly.undoSolve() def updateJCSPlacements(self, joint): - joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1) - joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2) + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) + joint.Placement2 = self.findPlacement( + joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True + ) """ So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex. @@ -194,12 +301,19 @@ class Joint: - if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face. """ - def findPlacement(self, obj, elt, vtx): + def findPlacement(self, joint, obj, part, elt, vtx, isSecond=False): + assembly = self.getAssembly(joint) plc = App.Placement() - if not obj or not elt or not vtx: + if not obj: return App.Placement() + if not elt or not vtx: + # case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem. + plc = UtilsAssembly.getGlobalPlacement(obj, part) + plc = assembly.Placement.inverse() * plc + return plc + elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt) vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx) @@ -211,11 +325,15 @@ class Joint: curve = edge.Curve # First we find the translation - if vtx_type == "Edge": - # In this case the edge is a circle/arc and the wanted vertex is its center. + if vtx_type == "Edge" or joint.JointType == "Distance": + # In this case the wanted vertex is the center. if curve.TypeId == "Part::GeomCircle": center_point = curve.Location plc.Base = (center_point.x, center_point.y, center_point.z) + elif curve.TypeId == "Part::GeomLine": + edge_points = UtilsAssembly.getPointsFromVertexes(edge.Vertexes) + line_middle = (edge_points[0] + edge_points[1]) * 0.5 + plc.Base = line_middle else: vertex = obj.Shape.Vertexes[vtx_index - 1] plc.Base = (vertex.X, vertex.Y, vertex.Z) @@ -229,31 +347,113 @@ class Joint: plane_origin = App.Vector(0, 0, 0) plane = Part.Plane(plane_origin, plane_normal) plc.Rotation = App.Rotation(plane.Rotation) - elif elt_type == "Face": face = obj.Shape.Faces[elt_index - 1] + surface = face.Surface # First we find the translation - if vtx_type == "Edge": + if vtx_type == "Face" or joint.JointType == "Distance": + if surface.TypeId == "Part::GeomCylinder" or surface.TypeId == "Part::GeomCone": + centerOfG = face.CenterOfGravity - surface.Center + centerPoint = surface.Center + centerOfG + centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) + plc.Base = centerPoint + elif surface.TypeId == "Part::GeomTorus" or surface.TypeId == "Part::GeomSphere": + plc.Base = surface.Center + else: + plc.Base = face.CenterOfGravity + elif vtx_type == "Edge": # In this case the edge is a circle/arc and the wanted vertex is its center. - circleOrArc = face.Edges[vtx_index - 1] - curve = circleOrArc.Curve + edge = face.Edges[vtx_index - 1] + curve = edge.Curve if curve.TypeId == "Part::GeomCircle": center_point = curve.Location plc.Base = (center_point.x, center_point.y, center_point.z) + elif ( + surface.TypeId == "Part::GeomCylinder" + and curve.TypeId == "Part::GeomBSplineCurve" + ): + # handle special case of 2 cylinder intersecting. + plc.Base = self.findCylindersIntersection(obj, surface, edge, elt_index) + else: vertex = obj.Shape.Vertexes[vtx_index - 1] plc.Base = (vertex.X, vertex.Y, vertex.Z) # Then we find the Rotation - surface = face.Surface if surface.TypeId == "Part::GeomPlane": plc.Rotation = App.Rotation(surface.Rotation) + else: + plc.Rotation = surface.Rotation + + # Now plc is the placement relative to the origin determined by the object placement. + # But it does not take into account Part placements. So if the solid is in a part and + # if the part has a placement then plc is wrong. + + # change plc to be relative to the object placement. + plc = obj.Placement.inverse() * plc + + # change plc to be relative to the origin of the document. + global_plc = UtilsAssembly.getGlobalPlacement(obj, part) + plc = global_plc * plc + + # change plc to be relative to the assembly. + plc = assembly.Placement.inverse() * plc + + # We apply rotation / reverse / offset it necessary, but only to the second JCS. + if isSecond: + if joint.Offset.Length != 0.0: + plc = self.applyOffsetToPlacement(plc, joint.Offset) + if joint.Rotation != 0.0: + plc = self.applyRotationToPlacement(plc, joint.Rotation) - # Now plc is the placement in the doc. But we need the placement relative to the solid origin. return plc + def applyOffsetToPlacement(self, plc, offset): + plc.Base = plc.Base + plc.Rotation.multVec(offset) + return plc + + def applyRotationToPlacement(self, plc, angle): + rot = plc.Rotation + zRotation = App.Rotation(App.Vector(0, 0, 1), angle) + rot = rot.multiply(zRotation) + plc.Rotation = rot + return plc + + def flipPart(self, joint): + if joint.FirstPartConnected: + plc = joint.Part2.Placement.inverse() * joint.Placement2 + localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) + joint.Part2.Placement = flipPlacement(joint.Part2.Placement, localXAxis) + else: + plc = joint.Part1.Placement.inverse() * joint.Placement1 + localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) + joint.Part1.Placement = flipPlacement(joint.Part1.Placement, localXAxis) + self.getAssembly(joint).solve() + + def findCylindersIntersection(self, obj, surface, edge, elt_index): + for j, facej in enumerate(obj.Shape.Faces): + surfacej = facej.Surface + if (elt_index - 1) == j or surfacej.TypeId != "Part::GeomCylinder": + continue + + for edgej in facej.Edges: + if ( + edgej.Curve.TypeId == "Part::GeomBSplineCurve" + and edgej.CenterOfGravity == edge.CenterOfGravity + and edgej.Length == edge.Length + ): + # we need intersection between the 2 cylinder axis. + line1 = Part.Line(surface.Center, surface.Center + surface.Axis) + line2 = Part.Line(surfacej.Center, surfacej.Center + surfacej.Axis) + + res = line1.intersect(line2, Part.Precision.confusion()) + + if res: + return App.Vector(res[0].X, res[0].Y, res[0].Z) + return surface.Center + class ViewProviderJoint: def __init__(self, vobj): @@ -394,25 +594,34 @@ class ViewProviderJoint: r = placement.Rotation.Q soTransform.rotation.setValue(r[0], r[1], r[2], r[3]) - def updateData(self, fp, prop): + def updateData(self, joint, 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 + # joint is the handled feature, prop is the name of the property that has changed if prop == "Placement1": - plc = fp.getPropertyByName("Placement1") - if fp.getPropertyByName("Object1"): + plc = joint.getPropertyByName("Placement1") + if joint.getPropertyByName("Object1"): self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL self.set_JCS_placement(self.transform1, plc) else: self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE if prop == "Placement2": - plc = fp.getPropertyByName("Placement2") - if fp.getPropertyByName("Object2"): + plc = joint.getPropertyByName("Placement2") + if joint.getPropertyByName("Object2"): self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL + if self.areJCSReversed(joint): + plc = flipPlacement(plc, App.Vector(1, 0, 0)) self.set_JCS_placement(self.transform2, plc) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE + def areJCSReversed(self, joint): + zaxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) + zaxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) + + sameDir = zaxis1.dot(zaxis2) > 0 + return not sameDir + def showPreviewJCS(self, visible, placement=None): if visible: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL @@ -454,12 +663,8 @@ class ViewProviderJoint: return ":/icons/Assembly_CreateJointSlider.svg" elif self.app_obj.JointType == "Ball": return ":/icons/Assembly_CreateJointBall.svg" - elif self.app_obj.JointType == "Planar": - return ":/icons/Assembly_CreateJointPlanar.svg" - elif self.app_obj.JointType == "Parallel": - return ":/icons/Assembly_CreateJointParallel.svg" - elif self.app_obj.JointType == "Tangent": - return ":/icons/Assembly_CreateJointTangent.svg" + elif self.app_obj.JointType == "Distance": + return ":/icons/Assembly_CreateJointDistance.svg" return ":/icons/Assembly_CreateJoint.svg" @@ -475,6 +680,10 @@ class ViewProviderJoint: return None def doubleClicked(self, vobj): + assembly = vobj.Object.InList[0] + if UtilsAssembly.activeAssembly() != assembly: + Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + panel = TaskAssemblyCreateJoint(0, vobj.Object) Gui.Control.showDialog(panel) @@ -555,6 +764,15 @@ class ViewProviderGroundedJoint: # App.Console.PrintMessage("Change property: " + str(prop) + "\n") pass + def onDelete(self, feature, subelements): # subelements is a tuple of strings + # Remove grounded tag. + if hasattr(feature.Object, "ObjectToGround"): + obj = feature.Object.ObjectToGround + if obj.Label.endswith(" 🔒"): + obj.Label = obj.Label[:-2] + + return True # If False is returned the object won't be deleted + def getIcon(self): return ":/icons/Assembly_ToggleGrounded.svg" @@ -570,8 +788,8 @@ class MakeJointSelGate: 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. + if self.assembly.Name not in objs_names: + # Only objects within the assembly. return False if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve): @@ -585,19 +803,28 @@ class MakeJointSelGate: full_obj_name = ".".join(objs_names) full_element_name = full_obj_name + "." + element_name selected_object = UtilsAssembly.getObject(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) for selection_dict in self.taskbox.current_selection: - if selection_dict["object"] == selected_object: + if selection_dict["part"] == part_containing_selected_object: # Can't join a solid to itself. So the user need to select 2 different parts. return False return True +activeTask = None + + class TaskAssemblyCreateJoint(QtCore.QObject): def __init__(self, jointTypeIndex, jointObj=None): super().__init__() + global activeTask + activeTask = self + self.assembly = UtilsAssembly.activeAssembly() self.view = Gui.activeDocument().activeView() self.doc = App.ActiveDocument @@ -612,6 +839,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.jointType.addItems(JointTypes) self.form.jointType.setCurrentIndex(jointTypeIndex) self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged) + self.form.distanceSpinbox.valueChanged.connect(self.onDistanceChanged) + self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged) + self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged) + self.form.PushButtonReverse.clicked.connect(self.onReverseClicked) Gui.Selection.clearSelection() @@ -631,6 +862,11 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.createJointObject() + self.toggleDistanceVisibility() + self.toggleOffsetVisibility() + self.toggleRotationVisibility() + self.toggleReverseVisibility() + Gui.Selection.addSelectionGate( MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve ) @@ -662,6 +898,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): return True def deactivate(self): + global activeTask + activeTask = None + self.assembly.clearUndo() + self.assembly.ViewObject.EnableMovement = True Gui.Selection.removeSelectionGate() Gui.Selection.removeObserver(self) @@ -678,11 +918,57 @@ class TaskAssemblyCreateJoint(QtCore.QObject): joint_group = UtilsAssembly.getJointGroup(self.assembly) self.joint = joint_group.newObject("App::FeaturePython", self.jointName) - Joint(self.joint, type_index) + Joint(self.joint, type_index, self.assembly) ViewProviderJoint(self.joint.ViewObject) def onJointTypeChanged(self, index): self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText()) + self.toggleDistanceVisibility() + self.toggleOffsetVisibility() + self.toggleRotationVisibility() + self.toggleReverseVisibility() + + def onDistanceChanged(self, quantity): + self.joint.Distance = self.form.distanceSpinbox.property("rawValue") + + def onOffsetChanged(self, quantity): + self.joint.Offset = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue")) + + def onRotationChanged(self, quantity): + self.joint.Rotation = self.form.rotationSpinbox.property("rawValue") + + def onReverseClicked(self): + self.joint.Proxy.flipPart(self.joint) + + def toggleDistanceVisibility(self): + if self.form.jointType.currentText() in JointUsingDistance: + self.form.distanceLabel.show() + self.form.distanceSpinbox.show() + else: + self.form.distanceLabel.hide() + self.form.distanceSpinbox.hide() + + def toggleOffsetVisibility(self): + if self.form.jointType.currentText() in JointUsingOffset: + self.form.offsetLabel.show() + self.form.offsetSpinbox.show() + else: + self.form.offsetLabel.hide() + self.form.offsetSpinbox.hide() + + def toggleRotationVisibility(self): + if self.form.jointType.currentText() in JointUsingRotation: + self.form.rotationLabel.show() + self.form.rotationSpinbox.show() + else: + self.form.rotationLabel.hide() + self.form.rotationSpinbox.hide() + + def toggleReverseVisibility(self): + if self.form.jointType.currentText() in JointUsingReverse: + self.form.PushButtonReverse.show() + else: + self.form.PushButtonReverse.hide() def updateTaskboxFromJoint(self): self.current_selection = [] @@ -690,12 +976,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject): selection_dict1 = { "object": self.joint.Object1, + "part": self.joint.Part1, "element_name": self.joint.Element1, "vertex_name": self.joint.Vertex1, } selection_dict2 = { "object": self.joint.Object2, + "part": self.joint.Part2, "element_name": self.joint.Element2, "vertex_name": self.joint.Vertex2, } @@ -712,9 +1000,17 @@ class TaskAssemblyCreateJoint(QtCore.QObject): elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2) Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName) + self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance) + self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z) + self.form.rotationSpinbox.setProperty("rawValue", self.joint.Rotation) + + self.form.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType)) self.updateJointList() def getObjSubNameFromObj(self, obj, elName): + if obj is None: + return elName + if obj.TypeId == "PartDesign::Body": return obj.Tip.Name + "." + elName elif obj.TypeId == "App::Link": @@ -738,14 +1034,16 @@ class TaskAssemblyCreateJoint(QtCore.QObject): 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"] + sname = sel["object"].Label + if sel["element_name"] != "": + sname = sname + "." + 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"] + and self.current_selection[0]["part"] == self.preselection_dict["part"] ): self.joint.ViewObject.Proxy.showPreviewJCS(False) return @@ -767,14 +1065,22 @@ class TaskAssemblyCreateJoint(QtCore.QObject): 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 - ) + if self.preselection_dict["element_name"] == "": + self.preselection_dict["vertex_name"] = "" + else: + self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex( + self.preselection_dict + ) + + isSecond = len(self.current_selection) == 1 placement = self.joint.Proxy.findPlacement( + self.joint, self.preselection_dict["object"], + self.preselection_dict["part"], self.preselection_dict["element_name"], self.preselection_dict["vertex_name"], + isSecond, ) self.joint.ViewObject.Proxy.showPreviewJCS(True, placement) self.previewJCSVisible = True @@ -793,15 +1099,22 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) selection_dict = { "object": selected_object, + "part": part_containing_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) + if element_name == "": + selection_dict["vertex_name"] = "" + else: + selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict) self.current_selection.append(selection_dict) self.updateJoint() @@ -810,11 +1123,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) # 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: + if selection_dict["part"] == part_containing_selected_object: selection_dict_to_remove = selection_dict break @@ -832,9 +1148,13 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) self.preselection_dict = { "object": selected_object, + "part": part_containing_selected_object, "sub_name": sub_name, "element_name": element_name, "full_element_name": full_element_name, diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 3feed5390b..c7e5d05edd 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -22,6 +22,7 @@ # ***************************************************************************/ import FreeCAD as App +import Part if App.GuiUp: import FreeCADGui as Gui @@ -36,17 +37,35 @@ __url__ = "https://www.freecad.org" def activeAssembly(): doc = Gui.ActiveDocument + if doc is None or doc.ActiveView is None: + return None + + active_assembly = doc.ActiveView.getActiveObject("part") + + if active_assembly is not None and active_assembly.Type == "Assembly": + return active_assembly + + return None + + +def activePart(): + doc = Gui.ActiveDocument + if doc is None or doc.ActiveView is None: return None active_part = doc.ActiveView.getActiveObject("part") - if active_part is not None and active_part.Type == "Assembly": + if active_part is not None and active_part.Type != "Assembly": return active_part return None +def isAssemblyCommandActive(): + return activeAssembly() is not None and not Gui.Control.activeDialog() + + def isDocTemporary(doc): # Guard against older versions of FreeCad which don't have the Temporary attribute try: @@ -56,19 +75,39 @@ def isDocTemporary(doc): return temp +def assembly_has_at_least_n_parts(n): + assembly = activeAssembly() + i = 0 + if not assembly: + return False + for obj in assembly.OutList: + # note : groundedJoints comes in the outlist so we filter those out. + if hasattr(obj, "Placement") and not hasattr(obj, "ObjectToGround"): + i = i + 1 + if i == n: + return True + return False + + def getObject(full_name): - # full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16" - # or "Assembly.Assembly1.Assembly2.Assembly3.Body.pad.Edge16" - # We want either Body or Box. - parts = full_name.split(".") + # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" + # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" + # or "Assembly.Assembly1.LinkOrPart1.Body.Local_CS.X" + # We want either Body or Box or Local_CS. + names = full_name.split(".") doc = App.ActiveDocument - if len(parts) < 3: + if len(names) < 3: App.Console.PrintError( "getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter" ) return None - obj = doc.getObject(parts[-3]) # So either 'Body', or 'Assembly' + obj = doc.getObject(names[-2]) + + if obj and obj.TypeId == "PartDesign::CoordinateSystem": + return doc.getObject(names[-2]) + + obj = doc.getObject(names[-3]) # So either 'Body', or 'Assembly' if not obj: return None @@ -80,8 +119,116 @@ def getObject(full_name): if linked_obj.TypeId == "PartDesign::Body": return obj - else: # primitive, fastener, gear ... or link to primitive, fastener, gear... - return doc.getObject(parts[-2]) + # primitive, fastener, gear ... or link to primitive, fastener, gear... + return doc.getObject(names[-2]) + + +def getContainingPart(full_name, selected_object): + # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" + # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" + # We want either Body or Box. + names = full_name.split(".") + doc = App.ActiveDocument + if len(names) < 3: + App.Console.PrintError( + "getContainingPart() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter" + ) + return None + + for objName in names: + obj = doc.getObject(objName) + + if not obj: + continue + + if ( + obj.TypeId == "PartDesign::Body" + and selected_object.TypeId == "PartDesign::CoordinateSystem" + ): + if obj.hasObject(selected_object, True): + return obj + + # Note here we may want to specify a specific behavior for Assembly::AssemblyObject. + if obj.TypeId == "App::Part": + if obj.hasObject(selected_object, True): + return obj + + elif obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "App::Part": + if linked_obj.hasObject(selected_object, True): + return obj + + # no container found so we return the object itself. + return selected_object + + +# The container is used to support cases where the same object appears at several places +# which happens when you have a link to a part. +def getGlobalPlacement(targetObj, container=None): + inContainerBranch = container is None + for part in App.activeDocument().RootObjects: + foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch) + if foundPlacement is not None: + return foundPlacement + + return App.Placement() + + +def getTargetPlacementRelativeTo( + targetObj, part, container, inContainerBranch, ignorePlacement=False +): + inContainerBranch = inContainerBranch or (not ignorePlacement and part == container) + + if targetObj == part and inContainerBranch and not ignorePlacement: + return targetObj.Placement + + if part.TypeId == "App::DocumentObjectGroup": + for obj in part.OutList: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch, ignorePlacement + ) + if foundPlacement is not None: + return foundPlacement + + elif part.TypeId == "App::Part" or part.TypeId == "Assembly::AssemblyObject": + for obj in part.OutList: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ) + if foundPlacement is None: + continue + + # If we were called from a link then we need to ignore this placement as we use the link placement instead. + if not ignorePlacement: + foundPlacement = part.Placement * foundPlacement + + return foundPlacement + + elif part.TypeId == "App::Link": + linked_obj = part.getLinkedObject() + + if linked_obj.TypeId == "App::Part" or linked_obj.TypeId == "Assembly::AssemblyObject": + for obj in linked_obj.OutList: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ) + if foundPlacement is None: + continue + + foundPlacement = part.Placement * foundPlacement + return foundPlacement + + foundPlacement = getTargetPlacementRelativeTo( + targetObj, linked_obj, container, inContainerBranch, True + ) + + if foundPlacement is not None and not ignorePlacement: + foundPlacement = part.Placement * foundPlacement + + return foundPlacement + + return None def getElementName(full_name): @@ -93,6 +240,10 @@ def getElementName(full_name): # At minimum "Assembly.Box.edge16". It shouldn't be shorter return "" + # case of PartDesign::CoordinateSystem + if parts[-1] == "X" or parts[-1] == "Y" or parts[-1] == "Z": + return "" + return parts[-1] @@ -147,14 +298,25 @@ def extract_type_and_number(element_name): def findElementClosestVertex(selection_dict): + obj = selection_dict["object"] + + mousePos = selection_dict["mouse_pos"] + + # We need mousePos to be relative to the part containing obj global placement + if selection_dict["object"] != selection_dict["part"]: + plc = App.Placement() + plc.Base = mousePos + global_plc = getGlobalPlacement(selection_dict["part"]) + plc = global_plc.inverse() * plc + mousePos = plc.Base + elt_type, elt_index = extract_type_and_number(selection_dict["element_name"]) if elt_type == "Vertex": return selection_dict["element_name"] elif elt_type == "Edge": - edge = selection_dict["object"].Shape.Edges[elt_index - 1] - + edge = obj.Shape.Edges[elt_index - 1] curve = edge.Curve if curve.TypeId == "Part::GeomCircle": # For centers, as they are not shape vertexes, we return the element name. @@ -162,17 +324,28 @@ def findElementClosestVertex(selection_dict): return selection_dict["element_name"] edge_points = getPointsFromVertexes(edge.Vertexes) - closest_vertex_index, _ = findClosestPointToMousePos( - edge_points, selection_dict["mouse_pos"] - ) - vertex_name = findVertexNameInObject( - edge.Vertexes[closest_vertex_index], selection_dict["object"] - ) + + if curve.TypeId == "Part::GeomLine": + # For lines we allow users to select the middle of lines as well. + line_middle = (edge_points[0] + edge_points[1]) * 0.5 + edge_points.append(line_middle) + + closest_vertex_index, _ = findClosestPointToMousePos(edge_points, mousePos) + + if curve.TypeId == "Part::GeomLine" and closest_vertex_index == 2: + # If line center is closest then we have no vertex name to set so we put element name + return selection_dict["element_name"] + + vertex_name = findVertexNameInObject(edge.Vertexes[closest_vertex_index], obj) return vertex_name elif elt_type == "Face": - face = selection_dict["object"].Shape.Faces[elt_index - 1] + face = obj.Shape.Faces[elt_index - 1] + surface = face.Surface + _type = surface.TypeId + if _type == "Part::GeomSphere" or _type == "Part::GeomTorus": + return selection_dict["element_name"] # Handle the circle/arc edges for their centers center_points = [] @@ -181,19 +354,46 @@ def findElementClosestVertex(selection_dict): for i, edge in enumerate(edges): curve = edge.Curve - if curve.TypeId == "Part::GeomCircle": + if curve.TypeId == "Part::GeomCircle" or curve.TypeId == "Part::GeomEllipse": center_points.append(curve.Location) center_points_edge_indexes.append(i) + elif _type == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve": + # handle special case of 2 cylinder intersecting. + for j, facej in enumerate(obj.Shape.Faces): + surfacej = facej.Surface + if (elt_index - 1) != j and surfacej.TypeId == "Part::GeomCylinder": + for edgej in facej.Edges: + if edgej.Curve.TypeId == "Part::GeomBSplineCurve": + if ( + edgej.CenterOfGravity == edge.CenterOfGravity + and edgej.Length == edge.Length + ): + center_points.append(edgej.CenterOfGravity) + center_points_edge_indexes.append(i) + if len(center_points) > 0: closest_center_index, closest_center_distance = findClosestPointToMousePos( - center_points, selection_dict["mouse_pos"] + center_points, mousePos ) - # Hendle the face vertexes - face_points = getPointsFromVertexes(face.Vertexes) + # Handle the face vertexes + face_points = [] + + if _type != "Part::GeomCylinder" and _type != "Part::GeomCone": + face_points = getPointsFromVertexes(face.Vertexes) + + # We also allow users to select the center of gravity. + if _type == "Part::GeomCylinder" or _type == "Part::GeomCone": + centerOfG = face.CenterOfGravity - surface.Center + centerPoint = surface.Center + centerOfG + centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) + face_points.append(centerPoint) + else: + face_points.append(face.CenterOfGravity) + closest_vertex_index, closest_vertex_distance = findClosestPointToMousePos( - face_points, selection_dict["mouse_pos"] + face_points, mousePos ) if len(center_points) > 0: @@ -202,9 +402,14 @@ def findElementClosestVertex(selection_dict): index = center_points_edge_indexes[closest_center_index] + 1 return "Edge" + str(index) - vertex_name = findVertexNameInObject( - face.Vertexes[closest_vertex_index], selection_dict["object"] - ) + if _type == "Part::GeomCylinder" or _type == "Part::GeomCone": + return selection_dict["element_name"] + + if closest_vertex_index == len(face.Vertexes): + # If center of gravity then we have no vertex name to set so we put element name + return selection_dict["element_name"] + + vertex_name = findVertexNameInObject(face.Vertexes[closest_vertex_index], obj) return vertex_name @@ -247,8 +452,51 @@ def color_from_unsigned(c): def getJointGroup(assembly): - joint_group = assembly.getObject("Joints") + joint_group = None + + for obj in assembly.OutList: + if obj.TypeId == "Assembly::JointGroup": + joint_group = obj + break if not joint_group: joint_group = assembly.newObject("Assembly::JointGroup", "Joints") + return joint_group + + +def isAssemblyGrounded(): + assembly = activeAssembly() + if not assembly: + return False + + jointGroup = getJointGroup(assembly) + + for joint in jointGroup.Group: + if hasattr(joint, "ObjectToGround"): + return True + + return False + + +def removeObjAndChilds(obj): + removeObjsAndChilds([obj]) + + +def removeObjsAndChilds(objs): + def addsubobjs(obj, toremoveset): + if obj.TypeId == "App::Origin": # Origins are already handled + return + + toremoveset.add(obj) + if obj.TypeId != "App::Link": + for subobj in obj.OutList: + addsubobjs(subobj, toremoveset) + + toremove = set() + for obj in objs: + addsubobjs(obj, toremove) + + for obj in toremove: + if obj: + obj.Document.removeObject(obj.Name) From 4a119c43f1e526239110ba10d90b3417b7f6b3fd Mon Sep 17 00:00:00 2001 From: Paddle Date: Tue, 19 Dec 2023 09:21:46 +0100 Subject: [PATCH 04/22] Assembly: Add support to external objects. (And various fixes) --- src/Mod/Assembly/App/AssemblyObject.cpp | 73 ++++++++++---- src/Mod/Assembly/App/AssemblyObject.h | 4 +- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 13 ++- src/Mod/Assembly/JointObject.py | 97 +++++++++++++------ src/Mod/Assembly/UtilsAssembly.py | 93 ++++++++++++++---- 5 files changed, 213 insertions(+), 67 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 490987e085..3e98a95299 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -444,8 +448,8 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeEdge(App::Doc { const char* elt1 = getElementFromProp(joint, "Element1"); const char* elt2 = getElementFromProp(joint, "Element2"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); - auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); if (isEdgeType(obj1, elt1, GeomAbs_Line) || isEdgeType(obj2, elt2, GeomAbs_Line)) { if (!isEdgeType(obj1, elt1, GeomAbs_Line)) { @@ -492,8 +496,8 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceFace(App::Doc { const char* elt1 = getElementFromProp(joint, "Element1"); const char* elt2 = getElementFromProp(joint, "Element2"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); - auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); if (isFaceType(obj1, elt1, GeomAbs_Plane) || isFaceType(obj2, elt2, GeomAbs_Plane)) { if (!isFaceType(obj1, elt1, GeomAbs_Plane)) { @@ -623,7 +627,7 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceVertex(App::DocumentObject* joint) { const char* elt1 = getElementFromProp(joint, "Element1"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); if (isFaceType(obj1, elt1, GeomAbs_Plane)) { auto mbdJoint = CREATE::With(); @@ -654,7 +658,7 @@ std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint) { const char* elt1 = getElementFromProp(joint, "Element1"); - auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); if (isEdgeType(obj1, elt1, GeomAbs_Line)) { // Point on line joint. auto mbdJoint = CREATE::With(); @@ -676,7 +680,7 @@ AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint) std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceEdge(App::DocumentObject* joint) { const char* elt2 = getElementFromProp(joint, "Element2"); - auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); if (isEdgeType(obj2, elt2, GeomAbs_Line)) { // Make line in plane joint. @@ -705,16 +709,16 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) return {}; } - std::string fullMarkerName1 = handleOneSideOfJoint(joint, jointType, "Part1", "Placement1"); - std::string fullMarkerName2 = handleOneSideOfJoint(joint, jointType, "Part2", "Placement2"); + std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Part1", "Placement1"); + std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Part2", "Placement2"); mbdJoint->setMarkerI(fullMarkerName1); mbdJoint->setMarkerJ(fullMarkerName2); return {mbdJoint}; } + std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, - JointType jointType, const char* propLinkName, const char* propPlcName) { @@ -843,10 +847,10 @@ void AssemblyObject::swapJCS(App::DocumentObject* joint) propPlacement1->setValue(propPlacement2->getValue()); propPlacement2->setValue(temp); } - auto propObject1 = dynamic_cast(joint->getPropertyByName("Object1")); - auto propObject2 = dynamic_cast(joint->getPropertyByName("Object2")); + auto propObject1 = dynamic_cast(joint->getPropertyByName("Object1")); + auto propObject2 = dynamic_cast(joint->getPropertyByName("Object2")); if (propObject1 && propObject2) { - auto temp = propObject1->getValue(); + auto temp = std::string(propObject1->getValue()); propObject1->setValue(propObject2->getValue()); propObject2->setValue(temp); } @@ -885,6 +889,10 @@ void AssemblyObject::savePlacementsForUndo() void AssemblyObject::undoSolve() { + if (previousPositions.size() == 0) { + return; + } + for (auto& pair : previousPositions) { App::DocumentObject* obj = pair.first; if (!obj) { @@ -1004,7 +1012,7 @@ bool AssemblyObject::isFaceType(App::DocumentObject* obj, GeomAbs_SurfaceType type) { auto base = static_cast(obj); - const PartApp::TopoShape& TopShape = base->Shape.getShape(); + PartApp::TopoShape TopShape = base->Shape.getShape(); // Check for valid face types TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elName)); @@ -1127,10 +1135,41 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi return propObj->getValue(); } -App::DocumentObject* AssemblyObject::getLinkedObjFromProp(App::DocumentObject* joint, - const char* propLinkName) +App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) { - return getLinkObjFromProp(joint, propLinkName)->getLinkedObject(true); + auto* propObjName = dynamic_cast(joint->getPropertyByName(pObjName)); + if (!propObjName) { + return nullptr; + } + std::string objName = std::string(propObjName->getValue()); + + App::DocumentObject* containingPart = getLinkObjFromProp(joint, pPart); + if (!containingPart) { + return nullptr; + } + + if (objName == containingPart->getNameInDocument()) { + return containingPart->getLinkedObject(true); + } + + if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { + App::Link* link = dynamic_cast(containingPart); + + containingPart = link->getLinkedObject(true); + if (!containingPart) { + return nullptr; + } + } + + for (auto obj : containingPart->getOutList()) { + if (objName == obj->getNameInDocument()) { + return obj->getLinkedObject(true); + } + } + + return nullptr; } Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 49a4e8765d..53b550ceda 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -103,7 +103,6 @@ public: std::shared_ptr makeMbdJointDistanceFaceFace(App::DocumentObject* joint); std::string handleOneSideOfJoint(App::DocumentObject* joint, - JointType jointType, const char* propObjLinkName, const char* propPlcName); void jointParts(std::vector joints); @@ -144,7 +143,8 @@ public: std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); - App::DocumentObject* getLinkedObjFromProp(App::DocumentObject* joint, const char* propName); + App::DocumentObject* + getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); private: std::shared_ptr mbdAssembly; diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 865dc51364..9c32514b13 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -72,7 +72,8 @@ bool ViewProviderAssembly::doubleClicked() { if (isInEditMode()) { // Part is already 'Active' so we exit edit mode. - Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()"); + // Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()"); + Gui::Application::Instance->activeDocument()->resetEdit(); } else { // Part is not 'Active' so we enter edit mode to make it so. @@ -160,6 +161,16 @@ void ViewProviderAssembly::unsetEdit(int ModNum) partMoving = false; docsToMove = {}; + // Check if the view is still active before trying to deactivate the assembly. + auto activeDoc = Gui::Application::Instance->activeDocument(); + if (!activeDoc) { + return; + } + auto activeView = activeDoc->getActiveView(); + if (!activeView) { + return; + } + // Set the part as not 'Activated' ie not bold in the tree. Gui::Command::doCommand(Gui::Command::Gui, "Gui.ActiveDocument.ActiveView.setActiveObject('%s', None)", diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index eac706658e..52514de94a 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -40,6 +40,7 @@ __url__ = "https://www.freecad.org" from pivy import coin import UtilsAssembly +import Preferences JointTypes = [ QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), @@ -73,6 +74,11 @@ JointUsingReverse = [ ] +def solveIfAllowed(assembly, storePrev=False): + if Preferences.preferences().GetBool("SolveInJointCreation", True): + assembly.solve(storePrev) + + def flipPlacement(plc, localXAxis): flipRot = App.Rotation(localXAxis, 180) plc.Rotation = plc.Rotation.multiply(flipRot) @@ -96,10 +102,10 @@ class Joint: # First Joint Connector joint.addProperty( - "App::PropertyLink", + "App::PropertyString", # Not PropertyLink because they don't support external objects "Object1", "Joint Connector 1", - QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"), + QT_TRANSLATE_NOOP("App::Property", "The name of the first object of the joint"), ) joint.addProperty( @@ -133,12 +139,22 @@ class Joint: ), ) + joint.addProperty( + "App::PropertyBool", + "Detach1", + "Joint Connector 1", + QT_TRANSLATE_NOOP( + "App::Property", + "This prevent Placement1 from recomputing, enabling custom positioning of the placement.", + ), + ) + # Second Joint Connector joint.addProperty( - "App::PropertyLink", + "App::PropertyString", "Object2", "Joint Connector 2", - QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"), + QT_TRANSLATE_NOOP("App::Property", "The name of the second object of the joint"), ) joint.addProperty( @@ -172,6 +188,16 @@ class Joint: ), ) + joint.addProperty( + "App::PropertyBool", + "Detach2", + "Joint Connector 2", + QT_TRANSLATE_NOOP( + "App::Property", + "This prevent Placement2 from recomputing, enabling custom positioning of the placement.", + ), + ) + joint.addProperty( "App::PropertyFloat", "Distance", @@ -236,7 +262,7 @@ class Joint: if hasattr( joint, "Vertex1" ): # during loading the onchanged may be triggered before full init. - self.getAssembly(joint).solve() + solveIfAllowed(self.getAssembly(joint)) def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -251,7 +277,7 @@ class Joint: joint.Part1 = None joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"]) - joint.Object1 = current_selection[0]["object"] + joint.Object1 = current_selection[0]["object"].Name joint.Part1 = current_selection[0]["part"] joint.Element1 = current_selection[0]["element_name"] joint.Vertex1 = current_selection[0]["vertex_name"] @@ -259,24 +285,24 @@ class Joint: joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 ) else: - joint.Object1 = None + joint.Object1 = "" joint.Part1 = None joint.Element1 = "" joint.Vertex1 = "" joint.Placement1 = App.Placement() if len(current_selection) >= 2: - joint.Object2 = current_selection[1]["object"] + joint.Object2 = current_selection[1]["object"].Name joint.Part2 = current_selection[1]["part"] joint.Element2 = current_selection[1]["element_name"] joint.Vertex2 = current_selection[1]["vertex_name"] joint.Placement2 = self.findPlacement( joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) - assembly.solve(True) + solveIfAllowed(assembly, True) else: - joint.Object2 = None + joint.Object2 = "" joint.Part2 = None joint.Element2 = "" joint.Vertex2 = "" @@ -284,12 +310,15 @@ class Joint: assembly.undoSolve() def updateJCSPlacements(self, joint): - joint.Placement1 = self.findPlacement( - joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 - ) - joint.Placement2 = self.findPlacement( - joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True - ) + if not joint.Detach1: + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) + + if not joint.Detach2: + joint.Placement2 = self.findPlacement( + joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True + ) """ So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex. @@ -301,7 +330,11 @@ class Joint: - if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face. """ - def findPlacement(self, joint, obj, part, elt, vtx, isSecond=False): + def findPlacement(self, joint, objName, part, elt, vtx, isSecond=False): + if not objName or not part: + return App.Placement() + + obj = UtilsAssembly.getObjectInPart(objName, part) assembly = self.getAssembly(joint) plc = App.Placement() @@ -430,7 +463,8 @@ class Joint: plc = joint.Part1.Placement.inverse() * joint.Placement1 localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) joint.Part1.Placement = flipPlacement(joint.Part1.Placement, localXAxis) - self.getAssembly(joint).solve() + + solveIfAllowed(self.getAssembly(joint)) def findCylindersIntersection(self, obj, surface, edge, elt_index): for j, facej in enumerate(obj.Shape.Faces): @@ -887,7 +921,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.deactivate() - self.assembly.solve() + solveIfAllowed(self.assembly) App.closeActiveTransaction() return True @@ -974,15 +1008,18 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.current_selection = [] self.preselection_dict = None + obj1 = UtilsAssembly.getObjectInPart(self.joint.Object1, self.joint.Part1) + obj2 = UtilsAssembly.getObjectInPart(self.joint.Object2, self.joint.Part2) + selection_dict1 = { - "object": self.joint.Object1, + "object": obj1, "part": self.joint.Part1, "element_name": self.joint.Element1, "vertex_name": self.joint.Vertex1, } selection_dict2 = { - "object": self.joint.Object2, + "object": obj2, "part": self.joint.Part2, "element_name": self.joint.Element2, "vertex_name": self.joint.Vertex2, @@ -991,14 +1028,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject): 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(obj1, self.joint.Element1) + if obj1 != self.joint.Part1: + elName = obj1.Name + "." + elName + Gui.Selection.addSelection(self.doc.Name, self.joint.Part1.Name, elName) - elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2) - Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName) + elName = self.getObjSubNameFromObj(obj2, self.joint.Element2) + if obj2 != self.joint.Part2: + elName = obj2.Name + "." + elName + Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.Name, elName) self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance) self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z) @@ -1033,7 +1071,6 @@ class TaskAssemblyCreateJoint(QtCore.QObject): 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 if sel["element_name"] != "": sname = sname + "." + sel["element_name"] @@ -1076,7 +1113,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): placement = self.joint.Proxy.findPlacement( self.joint, - self.preselection_dict["object"], + self.preselection_dict["object"].Name, self.preselection_dict["part"], self.preselection_dict["element_name"], self.preselection_dict["vertex_name"], diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index c7e5d05edd..09b906b928 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -90,43 +90,82 @@ def assembly_has_at_least_n_parts(n): def getObject(full_name): - # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" - # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" - # or "Assembly.Assembly1.LinkOrPart1.Body.Local_CS.X" - # We want either Body or Box or Local_CS. + # full_name is "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16" + # or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16" + # or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X" + # We want either LinkOrBody or LinkOrBox or Local_CS. names = full_name.split(".") doc = App.ActiveDocument + if len(names) < 3: App.Console.PrintError( "getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter" ) return None - obj = doc.getObject(names[-2]) + prevObj = None - if obj and obj.TypeId == "PartDesign::CoordinateSystem": - return doc.getObject(names[-2]) + for i, objName in enumerate(names): + if i == 0: + prevObj = doc.getObject(objName) + if prevObj.TypeId == "App::Link": + prevObj = prevObj.getLinkedObject() + continue - obj = doc.getObject(names[-3]) # So either 'Body', or 'Assembly' + obj = None + if prevObj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + for obji in prevObj.OutList: + if obji.Name == objName: + obj = obji + break - if not obj: - return None + if obj is None: + return None - if obj.TypeId == "PartDesign::Body": - return obj - elif obj.TypeId == "App::Link": - linked_obj = obj.getLinkedObject() - if linked_obj.TypeId == "PartDesign::Body": + # the last is the element name. So if we are at the last but one name, then it must be the selected + if i == len(names) - 2: return obj - # primitive, fastener, gear ... or link to primitive, fastener, gear... - return doc.getObject(names[-2]) + if obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "PartDesign::Body": + if i + 1 < len(names): + obj2 = doc.getObject(names[i + 1]) + if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem": + return obj2 + return obj + elif linked_obj.isDerivedFrom("Part::Feature"): + return obj + else: + prevObj = linked_obj + continue + + elif obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + prevObj = obj + continue + + elif obj.TypeId == "PartDesign::Body": + if i + 1 < len(names): + obj2 = doc.getObject(names[i + 1]) + if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem": + return obj2 + return obj + + elif obj.isDerivedFrom("Part::Feature"): + # primitive, fastener, gear ... + return obj + + return None def getContainingPart(full_name, selected_object): # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" # We want either Body or Box. + if selected_object is None: + App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None") + return None + names = full_name.split(".") doc = App.ActiveDocument if len(names) < 3: @@ -141,6 +180,9 @@ def getContainingPart(full_name, selected_object): if not obj: continue + if obj == selected_object: + return selected_object + if ( obj.TypeId == "PartDesign::Body" and selected_object.TypeId == "PartDesign::CoordinateSystem" @@ -156,6 +198,8 @@ def getContainingPart(full_name, selected_object): elif obj.TypeId == "App::Link": linked_obj = obj.getLinkedObject() if linked_obj.TypeId == "App::Part": + # linked_obj_doc = linked_obj.Document + # selected_obj_in_doc = doc.getObject(selected_object.Name) if linked_obj.hasObject(selected_object, True): return obj @@ -163,6 +207,21 @@ def getContainingPart(full_name, selected_object): return selected_object +def getObjectInPart(objName, part): + if part.Name == objName: + return part + + if part.TypeId == "App::Link": + part = part.getLinkedObject() + + if part.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + for obji in part.OutList: + if obji.Name == objName: + return obji + + return None + + # The container is used to support cases where the same object appears at several places # which happens when you have a link to a part. def getGlobalPlacement(targetObj, container=None): From 22e4d7f2ddeee6652146ef45b526786500708fb7 Mon Sep 17 00:00:00 2001 From: Paddle Date: Wed, 20 Dec 2023 23:32:29 +0100 Subject: [PATCH 05/22] Assembly: Change the joint placements to be relative to the object rather than to the doc origin. This enable 'detaching' them, so that they are not recomputed --- src/Mod/Assembly/App/AssemblyObject.cpp | 171 +++++++++++++++--- src/Mod/Assembly/App/AssemblyObject.h | 5 +- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 2 - src/Mod/Assembly/JointObject.py | 47 +++-- 4 files changed, 178 insertions(+), 47 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 3e98a95299..882e1e959d 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -80,6 +80,105 @@ namespace PartApp = Part; using namespace Assembly; using namespace MbD; +// ======================================= Utils ====================================== + +Base::Placement 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; +} +/* // Currently unused +Base::Placement* getTargetPlacementRelativeTo( + App::DocumentObject* targetObj, App::DocumentObject* part, App::DocumentObject* container, + bool inContainerBranch, bool ignorePlacement = false) +{ + inContainerBranch = inContainerBranch || (!ignorePlacement && part == container); + + Base::Console().Warning("sub --------------\n"); + if (targetObj == part && inContainerBranch && !ignorePlacement) { + Base::Console().Warning("found0\n"); + return &getPlacementFromProp(targetObj, "Placement"); + } + + if (auto group = dynamic_cast(part)) { + for (auto& obj : group->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch, ignorePlacement + ); + if (foundPlacement != nullptr) { + return foundPlacement; + } + } + } + else if (auto assembly = dynamic_cast(part)) { + Base::Console().Warning("h3\n"); + for (auto& obj : assembly->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ); + if (foundPlacement == nullptr) { + continue; + } + + if (!ignorePlacement) { + *foundPlacement = getPlacementFromProp(part, "Placement") * *foundPlacement; + } + + Base::Console().Warning("found\n"); + return foundPlacement; + } + } + else if (auto link = dynamic_cast(part)) { + Base::Console().Warning("h4\n"); + auto linked_obj = link->getLinkedObject(); + + if (dynamic_cast(linked_obj) || dynamic_cast(linked_obj)) { + for (auto& obj : linked_obj->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ); + if (foundPlacement == nullptr) { + continue; + } + + *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; + return foundPlacement; + } + } + + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, linked_obj, container, inContainerBranch, true + ); + + if (foundPlacement != nullptr && !ignorePlacement) { + *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; + } + + Base::Console().Warning("found2\n"); + return foundPlacement; + } + + return nullptr; +} + +Base::Placement getGlobalPlacement(App::DocumentObject* targetObj, App::DocumentObject* container = +nullptr) { bool inContainerBranch = container == nullptr; auto rootObjects = +App::GetApplication().getActiveDocument()->getRootObjects(); for (auto& part : rootObjects) { auto +foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch); if +(foundPlacement != nullptr) { Base::Placement plc(foundPlacement->toMatrix()); return plc; + } + } + + return Base::Placement(); +} +*/ + +// ================================ Assembly Object ============================ + PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) AssemblyObject::AssemblyObject() @@ -357,11 +456,7 @@ int AssemblyObject::solve(bool enableRedo) 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 apparently can't access the JointObject 'updateJCSPlacements' function. - getJoints(); + redrawJointPlacements(joints); return 0; } @@ -709,8 +804,8 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) return {}; } - std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Part1", "Placement1"); - std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Part2", "Placement2"); + std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Object1", "Part1", "Placement1"); + std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Object2", "Part2", "Placement2"); mbdJoint->setMarkerI(fullMarkerName1); mbdJoint->setMarkerJ(fullMarkerName2); @@ -719,18 +814,24 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) } std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, - const char* propLinkName, + const char* propObjName, + const char* propPartName, const char* propPlcName) { - App::DocumentObject* obj = getLinkObjFromProp(joint, propLinkName); + App::DocumentObject* part = getLinkObjFromProp(joint, propPartName); + App::DocumentObject* obj = getObjFromNameProp(joint, propObjName, propPartName); - std::shared_ptr mbdPart = getMbDPart(obj); + std::shared_ptr mbdPart = getMbDPart(part); + Base::Placement partPlc = getPlacementFromProp(part, "Placement"); 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. + // Now we have plc which is the JCS placement, but its relative to the Object, not to the + // containing Part. - plc = objPlc.inverse() * plc; + if (obj->getNameInDocument() != part->getNameInDocument()) { + // Make plc relative to the containing part + plc = objPlc * plc; + } std::string markerName = joint->getFullName(); auto mbdMarker = makeMbdMarker(markerName, plc); @@ -964,11 +1065,27 @@ void AssemblyObject::setNewPlacements() } } +void AssemblyObject::redrawJointPlacements(std::vector joints) +{ + // Notify the joint objects that the transform of the coin object changed. + for (auto* joint : joints) { + auto* propPlacement = + dynamic_cast(joint->getPropertyByName("Placement1")); + if (propPlacement) { + propPlacement->setValue(propPlacement->getValue()); + } + propPlacement = + dynamic_cast(joint->getPropertyByName("Placement2")); + if (propPlacement) { + propPlacement->setValue(propPlacement->getValue()); + } + } +} + 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; @@ -1135,9 +1252,9 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi return propObj->getValue(); } -App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObject* joint, - const char* pObjName, - const char* pPart) +App::DocumentObject* AssemblyObject::getObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) { auto* propObjName = dynamic_cast(joint->getPropertyByName(pObjName)); if (!propObjName) { @@ -1151,13 +1268,13 @@ App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObjec } if (objName == containingPart->getNameInDocument()) { - return containingPart->getLinkedObject(true); + return containingPart; } if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { App::Link* link = dynamic_cast(containingPart); - containingPart = link->getLinkedObject(true); + containingPart = link->getLinkedObject(); if (!containingPart) { return nullptr; } @@ -1165,23 +1282,25 @@ App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObjec for (auto obj : containingPart->getOutList()) { if (objName == obj->getNameInDocument()) { - return obj->getLinkedObject(true); + return obj; } } return nullptr; } -Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) +App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) { - Base::Placement plc = Base::Placement(); - auto* propPlacement = dynamic_cast(obj->getPropertyByName(propName)); - if (propPlacement) { - plc = propPlacement->getValue(); + auto* obj = getObjFromNameProp(joint, pObjName, pPart); + if (obj) { + return obj->getLinkedObject(true); } - return plc; + return nullptr; } + /*void Part::handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName, App::Property* prop) { diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 53b550ceda..4cf99e2774 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -104,6 +104,7 @@ public: std::string handleOneSideOfJoint(App::DocumentObject* joint, const char* propObjLinkName, + const char* propPartName, const char* propPlcName); void jointParts(std::vector joints); std::vector getJoints(); @@ -124,6 +125,7 @@ public: void swapJCS(App::DocumentObject* joint); void setNewPlacements(); + void redrawJointPlacements(std::vector joints); void recomputeJointPlacements(std::vector joints); bool isPartConnected(App::DocumentObject* obj); @@ -141,9 +143,10 @@ public: JointType getJointType(App::DocumentObject* joint); const char* getElementFromProp(App::DocumentObject* obj, const char* propName); std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); - Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); App::DocumentObject* + getObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); + App::DocumentObject* getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); private: diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 9c32514b13..77cc7bb9bd 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -205,8 +205,6 @@ App::DocumentObject* ViewProviderAssembly::getActivePart() 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; diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 52514de94a..d29a0f3101 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -428,11 +428,11 @@ class Joint: plc = obj.Placement.inverse() * plc # change plc to be relative to the origin of the document. - global_plc = UtilsAssembly.getGlobalPlacement(obj, part) - plc = global_plc * plc + # global_plc = UtilsAssembly.getGlobalPlacement(obj, part) + # plc = global_plc * plc # change plc to be relative to the assembly. - plc = assembly.Placement.inverse() * plc + # plc = assembly.Placement.inverse() * plc # We apply rotation / reverse / offset it necessary, but only to the second JCS. if isSecond: @@ -621,7 +621,12 @@ class ViewProviderJoint: return camera.height.getValue() / 20 - def set_JCS_placement(self, soTransform, placement): + def set_JCS_placement(self, soTransform, placement, objName, part): + # change plc to be relative to the origin of the document. + obj = UtilsAssembly.getObjectInPart(objName, part) + global_plc = UtilsAssembly.getGlobalPlacement(obj, part) + placement = global_plc * placement + t = placement.Base soTransform.translation.setValue(t.x, t.y, t.z) @@ -632,20 +637,22 @@ class ViewProviderJoint: """If a property of the handled feature has changed we have the chance to handle this here""" # joint is the handled feature, prop is the name of the property that has changed if prop == "Placement1": - plc = joint.getPropertyByName("Placement1") - if joint.getPropertyByName("Object1"): + if joint.Object1: + plc = joint.Placement1 self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform1, plc) + + if joint.Part1: # prevent an unwanted call by assembly.isPartConnected + self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1) else: self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE if prop == "Placement2": - plc = joint.getPropertyByName("Placement2") - if joint.getPropertyByName("Object2"): + if joint.Object2: + plc = joint.Placement2 self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL - if self.areJCSReversed(joint): - plc = flipPlacement(plc, App.Vector(1, 0, 0)) - self.set_JCS_placement(self.transform2, plc) + # if self.areJCSReversed(joint): + # plc = flipPlacement(plc, App.Vector(1, 0, 0)) + self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE @@ -656,10 +663,10 @@ class ViewProviderJoint: sameDir = zaxis1.dot(zaxis2) > 0 return not sameDir - def showPreviewJCS(self, visible, placement=None): + def showPreviewJCS(self, visible, placement=None, objName="", part=None): if visible: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform3, placement) + self.set_JCS_placement(self.transform3, placement, objName, part) else: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE @@ -1110,16 +1117,17 @@ class TaskAssemblyCreateJoint(QtCore.QObject): ) isSecond = len(self.current_selection) == 1 - + objName = self.preselection_dict["object"].Name + part = self.preselection_dict["part"] placement = self.joint.Proxy.findPlacement( self.joint, - self.preselection_dict["object"].Name, - self.preselection_dict["part"], + objName, + part, self.preselection_dict["element_name"], self.preselection_dict["vertex_name"], isSecond, ) - self.joint.ViewObject.Proxy.showPreviewJCS(True, placement) + self.joint.ViewObject.Proxy.showPreviewJCS(True, placement, objName, part) self.previewJCSVisible = True # 3D view keyboard handler @@ -1156,6 +1164,9 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.current_selection.append(selection_dict) self.updateJoint() + # We hide the preview JCS if we just added to the selection + self.joint.ViewObject.Proxy.showPreviewJCS(False) + 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) From 43019a8f345b55ff7b4238df6c048c9d51c186a7 Mon Sep 17 00:00:00 2001 From: Paddle Date: Thu, 21 Dec 2023 17:20:22 +0100 Subject: [PATCH 06/22] Assembly: Add support for body subobjects (coordinate systems, datums...) and various fixes --- src/Mod/Assembly/App/AssemblyObject.cpp | 1 + src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 10 +++++ src/Mod/Assembly/JointObject.py | 6 +-- src/Mod/Assembly/UtilsAssembly.py | 43 +++++++++++++------ 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 882e1e959d..14538ab331 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -807,6 +807,7 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Object1", "Part1", "Placement1"); std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Object2", "Part2", "Placement2"); + mbdJoint->setName(joint->getFullName()); mbdJoint->setMarkerI(fullMarkerName1); mbdJoint->setMarkerJ(fullMarkerName2); diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 77cc7bb9bd..8c469973e8 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -76,6 +76,16 @@ bool ViewProviderAssembly::doubleClicked() Gui::Application::Instance->activeDocument()->resetEdit(); } else { + // assure the PartDesign workbench + if (App::GetApplication() + .GetUserParameter() + .GetGroup("BaseApp") + ->GetGroup("Preferences") + ->GetGroup("Mod/Assembly") + ->GetBool("SwitchToWB", true)) { + Gui::Command::assureWorkbench("AssemblyWorkbench"); + } + // Part is not 'Active' so we enter edit mode to make it so. Gui::Application::Instance->activeDocument()->setEdit(this); } diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index d29a0f3101..7883e510e6 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -342,10 +342,8 @@ class Joint: return App.Placement() if not elt or not vtx: - # case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem. - plc = UtilsAssembly.getGlobalPlacement(obj, part) - plc = assembly.Placement.inverse() * plc - return plc + # case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem/Point/Line/Plane. + return App.Placement() elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt) vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx) diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 09b906b928..5f4f1a614f 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -131,7 +131,7 @@ def getObject(full_name): if linked_obj.TypeId == "PartDesign::Body": if i + 1 < len(names): obj2 = doc.getObject(names[i + 1]) - if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem": + if obj2 and isBodySubObject(obj2.TypeId): return obj2 return obj elif linked_obj.isDerivedFrom("Part::Feature"): @@ -147,7 +147,7 @@ def getObject(full_name): elif obj.TypeId == "PartDesign::Body": if i + 1 < len(names): obj2 = doc.getObject(names[i + 1]) - if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem": + if obj2 and isBodySubObject(obj2.TypeId): return obj2 return obj @@ -158,10 +158,20 @@ def getObject(full_name): return None +def isBodySubObject(typeId): + return ( + typeId == "Sketcher::SketchObject" + or typeId == "PartDesign::Point" + or typeId == "PartDesign::Line" + or typeId == "PartDesign::Plane" + or typeId == "PartDesign::CoordinateSystem" + ) + + def getContainingPart(full_name, selected_object): - # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" - # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" - # We want either Body or Box. + # full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1 + # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1 + # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.Sketch.Edge1" -> LinkOrBody if selected_object is None: App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None") return None @@ -183,10 +193,7 @@ def getContainingPart(full_name, selected_object): if obj == selected_object: return selected_object - if ( - obj.TypeId == "PartDesign::Body" - and selected_object.TypeId == "PartDesign::CoordinateSystem" - ): + if obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): if obj.hasObject(selected_object, True): return obj @@ -197,6 +204,9 @@ def getContainingPart(full_name, selected_object): elif obj.TypeId == "App::Link": linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): + if linked_obj.hasObject(selected_object, True): + return obj if linked_obj.TypeId == "App::Part": # linked_obj_doc = linked_obj.Document # selected_obj_in_doc = doc.getObject(selected_object.Name) @@ -214,7 +224,12 @@ def getObjectInPart(objName, part): if part.TypeId == "App::Link": part = part.getLinkedObject() - if part.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + if part.TypeId in { + "App::Part", + "Assembly::AssemblyObject", + "App::DocumentObjectGroup", + "PartDesign::Body", + }: for obji in part.OutList: if obji.Name == objName: return obji @@ -250,7 +265,7 @@ def getTargetPlacementRelativeTo( if foundPlacement is not None: return foundPlacement - elif part.TypeId == "App::Part" or part.TypeId == "Assembly::AssemblyObject": + elif part.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}: for obj in part.OutList: foundPlacement = getTargetPlacementRelativeTo( targetObj, obj, container, inContainerBranch @@ -267,7 +282,7 @@ def getTargetPlacementRelativeTo( elif part.TypeId == "App::Link": linked_obj = part.getLinkedObject() - if linked_obj.TypeId == "App::Part" or linked_obj.TypeId == "Assembly::AssemblyObject": + if linked_obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}: for obj in linked_obj.OutList: foundPlacement = getTargetPlacementRelativeTo( targetObj, obj, container, inContainerBranch @@ -299,8 +314,8 @@ def getElementName(full_name): # At minimum "Assembly.Box.edge16". It shouldn't be shorter return "" - # case of PartDesign::CoordinateSystem - if parts[-1] == "X" or parts[-1] == "Y" or parts[-1] == "Z": + # case of PartDesign datums : CoordinateSystem, point, line, plane + if parts[-1] in {"X", "Y", "Z", "Point", "Line", "Plane"}: return "" return parts[-1] From 48173f2b9d98c2ca1112c39673ac0f082a0e61be Mon Sep 17 00:00:00 2001 From: Paddle Date: Sat, 23 Dec 2023 21:21:22 +0100 Subject: [PATCH 07/22] Assembly: Implement special drag mode for revolute. --- src/Base/Vector3D.cpp | 16 + src/Base/Vector3D.h | 3 + src/Gui/View3DInventorViewer.cpp | 102 ++++ src/Gui/View3DInventorViewer.h | 6 + src/Mod/Assembly/App/AssemblyObject.cpp | 453 ++++++++++++++---- src/Mod/Assembly/App/AssemblyObject.h | 53 +- src/Mod/Assembly/App/AssemblyUtils.cpp | 249 ++++++++++ src/Mod/Assembly/App/AssemblyUtils.h | 72 +++ src/Mod/Assembly/App/CMakeLists.txt | 1 + src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 265 ++++++++-- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 21 +- src/Mod/Assembly/JointObject.py | 63 ++- 12 files changed, 1121 insertions(+), 183 deletions(-) create mode 100644 src/Mod/Assembly/App/AssemblyUtils.cpp create mode 100644 src/Mod/Assembly/App/AssemblyUtils.h diff --git a/src/Base/Vector3D.cpp b/src/Base/Vector3D.cpp index 88a8a37397..4d6b049634 100644 --- a/src/Base/Vector3D.cpp +++ b/src/Base/Vector3D.cpp @@ -464,6 +464,22 @@ float_type Vector3::GetAngle(const Vector3& rcVect) const return float_type(acos(dot)); } +template +float_type Vector3::GetAngleOriented(const Vector3& rcVect, const Vector3& norm) const +{ + float_type angle = GetAngle(rcVect); + + Vector3 crossProduct = Cross(rcVect); + + // Use dot product to determine the sign + float_type dot = crossProduct.Dot(norm); + if (dot < 0) { + angle = 2 * traits_type::pi() - angle; + } + + return angle; +} + template void Vector3::TransformToCoordinateSystem(const Vector3& rclBase, const Vector3& rclDirX, diff --git a/src/Base/Vector3D.h b/src/Base/Vector3D.h index de9b2d19c7..926c87aaf2 100644 --- a/src/Base/Vector3D.h +++ b/src/Base/Vector3D.h @@ -194,6 +194,9 @@ public: bool IsNull() const; /// Get angle between both vectors. The returned value lies in the interval [0,pi]. float_type GetAngle(const Vector3& rcVect) const; + /// Get oriented angle between both vectors using a normal. The returned value lies in the + /// interval [0,2*pi]. + float_type GetAngleOriented(const Vector3& rcVect, const Vector3& norm) const; /** Transforms this point to the coordinate system defined by origin \a rclBase, * vector \a vector rclDirX and vector \a vector rclDirY. * \note \a rclDirX must be perpendicular to \a rclDirY, i.e. \a rclDirX * \a rclDirY = 0.. diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index f713e476b1..065327606d 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -2647,6 +2647,108 @@ SbVec2f View3DInventorViewer::getNormalizedPosition(const SbVec2s& pnt) const return {pX, pY}; } +SbVec3f View3DInventorViewer::getPointOnXYPlaneOfPlacement(const SbVec2s& pnt, Base::Placement& plc) const +{ + SbVec2f pnt2d = getNormalizedPosition(pnt); + SoCamera* pCam = this->getSoRenderManager()->getCamera(); + + if (!pCam) { + // return invalid point + return {}; + } + + SbViewVolume vol = pCam->getViewVolume(); + SbLine line; + vol.projectPointToLine(pnt2d, line); + + // Calculate the plane using plc + Base::Rotation rot = plc.getRotation(); + Base::Vector3d normalVector = rot.multVec(Base::Vector3d(0, 0, 1)); + SbVec3f planeNormal(normalVector.x, normalVector.y, normalVector.z); + + // Get the position and convert Base::Vector3d to SbVec3f + Base::Vector3d pos = plc.getPosition(); + SbVec3f planePosition(pos.x, pos.y, pos.z); + SbPlane xyPlane(planeNormal, planePosition); + + SbVec3f pt; + if (xyPlane.intersect(line, pt)) { + return pt; // Intersection point on the XY plane + } + else { + // No intersection found + return {}; + } + + return pt; +} + +SbVec3f projectPointOntoPlane(const SbVec3f& point, const SbPlane& plane) { + SbVec3f planeNormal = plane.getNormal(); + float d = plane.getDistanceFromOrigin(); + float distance = planeNormal.dot(point) + d; + return point - planeNormal * distance; +} + +// Project a line onto a plane +SbLine projectLineOntoPlane(const SbVec3f& p1, const SbVec3f& p2, const SbPlane& plane) { + SbVec3f projectedPoint1 = projectPointOntoPlane(p1, plane); + SbVec3f projectedPoint2 = projectPointOntoPlane(p2, plane); + return SbLine(projectedPoint1, projectedPoint2); +} + +SbVec3f intersection(const SbVec3f& p11, const SbVec3f& p12, const SbVec3f& p21, const SbVec3f& p22) +{ + SbVec3f da = p12 - p11; + SbVec3f db = p22 - p21; + SbVec3f dc = p21 - p11; + + double s = (dc.cross(db)).dot(da.cross(db)) / da.cross(db).sqrLength(); + return p11 + da * s; +} + +SbVec3f View3DInventorViewer::getPointOnLine(const SbVec2s& pnt, const SbVec3f& axisCenter, const SbVec3f& axis) const +{ + SbVec2f pnt2d = getNormalizedPosition(pnt); + SoCamera* pCam = this->getSoRenderManager()->getCamera(); + + if (!pCam) { + // return invalid point + return {}; + } + + // First we get pnt projection on the focal plane + SbViewVolume vol = pCam->getViewVolume(); + + float nearDist = pCam->nearDistance.getValue(); + float farDist = pCam->farDistance.getValue(); + float focalDist = pCam->focalDistance.getValue(); + + if (focalDist < nearDist || focalDist > farDist) { + focalDist = 0.5F * (nearDist + farDist); // NOLINT + } + + SbLine line; + SbVec3f pt, ptOnFocalPlaneAndOnLine, ptOnFocalPlane; + SbPlane focalPlane = vol.getPlane(focalDist); + vol.projectPointToLine(pnt2d, line); + focalPlane.intersect(line, ptOnFocalPlane); + + SbLine projectedLine = projectLineOntoPlane(axisCenter, axisCenter + axis, focalPlane); + ptOnFocalPlaneAndOnLine = projectedLine.getClosestPoint(ptOnFocalPlane); + + // now we need the intersection point between + // - the line passing by ptOnFocalPlaneAndOnLine normal to focalPlane + // - The line (axisCenter, axisCenter + axis) + + // Line normal to focal plane through ptOnFocalPlane + SbLine normalLine(ptOnFocalPlane, ptOnFocalPlane + focalPlane.getNormal()); + SbLine axisLine(axisCenter, axisCenter + axis); + pt = intersection(ptOnFocalPlane, ptOnFocalPlane + focalPlane.getNormal(), axisCenter, axisCenter + axis); + + return pt; +} + SbVec3f View3DInventorViewer::getPointOnFocalPlane(const SbVec2s& pnt) const { SbVec2f pnt2d = getNormalizedPosition(pnt); diff --git a/src/Gui/View3DInventorViewer.h b/src/Gui/View3DInventorViewer.h index 5efa45de4f..221ad4ba95 100644 --- a/src/Gui/View3DInventorViewer.h +++ b/src/Gui/View3DInventorViewer.h @@ -318,6 +318,12 @@ public: /** Returns the 3d point on the focal plane to the given 2d point. */ SbVec3f getPointOnFocalPlane(const SbVec2s&) const; + /** Returns the 3d point on a line to the given 2d point. */ + SbVec3f getPointOnLine(const SbVec2s&, const SbVec3f& axisCenter, const SbVec3f& axis) const; + + /** Returns the 3d point on the XY plane of a placement to the given 2d point. */ + SbVec3f getPointOnXYPlaneOfPlacement(const SbVec2s&, Base::Placement&) const; + /** Returns the 2d coordinates on the viewport to the given 3d point. */ SbVec2s getPointOnViewport(const SbVec3f&) const; diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 14538ab331..506613640c 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -49,6 +49,7 @@ #include #include +#include #include #include @@ -80,103 +81,6 @@ namespace PartApp = Part; using namespace Assembly; using namespace MbD; -// ======================================= Utils ====================================== - -Base::Placement 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; -} -/* // Currently unused -Base::Placement* getTargetPlacementRelativeTo( - App::DocumentObject* targetObj, App::DocumentObject* part, App::DocumentObject* container, - bool inContainerBranch, bool ignorePlacement = false) -{ - inContainerBranch = inContainerBranch || (!ignorePlacement && part == container); - - Base::Console().Warning("sub --------------\n"); - if (targetObj == part && inContainerBranch && !ignorePlacement) { - Base::Console().Warning("found0\n"); - return &getPlacementFromProp(targetObj, "Placement"); - } - - if (auto group = dynamic_cast(part)) { - for (auto& obj : group->getOutList()) { - auto foundPlacement = getTargetPlacementRelativeTo( - targetObj, obj, container, inContainerBranch, ignorePlacement - ); - if (foundPlacement != nullptr) { - return foundPlacement; - } - } - } - else if (auto assembly = dynamic_cast(part)) { - Base::Console().Warning("h3\n"); - for (auto& obj : assembly->getOutList()) { - auto foundPlacement = getTargetPlacementRelativeTo( - targetObj, obj, container, inContainerBranch - ); - if (foundPlacement == nullptr) { - continue; - } - - if (!ignorePlacement) { - *foundPlacement = getPlacementFromProp(part, "Placement") * *foundPlacement; - } - - Base::Console().Warning("found\n"); - return foundPlacement; - } - } - else if (auto link = dynamic_cast(part)) { - Base::Console().Warning("h4\n"); - auto linked_obj = link->getLinkedObject(); - - if (dynamic_cast(linked_obj) || dynamic_cast(linked_obj)) { - for (auto& obj : linked_obj->getOutList()) { - auto foundPlacement = getTargetPlacementRelativeTo( - targetObj, obj, container, inContainerBranch - ); - if (foundPlacement == nullptr) { - continue; - } - - *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; - return foundPlacement; - } - } - - auto foundPlacement = getTargetPlacementRelativeTo( - targetObj, linked_obj, container, inContainerBranch, true - ); - - if (foundPlacement != nullptr && !ignorePlacement) { - *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; - } - - Base::Console().Warning("found2\n"); - return foundPlacement; - } - - return nullptr; -} - -Base::Placement getGlobalPlacement(App::DocumentObject* targetObj, App::DocumentObject* container = -nullptr) { bool inContainerBranch = container == nullptr; auto rootObjects = -App::GetApplication().getActiveDocument()->getRootObjects(); for (auto& part : rootObjects) { auto -foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch); if -(foundPlacement != nullptr) { Base::Placement plc(foundPlacement->toMatrix()); return plc; - } - } - - return Base::Placement(); -} -*/ - // ================================ Assembly Object ============================ PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) @@ -196,7 +100,90 @@ PyObject* AssemblyObject::getPyObject() return Py::new_reference_to(PythonObject); } -std::vector AssemblyObject::getJoints() +std::vector AssemblyObject::getJointsOfObj(App::DocumentObject* obj) +{ + std::vector joints = getJoints(false); + std::vector jointsOf; + + for (auto joint : joints) { + App::DocumentObject* obj1 = getObjFromNameProp(joint, "object1", "Part1"); + App::DocumentObject* obj2 = getObjFromNameProp(joint, "Object2", "Part2"); + if (obj == obj1 || obj == obj2) { + jointsOf.push_back(obj); + } + } + + return jointsOf; +} + +std::vector AssemblyObject::getJointsOfPart(App::DocumentObject* part) +{ + std::vector joints = getJoints(false); + std::vector jointsOf; + + for (auto joint : joints) { + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + if (part == part1 || part == part2) { + jointsOf.push_back(joint); + } + } + + return jointsOf; +} + +App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::DocumentObject* part, + std::string& name) +{ + std::vector joints = getJointsOfPart(part); + + for (auto joint : joints) { + if (!joint) { + continue; + } + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + if (!part1 || !part2) { + continue; + } + + if (part == part1 && isJointConnectingPartToGround(joint, "Part1")) { + name = "Part1"; + return joint; + } + if (part == part2 && isJointConnectingPartToGround(joint, "Part2")) { + name = "Part2"; + return joint; + } + } + + return nullptr; +} + +bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname) +{ + auto* propPart = dynamic_cast(joint->getPropertyByName(propname)); + if (!propPart) { + return false; + } + App::DocumentObject* part = propPart->getValue(); + // Check if the part is disconnected even with the joint + bool isConnected = isPartConnected(part); + if (!isConnected) { + return false; + } + + // now we disconnect this joint temporarily + propPart->setValue(nullptr); + + isConnected = isPartConnected(part); + + propPart->setValue(part); + + return !isConnected; +} + +std::vector AssemblyObject::getJoints(bool updateJCS) { std::vector joints = {}; @@ -221,7 +208,9 @@ std::vector AssemblyObject::getJoints() } // Make sure the joints are up to date. - recomputeJointPlacements(joints); + if (updateJCS) { + recomputeJointPlacements(joints); + } return joints; } @@ -318,10 +307,23 @@ AssemblyObject::getConnectedParts(App::DocumentObject* part, return connectedParts; } -bool AssemblyObject::isPartConnected(App::DocumentObject* obj) +bool AssemblyObject::isPartGrounded(App::DocumentObject* obj) { std::vector groundedObjs = fixGroundedParts(); - std::vector joints = getJoints(); + + for (auto* groundedObj : groundedObjs) { + if (groundedObj->getFullName() == obj->getFullName()) { + return true; + } + } + + return false; +} + +bool AssemblyObject::isPartConnected(App::DocumentObject* obj) +{ + std::vector groundedObjs = getGroundedParts(); + std::vector joints = getJoints(false); std::set connectedParts; @@ -344,6 +346,81 @@ bool AssemblyObject::isPartConnected(App::DocumentObject* obj) return false; } +std::vector AssemblyObject::getDownstreamParts(App::DocumentObject* part, + int limit) +{ + if (limit > 1000) { // Inifinite loop protection + return {}; + } + limit++; + + std::vector downstreamParts = {part}; + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + std::vector jointsOfPart = getJointsOfPart(part); + + // remove connectingJoint from jointsOfPart + auto it = std::remove(jointsOfPart.begin(), jointsOfPart.end(), connectingJoint); + jointsOfPart.erase(it, jointsOfPart.end()); + for (auto joint : jointsOfPart) { + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + App::DocumentObject* downstreamPart = + part->getFullName() == part1->getFullName() ? part2 : part1; + + std::vector subDownstreamParts = + getDownstreamParts(downstreamPart, limit); + for (auto downPart : subDownstreamParts) { + if (std::find(downstreamParts.begin(), downstreamParts.end(), downPart) + == downstreamParts.end()) { + downstreamParts.push_back(downPart); + } + } + } + return downstreamParts; +} + +std::vector AssemblyObject::getUpstreamParts(App::DocumentObject* part, + int limit) +{ + if (limit > 1000) { // Inifinite loop protection + return {}; + } + limit++; + + if (isPartGrounded(part)) { + return {part}; + } + + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + App::DocumentObject* upPart = + getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); + + std::vector upstreamParts = getUpstreamParts(upPart, limit); + upstreamParts.push_back(part); + return upstreamParts; +} + +App::DocumentObject* AssemblyObject::getUpstreamMovingPart(App::DocumentObject* part) +{ + if (isPartGrounded(part)) { + return nullptr; + } + + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + JointType jointType = getJointType(connectingJoint); + if (jointType != JointType::Fixed) { + return part; + } + + App::DocumentObject* upPart = + getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); + + return getUpstreamMovingPart(upPart); +} + JointGroup* AssemblyObject::getJointGroup() { App::Document* doc = getDocument(); @@ -361,6 +438,27 @@ JointGroup* AssemblyObject::getJointGroup() return nullptr; } +std::vector AssemblyObject::getGroundedParts() +{ + std::vector groundedJoints = getGroundedJoints(); + + std::vector groundedObjs; + for (auto gJoint : groundedJoints) { + if (!gJoint) { + continue; + } + + auto* propObj = + dynamic_cast(gJoint->getPropertyByName("ObjectToGround")); + + if (propObj) { + App::DocumentObject* objToGround = propObj->getValue(); + groundedObjs.push_back(objToGround); + } + } + return groundedObjs; +} + std::vector AssemblyObject::fixGroundedParts() { std::vector groundedJoints = getGroundedJoints(); @@ -831,7 +929,13 @@ std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, if (obj->getNameInDocument() != part->getNameInDocument()) { // Make plc relative to the containing part - plc = objPlc * plc; + // plc = objPlc * plc; // this would not work for nested parts. + + Base::Placement obj_global_plc = getGlobalPlacement(obj, part); + plc = obj_global_plc * plc; + + Base::Placement part_global_plc = getGlobalPlacement(part); + plc = part_global_plc.inverse() * plc; } std::string markerName = joint->getFullName(); @@ -1196,7 +1300,149 @@ double AssemblyObject::getEdgeRadius(App::DocumentObject* obj, const char* elt) return 0.0; } -// getters to get from properties + +// ======================================= Utils ====================================== + +void printPlacement(Base::Placement plc, const char* name) +{ + Base::Vector3d pos = plc.getPosition(); + Base::Vector3d axis; + double angle; + Base::Rotation rot = plc.getRotation(); + rot.getRawValue(axis, angle); + Base::Console().Warning( + "placement %s : position (%.1f, %.1f, %.1f) - axis (%.1f, %.1f, %.1f) angle %.1f\n", + name, + pos.x, + pos.y, + pos.z, + axis.x, + axis.y, + axis.z, + angle); +} + +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; +} + +bool AssemblyObject::getTargetPlacementRelativeTo(Base::Placement& foundPlc, + App::DocumentObject* targetObj, + App::DocumentObject* part, + App::DocumentObject* container, + bool inContainerBranch, + bool ignorePlacement) +{ + inContainerBranch = inContainerBranch || (!ignorePlacement && part == container); + + if (targetObj == part && inContainerBranch && !ignorePlacement) { + foundPlc = getPlacementFromProp(targetObj, "Placement"); + return true; + } + + if (part->isDerivedFrom(App::DocumentObjectGroup::getClassTypeId())) { + for (auto& obj : part->getOutList()) { + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + obj, + container, + inContainerBranch, + ignorePlacement); + if (found) { + return true; + } + } + } + else if (part->isDerivedFrom(Assembly::AssemblyObject::getClassTypeId()) + || part->isDerivedFrom(App::Part::getClassTypeId()) + || part->isDerivedFrom(PartDesign::Body::getClassTypeId())) { + for (auto& obj : part->getOutList()) { + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + obj, + container, + inContainerBranch); + if (!found) { + continue; + } + + if (!ignorePlacement) { + foundPlc = getPlacementFromProp(part, "Placement") * foundPlc; + } + + return true; + } + } + else if (auto link = dynamic_cast(part)) { + auto linked_obj = link->getLinkedObject(); + + if (dynamic_cast(linked_obj) || dynamic_cast(linked_obj)) { + for (auto& obj : linked_obj->getOutList()) { + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + obj, + container, + inContainerBranch); + if (!found) { + continue; + } + + foundPlc = getPlacementFromProp(link, "Placement") * foundPlc; + return true; + } + } + + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + linked_obj, + container, + inContainerBranch, + true); + + if (found) { + if (!ignorePlacement) { + foundPlc = getPlacementFromProp(link, "Placement") * foundPlc; + } + + return true; + } + } + + return false; +} + +Base::Placement AssemblyObject::getGlobalPlacement(App::DocumentObject* targetObj, + App::DocumentObject* container) +{ + bool inContainerBranch = (container == nullptr); + auto rootObjects = App::GetApplication().getActiveDocument()->getRootObjects(); + for (auto& part : rootObjects) { + Base::Placement foundPlc; + bool found = + getTargetPlacementRelativeTo(foundPlc, targetObj, part, container, inContainerBranch); + if (found) { + return foundPlc; + } + } + + return Base::Placement(); +} + +Base::Placement AssemblyObject::getGlobalPlacement(App::DocumentObject* joint, + const char* targetObj, + const char* container) +{ + App::DocumentObject* obj = getObjFromNameProp(joint, targetObj, container); + App::DocumentObject* part = getLinkObjFromProp(joint, container); + return getGlobalPlacement(obj, part); +} + double AssemblyObject::getJointDistance(App::DocumentObject* joint) { double distance = 0.0; @@ -1248,6 +1494,7 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi { auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); if (!propObj) { + Base::Console().Warning("getLinkObjFromProp nullptr\n"); return nullptr; } return propObj->getValue(); diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 4cf99e2774..2f870ad09f 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -107,10 +107,16 @@ public: const char* propPartName, const char* propPlcName); void jointParts(std::vector joints); - std::vector getJoints(); + std::vector getJoints(bool updateJCS = true); + std::vector getJointsOfObj(App::DocumentObject* obj); + std::vector getJointsOfPart(App::DocumentObject* part); + App::DocumentObject* getJointOfPartConnectingToGround(App::DocumentObject* part, + std::string& name); + bool isJointConnectingPartToGround(App::DocumentObject* joint, const char* partPropName); std::vector getGroundedJoints(); void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); std::vector fixGroundedParts(); + std::vector getGroundedParts(); void removeUnconnectedJoints(std::vector& joints, std::vector groundedObjs); @@ -128,7 +134,11 @@ public: void redrawJointPlacements(std::vector joints); void recomputeJointPlacements(std::vector joints); - bool isPartConnected(App::DocumentObject* obj); + bool isPartGrounded(App::DocumentObject* part); + bool isPartConnected(App::DocumentObject* part); + std::vector getDownstreamParts(App::DocumentObject* part, int limit = 0); + std::vector getUpstreamParts(App::DocumentObject* part, int limit = 0); + App::DocumentObject* getUpstreamMovingPart(App::DocumentObject* part); double getObjMass(App::DocumentObject* obj); void setObjMasses(std::vector> objectMasses); @@ -138,16 +148,6 @@ public: double getFaceRadius(App::DocumentObject* obj, const char* elName); double getEdgeRadius(App::DocumentObject* obj, const char* elName); - // getters to get from properties - double getJointDistance(App::DocumentObject* joint); - JointType getJointType(App::DocumentObject* joint); - const char* getElementFromProp(App::DocumentObject* obj, const char* propName); - std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); - App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); - App::DocumentObject* - getObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); - App::DocumentObject* - getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); private: std::shared_ptr mbdAssembly; @@ -159,6 +159,35 @@ private: // void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property // *prop) override; + +public: + // ---------------- Utils ------------------- + // Can't put the functions by themselves in AssemblyUtils.cpp : + // see https://forum.freecad.org/viewtopic.php?p=729577#p729577 + + // getters to get from properties + static double getJointDistance(App::DocumentObject* joint); + static JointType getJointType(App::DocumentObject* joint); + static const char* getElementFromProp(App::DocumentObject* obj, const char* propName); + static std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); + static App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, + const char* propName); + static App::DocumentObject* + getObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); + static App::DocumentObject* + getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); + static Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); + static bool getTargetPlacementRelativeTo(Base::Placement& foundPlc, + App::DocumentObject* targetObj, + App::DocumentObject* part, + App::DocumentObject* container, + bool inContainerBranch, + bool ignorePlacement = false); + static Base::Placement getGlobalPlacement(App::DocumentObject* targetObj, + App::DocumentObject* container = nullptr); + static Base::Placement getGlobalPlacement(App::DocumentObject* joint, + const char* targetObj, + const char* container = ""); }; // using AssemblyObjectPython = App::FeaturePythonT; diff --git a/src/Mod/Assembly/App/AssemblyUtils.cpp b/src/Mod/Assembly/App/AssemblyUtils.cpp new file mode 100644 index 0000000000..4c495628ac --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyUtils.cpp @@ -0,0 +1,249 @@ +// 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 +#include +#include +#include + +#include + +#include "AssemblyUtils.h" + +// ======================================= Utils ====================================== +/* +namespace Assembly +{ + +Base::Placement 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; +} + +/* // Currently unused +Base::Placement* getTargetPlacementRelativeTo( + App::DocumentObject* targetObj, App::DocumentObject* part, App::DocumentObject* container, + bool inContainerBranch, bool ignorePlacement = false) +{ + inContainerBranch = inContainerBranch || (!ignorePlacement && part == container); + + Base::Console().Warning("sub --------------\n"); + if (targetObj == part && inContainerBranch && !ignorePlacement) { + Base::Console().Warning("found0\n"); + return &getPlacementFromProp(targetObj, "Placement"); + } + + if (auto group = dynamic_cast(part)) { + for (auto& obj : group->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch, ignorePlacement + ); + if (foundPlacement != nullptr) { + return foundPlacement; + } + } + } + else if (auto assembly = dynamic_cast(part)) { + Base::Console().Warning("h3\n"); + for (auto& obj : assembly->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ); + if (foundPlacement == nullptr) { + continue; + } + + if (!ignorePlacement) { + *foundPlacement = getPlacementFromProp(part, "Placement") * *foundPlacement; + } + + Base::Console().Warning("found\n"); + return foundPlacement; + } + } + else if (auto link = dynamic_cast(part)) { + Base::Console().Warning("h4\n"); + auto linked_obj = link->getLinkedObject(); + + if (dynamic_cast(linked_obj) || dynamic_cast(linked_obj)) { + for (auto& obj : linked_obj->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ); + if (foundPlacement == nullptr) { + continue; + } + + *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; + return foundPlacement; + } + } + + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, linked_obj, container, inContainerBranch, true + ); + + if (foundPlacement != nullptr && !ignorePlacement) { + *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; + } + + Base::Console().Warning("found2\n"); + return foundPlacement; + } + + return nullptr; +} + +Base::Placement getGlobalPlacement(App::DocumentObject* targetObj, App::DocumentObject* container = +nullptr) { bool inContainerBranch = container == nullptr; auto rootObjects = +App::GetApplication().getActiveDocument()->getRootObjects(); for (auto& part : rootObjects) { auto +foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch); if +(foundPlacement != nullptr) { Base::Placement plc(foundPlacement->toMatrix()); return plc; + } + } + + return Base::Placement(); +} +*/ +/* +double getJointDistance(App::DocumentObject* joint) +{ + double distance = 0.0; + + auto* prop = dynamic_cast(joint->getPropertyByName("Distance")); + if (prop) { + distance = prop->getValue(); + } + + return distance; +} + +JointType getJointType(App::DocumentObject* joint) +{ + JointType jointType = JointType::Fixed; + + auto* prop = dynamic_cast(joint->getPropertyByName("JointType")); + if (prop) { + jointType = static_cast(prop->getValue()); + } + + return jointType; +} + +const char* getElementFromProp(App::DocumentObject* obj, const char* propName) +{ + auto* prop = dynamic_cast(obj->getPropertyByName(propName)); + if (!prop) { + return ""; + } + + return prop->getValue(); +} + +std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName) +{ + // The prop is going to be something like 'Edge14' or 'Face7'. We need 'Edge' or 'Face' + std::string elementType; + for (char ch : std::string(getElementFromProp(obj, propName))) { + if (std::isalpha(ch)) { + elementType += ch; + } + } + return elementType; +} + +App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, + const char* propLinkName) +{ + auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); + if (!propObj) { + return nullptr; + } + return propObj->getValue(); +} + +App::DocumentObject* getObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) +{ + auto* propObjName = dynamic_cast(joint->getPropertyByName(pObjName)); + if (!propObjName) { + return nullptr; + } + std::string objName = std::string(propObjName->getValue()); + + App::DocumentObject* containingPart = getLinkObjFromProp(joint, pPart); + if (!containingPart) { + return nullptr; + } + + if (objName == containingPart->getNameInDocument()) { + return containingPart; + } + + if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { + App::Link* link = dynamic_cast(containingPart); + + containingPart = link->getLinkedObject(); + if (!containingPart) { + return nullptr; + } + } + + for (auto obj : containingPart->getOutList()) { + if (objName == obj->getNameInDocument()) { + return obj; + } + } + + return nullptr; +} + +App::DocumentObject* getLinkedObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) + { + auto* obj = getObjFromNameProp(joint, pObjName, pPart); + if (obj) { + return obj->getLinkedObject(true); + } + return nullptr; + } + +} // namespace Assembly +*/ diff --git a/src/Mod/Assembly/App/AssemblyUtils.h b/src/Mod/Assembly/App/AssemblyUtils.h new file mode 100644 index 0000000000..c5851fe30a --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyUtils.h @@ -0,0 +1,72 @@ +// 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_AssemblyUtils_H +#define ASSEMBLY_AssemblyUtils_H + + +#include + +#include +#include + +namespace App +{ +class DocumentObject; +} // namespace App + +namespace Base +{ +class Placement; +} // 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, + Distance +}; + +// getters to get from properties +double getJointDistance(App::DocumentObject* joint); +JointType getJointType(App::DocumentObject* joint); +const char* getElementFromProp(App::DocumentObject* obj, const char* propName); +std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); +App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); +App::DocumentObject* getObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const +char* pPart); App::DocumentObject* getLinkedObjFromNameProp(App::DocumentObject* joint, const char* +pObjName, const char* pPart); Base::Placement getPlacementFromProp(App::DocumentObject* obj, const +char* propName);*/ + +} // namespace Assembly + + +#endif // ASSEMBLY_AssemblyUtils_H diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt index fc314577af..724f22e1f8 100644 --- a/src/Mod/Assembly/App/CMakeLists.txt +++ b/src/Mod/Assembly/App/CMakeLists.txt @@ -12,6 +12,7 @@ link_directories(${OCC_LIBRARY_DIR}) set(Assembly_LIBS Part + PartDesign FreeCADApp OndselSolver ) diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 8c469973e8..174d46e6d6 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -51,6 +52,25 @@ using namespace Assembly; using namespace AssemblyGui; +void printPlacement(Base::Placement plc, const char* name) +{ + Base::Vector3d pos = plc.getPosition(); + Base::Vector3d axis; + double angle; + Base::Rotation rot = plc.getRotation(); + rot.getRawValue(axis, angle); + Base::Console().Warning( + "placement %s : position (%.1f, %.1f, %.1f) - axis (%.1f, %.1f, %.1f) angle %.1f\n", + name, + pos.x, + pos.y, + pos.z, + axis.x, + axis.y, + axis.z, + angle); +} + PROPERTY_SOURCE(AssemblyGui::ViewProviderAssembly, Gui::ViewProviderPart) ViewProviderAssembly::ViewProviderAssembly() @@ -76,7 +96,7 @@ bool ViewProviderAssembly::doubleClicked() Gui::Application::Instance->activeDocument()->resetEdit(); } else { - // assure the PartDesign workbench + // assure the Assembly workbench if (App::GetApplication() .GetUserParameter() .GetGroup("BaseApp") @@ -113,11 +133,13 @@ bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const Gui::Command::openCommand(tr("Delete associated joints").toStdString().c_str()); for (auto joint : allJoints) { - // Assume getLinkObjFromProp can return nullptr if the property doesn't exist. - App::DocumentObject* obj1 = assemblyPart->getLinkObjFromProp(joint, "Part1"); - App::DocumentObject* obj2 = assemblyPart->getLinkObjFromProp(joint, "Part2"); - App::DocumentObject* obj3 = assemblyPart->getLinkObjFromProp(joint, "ObjectToGround"); - if (obj == obj1 || obj == obj2 || obj == obj3) { + // getLinkObjFromProp returns nullptr if the property doesn't exist. + App::DocumentObject* obj1 = AssemblyObject::getObjFromNameProp(joint, "Object1", "Part1"); + App::DocumentObject* obj2 = AssemblyObject::getObjFromNameProp(joint, "Object2", "Part2"); + App::DocumentObject* part1 = AssemblyObject::getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = AssemblyObject::getLinkObjFromProp(joint, "Part2"); + App::DocumentObject* obj3 = AssemblyObject::getLinkObjFromProp(joint, "ObjectToGround"); + if (obj == obj1 || obj == obj2 || obj == part1 || obj == part2 || obj == obj3) { if (!prompted) { prompted = true; QMessageBox msgBox; @@ -166,24 +188,25 @@ bool ViewProviderAssembly::setEdit(int ModNum) void ViewProviderAssembly::unsetEdit(int ModNum) { Q_UNUSED(ModNum); - canStartDragging = false; partMoving = false; docsToMove = {}; // Check if the view is still active before trying to deactivate the assembly. - auto activeDoc = Gui::Application::Instance->activeDocument(); - if (!activeDoc) { + auto doc = getDocument(); + if (!doc) { return; } - auto activeView = activeDoc->getActiveView(); + auto activeView = doc->getActiveView(); if (!activeView) { return; } // Set the part as not 'Activated' ie not bold in the tree. Gui::Command::doCommand(Gui::Command::Gui, - "Gui.ActiveDocument.ActiveView.setActiveObject('%s', None)", + "appDoc = App.getDocument('%s')\n" + "Gui.getDocument(appDoc).ActiveView.setActiveObject('%s', None)", + this->getObject()->getDocument()->getName(), PARTKEY); } @@ -220,28 +243,106 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent canStartDragging = false; if (enableMovement && getSelectedObjectsWithinAssembly()) { - SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos); - Base::Vector3d mousePosition = Base::Vector3d(vec[0], vec[1], vec[2]); + moveMode = findMoveMode(); - initMove(mousePosition); + SbVec3f vec; + if (moveMode == MoveMode::RotationOnPlane + || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + vec = viewer->getPointOnXYPlaneOfPlacement(cursorPos, jcsGlobalPlc); + initialPositionRot = Base::Vector3d(vec[0], vec[1], vec[2]); + } + + if (moveMode == MoveMode::TranslationOnAxis + || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + Base::Vector3d zAxis = + jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., 1.)); + Base::Vector3d pos = jcsGlobalPlc.getPosition(); + SbVec3f axisCenter(pos.x, pos.y, pos.z); + SbVec3f axis(zAxis.x, zAxis.y, zAxis.z); + vec = viewer->getPointOnLine(cursorPos, axisCenter, axis); + initialPosition = Base::Vector3d(vec[0], vec[1], vec[2]); + } + else if (moveMode != MoveMode::RotationOnPlane) { + vec = viewer->getPointOnFocalPlane(cursorPos); + initialPosition = Base::Vector3d(vec[0], vec[1], vec[2]); + } + + initMove(); } } // Do the dragging of parts if (partMoving) { - SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos); - Base::Vector3d mousePosition = Base::Vector3d(vec[0], vec[1], vec[2]); + Base::Vector3d newPos, newPosRot; + if (moveMode == MoveMode::RotationOnPlane + || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + SbVec3f vec = viewer->getPointOnXYPlaneOfPlacement(cursorPos, jcsGlobalPlc); + newPosRot = Base::Vector3d(vec[0], vec[1], vec[2]); + } + + if (moveMode == MoveMode::TranslationOnAxis + || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + Base::Vector3d zAxis = jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., 1.)); + Base::Vector3d pos = jcsGlobalPlc.getPosition(); + SbVec3f axisCenter(pos.x, pos.y, pos.z); + SbVec3f axis(zAxis.x, zAxis.y, zAxis.z); + SbVec3f vec = viewer->getPointOnLine(cursorPos, axisCenter, axis); + newPos = Base::Vector3d(vec[0], vec[1], vec[2]); + } + else if (moveMode != MoveMode::RotationOnPlane) { + SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos); + newPos = 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); + Base::Placement plc = pair.second; + // Base::Console().Warning("newPos %f %f %f\n", newPos.x, newPos.y, newPos.z); + + if (moveMode == MoveMode::RotationOnPlane) { + Base::Vector3d center = jcsGlobalPlc.getPosition(); + Base::Vector3d norm = + jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., -1.)); + double angle = + (newPosRot - center).GetAngleOriented(initialPositionRot - center, norm); + // Base::Console().Warning("angle %f\n", angle); + Base::Rotation zRotation = Base::Rotation(Base::Vector3d(0., 0., 1.), angle); + Base::Placement rotatedGlovalJcsPlc = + jcsGlobalPlc * Base::Placement(Base::Vector3d(), zRotation); + Base::Placement jcsPlcRelativeToPart = plc.inverse() * jcsGlobalPlc; + plc = rotatedGlovalJcsPlc * jcsPlcRelativeToPart.inverse(); + } + else if (moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + Base::Vector3d pos = plc.getPosition() + (newPos - initialPosition); + plc.setPosition(pos); + + Base::Placement newJcsGlobalPlc = jcsGlobalPlc; + newJcsGlobalPlc.setPosition(jcsGlobalPlc.getPosition() + + (newPos - initialPosition)); + Base::Vector3d center = newJcsGlobalPlc.getPosition(); + Base::Vector3d norm = + newJcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., -1.)); + + Base::Vector3d projInitialPositionRot = + initialPositionRot.ProjectToPlane(newJcsGlobalPlc.getPosition(), norm); + double angle = + (newPosRot - center).GetAngleOriented(initialPositionRot - center, norm); + // Base::Console().Warning("angle %f\n", angle); + Base::Rotation zRotation = Base::Rotation(Base::Vector3d(0., 0., 1.), angle); + Base::Placement rotatedGlovalJcsPlc = + newJcsGlobalPlc * Base::Placement(Base::Vector3d(), zRotation); + Base::Placement jcsPlcRelativeToPart = plc.inverse() * newJcsGlobalPlc; + plc = rotatedGlovalJcsPlc * jcsPlcRelativeToPart.inverse(); + } + else { + Base::Vector3d pos = newPos + (plc.getPosition() - initialPosition); + plc.setPosition(pos); + } + propPlacement->setValue(plc); } } @@ -315,9 +416,7 @@ bool ViewProviderAssembly::getSelectedObjectsWithinAssembly() auto* propPlacement = dynamic_cast(obj->getPropertyByName("Placement")); if (propPlacement) { - Base::Placement plc = propPlacement->getValue(); - Base::Vector3d pos = plc.getPosition(); - docsToMove.emplace_back(obj, pos); + docsToMove.emplace_back(obj, propPlacement->getValue()); } } } @@ -334,25 +433,21 @@ bool ViewProviderAssembly::getSelectedObjectsWithinAssembly() 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 (preselectedObj && 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); - } + if (!alreadyIn) { + auto* propPlacement = dynamic_cast( + preselectedObj->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(preselectedObj, propPlacement->getValue()); } } } @@ -387,13 +482,12 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vector bodyOrLink should be the moving - // entity partOrLink.bodyOrLink.pad.face1 -> partOrLink should be the - // moving entity partOrLink.box.face1 -> partOrLink should - // be the moving entity partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> partOrLink1 - // should be the moving entity assembly1.partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> - // partOrLink1 should be the moving entity assembly1.boxOrLink1.face1 -> boxOrLink1 should be - // the moving entity + // bodyOrLink.pad.face1 -> bodyOrLink should be the moving entity + // partOrLink.bodyOrLink.pad.face1 -> partOrLink should be the moving entity + // partOrLink.box.face1 -> partOrLink should be the moving entity + // partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> partOrLink1 should be the moving entity + // assembly1.partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> partOrLink1 should be the moving + // entity assembly1.boxOrLink1.face1 -> boxOrLink1 should be the moving entity for (auto objName : subNames) { App::DocumentObject* obj = appDoc->getObject(objName.c_str()); @@ -429,7 +523,77 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vectorgetObject(objName.c_str()); } -void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition) +ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() +{ + if (docsToMove.size() == 1) { + auto* assemblyPart = static_cast(getObject()); + std::string partPropName; + App::DocumentObject* joint = + assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); + + if (!joint) { + return MoveMode::Translation; + } + + JointType jointType = AssemblyObject::getJointType(joint); + if (jointType == JointType::Fixed) { + // If fixed joint we need to find the upstream joint to find move mode. + // For example : Gnd -(revolute)- A -(fixed)- B : if user try to move B, then we should + // actually move A + App::DocumentObject* upstreamPart = + assemblyPart->getUpstreamMovingPart(docsToMove[0].first); + + docsToMove.clear(); + auto* propPlacement = + dynamic_cast(upstreamPart->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(upstreamPart, propPlacement->getValue()); + } + + joint = + assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); + if (!joint) { + return MoveMode::Translation; + } + jointType = AssemblyObject::getJointType(joint); + } + + const char* plcPropName = (partPropName == "Part1") ? "Placement1" : "Placement2"; + const char* objPropName = (partPropName == "Part1") ? "Object1" : "Object2"; + + // jcsPlc is relative to the Object + jcsPlc = AssemblyObject::getPlacementFromProp(joint, plcPropName); + + // Make jcsGlobalPlc relative to the origin of the doc + Base::Placement global_plc = + AssemblyObject::getGlobalPlacement(joint, objPropName, partPropName.c_str()); + jcsGlobalPlc = global_plc * jcsPlc; + + // Add downstream parts so that they move together + auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first); + docsToMove.clear(); // current [0] is added by the recursive getDownstreamParts. + for (auto part : downstreamParts) { + auto* propPlacement = + dynamic_cast(part->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(part, propPlacement->getValue()); + } + } + + if (jointType == JointType::Revolute) { + return MoveMode::RotationOnPlane; + } + else if (jointType == JointType::Slider) { + return MoveMode::TranslationOnAxis; + } + else if (jointType == JointType::Cylindrical) { + return MoveMode::TranslationOnAxisAndRotationOnePlane; + } + } + return MoveMode::Translation; +} + +void ViewProviderAssembly::initMove() { Gui::Command::openCommand(tr("Move part").toStdString().c_str()); partMoving = true; @@ -446,7 +610,6 @@ void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition) objectMasses.clear(); for (auto& pair : docsToMove) { - pair.second = pair.second - mousePosition; objectMasses.push_back({pair.first, 10.0}); } diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 32b6d8f848..151c1ff0e1 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -70,6 +70,16 @@ public: App::DocumentObject* getActivePart(); + enum class MoveMode + { + Translation, + TranslationOnAxis, + Rotation, + RotationOnPlane, + TranslationOnAxisAndRotationOnePlane, + }; + MoveMode moveMode; + /// 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 @@ -77,10 +87,11 @@ public: bool pressed, const SbVec2s& cursorPos, const Gui::View3DInventorViewer* viewer) override; - - void initMove(Base::Vector3d& mousePosition); + MoveMode findMoveMode(); + void initMove(); void endMove(); + bool getSelectedObjectsWithinAssembly(); App::DocumentObject* getObjectFromSubNames(std::vector& subNames); std::vector parseSubNames(std::string& subNamesStr); @@ -107,9 +118,13 @@ public: bool partMoving; bool enableMovement; int numberOfSel; + Base::Vector3d initialPosition; + Base::Vector3d initialPositionRot; + Base::Placement jcsPlc; + Base::Placement jcsGlobalPlc; std::vector> objectMasses; - std::vector> docsToMove; + std::vector> docsToMove; }; } // namespace AssemblyGui diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 7883e510e6..ff9f9d15b8 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -299,6 +299,7 @@ class Joint: joint.Placement2 = self.findPlacement( joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) + self.preventOrthogonal(joint) solveIfAllowed(assembly, True) else: @@ -446,21 +447,42 @@ class Joint: return plc def applyRotationToPlacement(self, plc, angle): + return self.applyRotationToPlacementAlongAxis(plc, angle, App.Vector(0, 0, 1)) + + def applyRotationToPlacementAlongAxis(self, plc, angle, axis): rot = plc.Rotation - zRotation = App.Rotation(App.Vector(0, 0, 1), angle) - rot = rot.multiply(zRotation) - plc.Rotation = rot + zRotation = App.Rotation(axis, angle) + plc.Rotation = rot * zRotation return plc def flipPart(self, joint): if joint.FirstPartConnected: - plc = joint.Part2.Placement.inverse() * joint.Placement2 - localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) - joint.Part2.Placement = flipPlacement(joint.Part2.Placement, localXAxis) + plc = joint.Placement2 # relative to obj + obj = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) + + # we need plc to be relative to the containing part + obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part2) + part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part2) + plc = obj_global_plc * plc + plc = part_global_plc.inverse() * plc + + jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) + + joint.Part2.Placement = flipPlacement(joint.Part2.Placement, jcsXAxis) + else: - plc = joint.Part1.Placement.inverse() * joint.Placement1 - localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) - joint.Part1.Placement = flipPlacement(joint.Part1.Placement, localXAxis) + plc = joint.Placement1 # relative to obj + obj = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) + + # we need plc to be relative to the containing part + obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part1) + part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part1) + plc = obj_global_plc * plc + plc = part_global_plc.inverse() * plc + + jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) + + joint.Part1.Placement = flipPlacement(joint.Part1.Placement, jcsXAxis) solveIfAllowed(self.getAssembly(joint)) @@ -486,6 +508,19 @@ class Joint: return App.Vector(res[0].X, res[0].Y, res[0].Z) return surface.Center + def preventOrthogonal(self, joint): + zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) + zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) + if abs(zAxis1.dot(zAxis2)) < Part.Precision.confusion(): + if joint.FirstPartConnected: + joint.Part2.Placement = self.applyRotationToPlacementAlongAxis( + joint.Part2.Placement, 30.0, App.Vector(1, 2, 0) + ) + else: + joint.Part1.Placement = self.applyRotationToPlacementAlongAxis( + joint.Part1.Placement, 30.0, App.Vector(1, 2, 0) + ) + class ViewProviderJoint: def __init__(self, vobj): @@ -639,8 +674,7 @@ class ViewProviderJoint: plc = joint.Placement1 self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL - if joint.Part1: # prevent an unwanted call by assembly.isPartConnected - self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1) + self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1) else: self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE @@ -650,15 +684,16 @@ class ViewProviderJoint: self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL # if self.areJCSReversed(joint): # plc = flipPlacement(plc, App.Vector(1, 0, 0)) + self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE def areJCSReversed(self, joint): - zaxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) - zaxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) + zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) + zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) - sameDir = zaxis1.dot(zaxis2) > 0 + sameDir = zAxis1.dot(zAxis2) > 0 return not sameDir def showPreviewJCS(self, visible, placement=None, objName="", part=None): From 672b04c0c43e7813c9656004d52ede69a561790f Mon Sep 17 00:00:00 2001 From: howie-j <82042580+howie-j@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:16:04 +0100 Subject: [PATCH 08/22] Assembly: Fixes FreeCAD#10748 (Card 4: Elements should highlight) --- src/Mod/Assembly/JointObject.py | 41 +++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index ff9f9d15b8..a30854a380 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -566,7 +566,11 @@ class ViewProviderJoint: self.switch_JCS2 = self.JCS_sep(self.transform2) self.switch_JCS_preview = self.JCS_sep(self.transform3) - self.display_mode = coin.SoGroup() + self.pick = coin.SoPickStyle() + self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE) + + self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() + self.display_mode.addChild(self.pick) self.display_mode.addChild(self.switch_JCS1) self.display_mode.addChild(self.switch_JCS2) self.display_mode.addChild(self.switch_JCS_preview) @@ -577,12 +581,8 @@ class ViewProviderJoint: self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF) def JCS_sep(self, soTransform): - pick = coin.SoPickStyle() - pick.style.setValue(coin.SoPickStyle.UNPICKABLE) - JCS = coin.SoAnnotation() JCS.addChild(soTransform) - JCS.addChild(pick) base_plane_sep = self.plane_sep(0.4, 15) X_axis_sep = self.line_sep([0.5, 0, 0], [1, 0, 0], self.x_axis_so_color) @@ -632,11 +632,11 @@ class ViewProviderJoint: draw_style.style = coin.SoDrawStyle.FILLED material = coin.SoMaterial() - material.diffuseColor.setValue([1, 1, 1]) - material.ambientColor.setValue([1, 1, 1]) - material.specularColor.setValue([1, 1, 1]) - material.emissiveColor.setValue([1, 1, 1]) - material.transparency.setValue(0.7) + material.diffuseColor.setValue([0.5, 0.5, 0.5]) + material.ambientColor.setValue([0.5, 0.5, 0.5]) + material.specularColor.setValue([0.5, 0.5, 0.5]) + material.emissiveColor.setValue([0.5, 0.5, 0.5]) + material.transparency.setValue(0.3) face_sep = coin.SoAnnotation() face_sep.addChild(self.axisScale) @@ -703,6 +703,13 @@ class ViewProviderJoint: else: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE + def setPickableState(self, state: bool): + """Set JCS selectable or unselectable in 3D view""" + if not state: + self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE) + else: + self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP) + def getDisplayModes(self, obj): """Return a list of display modes.""" modes = [] @@ -941,6 +948,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.toggleRotationVisibility() self.toggleReverseVisibility() + self.setJointsPickableState(False) + Gui.Selection.addSelectionGate( MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve ) @@ -983,6 +992,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): Gui.Selection.clearSelection() self.view.removeEventCallback("SoLocation2Event", self.callbackMove) self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey) + self.setJointsPickableState(True) if Gui.Control.activeDialog(): Gui.Control.closeDialog() @@ -1245,3 +1255,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def clearSelection(self, doc_name): self.current_selection.clear() self.updateJoint() + + def setJointsPickableState(self, state: bool): + """Make all joints in assembly selectable (True) or unselectable (False) in 3D view""" + try: + jointGroup = UtilsAssembly.getJointGroup(self.assembly) + for joint in jointGroup.Group: + if hasattr(joint, "Part1"): + joint.ViewObject.Proxy.setPickableState(state) + except Exception as e: + s = "" if state else "un" + App.Console.PrintWarning(f"Failed to set joints {s}pickable: {e}") From d13df26333b62dc7cfce7e972d128627bc9885db Mon Sep 17 00:00:00 2001 From: Paddle Date: Mon, 8 Jan 2024 22:12:42 +0100 Subject: [PATCH 09/22] Assembly: Adds a pre-solve when creating joint, preventing wrong orthogonal solutions from solver. --- src/Mod/Assembly/App/AssemblyObject.cpp | 108 ++++++-- src/Mod/Assembly/App/AssemblyObject.h | 5 +- src/Mod/Assembly/AssemblyImport.py | 4 +- src/Mod/Assembly/CommandCreateAssembly.py | 26 +- src/Mod/Assembly/CommandCreateJoint.py | 13 +- src/Mod/Assembly/CommandExportASMT.py | 4 +- src/Mod/Assembly/CommandInsertLink.py | 4 +- src/Mod/Assembly/CommandSolveAssembly.py | 4 +- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 34 ++- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 2 + .../Gui/ViewProviderAssemblyPyImp.cpp | 2 +- src/Mod/Assembly/Init.py | 4 +- src/Mod/Assembly/InitGui.py | 4 +- src/Mod/Assembly/JointObject.py | 230 ++++++++++++------ src/Mod/Assembly/Preferences.py | 4 +- src/Mod/Assembly/TestAssemblyWorkbench.py | 4 +- src/Mod/Assembly/UtilsAssembly.py | 38 ++- 17 files changed, 367 insertions(+), 123 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 506613640c..624196f3af 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -162,6 +162,7 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::Docum bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname) { + auto* propPart = dynamic_cast(joint->getPropertyByName(propname)); if (!propPart) { return false; @@ -173,14 +174,32 @@ bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, c return false; } - // now we disconnect this joint temporarily - propPart->setValue(nullptr); + // to know if a joint is connecting to ground we disable all the other joints + std::vector jointsOfPart = getJointsOfPart(part); + std::vector activatedStates; + + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName()) { + continue; + } + + activatedStates.push_back(getJointActivated(jointi)); + setJointActivated(jointi, false); + } isConnected = isPartConnected(part); - propPart->setValue(part); + // restore activation states + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) { + continue; + } - return !isConnected; + setJointActivated(jointi, activatedStates[0]); + activatedStates.erase(activatedStates.begin()); + } + + return isConnected; } std::vector AssemblyObject::getJoints(bool updateJCS) @@ -193,16 +212,20 @@ std::vector AssemblyObject::getJoints(bool updateJCS) } Base::PyGILStateLocker lock; - for (auto obj : jointGroup->getObjects()) { - if (!obj) { + for (auto joint : jointGroup->getObjects()) { + if (!joint) { continue; } - auto proxy = dynamic_cast(obj->getPropertyByName("Proxy")); + auto* prop = dynamic_cast(joint->getPropertyByName("Activated")); + if (prop && !prop->getValue()) { + continue; + } + + auto proxy = dynamic_cast(joint->getPropertyByName("Proxy")); if (proxy) { - Py::Object joint = proxy->getValue(); - if (joint.hasAttr("setJointConnectors")) { - joints.push_back(obj); + if (proxy->getValue().hasAttr("setJointConnectors")) { + joints.push_back(joint); } } } @@ -347,16 +370,37 @@ bool AssemblyObject::isPartConnected(App::DocumentObject* obj) } std::vector AssemblyObject::getDownstreamParts(App::DocumentObject* part, - int limit) + App::DocumentObject* joint) { - if (limit > 1000) { // Inifinite loop protection + // First we deactivate the joint + bool state = getJointActivated(joint); + setJointActivated(joint, false); + + std::vector joints = getJoints(false); + + std::set connectedParts = {part}; + traverseAndMarkConnectedParts(part, connectedParts, joints); + + std::vector downstreamParts; + for (auto parti : connectedParts) { + if (!isPartConnected(parti) && (parti != part)) { + downstreamParts.push_back(parti); + } + } + + AssemblyObject::setJointActivated(joint, state); + /*if (limit > 1000) { // Inifinite loop protection return {}; } limit++; + Base::Console().Warning("limit %d\n", limit); std::vector downstreamParts = {part}; std::string name; - App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + App::DocumentObject* connectingJoint = + getJointOfPartConnectingToGround(part, + name); // ?????????????????????????????? if we remove + // connection to ground then it can't work for tom std::vector jointsOfPart = getJointsOfPart(part); // remove connectingJoint from jointsOfPart @@ -365,8 +409,23 @@ std::vector AssemblyObject::getDownstreamParts(App::Docume for (auto joint : jointsOfPart) { App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); - App::DocumentObject* downstreamPart = - part->getFullName() == part1->getFullName() ? part2 : part1; + bool firstIsDown = part->getFullName() == part2->getFullName(); + App::DocumentObject* downstreamPart = firstIsDown ? part1 : part2; + + Base::Console().Warning("looping\n"); + // it is possible that the part is connected to ground by this joint. + // In which case we should not select those parts. To test we disconnect : + auto* propObj = dynamic_cast(joint->getPropertyByName("Part1")); + if (!propObj) { + continue; + } + propObj->setValue(nullptr); + bool isConnected = isPartConnected(downstreamPart); + propObj->setValue(part1); + if (isConnected) { + Base::Console().Warning("continue\n"); + continue; + } std::vector subDownstreamParts = getDownstreamParts(downstreamPart, limit); @@ -376,7 +435,7 @@ std::vector AssemblyObject::getDownstreamParts(App::Docume downstreamParts.push_back(downPart); } } - } + }*/ return downstreamParts; } @@ -1322,6 +1381,22 @@ void printPlacement(Base::Placement plc, const char* name) angle); } +void AssemblyObject::setJointActivated(App::DocumentObject* joint, bool val) +{ + auto* propActivated = dynamic_cast(joint->getPropertyByName("Activated")); + if (propActivated) { + propActivated->setValue(val); + } +} +bool AssemblyObject::getJointActivated(App::DocumentObject* joint) +{ + auto* propActivated = dynamic_cast(joint->getPropertyByName("Activated")); + if (propActivated) { + return propActivated->getValue(); + } + return false; +} + Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) { Base::Placement plc = Base::Placement(); @@ -1494,7 +1569,6 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi { auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); if (!propObj) { - Base::Console().Warning("getLinkObjFromProp nullptr\n"); return nullptr; } return propObj->getValue(); diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 2f870ad09f..7bd7a51b78 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -136,7 +136,8 @@ public: bool isPartGrounded(App::DocumentObject* part); bool isPartConnected(App::DocumentObject* part); - std::vector getDownstreamParts(App::DocumentObject* part, int limit = 0); + std::vector getDownstreamParts(App::DocumentObject* part, + App::DocumentObject* joint); std::vector getUpstreamParts(App::DocumentObject* part, int limit = 0); App::DocumentObject* getUpstreamMovingPart(App::DocumentObject* part); @@ -166,6 +167,8 @@ public: // see https://forum.freecad.org/viewtopic.php?p=729577#p729577 // getters to get from properties + static void setJointActivated(App::DocumentObject* joint, bool val); + static bool getJointActivated(App::DocumentObject* joint); static double getJointDistance(App::DocumentObject* joint); static JointType getJointType(App::DocumentObject* joint); static const char* getElementFromProp(App::DocumentObject* obj, const char* propName); diff --git a/src/Mod/Assembly/AssemblyImport.py b/src/Mod/Assembly/AssemblyImport.py index be4ffb8b46..5379122a46 100644 --- a/src/Mod/Assembly/AssemblyImport.py +++ b/src/Mod/Assembly/AssemblyImport.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ def open(filename): diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index 6c6cfd702a..0f22d29a29 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App @@ -28,6 +28,9 @@ from PySide.QtCore import QT_TRANSLATE_NOOP if App.GuiUp: import FreeCADGui as Gui +import UtilsAssembly +import Preferences + # translate = App.Qt.translate __title__ = "Assembly Command Create Assembly" @@ -46,19 +49,32 @@ class CommandCreateAssembly: "Accel": "A", "ToolTip": QT_TRANSLATE_NOOP( "Assembly_CreateAssembly", - "Create an assembly object in the current document.", + "Create an assembly object in the current document or if in the current active assembly if any. One root assembly per file max.", ), "CmdType": "ForEdit", } def IsActive(self): + if Preferences.preferences().GetBool("EnforceOneAssemblyRule", True): + activeAssembly = UtilsAssembly.activeAssembly() + + if UtilsAssembly.isThereOneRootAssembly() and not activeAssembly: + return False + return App.ActiveDocument is not None def Activated(self): App.setActiveTransaction("Create assembly") - assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") + + activeAssembly = UtilsAssembly.activeAssembly() + if activeAssembly: + assembly = activeAssembly.newObject("Assembly::AssemblyObject", "Assembly") + else: + assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") + assembly.Type = "Assembly" - Gui.ActiveDocument.setEdit(assembly) + if not activeAssembly: + Gui.ActiveDocument.setEdit(assembly) assembly.newObject("Assembly::JointGroup", "Joints") App.closeActiveTransaction() diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 765de4c994..09ef98048a 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import os import FreeCAD as App @@ -254,16 +254,15 @@ class CommandToggleGrounded: # 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: - - full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub) - obj = UtilsAssembly.getObject(full_element_name) - part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj) - # Only objects within the assembly. objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(sel.ObjectName, sub) if assembly.Name not in objs_names: continue + full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub) + obj = UtilsAssembly.getObject(full_element_name) + part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj) + # Check if part is grounded and if so delete the joint. for joint in joint_group.Group: if ( diff --git a/src/Mod/Assembly/CommandExportASMT.py b/src/Mod/Assembly/CommandExportASMT.py index b32734f388..c175e28c53 100644 --- a/src/Mod/Assembly/CommandExportASMT.py +++ b/src/Mod/Assembly/CommandExportASMT.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App import UtilsAssembly diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index f70cd8732b..70e19b11df 100644 --- a/src/Mod/Assembly/CommandInsertLink.py +++ b/src/Mod/Assembly/CommandInsertLink.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import re import os diff --git a/src/Mod/Assembly/CommandSolveAssembly.py b/src/Mod/Assembly/CommandSolveAssembly.py index b332f742d2..fec765b682 100644 --- a/src/Mod/Assembly/CommandSolveAssembly.py +++ b/src/Mod/Assembly/CommandSolveAssembly.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import os import FreeCAD as App diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 174d46e6d6..2c90bcd1ac 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -117,7 +117,6 @@ bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const { Base::Console().Warning("ViewProviderAssembly::canDragObject\n"); if (!obj || obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { - Base::Console().Warning("so should be false...\n"); return false; } @@ -245,6 +244,10 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent if (enableMovement && getSelectedObjectsWithinAssembly()) { moveMode = findMoveMode(); + if (moveMode == MoveMode::None) { + return false; + } + SbVec3f vec; if (moveMode == MoveMode::RotationOnPlane || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { @@ -346,8 +349,13 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent } } - auto* assemblyPart = static_cast(getObject()); - assemblyPart->solve(); + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + bool solveOnMove = hGrp->GetBool("SolveOnMove", true); + if (solveOnMove) { + auto* assemblyPart = static_cast(getObject()); + assemblyPart->solve(); + } } return false; } @@ -542,8 +550,11 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() // actually move A App::DocumentObject* upstreamPart = assemblyPart->getUpstreamMovingPart(docsToMove[0].first); - docsToMove.clear(); + if (!upstreamPart) { + return MoveMode::None; + } + auto* propPlacement = dynamic_cast(upstreamPart->getPropertyByName("Placement")); if (propPlacement) { @@ -570,8 +581,7 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() jcsGlobalPlc = global_plc * jcsPlc; // Add downstream parts so that they move together - auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first); - docsToMove.clear(); // current [0] is added by the recursive getDownstreamParts. + auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first, joint); for (auto part : downstreamParts) { auto* propPlacement = dynamic_cast(part->getPropertyByName("Placement")); @@ -648,6 +658,18 @@ void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg) } } +bool ViewProviderAssembly::onDelete(const std::vector& subNames) +{ + // Delete the joingroup when assembly is deleted + for (auto obj : getObject()->getOutList()) { + if (obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { + obj->getDocument()->removeObject(obj->getNameInDocument()); + } + } + + return ViewProviderPart::onDelete(subNames); +} + PyObject* ViewProviderAssembly::getPyObject() { if (!pyViewObject) { diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 151c1ff0e1..727a934d9d 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -53,6 +53,7 @@ public: QIcon getIcon() const override; bool doubleClicked() override; + bool onDelete(const std::vector& subNames) override; /** @name enter/exit edit mode */ //@{ @@ -77,6 +78,7 @@ public: Rotation, RotationOnPlane, TranslationOnAxisAndRotationOnePlane, + None, }; MoveMode moveMode; diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp index 31a4090a65..5d6cffe4f1 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp @@ -18,7 +18,7 @@ * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * - ***************************************************************************/ + **************************************************************************/ #include "PreCompiled.h" diff --git a/src/Mod/Assembly/Init.py b/src/Mod/Assembly/Init.py index e8af397da3..350b5238a1 100644 --- a/src/Mod/Assembly/Init.py +++ b/src/Mod/Assembly/Init.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ # Get the Parameter Group of this module ParGrp = App.ParamGet("System parameter:Modules").GetGroup("Assembly") diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index 6968a2a350..d8493837a3 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import Assembly_rc diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index a30854a380..6a7d31e31b 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import math @@ -79,12 +79,6 @@ def solveIfAllowed(assembly, storePrev=False): assembly.solve(storePrev) -def flipPlacement(plc, localXAxis): - flipRot = App.Rotation(localXAxis, 180) - plc.Rotation = plc.Rotation.multiply(flipRot) - return plc - - class Joint: def __init__(self, joint, type_index, assembly): self.Type = "Joint" @@ -230,13 +224,14 @@ class Joint: joint.addProperty( "App::PropertyBool", - "FirstPartConnected", + "Activated", "Joint", QT_TRANSLATE_NOOP( "App::Property", - "This indicate if the first part was connected to ground at the time of joint creation.", + "This indicate if the joint is active.", ), ) + joint.Activated = True self.setJointConnectors(joint, []) @@ -275,7 +270,7 @@ class Joint: if len(current_selection) >= 1: joint.Part1 = None - joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"]) + self.part1Connected = assembly.isPartConnected(current_selection[0]["part"]) joint.Object1 = current_selection[0]["object"].Name joint.Part1 = current_selection[0]["part"] @@ -290,8 +285,12 @@ class Joint: joint.Element1 = "" joint.Vertex1 = "" joint.Placement1 = App.Placement() + self.partMovedByPresolved = None if len(current_selection) >= 2: + joint.Part2 = None + self.part2Connected = assembly.isPartConnected(current_selection[1]["part"]) + joint.Object2 = current_selection[1]["object"].Name joint.Part2 = current_selection[1]["part"] joint.Element2 = current_selection[1]["element_name"] @@ -299,7 +298,13 @@ class Joint: joint.Placement2 = self.findPlacement( joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) - self.preventOrthogonal(joint) + self.preSolve( + joint, + current_selection[0]["object"], + joint.Part1, + current_selection[1]["object"], + joint.Part2, + ) solveIfAllowed(assembly, True) else: @@ -309,6 +314,7 @@ class Joint: joint.Vertex2 = "" joint.Placement2 = App.Placement() assembly.undoSolve() + self.undoPreSolve() def updateJCSPlacements(self, joint): if not joint.Detach1: @@ -455,34 +461,29 @@ class Joint: plc.Rotation = rot * zRotation return plc - def flipPart(self, joint): - if joint.FirstPartConnected: - plc = joint.Placement2 # relative to obj - obj = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) + def flipPlacement(self, plc): + return self.applyRotationToPlacementAlongAxis(plc, 180, App.Vector(1, 0, 0)) - # we need plc to be relative to the containing part - obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part2) - part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part2) - plc = obj_global_plc * plc - plc = part_global_plc.inverse() * plc - - jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) - - joint.Part2.Placement = flipPlacement(joint.Part2.Placement, jcsXAxis) + def flipOnePart(self, joint): + if hasattr(self, "part2Connected") and not self.part2Connected: + jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement2, joint.Object2, joint.Part2 + ) + globalJcsPlc = UtilsAssembly.getJcsGlobalPlc( + joint.Placement2, joint.Object2, joint.Part2 + ) + jcsPlc = self.flipPlacement(jcsPlc) + joint.Part2.Placement = globalJcsPlc * jcsPlc.inverse() else: - plc = joint.Placement1 # relative to obj - obj = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) - - # we need plc to be relative to the containing part - obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part1) - part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part1) - plc = obj_global_plc * plc - plc = part_global_plc.inverse() * plc - - jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) - - joint.Part1.Placement = flipPlacement(joint.Part1.Placement, jcsXAxis) + jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement1, joint.Object1, joint.Part1 + ) + globalJcsPlc = UtilsAssembly.getJcsGlobalPlc( + joint.Placement1, joint.Object1, joint.Part1 + ) + jcsPlc = self.flipPlacement(jcsPlc) + joint.Part1.Placement = globalJcsPlc * jcsPlc.inverse() solveIfAllowed(self.getAssembly(joint)) @@ -508,18 +509,55 @@ class Joint: return App.Vector(res[0].X, res[0].Y, res[0].Z) return surface.Center - def preventOrthogonal(self, joint): - zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) - zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) - if abs(zAxis1.dot(zAxis2)) < Part.Precision.confusion(): - if joint.FirstPartConnected: - joint.Part2.Placement = self.applyRotationToPlacementAlongAxis( - joint.Part2.Placement, 30.0, App.Vector(1, 2, 0) - ) - else: - joint.Part1.Placement = self.applyRotationToPlacementAlongAxis( - joint.Part1.Placement, 30.0, App.Vector(1, 2, 0) - ) + def preSolve(self, joint, obj1, part1, obj2, part2): + # The goal of this is to put the part in the correct position to avoid wrong placement by the solve. + + # we actually don't want to match perfectly the JCS, it is best to match them + # in the current closest direction, ie either matched or flipped. + sameDir = self.areJcsSameDir(joint) + + if hasattr(self, "part2Connected") and not self.part2Connected: + self.partMovedByPresolved = joint.Part2 + self.presolveBackupPlc = joint.Part2.Placement + + globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc( + joint.Placement1, joint.Object1, joint.Part1 + ) + jcsPlc2 = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement2, joint.Object2, joint.Part2 + ) + if not sameDir: + jcsPlc2 = self.flipPlacement(jcsPlc2) + joint.Part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() + + elif hasattr(self, "part1Connected") and not self.part1Connected: + self.partMovedByPresolved = joint.Part1 + self.presolveBackupPlc = joint.Part1.Placement + + globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc( + joint.Placement2, joint.Object2, joint.Part2 + ) + jcsPlc1 = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement1, joint.Object1, joint.Part1 + ) + if not sameDir: + jcsPlc1 = self.flipPlacement(jcsPlc1) + joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse() + + def undoPreSolve( + self, + ): + if self.partMovedByPresolved: + self.partMovedByPresolved.Placement = self.presolveBackupPlc + self.partMovedByPresolved = None + + def areJcsSameDir(self, joint): + globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1) + globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2) + + zAxis1 = globalJcsPlc1.Rotation.multVec(App.Vector(0, 0, 1)) + zAxis2 = globalJcsPlc2.Rotation.multVec(App.Vector(0, 0, 1)) + return zAxis1.dot(zAxis2) > 0 class ViewProviderJoint: @@ -567,7 +605,7 @@ class ViewProviderJoint: self.switch_JCS_preview = self.JCS_sep(self.transform3) self.pick = coin.SoPickStyle() - self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE) + self.setPickableState(True) self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() self.display_mode.addChild(self.pick) @@ -682,20 +720,11 @@ class ViewProviderJoint: if joint.Object2: plc = joint.Placement2 self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL - # if self.areJCSReversed(joint): - # plc = flipPlacement(plc, App.Vector(1, 0, 0)) self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE - def areJCSReversed(self, joint): - zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) - zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) - - sameDir = zAxis1.dot(zAxis2) > 0 - return not sameDir - def showPreviewJCS(self, visible, placement=None, objName="", part=None): if visible: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL @@ -763,7 +792,7 @@ class ViewProviderJoint: def doubleClicked(self, vobj): assembly = vobj.Object.InList[0] if UtilsAssembly.activeAssembly() != assembly: - Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + Gui.ActiveDocument.setEdit(assembly) panel = TaskAssemblyCreateJoint(0, vobj.Object) Gui.Control.showDialog(panel) @@ -925,16 +954,19 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged) self.form.PushButtonReverse.clicked.connect(self.onReverseClicked) - Gui.Selection.clearSelection() - if jointObj: + Gui.Selection.clearSelection() + self.creating = False self.joint = jointObj self.jointName = jointObj.Label App.setActiveTransaction("Edit " + self.jointName + " Joint") self.updateTaskboxFromJoint() + self.visibilityBackup = self.joint.Visibility + self.joint.Visibility = True else: + self.creating = True self.jointName = self.form.jointType.currentText().replace(" ", "") App.setActiveTransaction("Create " + self.jointName + " Joint") @@ -942,6 +974,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.preselection_dict = None self.createJointObject() + self.visibilityBackup = False + self.handleInitialSelection() self.toggleDistanceVisibility() self.toggleOffsetVisibility() @@ -964,13 +998,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): 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() solveIfAllowed(self.assembly) + self.joint.Visibility = self.visibilityBackup App.closeActiveTransaction() return True @@ -978,6 +1009,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def reject(self): self.deactivate() App.closeActiveTransaction(True) + if not self.creating: # update visibility only if we are editing the joint + self.joint.Visibility = self.visibilityBackup return True def deactivate(self): @@ -996,6 +1029,64 @@ class TaskAssemblyCreateJoint(QtCore.QObject): if Gui.Control.activeDialog(): Gui.Control.closeDialog() + def handleInitialSelection(self): + selection = Gui.Selection.getSelectionEx("*", 0) + if not selection: + return + for sel in selection: + # If you select 2 solids (bodies for example) within an assembly. + # There'll be a single sel but 2 SubElementNames. + + if not sel.SubElementNames: + # no subnames, so its a root assembly itself that is selected. + Gui.Selection.removeSelection(sel.Object) + continue + + for sub_name in sel.SubElementNames: + # Only objects within the assembly. + objs_names, element_name = UtilsAssembly.getObjsNamesAndElement( + sel.ObjectName, sub_name + ) + if len(self.current_selection) >= 2 or self.assembly.Name not in objs_names: + Gui.Selection.removeSelection(sel.Object, sub_name) + continue + + obj_name = sel.ObjectName + + 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) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) + + if selected_object == self.assembly: + # do not accept selection of assembly itself + Gui.Selection.removeSelection(sel.Object, sub_name) + continue + + if ( + len(self.current_selection) == 1 + and selected_object == self.current_selection[0]["object"] + ): + # do not select several feature of the same object. + Gui.Selection.removeSelection(sel.Object, sub_name) + continue + + selection_dict = { + "object": selected_object, + "part": part_containing_selected_object, + "element_name": element_name, + "full_element_name": full_element_name, + "full_obj_name": full_obj_name, + "vertex_name": element_name, + } + + self.current_selection.append(selection_dict) + + self.updateJoint() + def createJointObject(self): type_index = self.form.jointType.currentIndex() @@ -1022,7 +1113,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.joint.Rotation = self.form.rotationSpinbox.property("rawValue") def onReverseClicked(self): - self.joint.Proxy.flipPart(self.joint) + self.joint.Proxy.flipOnePart(self.joint) def toggleDistanceVisibility(self): if self.form.jointType.currentText() in JointUsingDistance: @@ -1130,7 +1221,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def moveMouse(self, info): if len(self.current_selection) >= 2 or ( len(self.current_selection) == 1 - and self.current_selection[0]["part"] == self.preselection_dict["part"] + and ( + not self.preselection_dict + or self.current_selection[0]["part"] == self.preselection_dict["part"] + ) ): self.joint.ViewObject.Proxy.showPreviewJCS(False) return diff --git a/src/Mod/Assembly/Preferences.py b/src/Mod/Assembly/Preferences.py index f5d9b5e57d..71c60323ef 100644 --- a/src/Mod/Assembly/Preferences.py +++ b/src/Mod/Assembly/Preferences.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD import FreeCADGui diff --git a/src/Mod/Assembly/TestAssemblyWorkbench.py b/src/Mod/Assembly/TestAssemblyWorkbench.py index 331d645b1d..000241de0c 100644 --- a/src/Mod/Assembly/TestAssemblyWorkbench.py +++ b/src/Mod/Assembly/TestAssemblyWorkbench.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import TestApp diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 5f4f1a614f..6e6f4a6ea0 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App import Part @@ -237,6 +237,33 @@ def getObjectInPart(objName, part): return None +# get the placement of Obj relative to its containing Part +# Example : assembly.part1.part2.partn.body1 : placement of Obj relative to part1 +def getObjPlcRelativeToPart(objName, part): + obj = getObjectInPart(objName, part) + + # we need plc to be relative to the containing part + obj_global_plc = getGlobalPlacement(obj, part) + part_global_plc = getGlobalPlacement(part) + + return part_global_plc.inverse() * obj_global_plc + + +# Example : assembly.part1.part2.partn.body1 : jcsPlc is relative to body1 +# This function returns jcsPlc relative to part1 +def getJcsPlcRelativeToPart(jcsPlc, objName, part): + obj_relative_plc = getObjPlcRelativeToPart(objName, part) + return obj_relative_plc * jcsPlc + + +# Return the jcs global placement +def getJcsGlobalPlc(jcsPlc, objName, part): + obj = getObjectInPart(objName, part) + + obj_global_plc = getGlobalPlacement(obj, part) + return obj_global_plc * jcsPlc + + # The container is used to support cases where the same object appears at several places # which happens when you have a link to a part. def getGlobalPlacement(targetObj, container=None): @@ -249,6 +276,13 @@ def getGlobalPlacement(targetObj, container=None): return App.Placement() +def isThereOneRootAssembly(): + for part in App.activeDocument().RootObjects: + if part.TypeId == "Assembly::AssemblyObject": + return True + return False + + def getTargetPlacementRelativeTo( targetObj, part, container, inContainerBranch, ignorePlacement=False ): From a0749888c4635ce4c705108f87c0b47256431d4b Mon Sep 17 00:00:00 2001 From: Paddle Date: Thu, 11 Jan 2024 11:17:55 +0100 Subject: [PATCH 10/22] Assembly: Enable part manipulation in active App::Part with the fixed joint. --- src/Mod/Assembly/CommandCreateJoint.py | 10 ++- src/Mod/Assembly/JointObject.py | 115 +++++++++++++++++-------- src/Mod/Assembly/UtilsAssembly.py | 23 ++++- 3 files changed, 107 insertions(+), 41 deletions(-) diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 09ef98048a..297f9392cf 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -62,7 +62,10 @@ class CommandCreateJointFixed: return { "Pixmap": "Assembly_CreateJointFixed", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"), + "MenuText": QT_TRANSLATE_NOOP( + "Assembly_CreateJointFixed", + "If an assembly is active : Create a Fixed Joint.\n If a part is active : Position sub parts by matching.", + ), "Accel": "J", "ToolTip": "

" + QT_TRANSLATE_NOOP( @@ -74,7 +77,10 @@ class CommandCreateJointFixed: } def IsActive(self): - return isCreateJointActive() + if UtilsAssembly.activePart: + return UtilsAssembly.assembly_has_at_least_n_parts(2) + + return UtilsAssembly.isAssemblyGrounded() and UtilsAssembly.assembly_has_at_least_n_parts(2) def Activated(self): activateJoint(0) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 6a7d31e31b..8e445f7a47 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -75,12 +75,14 @@ JointUsingReverse = [ def solveIfAllowed(assembly, storePrev=False): - if Preferences.preferences().GetBool("SolveInJointCreation", True): + if assembly.Type == "Assembly" and Preferences.preferences().GetBool( + "SolveInJointCreation", True + ): assembly.solve(storePrev) class Joint: - def __init__(self, joint, type_index, assembly): + def __init__(self, joint, type_index): self.Type = "Joint" joint.Proxy = self @@ -257,7 +259,20 @@ class Joint: if hasattr( joint, "Vertex1" ): # during loading the onchanged may be triggered before full init. - solveIfAllowed(self.getAssembly(joint)) + isAssembly = self.getAssembly(joint).Type == "Assembly" + if isAssembly: + solveIfAllowed(self.getAssembly(joint)) + else: + self.updateJCSPlacements(joint) + obj1 = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) + obj2 = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) + self.preSolve( + joint, + obj1, + joint.Part1, + obj2, + joint.Part2, + ) def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -267,10 +282,14 @@ class Joint: 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 assembly = self.getAssembly(joint) + isAssembly = assembly.Type == "Assembly" if len(current_selection) >= 1: joint.Part1 = None - self.part1Connected = assembly.isPartConnected(current_selection[0]["part"]) + if isAssembly: + self.part1Connected = assembly.isPartConnected(current_selection[0]["part"]) + else: + self.part1Connected = True joint.Object1 = current_selection[0]["object"].Name joint.Part1 = current_selection[0]["part"] @@ -289,7 +308,10 @@ class Joint: if len(current_selection) >= 2: joint.Part2 = None - self.part2Connected = assembly.isPartConnected(current_selection[1]["part"]) + if isAssembly: + self.part2Connected = assembly.isPartConnected(current_selection[1]["part"]) + else: + self.part2Connected = False joint.Object2 = current_selection[1]["object"].Name joint.Part2 = current_selection[1]["part"] @@ -305,7 +327,10 @@ class Joint: current_selection[1]["object"], joint.Part2, ) - solveIfAllowed(assembly, True) + if isAssembly: + solveIfAllowed(assembly, True) + else: + self.updateJCSPlacements(joint) else: joint.Object2 = "" @@ -313,7 +338,8 @@ class Joint: joint.Element2 = "" joint.Vertex2 = "" joint.Placement2 = App.Placement() - assembly.undoSolve() + if isAssembly: + assembly.undoSolve() self.undoPreSolve() def updateJCSPlacements(self, joint): @@ -342,7 +368,6 @@ class Joint: return App.Placement() obj = UtilsAssembly.getObjectInPart(objName, part) - assembly = self.getAssembly(joint) plc = App.Placement() if not obj: @@ -437,6 +462,7 @@ class Joint: # plc = global_plc * plc # change plc to be relative to the assembly. + # assembly = self.getAssembly(joint) # plc = assembly.Placement.inverse() * plc # We apply rotation / reverse / offset it necessary, but only to the second JCS. @@ -544,9 +570,7 @@ class Joint: jcsPlc1 = self.flipPlacement(jcsPlc1) joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse() - def undoPreSolve( - self, - ): + def undoPreSolve(self): if self.partMovedByPresolved: self.partMovedByPresolved.Placement = self.presolveBackupPlc self.partMovedByPresolved = None @@ -913,8 +937,9 @@ class MakeJointSelGate: full_obj_name = ".".join(objs_names) full_element_name = full_obj_name + "." + element_name selected_object = UtilsAssembly.getObject(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( - full_element_name, selected_object + full_element_name, selected_object, self.assembly ) for selection_dict in self.taskbox.current_selection: @@ -936,19 +961,30 @@ class TaskAssemblyCreateJoint(QtCore.QObject): activeTask = self self.assembly = UtilsAssembly.activeAssembly() + if not self.assembly: + self.assembly = UtilsAssembly.activePart() + self.activeType = "Part" + else: + self.activeType = "Assembly" + 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 + if self.activeType == "Assembly": + self.assembly.ViewObject.EnableMovement = False self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui") + if self.activeType == "Part": + self.form.setWindowTitle("Match parts") + self.form.jointType.hide() self.form.jointType.addItems(JointTypes) self.form.jointType.setCurrentIndex(jointTypeIndex) self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged) + self.form.distanceSpinbox.valueChanged.connect(self.onDistanceChanged) self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged) self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged) @@ -968,7 +1004,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): else: self.creating = True self.jointName = self.form.jointType.currentText().replace(" ", "") - App.setActiveTransaction("Create " + self.jointName + " Joint") + if self.activeType == "Part": + App.setActiveTransaction("Transform") + else: + App.setActiveTransaction("Create " + self.jointName + " Joint") self.current_selection = [] self.preselection_dict = None @@ -1001,7 +1040,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.deactivate() solveIfAllowed(self.assembly) - self.joint.Visibility = self.visibilityBackup + if self.activeType == "Assembly": + self.joint.Visibility = self.visibilityBackup + else: + self.joint.Document.removeObject(self.joint.Name) App.closeActiveTransaction() return True @@ -1016,9 +1058,11 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def deactivate(self): global activeTask activeTask = None - self.assembly.clearUndo() - self.assembly.ViewObject.EnableMovement = True + if self.activeType == "Assembly": + self.assembly.clearUndo() + self.assembly.ViewObject.EnableMovement = True + Gui.Selection.removeSelectionGate() Gui.Selection.removeObserver(self) Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection) @@ -1057,7 +1101,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) - part_containing_selected_object = UtilsAssembly.getContainingPart( + part_containing_selected_object = self.getContainingPart( full_element_name, selected_object ) @@ -1090,10 +1134,13 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def createJointObject(self): type_index = self.form.jointType.currentIndex() - joint_group = UtilsAssembly.getJointGroup(self.assembly) + if self.activeType == "Part": + self.joint = self.assembly.newObject("App::FeaturePython", "Temporary joint") + else: + joint_group = UtilsAssembly.getJointGroup(self.assembly) + self.joint = joint_group.newObject("App::FeaturePython", self.jointName) - self.joint = joint_group.newObject("App::FeaturePython", self.jointName) - Joint(self.joint, type_index, self.assembly) + Joint(self.joint, type_index) ViewProviderJoint(self.joint.ViewObject) def onJointTypeChanged(self, index): @@ -1275,15 +1322,16 @@ class TaskAssemblyCreateJoint(QtCore.QObject): if info["State"] == "UP" and info["Key"] == "RETURN": self.accept() + def getContainingPart(self, full_element_name, obj): + return UtilsAssembly.getContainingPart(full_element_name, obj, self.assembly) + # 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) - part_containing_selected_object = UtilsAssembly.getContainingPart( - full_element_name, selected_object - ) + part_containing_selected_object = self.getContainingPart(full_element_name, selected_object) selection_dict = { "object": selected_object, @@ -1308,9 +1356,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) - part_containing_selected_object = UtilsAssembly.getContainingPart( - full_element_name, selected_object - ) + part_containing_selected_object = self.getContainingPart(full_element_name, selected_object) # Find and remove the corresponding dictionary from the combined list selection_dict_to_remove = None @@ -1333,9 +1379,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) - part_containing_selected_object = UtilsAssembly.getContainingPart( - full_element_name, selected_object - ) + part_containing_selected_object = self.getContainingPart(full_element_name, selected_object) self.preselection_dict = { "object": selected_object, @@ -1352,11 +1396,12 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def setJointsPickableState(self, state: bool): """Make all joints in assembly selectable (True) or unselectable (False) in 3D view""" - try: + if self.activeType == "Assembly": jointGroup = UtilsAssembly.getJointGroup(self.assembly) for joint in jointGroup.Group: - if hasattr(joint, "Part1"): + if hasattr(joint, "JointType"): joint.ViewObject.Proxy.setPickableState(state) - except Exception as e: - s = "" if state else "un" - App.Console.PrintWarning(f"Failed to set joints {s}pickable: {e}") + else: + for obj in self.assembly.OutList: + if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"): + obj.ViewObject.Proxy.setPickableState(state) diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 6e6f4a6ea0..4bdaa28b2e 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -79,7 +79,9 @@ def assembly_has_at_least_n_parts(n): assembly = activeAssembly() i = 0 if not assembly: - return False + assembly = activePart() + if not assembly: + return False for obj in assembly.OutList: # note : groundedJoints comes in the outlist so we filter those out. if hasattr(obj, "Placement") and not hasattr(obj, "ObjectToGround"): @@ -168,7 +170,7 @@ def isBodySubObject(typeId): ) -def getContainingPart(full_name, selected_object): +def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): # full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1 # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1 # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.Sketch.Edge1" -> LinkOrBody @@ -200,7 +202,12 @@ def getContainingPart(full_name, selected_object): # Note here we may want to specify a specific behavior for Assembly::AssemblyObject. if obj.TypeId == "App::Part": if obj.hasObject(selected_object, True): - return obj + if not activeAssemblyOrPart: + return obj + elif obj.hasObject(activeAssemblyOrPart, True) or obj == activeAssemblyOrPart: + continue + else: + return obj elif obj.TypeId == "App::Link": linked_obj = obj.getLinkedObject() @@ -211,7 +218,15 @@ def getContainingPart(full_name, selected_object): # linked_obj_doc = linked_obj.Document # selected_obj_in_doc = doc.getObject(selected_object.Name) if linked_obj.hasObject(selected_object, True): - return obj + if not activeAssemblyOrPart: + return obj + elif ( + linked_obj.hasObject(activeAssemblyOrPart, True) + or linked_obj == activeAssemblyOrPart + ): + continue + else: + return obj # no container found so we return the object itself. return selected_object From 46b1a1ebfc4f77a02fe9ccd686df817c96fc453a Mon Sep 17 00:00:00 2001 From: Paddle Date: Thu, 11 Jan 2024 12:26:30 +0100 Subject: [PATCH 11/22] Assembly: set the rotating joint visible when dragging. --- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 34 ++++++++++++++----- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 4 +++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 2c90bcd1ac..8c70690d04 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -78,6 +78,7 @@ ViewProviderAssembly::ViewProviderAssembly() , canStartDragging(false) , partMoving(false) , enableMovement(true) + , jointVisibilityBackup(false) , docsToMove({}) {} @@ -536,14 +537,14 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() if (docsToMove.size() == 1) { auto* assemblyPart = static_cast(getObject()); std::string partPropName; - App::DocumentObject* joint = + movingJoint = assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); - if (!joint) { + if (!movingJoint) { return MoveMode::Translation; } - JointType jointType = AssemblyObject::getJointType(joint); + JointType jointType = AssemblyObject::getJointType(movingJoint); if (jointType == JointType::Fixed) { // If fixed joint we need to find the upstream joint to find move mode. // For example : Gnd -(revolute)- A -(fixed)- B : if user try to move B, then we should @@ -561,27 +562,27 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() docsToMove.emplace_back(upstreamPart, propPlacement->getValue()); } - joint = + movingJoint = assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); - if (!joint) { + if (!movingJoint) { return MoveMode::Translation; } - jointType = AssemblyObject::getJointType(joint); + jointType = AssemblyObject::getJointType(movingJoint); } const char* plcPropName = (partPropName == "Part1") ? "Placement1" : "Placement2"; const char* objPropName = (partPropName == "Part1") ? "Object1" : "Object2"; // jcsPlc is relative to the Object - jcsPlc = AssemblyObject::getPlacementFromProp(joint, plcPropName); + jcsPlc = AssemblyObject::getPlacementFromProp(movingJoint, plcPropName); // Make jcsGlobalPlc relative to the origin of the doc Base::Placement global_plc = - AssemblyObject::getGlobalPlacement(joint, objPropName, partPropName.c_str()); + AssemblyObject::getGlobalPlacement(movingJoint, objPropName, partPropName.c_str()); jcsGlobalPlc = global_plc * jcsPlc; // Add downstream parts so that they move together - auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first, joint); + auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first, movingJoint); for (auto part : downstreamParts) { auto* propPlacement = dynamic_cast(part->getPropertyByName("Placement")); @@ -590,6 +591,11 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() } } + jointVisibilityBackup = movingJoint->Visibility.getValue(); + if (!jointVisibilityBackup) { + movingJoint->Visibility.setValue(true); + } + if (jointType == JointType::Revolute) { return MoveMode::RotationOnPlane; } @@ -599,6 +605,10 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() else if (jointType == JointType::Cylindrical) { return MoveMode::TranslationOnAxisAndRotationOnePlane; } + else if (jointType == JointType::Distance) { + // depends on the type of distance. For example plane-plane: + // return MoveMode::TranslationOnPlane; + } } return MoveMode::Translation; } @@ -633,6 +643,12 @@ void ViewProviderAssembly::endMove() partMoving = false; canStartDragging = false; + if (movingJoint && !jointVisibilityBackup) { + movingJoint->Visibility.setValue(false); + } + + movingJoint = nullptr; + // enable selection after the move auto* view = dynamic_cast( Gui::Application::Instance->editDocument()->getActiveView()); diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 727a934d9d..9f57192e19 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -75,6 +75,7 @@ public: { Translation, TranslationOnAxis, + TranslationOnPlane, Rotation, RotationOnPlane, TranslationOnAxisAndRotationOnePlane, @@ -119,12 +120,15 @@ public: bool canStartDragging; bool partMoving; bool enableMovement; + bool jointVisibilityBackup; int numberOfSel; Base::Vector3d initialPosition; Base::Vector3d initialPositionRot; Base::Placement jcsPlc; Base::Placement jcsGlobalPlc; + App::DocumentObject* movingJoint; + std::vector> objectMasses; std::vector> docsToMove; }; From cdb1af6543dfa344600957a7cfaf2d42e647a35c Mon Sep 17 00:00:00 2001 From: Paddle Date: Thu, 11 Jan 2024 16:51:54 +0100 Subject: [PATCH 12/22] Assembly : Add support for perspective camera. Remove exportASMT from toolbar. Fix Fixedjoint name and tooltip Fix various bugs Introduce solver drag functions. --- src/Gui/View3DInventorViewer.cpp | 4 +- src/Mod/Assembly/App/AssemblyObject.cpp | 1036 +++++++++-------- src/Mod/Assembly/App/AssemblyObject.h | 50 +- src/Mod/Assembly/CommandCreateJoint.py | 17 +- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 109 +- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 49 +- src/Mod/Assembly/InitGui.py | 9 +- src/Mod/Assembly/JointObject.py | 96 +- src/Mod/Assembly/UtilsAssembly.py | 13 +- 9 files changed, 760 insertions(+), 623 deletions(-) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index 065327606d..110ddf64a7 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -2737,7 +2737,7 @@ SbVec3f View3DInventorViewer::getPointOnLine(const SbVec2s& pnt, const SbVec3f& SbLine projectedLine = projectLineOntoPlane(axisCenter, axisCenter + axis, focalPlane); ptOnFocalPlaneAndOnLine = projectedLine.getClosestPoint(ptOnFocalPlane); - // now we need the intersection point between + // now we need the intersection point between // - the line passing by ptOnFocalPlaneAndOnLine normal to focalPlane // - The line (axisCenter, axisCenter + axis) @@ -4077,4 +4077,4 @@ void View3DInventorViewer::dragLeaveEvent(QDragLeaveEvent* ev) inherited::dragLeaveEvent(ev); } -#include "moc_View3DInventorViewer.cpp" +#include "moc_View3DInventorViewer.cpp" // NOLINT diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 624196f3af..fab9d80184 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -100,36 +100,257 @@ PyObject* AssemblyObject::getPyObject() return Py::new_reference_to(PythonObject); } -std::vector AssemblyObject::getJointsOfObj(App::DocumentObject* obj) -{ - std::vector joints = getJoints(false); - std::vector jointsOf; - for (auto joint : joints) { - App::DocumentObject* obj1 = getObjFromNameProp(joint, "object1", "Part1"); - App::DocumentObject* obj2 = getObjFromNameProp(joint, "Object2", "Part2"); - if (obj == obj1 || obj == obj2) { - jointsOf.push_back(obj); - } +int AssemblyObject::solve(bool enableRedo) +{ + mbdAssembly = makeMbdAssembly(); + objectPartMap.clear(); + + std::vector groundedObjs = fixGroundedParts(); + if (groundedObjs.empty()) { + // If no part fixed we can't solve. + return -6; } - return jointsOf; + std::vector joints = getJoints(); + + removeUnconnectedJoints(joints, groundedObjs); + + jointParts(joints); + + if (enableRedo) { + savePlacementsForUndo(); + } + + try { + mbdAssembly->solve(); + } + catch (...) { + Base::Console().Error("Solve failed\n"); + return -1; + } + + setNewPlacements(); + + redrawJointPlacements(joints); + + return 0; } -std::vector AssemblyObject::getJointsOfPart(App::DocumentObject* part) +void AssemblyObject::preDrag(std::vector dragParts) { - std::vector joints = getJoints(false); - std::vector jointsOf; + solve(); - for (auto joint : joints) { - App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); - App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); - if (part == part1 || part == part2) { - jointsOf.push_back(joint); - } + dragMbdParts.clear(); + for (auto part : dragParts) { + dragMbdParts.push_back(getMbDPart(part)); } - return jointsOf; + mbdAssembly->runPreDrag(); +} + +void AssemblyObject::doDragStep() +{ + for (auto& mbdPart : dragMbdParts) { + App::DocumentObject* part = nullptr; + for (auto& pair : objectPartMap) { + if (pair.second == mbdPart) { + part = pair.first; + break; + } + } + if (!part) { + continue; + } + + Base::Placement plc = getPlacementFromProp(part, "Placement"); + Base::Vector3d pos = plc.getPosition(); + mbdPart->setPosition3D(pos.x, pos.y, pos.z); + + 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); + mbdPart->setRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z); + } + + auto dragPartsVec = std::make_shared>>(dragMbdParts); + mbdAssembly->runDragStep(dragPartsVec); + setNewPlacements(); + redrawJointPlacements(getJoints()); +} + +void AssemblyObject::postDrag() +{ + mbdAssembly->runPostDrag(); // Do this after last drag +} + +void AssemblyObject::savePlacementsForUndo() +{ + previousPositions.clear(); + + for (auto& pair : objectPartMap) { + App::DocumentObject* obj = pair.first; + if (!obj) { + continue; + } + + std::pair savePair; + savePair.first = obj; + + // Check if the object has a "Placement" property + auto* propPlc = dynamic_cast(obj->getPropertyByName("Placement")); + if (!propPlc) { + continue; + } + savePair.second = propPlc->getValue(); + + previousPositions.push_back(savePair); + } +} + +void AssemblyObject::undoSolve() +{ + if (previousPositions.size() == 0) { + return; + } + + for (auto& pair : previousPositions) { + App::DocumentObject* obj = pair.first; + if (!obj) { + continue; + } + + // Check if the object has a "Placement" property + auto* propPlacement = + dynamic_cast(obj->getPropertyByName("Placement")); + if (!propPlacement) { + continue; + } + + propPlacement->setValue(pair.second); + } + previousPositions.clear(); + + // update joint placements: + getJoints(); +} + +void AssemblyObject::clearUndo() +{ + previousPositions.clear(); +} + +void AssemblyObject::exportAsASMT(std::string fileName) +{ + mbdAssembly = makeMbdAssembly(); + objectPartMap.clear(); + fixGroundedParts(); + + std::vector joints = getJoints(); + + jointParts(joints); + + mbdAssembly->outputFile(fileName); +} + +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) { + continue; + } + + 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::redrawJointPlacements(std::vector joints) +{ + // Notify the joint objects that the transform of the coin object changed. + for (auto* joint : joints) { + auto* propPlacement = + dynamic_cast(joint->getPropertyByName("Placement1")); + if (propPlacement) { + propPlacement->setValue(propPlacement->getValue()); + } + propPlacement = + dynamic_cast(joint->getPropertyByName("Placement2")); + if (propPlacement) { + propPlacement->setValue(propPlacement->getValue()); + } + } +} + +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); + } + } +} + +std::shared_ptr AssemblyObject::makeMbdAssembly() +{ + auto assembly = CREATE::With(); + assembly->setName("OndselAssembly"); + + return assembly; } App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::DocumentObject* part, @@ -160,46 +381,21 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::Docum return nullptr; } -bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname) +JointGroup* AssemblyObject::getJointGroup() { + App::Document* doc = getDocument(); - auto* propPart = dynamic_cast(joint->getPropertyByName(propname)); - if (!propPart) { - return false; + std::vector jointGroups = + doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId()); + if (jointGroups.empty()) { + return nullptr; } - App::DocumentObject* part = propPart->getValue(); - // Check if the part is disconnected even with the joint - bool isConnected = isPartConnected(part); - if (!isConnected) { - return false; - } - - // to know if a joint is connecting to ground we disable all the other joints - std::vector jointsOfPart = getJointsOfPart(part); - std::vector activatedStates; - - for (auto jointi : jointsOfPart) { - if (jointi->getFullName() == joint->getFullName()) { - continue; + for (auto jointGroup : jointGroups) { + if (hasObject(jointGroup)) { + return dynamic_cast(jointGroup); } - - activatedStates.push_back(getJointActivated(jointi)); - setJointActivated(jointi, false); } - - isConnected = isPartConnected(part); - - // restore activation states - for (auto jointi : jointsOfPart) { - if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) { - continue; - } - - setJointActivated(jointi, activatedStates[0]); - activatedStates.erase(activatedStates.begin()); - } - - return isConnected; + return nullptr; } std::vector AssemblyObject::getJoints(bool updateJCS) @@ -263,6 +459,151 @@ std::vector AssemblyObject::getGroundedJoints() return joints; } +std::vector AssemblyObject::getJointsOfObj(App::DocumentObject* obj) +{ + std::vector joints = getJoints(false); + std::vector jointsOf; + + for (auto joint : joints) { + App::DocumentObject* obj1 = getObjFromNameProp(joint, "object1", "Part1"); + App::DocumentObject* obj2 = getObjFromNameProp(joint, "Object2", "Part2"); + if (obj == obj1 || obj == obj2) { + jointsOf.push_back(obj); + } + } + + return jointsOf; +} + +std::vector AssemblyObject::getJointsOfPart(App::DocumentObject* part) +{ + std::vector joints = getJoints(false); + std::vector jointsOf; + + for (auto joint : joints) { + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + if (part == part1 || part == part2) { + jointsOf.push_back(joint); + } + } + + return jointsOf; +} + +std::vector AssemblyObject::getGroundedParts() +{ + std::vector groundedJoints = getGroundedJoints(); + + std::vector groundedObjs; + for (auto gJoint : groundedJoints) { + if (!gJoint) { + continue; + } + + auto* propObj = + dynamic_cast(gJoint->getPropertyByName("ObjectToGround")); + + if (propObj) { + App::DocumentObject* objToGround = propObj->getValue(); + groundedObjs.push_back(objToGround); + } + } + return groundedObjs; +} + +std::vector AssemblyObject::fixGroundedParts() +{ + std::vector groundedJoints = getGroundedJoints(); + + std::vector groundedObjs; + for (auto obj : groundedJoints) { + if (!obj) { + continue; + } + + 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); + groundedObjs.push_back(objToGround); + } + } + return groundedObjs; +} + +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"; + Base::Placement basePlc = Base::Placement(); + auto mbdMarker2 = makeMbdMarker(markerName2, basePlc); + 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); +} + +bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname) +{ + + auto* propPart = dynamic_cast(joint->getPropertyByName(propname)); + if (!propPart) { + return false; + } + App::DocumentObject* part = propPart->getValue(); + // Check if the part is disconnected even with the joint + bool isConnected = isPartConnected(part); + if (!isConnected) { + return false; + } + + // to know if a joint is connecting to ground we disable all the other joints + std::vector jointsOfPart = getJointsOfPart(part); + std::vector activatedStates; + + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName()) { + continue; + } + + activatedStates.push_back(getJointActivated(jointi)); + setJointActivated(jointi, false); + } + + isConnected = isPartConnected(part); + + // restore activation states + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) { + continue; + } + + setJointActivated(jointi, activatedStates[0]); + activatedStates.erase(activatedStates.begin()); + } + + return isConnected; +} + void AssemblyObject::removeUnconnectedJoints(std::vector& joints, std::vector groundedObjs) { @@ -369,205 +710,6 @@ bool AssemblyObject::isPartConnected(App::DocumentObject* obj) return false; } -std::vector AssemblyObject::getDownstreamParts(App::DocumentObject* part, - App::DocumentObject* joint) -{ - // First we deactivate the joint - bool state = getJointActivated(joint); - setJointActivated(joint, false); - - std::vector joints = getJoints(false); - - std::set connectedParts = {part}; - traverseAndMarkConnectedParts(part, connectedParts, joints); - - std::vector downstreamParts; - for (auto parti : connectedParts) { - if (!isPartConnected(parti) && (parti != part)) { - downstreamParts.push_back(parti); - } - } - - AssemblyObject::setJointActivated(joint, state); - /*if (limit > 1000) { // Inifinite loop protection - return {}; - } - limit++; - Base::Console().Warning("limit %d\n", limit); - - std::vector downstreamParts = {part}; - std::string name; - App::DocumentObject* connectingJoint = - getJointOfPartConnectingToGround(part, - name); // ?????????????????????????????? if we remove - // connection to ground then it can't work for tom - std::vector jointsOfPart = getJointsOfPart(part); - - // remove connectingJoint from jointsOfPart - auto it = std::remove(jointsOfPart.begin(), jointsOfPart.end(), connectingJoint); - jointsOfPart.erase(it, jointsOfPart.end()); - for (auto joint : jointsOfPart) { - App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); - App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); - bool firstIsDown = part->getFullName() == part2->getFullName(); - App::DocumentObject* downstreamPart = firstIsDown ? part1 : part2; - - Base::Console().Warning("looping\n"); - // it is possible that the part is connected to ground by this joint. - // In which case we should not select those parts. To test we disconnect : - auto* propObj = dynamic_cast(joint->getPropertyByName("Part1")); - if (!propObj) { - continue; - } - propObj->setValue(nullptr); - bool isConnected = isPartConnected(downstreamPart); - propObj->setValue(part1); - if (isConnected) { - Base::Console().Warning("continue\n"); - continue; - } - - std::vector subDownstreamParts = - getDownstreamParts(downstreamPart, limit); - for (auto downPart : subDownstreamParts) { - if (std::find(downstreamParts.begin(), downstreamParts.end(), downPart) - == downstreamParts.end()) { - downstreamParts.push_back(downPart); - } - } - }*/ - return downstreamParts; -} - -std::vector AssemblyObject::getUpstreamParts(App::DocumentObject* part, - int limit) -{ - if (limit > 1000) { // Inifinite loop protection - return {}; - } - limit++; - - if (isPartGrounded(part)) { - return {part}; - } - - std::string name; - App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); - App::DocumentObject* upPart = - getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); - - std::vector upstreamParts = getUpstreamParts(upPart, limit); - upstreamParts.push_back(part); - return upstreamParts; -} - -App::DocumentObject* AssemblyObject::getUpstreamMovingPart(App::DocumentObject* part) -{ - if (isPartGrounded(part)) { - return nullptr; - } - - std::string name; - App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); - JointType jointType = getJointType(connectingJoint); - if (jointType != JointType::Fixed) { - return part; - } - - App::DocumentObject* upPart = - getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); - - return getUpstreamMovingPart(upPart); -} - -JointGroup* AssemblyObject::getJointGroup() -{ - App::Document* doc = getDocument(); - - std::vector jointGroups = - doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId()); - if (jointGroups.empty()) { - return nullptr; - } - for (auto jointGroup : jointGroups) { - if (hasObject(jointGroup)) { - return dynamic_cast(jointGroup); - } - } - return nullptr; -} - -std::vector AssemblyObject::getGroundedParts() -{ - std::vector groundedJoints = getGroundedJoints(); - - std::vector groundedObjs; - for (auto gJoint : groundedJoints) { - if (!gJoint) { - continue; - } - - auto* propObj = - dynamic_cast(gJoint->getPropertyByName("ObjectToGround")); - - if (propObj) { - App::DocumentObject* objToGround = propObj->getValue(); - groundedObjs.push_back(objToGround); - } - } - return groundedObjs; -} - -std::vector AssemblyObject::fixGroundedParts() -{ - std::vector groundedJoints = getGroundedJoints(); - - std::vector groundedObjs; - for (auto obj : groundedJoints) { - if (!obj) { - continue; - } - - 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); - groundedObjs.push_back(objToGround); - } - } - return groundedObjs; -} - -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"; - Base::Placement basePlc = Base::Placement(); - auto mbdMarker2 = makeMbdMarker(markerName2, basePlc); - 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) { @@ -582,55 +724,6 @@ void AssemblyObject::jointParts(std::vector joints) } } -int AssemblyObject::solve(bool enableRedo) -{ - mbdAssembly = makeMbdAssembly(); - objectPartMap.clear(); - - std::vector groundedObjs = fixGroundedParts(); - if (groundedObjs.empty()) { - // If no part fixed we can't solve. - return -6; - } - - std::vector joints = getJoints(); - - removeUnconnectedJoints(joints, groundedObjs); - - jointParts(joints); - - if (enableRedo) { - savePlacementsForUndo(); - } - - try { - mbdAssembly->solve(); - } - catch (...) { - Base::Console().Error("Solve failed\n"); - return -1; - } - - setNewPlacements(); - - redrawJointPlacements(joints); - - return 0; -} - -void AssemblyObject::exportAsASMT(std::string fileName) -{ - mbdAssembly = makeMbdAssembly(); - objectPartMap.clear(); - fixGroundedParts(); - - std::vector joints = getJoints(); - - jointParts(joints); - - mbdAssembly->outputFile(fileName); -} - std::shared_ptr AssemblyObject::makeMbdJointOfType(App::DocumentObject* joint, JointType type) { @@ -1029,17 +1122,17 @@ std::shared_ptr AssemblyObject::getMbDPart(App::DocumentObject* obj) std::shared_ptr AssemblyObject::makeMbdPart(std::string& name, Base::Placement plc, double mass) { - auto mdbPart = CREATE::With(); - mdbPart->setName(name); + auto mbdPart = CREATE::With(); + mbdPart->setName(name); auto massMarker = CREATE::With(); massMarker->setMass(mass); massMarker->setDensity(1.0); massMarker->setMomentOfInertias(1.0, 1.0, 1.0); - mdbPart->setPrincipalMassMarker(massMarker); + mbdPart->setPrincipalMassMarker(massMarker); Base::Vector3d pos = plc.getPosition(); - mdbPart->setPosition3D(pos.x, pos.y, pos.z); + mbdPart->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 @@ -1049,20 +1142,12 @@ AssemblyObject::makeMbdPart(std::string& name, Base::Placement plc, double mass) 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); + mbdPart->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);*/ + mbdPart->setQuarternions(q0, q1, q2, q3);*/ - return mdbPart; -} - -std::shared_ptr AssemblyObject::makeMbdAssembly() -{ - auto assembly = CREATE::With(); - assembly->setName("OndselAssembly"); - - return assembly; + return mbdPart; } std::shared_ptr AssemblyObject::makeMbdMarker(std::string& name, Base::Placement& plc) @@ -1087,6 +1172,134 @@ std::shared_ptr AssemblyObject::makeMbdMarker(std::string& name, Bas return mbdMarker; } +std::vector AssemblyObject::getDownstreamParts(App::DocumentObject* part, + App::DocumentObject* joint) +{ + // First we deactivate the joint + bool state = getJointActivated(joint); + setJointActivated(joint, false); + + std::vector joints = getJoints(false); + + std::set connectedParts = {part}; + traverseAndMarkConnectedParts(part, connectedParts, joints); + + std::vector downstreamParts; + for (auto parti : connectedParts) { + if (!isPartConnected(parti) && (parti != part)) { + downstreamParts.push_back(parti); + } + } + + AssemblyObject::setJointActivated(joint, state); + /*if (limit > 1000) { // Inifinite loop protection + return {}; + } + limit++; + Base::Console().Warning("limit %d\n", limit); + + std::vector downstreamParts = {part}; + std::string name; + App::DocumentObject* connectingJoint = + getJointOfPartConnectingToGround(part, + name); // ?????????????????????????????? if we remove + // connection to ground then it can't work for tom + std::vector jointsOfPart = getJointsOfPart(part); + + // remove connectingJoint from jointsOfPart + auto it = std::remove(jointsOfPart.begin(), jointsOfPart.end(), connectingJoint); + jointsOfPart.erase(it, jointsOfPart.end()); + for (auto joint : jointsOfPart) { + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + bool firstIsDown = part->getFullName() == part2->getFullName(); + App::DocumentObject* downstreamPart = firstIsDown ? part1 : part2; + + Base::Console().Warning("looping\n"); + // it is possible that the part is connected to ground by this joint. + // In which case we should not select those parts. To test we disconnect : + auto* propObj = dynamic_cast(joint->getPropertyByName("Part1")); + if (!propObj) { + continue; + } + propObj->setValue(nullptr); + bool isConnected = isPartConnected(downstreamPart); + propObj->setValue(part1); + if (isConnected) { + Base::Console().Warning("continue\n"); + continue; + } + + std::vector subDownstreamParts = + getDownstreamParts(downstreamPart, limit); + for (auto downPart : subDownstreamParts) { + if (std::find(downstreamParts.begin(), downstreamParts.end(), downPart) + == downstreamParts.end()) { + downstreamParts.push_back(downPart); + } + } + }*/ + return downstreamParts; +} + +std::vector AssemblyObject::getUpstreamParts(App::DocumentObject* part, + int limit) +{ + if (limit > 1000) { // Inifinite loop protection + return {}; + } + limit++; + + if (isPartGrounded(part)) { + return {part}; + } + + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + App::DocumentObject* upPart = + getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); + + std::vector upstreamParts = getUpstreamParts(upPart, limit); + upstreamParts.push_back(part); + return upstreamParts; +} + +App::DocumentObject* AssemblyObject::getUpstreamMovingPart(App::DocumentObject* part) +{ + if (isPartGrounded(part)) { + return nullptr; + } + + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + JointType jointType = getJointType(connectingJoint); + if (jointType != JointType::Fixed) { + return part; + } + + App::DocumentObject* upPart = + getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); + + return getUpstreamMovingPart(upPart); +} + +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; +} + +// ======================================= Utils ====================================== + void AssemblyObject::swapJCS(App::DocumentObject* joint) { auto propElement1 = dynamic_cast(joint->getPropertyByName("Element1")); @@ -1128,164 +1341,22 @@ void AssemblyObject::swapJCS(App::DocumentObject* joint) } } -void AssemblyObject::savePlacementsForUndo() +bool AssemblyObject::isEdgeType(App::DocumentObject* obj, + const char* elName, + GeomAbs_CurveType type) { - previousPositions.clear(); + PartApp::Feature* base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); - for (auto& pair : objectPartMap) { - App::DocumentObject* obj = pair.first; - if (!obj) { - continue; - } + // Check for valid face types + TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(elName)); + BRepAdaptor_Curve sf(edge); - std::pair savePair; - savePair.first = obj; - - // Check if the object has a "Placement" property - auto* propPlc = dynamic_cast(obj->getPropertyByName("Placement")); - if (!propPlc) { - continue; - } - savePair.second = propPlc->getValue(); - - previousPositions.push_back(savePair); - } -} - -void AssemblyObject::undoSolve() -{ - if (previousPositions.size() == 0) { - return; + if (sf.GetType() == type) { + return true; } - for (auto& pair : previousPositions) { - App::DocumentObject* obj = pair.first; - if (!obj) { - continue; - } - - // Check if the object has a "Placement" property - auto* propPlacement = - dynamic_cast(obj->getPropertyByName("Placement")); - if (!propPlacement) { - continue; - } - - propPlacement->setValue(pair.second); - } - previousPositions.clear(); - - // update joint placements: - getJoints(); -} - -void AssemblyObject::clearUndo() -{ - previousPositions.clear(); -} - -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) { - continue; - } - - 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::redrawJointPlacements(std::vector joints) -{ - // Notify the joint objects that the transform of the coin object changed. - for (auto* joint : joints) { - auto* propPlacement = - dynamic_cast(joint->getPropertyByName("Placement1")); - if (propPlacement) { - propPlacement->setValue(propPlacement->getValue()); - } - propPlacement = - dynamic_cast(joint->getPropertyByName("Placement2")); - if (propPlacement) { - propPlacement->setValue(propPlacement->getValue()); - } - } -} - -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; + return false; } bool AssemblyObject::isFaceType(App::DocumentObject* obj, @@ -1306,24 +1377,6 @@ bool AssemblyObject::isFaceType(App::DocumentObject* obj, return false; } -bool AssemblyObject::isEdgeType(App::DocumentObject* obj, - const char* elName, - GeomAbs_CurveType type) -{ - PartApp::Feature* base = static_cast(obj); - const PartApp::TopoShape& TopShape = base->Shape.getShape(); - - // Check for valid face types - TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(elName)); - BRepAdaptor_Curve sf(edge); - - if (sf.GetType() == type) { - return true; - } - - return false; -} - double AssemblyObject::getFaceRadius(App::DocumentObject* obj, const char* elt) { auto base = static_cast(obj); @@ -1359,9 +1412,6 @@ double AssemblyObject::getEdgeRadius(App::DocumentObject* obj, const char* elt) return 0.0; } - -// ======================================= Utils ====================================== - void printPlacement(Base::Placement plc, const char* name) { Base::Vector3d pos = plc.getPosition(); diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 7bd7a51b78..0bf11a3fe6 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -50,6 +50,7 @@ class Placement; class Rotation; } // namespace Base + namespace Assembly { @@ -82,11 +83,25 @@ public: return "AssemblyGui::ViewProviderAssembly"; } + /* Solve the assembly. It will update first the joints, solve, update placements of the parts + and redraw the joints Args : enableRedo : This store initial positions to enable undo while + being in an active transaction (joint creation).*/ int solve(bool enableRedo = false); + void preDrag(std::vector dragParts); + void doDragStep(); + void postDrag(); void savePlacementsForUndo(); void undoSolve(); void clearUndo(); + void exportAsASMT(std::string fileName); + + void setNewPlacements(); + void recomputeJointPlacements(std::vector joints); + void redrawJointPlacements(std::vector joints); + + + // Ondsel Solver interface std::shared_ptr makeMbdAssembly(); std::shared_ptr makeMbdPart(std::string& name, Base::Placement plc = Base::Placement(), double mass = 1.0); @@ -101,22 +116,24 @@ public: std::shared_ptr makeMbdJointDistanceFaceEdge(App::DocumentObject* joint); std::shared_ptr makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint); std::shared_ptr makeMbdJointDistanceFaceFace(App::DocumentObject* joint); - std::string handleOneSideOfJoint(App::DocumentObject* joint, const char* propObjLinkName, const char* propPartName, const char* propPlcName); + void jointParts(std::vector joints); + JointGroup* getJointGroup(); std::vector getJoints(bool updateJCS = true); + std::vector getGroundedJoints(); std::vector getJointsOfObj(App::DocumentObject* obj); std::vector getJointsOfPart(App::DocumentObject* part); App::DocumentObject* getJointOfPartConnectingToGround(App::DocumentObject* part, std::string& name); - bool isJointConnectingPartToGround(App::DocumentObject* joint, const char* partPropName); - std::vector getGroundedJoints(); - void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); - std::vector fixGroundedParts(); std::vector getGroundedParts(); + std::vector fixGroundedParts(); + void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); + + bool isJointConnectingPartToGround(App::DocumentObject* joint, const char* partPropName); void removeUnconnectedJoints(std::vector& joints, std::vector groundedObjs); @@ -125,17 +142,9 @@ public: const std::vector& joints); std::vector getConnectedParts(App::DocumentObject* part, const std::vector& joints); - - JointGroup* getJointGroup(); - - void swapJCS(App::DocumentObject* joint); - - void setNewPlacements(); - void redrawJointPlacements(std::vector joints); - void recomputeJointPlacements(std::vector joints); - bool isPartGrounded(App::DocumentObject* part); bool isPartConnected(App::DocumentObject* part); + std::vector getDownstreamParts(App::DocumentObject* part, App::DocumentObject* joint); std::vector getUpstreamParts(App::DocumentObject* part, int limit = 0); @@ -144,17 +153,13 @@ public: double getObjMass(App::DocumentObject* obj); void setObjMasses(std::vector> objectMasses); - bool isEdgeType(App::DocumentObject* obj, const char* elName, GeomAbs_CurveType type); - bool isFaceType(App::DocumentObject* obj, const char* elName, GeomAbs_SurfaceType type); - double getFaceRadius(App::DocumentObject* obj, const char* elName); - double getEdgeRadius(App::DocumentObject* obj, const char* elName); - private: std::shared_ptr mbdAssembly; std::unordered_map> objectPartMap; std::vector> objMasses; + std::vector> dragMbdParts; std::vector> previousPositions; @@ -166,6 +171,13 @@ public: // Can't put the functions by themselves in AssemblyUtils.cpp : // see https://forum.freecad.org/viewtopic.php?p=729577#p729577 + void swapJCS(App::DocumentObject* joint); + + bool isEdgeType(App::DocumentObject* obj, const char* elName, GeomAbs_CurveType type); + bool isFaceType(App::DocumentObject* obj, const char* elName, GeomAbs_SurfaceType type); + double getFaceRadius(App::DocumentObject* obj, const char* elName); + double getEdgeRadius(App::DocumentObject* obj, const char* elName); + // getters to get from properties static void setJointActivated(App::DocumentObject* joint, bool val); static bool getJointActivated(App::DocumentObject* joint); diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 297f9392cf..1c6ea545c8 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -64,13 +64,19 @@ class CommandCreateJointFixed: "Pixmap": "Assembly_CreateJointFixed", "MenuText": QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", - "If an assembly is active : Create a Fixed Joint.\n If a part is active : Position sub parts by matching.", + "Create a Fixed Joint", ), "Accel": "J", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", - "Create a Fixed Joint: Permanently locks two parts together, preventing any movement or rotation.", + "1 - If an assembly is active : Create a joint permanently locking two parts together, preventing any movement or rotation.", + ) + + "

" + + "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointFixed", + "2 - If a part is active : Position sub parts by matching seleted coordinate systems. The second part selected will move.", ) + "

", "CmdType": "ForEdit", @@ -270,6 +276,7 @@ class CommandToggleGrounded: part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj) # Check if part is grounded and if so delete the joint. + ungrounded = False for joint in joint_group.Group: if ( hasattr(joint, "ObjectToGround") @@ -281,10 +288,12 @@ class CommandToggleGrounded: doc = App.ActiveDocument doc.removeObject(joint.Name) doc.recompute() - return + ungrounded = True + break + if ungrounded: + continue # Create groundedJoint. - part_containing_obj.Label = part_containing_obj.Label + " 🔒" ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") JointObject.GroundedJoint(ground, part_containing_obj) diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 8c70690d04..ab7a4a1c46 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -75,6 +75,7 @@ PROPERTY_SOURCE(AssemblyGui::ViewProviderAssembly, Gui::ViewProviderPart) ViewProviderAssembly::ViewProviderAssembly() : SelectionObserver(true) + , dragMode(DragMode::None) , canStartDragging(false) , partMoving(false) , enableMovement(true) @@ -116,7 +117,7 @@ bool ViewProviderAssembly::doubleClicked() bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const { - Base::Console().Warning("ViewProviderAssembly::canDragObject\n"); + // The user should not be able to drag the joint group out of the assembly if (!obj || obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { return false; } @@ -210,7 +211,7 @@ void ViewProviderAssembly::unsetEdit(int ModNum) PARTKEY); } -bool ViewProviderAssembly::isInEditMode() +bool ViewProviderAssembly::isInEditMode() const { App::DocumentObject* activePart = getActivePart(); if (!activePart) { @@ -220,20 +221,18 @@ bool ViewProviderAssembly::isInEditMode() return activePart == this->getObject(); } -App::DocumentObject* ViewProviderAssembly::getActivePart() +App::DocumentObject* ViewProviderAssembly::getActivePart() const { - App::DocumentObject* activePart = nullptr; auto activeDoc = Gui::Application::Instance->activeDocument(); if (!activeDoc) { activeDoc = getDocument(); } - auto activeView = activeDoc->setActiveView(this); + auto activeView = activeDoc->getActiveView(); if (!activeView) { return nullptr; } - activePart = activeView->getActiveObject(PARTKEY); - return activePart; + return activeView->getActiveObject(PARTKEY); } bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer) @@ -243,21 +242,21 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent canStartDragging = false; if (enableMovement && getSelectedObjectsWithinAssembly()) { - moveMode = findMoveMode(); + dragMode = findDragMode(); - if (moveMode == MoveMode::None) { + if (dragMode == DragMode::None) { return false; } SbVec3f vec; - if (moveMode == MoveMode::RotationOnPlane - || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + if (dragMode == DragMode::RotationOnPlane + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { vec = viewer->getPointOnXYPlaneOfPlacement(cursorPos, jcsGlobalPlc); initialPositionRot = Base::Vector3d(vec[0], vec[1], vec[2]); } - if (moveMode == MoveMode::TranslationOnAxis - || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + if (dragMode == DragMode::TranslationOnAxis + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { Base::Vector3d zAxis = jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., 1.)); Base::Vector3d pos = jcsGlobalPlc.getPosition(); @@ -266,9 +265,10 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent vec = viewer->getPointOnLine(cursorPos, axisCenter, axis); initialPosition = Base::Vector3d(vec[0], vec[1], vec[2]); } - else if (moveMode != MoveMode::RotationOnPlane) { + else if (dragMode != DragMode::RotationOnPlane) { vec = viewer->getPointOnFocalPlane(cursorPos); initialPosition = Base::Vector3d(vec[0], vec[1], vec[2]); + prevPosition = initialPosition; } initMove(); @@ -278,14 +278,14 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent // Do the dragging of parts if (partMoving) { Base::Vector3d newPos, newPosRot; - if (moveMode == MoveMode::RotationOnPlane - || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + if (dragMode == DragMode::RotationOnPlane + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { SbVec3f vec = viewer->getPointOnXYPlaneOfPlacement(cursorPos, jcsGlobalPlc); newPosRot = Base::Vector3d(vec[0], vec[1], vec[2]); } - if (moveMode == MoveMode::TranslationOnAxis - || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + if (dragMode == DragMode::TranslationOnAxis + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { Base::Vector3d zAxis = jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., 1.)); Base::Vector3d pos = jcsGlobalPlc.getPosition(); SbVec3f axisCenter(pos.x, pos.y, pos.z); @@ -293,7 +293,7 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent SbVec3f vec = viewer->getPointOnLine(cursorPos, axisCenter, axis); newPos = Base::Vector3d(vec[0], vec[1], vec[2]); } - else if (moveMode != MoveMode::RotationOnPlane) { + else if (dragMode != DragMode::RotationOnPlane) { SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos); newPos = Base::Vector3d(vec[0], vec[1], vec[2]); } @@ -307,7 +307,7 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent Base::Placement plc = pair.second; // Base::Console().Warning("newPos %f %f %f\n", newPos.x, newPos.y, newPos.z); - if (moveMode == MoveMode::RotationOnPlane) { + if (dragMode == DragMode::RotationOnPlane) { Base::Vector3d center = jcsGlobalPlc.getPosition(); Base::Vector3d norm = jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., -1.)); @@ -320,13 +320,18 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent Base::Placement jcsPlcRelativeToPart = plc.inverse() * jcsGlobalPlc; plc = rotatedGlovalJcsPlc * jcsPlcRelativeToPart.inverse(); } - else if (moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { + else if (dragMode == DragMode::TranslationOnAxis) { + Base::Vector3d pos = plc.getPosition() + (newPos - initialPosition); + plc.setPosition(pos); + } + else if (dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { Base::Vector3d pos = plc.getPosition() + (newPos - initialPosition); plc.setPosition(pos); Base::Placement newJcsGlobalPlc = jcsGlobalPlc; newJcsGlobalPlc.setPosition(jcsGlobalPlc.getPosition() + (newPos - initialPosition)); + Base::Vector3d center = newJcsGlobalPlc.getPosition(); Base::Vector3d norm = newJcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., -1.)); @@ -342,8 +347,12 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent Base::Placement jcsPlcRelativeToPart = plc.inverse() * newJcsGlobalPlc; plc = rotatedGlovalJcsPlc * jcsPlcRelativeToPart.inverse(); } - else { - Base::Vector3d pos = newPos + (plc.getPosition() - initialPosition); + else { // DragMode::Translation + Base::Vector3d delta = newPos - prevPosition; + prevPosition = newPos; + + Base::Vector3d pos = propPlacement->getValue().getPosition() + delta; + // Base::Vector3d pos = newPos + (plc.getPosition() - initialPosition); plc.setPosition(pos); } propPlacement->setValue(plc); @@ -356,6 +365,7 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent if (solveOnMove) { auto* assemblyPart = static_cast(getObject()); assemblyPart->solve(); + // assemblyPart->doDragStep(); } } return false; @@ -532,7 +542,7 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vectorgetObject(objName.c_str()); } -ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() +ViewProviderAssembly::DragMode ViewProviderAssembly::findDragMode() { if (docsToMove.size() == 1) { auto* assemblyPart = static_cast(getObject()); @@ -541,7 +551,7 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); if (!movingJoint) { - return MoveMode::Translation; + return DragMode::Translation; } JointType jointType = AssemblyObject::getJointType(movingJoint); @@ -553,7 +563,7 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() assemblyPart->getUpstreamMovingPart(docsToMove[0].first); docsToMove.clear(); if (!upstreamPart) { - return MoveMode::None; + return DragMode::None; } auto* propPlacement = @@ -565,7 +575,7 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() movingJoint = assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); if (!movingJoint) { - return MoveMode::Translation; + return DragMode::Translation; } jointType = AssemblyObject::getJointType(movingJoint); } @@ -597,20 +607,23 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() } if (jointType == JointType::Revolute) { - return MoveMode::RotationOnPlane; + return DragMode::RotationOnPlane; } else if (jointType == JointType::Slider) { - return MoveMode::TranslationOnAxis; + return DragMode::TranslationOnAxis; } else if (jointType == JointType::Cylindrical) { - return MoveMode::TranslationOnAxisAndRotationOnePlane; + return DragMode::TranslationOnAxisAndRotationOnePlane; + } + else if (jointType == JointType::Ball) { + // return DragMode::Ball; } else if (jointType == JointType::Distance) { // depends on the type of distance. For example plane-plane: - // return MoveMode::TranslationOnPlane; + // return DragMode::TranslationOnPlane; } } - return MoveMode::Translation; + return DragMode::Translation; } void ViewProviderAssembly::initMove() @@ -627,14 +640,23 @@ void ViewProviderAssembly::initMove() viewerNotConst->setSelectionEnabled(false); } - objectMasses.clear(); + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + bool solveOnMove = hGrp->GetBool("SolveOnMove", true); + if (solveOnMove) { + objectMasses.clear(); + for (auto& pair : docsToMove) { + objectMasses.push_back({pair.first, 10.0}); + } - for (auto& pair : docsToMove) { - objectMasses.push_back({pair.first, 10.0}); + auto* assemblyPart = static_cast(getObject()); + assemblyPart->setObjMasses(objectMasses); + /*std::vector dragParts; + for (auto& pair : docsToMove) { + dragParts.push_back(pair.first); + } + assemblyPart->preDrag(dragParts);*/ } - - auto* assemblyPart = static_cast(getObject()); - assemblyPart->setObjMasses(objectMasses); } void ViewProviderAssembly::endMove() @@ -658,13 +680,18 @@ void ViewProviderAssembly::endMove() viewerNotConst->setSelectionEnabled(true); } - auto* assemblyPart = static_cast(getObject()); - assemblyPart->setObjMasses({}); + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + bool solveOnMove = hGrp->GetBool("SolveOnMove", true); + if (solveOnMove) { + auto* assemblyPart = static_cast(getObject()); + // assemblyPart->postDrag(); + assemblyPart->setObjMasses({}); + } Gui::Command::commitCommand(); } - void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg) { if (msg.Type == Gui::SelectionChanges::AddSelection diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 9f57192e19..dc2dc8a4ff 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -45,6 +45,18 @@ class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart, Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssembly) PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly); + enum class DragMode + { + Translation, + TranslationOnAxis, + TranslationOnPlane, + Rotation, + RotationOnPlane, + TranslationOnAxisAndRotationOnePlane, + Ball, + None, + }; + public: ViewProviderAssembly(); ~ViewProviderAssembly() override; @@ -59,7 +71,7 @@ public: //@{ bool setEdit(int ModNum) override; void unsetEdit(int ModNum) override; - bool isInEditMode(); + bool isInEditMode() const; /// Ask the view provider if it accepts object deletions while in edit bool acceptDeletionsInEdit() override @@ -69,19 +81,8 @@ public: bool canDragObject(App::DocumentObject*) const override; - App::DocumentObject* getActivePart(); + App::DocumentObject* getActivePart() const; - enum class MoveMode - { - Translation, - TranslationOnAxis, - TranslationOnPlane, - Rotation, - RotationOnPlane, - TranslationOnAxisAndRotationOnePlane, - None, - }; - MoveMode moveMode; /// is called when the provider is in edit and the mouse is moved bool mouseMove(const SbVec2s& pos, Gui::View3DInventorViewer* viewer) override; @@ -90,18 +91,11 @@ public: bool pressed, const SbVec2s& cursorPos, const Gui::View3DInventorViewer* viewer) override; - MoveMode findMoveMode(); + + /// Finds what drag mode should be used based on the user selection. + DragMode findDragMode(); void initMove(); 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; @@ -111,17 +105,26 @@ public: return enableMovement; } + bool getSelectedObjectsWithinAssembly(); + App::DocumentObject* getObjectFromSubNames(std::vector& subNames); + std::vector parseSubNames(std::string& subNamesStr); + + /// Get the python wrapper for that ViewProvider + PyObject* getPyObject() override; + // 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; + DragMode dragMode; bool canStartDragging; bool partMoving; bool enableMovement; bool jointVisibilityBackup; int numberOfSel; + Base::Vector3d prevPosition; Base::Vector3d initialPosition; Base::Vector3d initialPositionRot; Base::Placement jcsPlc; diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index d8493837a3..b49cba344c 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -75,10 +75,13 @@ class AssemblyWorkbench(Workbench): FreeCADGui.addPreferencePage(PreferencesPage, QT_TRANSLATE_NOOP("QObject", "Assembly")) # build commands list - cmdlist = [ + cmdList = [ "Assembly_CreateAssembly", "Assembly_InsertLink", "Assembly_SolveAssembly", + ] + + cmdListMenuOnly = [ "Assembly_ExportASMT", ] @@ -93,12 +96,12 @@ class AssemblyWorkbench(Workbench): "Assembly_CreateJointDistance", ] - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist) + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdList) self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly Joints"), cmdListJoints) self.appendMenu( [QT_TRANSLATE_NOOP("Workbench", "&Assembly")], - cmdlist + ["Separator"] + cmdListJoints, + cmdList + cmdListMenuOnly + ["Separator"] + cmdListJoints, ) print("Assembly workbench loaded") diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 8e445f7a47..f186a7da89 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -83,8 +83,6 @@ def solveIfAllowed(assembly, storePrev=False): class Joint: def __init__(self, joint, type_index): - self.Type = "Joint" - joint.Proxy = self joint.addProperty( @@ -237,12 +235,11 @@ class Joint: self.setJointConnectors(joint, []) - def __getstate__(self): - return self.Type + def dumps(self): + return None - def __setstate__(self, state): - if state: - self.Type = state + def loads(self, state): + return None def getAssembly(self, joint): return joint.InList[0] @@ -256,23 +253,25 @@ class Joint: # App.Console.PrintMessage("Change property: " + str(prop) + "\n") if prop == "Rotation" or prop == "Offset" or prop == "Distance": - if hasattr( - joint, "Vertex1" - ): # during loading the onchanged may be triggered before full init. + # during loading the onchanged may be triggered before full init. + if hasattr(joint, "Vertex1"): # so we check Vertex1 + self.updateJCSPlacements(joint) + obj1 = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) + obj2 = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) + presolved = self.preSolve( + joint, + obj1, + joint.Part1, + obj2, + joint.Part2, + False, + ) + isAssembly = self.getAssembly(joint).Type == "Assembly" - if isAssembly: + if isAssembly and not presolved: solveIfAllowed(self.getAssembly(joint)) else: self.updateJCSPlacements(joint) - obj1 = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) - obj2 = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) - self.preSolve( - joint, - obj1, - joint.Part1, - obj2, - joint.Part2, - ) def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -380,6 +379,8 @@ class Joint: elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt) vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx) + isLine = False + if elt_type == "Vertex": vertex = obj.Shape.Vertexes[elt_index - 1] plc.Base = (vertex.X, vertex.Y, vertex.Z) @@ -406,6 +407,7 @@ class Joint: plc.Rotation = App.Rotation(curve.Rotation) if curve.TypeId == "Part::GeomLine": + isLine = True plane_normal = curve.Direction plane_origin = App.Vector(0, 0, 0) plane = Part.Plane(plane_origin, plane_normal) @@ -457,6 +459,15 @@ class Joint: # change plc to be relative to the object placement. plc = obj.Placement.inverse() * plc + # post-process of plc for some special cases + if elt_type == "Vertex": + plc.Rotation = App.Rotation() + elif isLine: + plane_normal = plc.Rotation.multVec(App.Vector(0, 0, 1)) + plane_origin = App.Vector(0, 0, 0) + plane = Part.Plane(plane_origin, plane_normal) + plc.Rotation = App.Rotation(plane.Rotation) + # change plc to be relative to the origin of the document. # global_plc = UtilsAssembly.getGlobalPlacement(obj, part) # plc = global_plc * plc @@ -535,7 +546,7 @@ class Joint: return App.Vector(res[0].X, res[0].Y, res[0].Z) return surface.Center - def preSolve(self, joint, obj1, part1, obj2, part2): + def preSolve(self, joint, obj1, part1, obj2, part2, savePlc=True): # The goal of this is to put the part in the correct position to avoid wrong placement by the solve. # we actually don't want to match perfectly the JCS, it is best to match them @@ -543,8 +554,9 @@ class Joint: sameDir = self.areJcsSameDir(joint) if hasattr(self, "part2Connected") and not self.part2Connected: - self.partMovedByPresolved = joint.Part2 - self.presolveBackupPlc = joint.Part2.Placement + if savePlc: + self.partMovedByPresolved = joint.Part2 + self.presolveBackupPlc = joint.Part2.Placement globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc( joint.Placement1, joint.Object1, joint.Part1 @@ -555,10 +567,12 @@ class Joint: if not sameDir: jcsPlc2 = self.flipPlacement(jcsPlc2) joint.Part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() + return True elif hasattr(self, "part1Connected") and not self.part1Connected: - self.partMovedByPresolved = joint.Part1 - self.presolveBackupPlc = joint.Part1.Placement + if savePlc: + self.partMovedByPresolved = joint.Part1 + self.presolveBackupPlc = joint.Part1.Placement globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc( joint.Placement2, joint.Object2, joint.Part2 @@ -569,6 +583,8 @@ class Joint: if not sameDir: jcsPlc1 = self.flipPlacement(jcsPlc1) joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse() + return True + return False def undoPreSolve(self): if self.partMovedByPresolved: @@ -608,7 +624,10 @@ class ViewProviderJoint: camera = Gui.ActiveDocument.ActiveView.getCameraNode() self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera) - self.cameraSensor.attach(camera.height) + if isinstance(camera, coin.SoPerspectiveCamera): + self.cameraSensor.attach(camera.focalDistance) + elif isinstance(camera, coin.SoOrthographicCamera): + self.cameraSensor.attach(camera.height) self.app_obj = vobj.Object @@ -711,7 +730,14 @@ class ViewProviderJoint: def get_JCS_size(self): camera = Gui.ActiveDocument.ActiveView.getCameraNode() - if not camera: + + # Check if the camera is a perspective camera + if isinstance(camera, coin.SoPerspectiveCamera): + return camera.focalDistance.getValue() / 20 + elif isinstance(camera, coin.SoOrthographicCamera): + return camera.height.getValue() / 20 + else: + # Default value if camera type is unknown return 10 return camera.height.getValue() / 20 @@ -736,7 +762,8 @@ class ViewProviderJoint: plc = joint.Placement1 self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1) + if joint.Part1: + self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1) else: self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE @@ -745,7 +772,8 @@ class ViewProviderJoint: plc = joint.Placement2 self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2) + if joint.Part2: + self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE @@ -827,7 +855,6 @@ class ViewProviderJoint: class GroundedJoint: def __init__(self, joint, obj_to_ground): - self.Type = "GoundedJoint" joint.Proxy = self self.joint = joint @@ -852,12 +879,11 @@ class GroundedJoint: joint.Placement = obj_to_ground.Placement - def __getstate__(self): - return self.Type + def dumps(self): + return None - def __setstate__(self, state): - if state: - self.Type = state + def loads(self, state): + return None def onChanged(self, fp, prop): """Do something when a property has changed""" diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 4bdaa28b2e..7e2630160a 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -220,7 +220,7 @@ def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): if linked_obj.hasObject(selected_object, True): if not activeAssemblyOrPart: return obj - elif ( + elif (linked_obj.Document == activeAssemblyOrPart.Document) and ( linked_obj.hasObject(activeAssemblyOrPart, True) or linked_obj == activeAssemblyOrPart ): @@ -282,9 +282,14 @@ def getJcsGlobalPlc(jcsPlc, objName, part): # The container is used to support cases where the same object appears at several places # which happens when you have a link to a part. def getGlobalPlacement(targetObj, container=None): + if targetObj is None: + return App.Placement() + inContainerBranch = container is None - for part in App.activeDocument().RootObjects: - foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch) + for rootObj in App.activeDocument().RootObjects: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, rootObj, container, inContainerBranch + ) if foundPlacement is not None: return foundPlacement @@ -330,6 +335,8 @@ def getTargetPlacementRelativeTo( elif part.TypeId == "App::Link": linked_obj = part.getLinkedObject() + if part == linked_obj or linked_obj is None: + return None # upon loading this can happen for external links. if linked_obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}: for obj in linked_obj.OutList: From 7af02003d827795ec83408d130b8ca285eed63c2 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Wed, 24 Jan 2024 15:15:12 +0100 Subject: [PATCH 13/22] vector3d getAngleOriented test. --- tests/src/Base/Vector3D.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/src/Base/Vector3D.cpp b/tests/src/Base/Vector3D.cpp index cd4150374b..6020637100 100644 --- a/tests/src/Base/Vector3D.cpp +++ b/tests/src/Base/Vector3D.cpp @@ -316,4 +316,16 @@ TEST(Vector, TestIsParallelShortVectors) EXPECT_FALSE(vec.IsParallel(Base::Vector3d(0.01, 0.02, 0.04), 0.02)); } +TEST(Vector, TestAngleOriented) +{ + Base::Vector3d vec1(0.000001, 0, 0); + Base::Vector3d vec2(0, 0.000001, 0); + Base::Vector3d norm(0, 0, 0.000001); + double angle = vec1.GetAngleOriented(vec2, norm); + EXPECT_EQ(angle, Base::float_traits::pi() * 0.5); + angle = vec2.GetAngleOriented(vec1, norm); + EXPECT_EQ(angle, Base::float_traits::pi() * 1.5); +} + // NOLINTEND + From 1062ea4eb04bf62f8919f38e98a7c1918f6527b9 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Wed, 24 Jan 2024 18:42:47 +0100 Subject: [PATCH 14/22] AssemblyTest: Introduce assembly test module --- src/Mod/Assembly/AssemblyTests/TestCore.py | 169 ++++++++++++++++-- src/Mod/Assembly/TestAssemblyWorkbench.py | 5 +- tests/CMakeLists.txt | 3 + tests/src/Mod/Assembly/App/AssemblyObject.cpp | 57 ++++++ tests/src/Mod/Assembly/App/CMakeLists.txt | 5 + tests/src/Mod/Assembly/CMakeLists.txt | 15 ++ tests/src/Mod/CMakeLists.txt | 3 + 7 files changed, 244 insertions(+), 13 deletions(-) create mode 100644 tests/src/Mod/Assembly/App/AssemblyObject.cpp create mode 100644 tests/src/Mod/Assembly/App/CMakeLists.txt create mode 100644 tests/src/Mod/Assembly/CMakeLists.txt diff --git a/src/Mod/Assembly/AssemblyTests/TestCore.py b/src/Mod/Assembly/AssemblyTests/TestCore.py index e2e3796733..44bab3adfc 100644 --- a/src/Mod/Assembly/AssemblyTests/TestCore.py +++ b/src/Mod/Assembly/AssemblyTests/TestCore.py @@ -21,10 +21,18 @@ # * # ***************************************************************************/ -import FreeCAD +import FreeCAD as App import Part import unittest +import UtilsAssembly +import JointObject + + +def _msg(text, end="\n"): + """Write messages to the console including the line ending.""" + App.Console.PrintMessage(text + end) + class TestCore(unittest.TestCase): @classmethod @@ -49,26 +57,167 @@ class TestCore(unittest.TestCase): """ pass - # Close geometry document without saving - # FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) - # Setup and tear down methods called before and after each unit test def setUp(self): """setUp()... This method is called prior to each `test()` method. Add code and objects here that are needed for multiple `test()` methods. """ - self.doc = FreeCAD.ActiveDocument - self.con = FreeCAD.Console + doc_name = self.__class__.__name__ + if App.ActiveDocument: + if App.ActiveDocument.Name != doc_name: + App.newDocument(doc_name) + else: + App.newDocument(doc_name) + App.setActiveDocument(doc_name) + self.doc = App.ActiveDocument + + self.assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") + if self.assembly: + self.jointgroup = self.assembly.newObject("Assembly::JointGroup", "Joints") + + _msg(" Temporary document '{}'".format(self.doc.Name)) def tearDown(self): """tearDown()... This method is called after each test() method. Add cleanup instructions here. Such cleanup instructions will likely undo those in the setUp() method. """ - pass + App.closeDocument(self.doc.Name) - def test00(self): - pass + def test_create_assembly(self): + """Create an assembly.""" + operation = "Create Assembly Object" + _msg(" Test '{}'".format(operation)) + self.assertTrue(self.assembly, "'{}' failed".format(operation)) - self.assertTrue(True) + def test_create_jointGroup(self): + """Create a joint group in an assembly.""" + operation = "Create JointGroup Object" + _msg(" Test '{}'".format(operation)) + self.assertTrue(self.jointgroup, "'{}' failed".format(operation)) + + def test_create_joint(self): + """Create a joint in an assembly.""" + operation = "Create Joint Object" + _msg(" Test '{}'".format(operation)) + + joint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + self.assertTrue(joint, "'{}' failed (FeaturePython creation failed)".format(operation)) + JointObject.Joint(joint, 0) + + self.assertTrue(hasattr(joint, "JointType"), "'{}' failed".format(operation)) + + def test_create_grounded_joint(self): + """Create a grounded joint in an assembly.""" + operation = "Create Grounded Joint Object" + _msg(" Test '{}'".format(operation)) + + groundedjoint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + self.assertTrue( + groundedjoint, "'{}' failed (FeaturePython creation failed)".format(operation) + ) + + box = self.assembly.newObject("Part::Box", "Box") + + JointObject.GroundedJoint(groundedjoint, box) + + self.assertTrue( + hasattr(groundedjoint, "ObjectToGround"), + "'{}' failed: No attribute 'ObjectToGround'".format(operation), + ) + self.assertTrue( + groundedjoint.ObjectToGround == box, + "'{}' failed: ObjectToGround not set correctly.".format(operation), + ) + + def test_find_placement(self): + """Test find placement of joint.""" + operation = "Find placement" + _msg(" Test '{}'".format(operation)) + + joint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + JointObject.Joint(joint, 0) + + L = 2 + W = 3 + H = 7 + box = self.assembly.newObject("Part::Box", "Box") + box.Length = L + box.Width = W + box.Height = H + box.Placement = App.Placement(App.Vector(10, 20, 30), App.Rotation(15, 25, 35)) + + # Step 0 : box with placement. No element selected + plc = joint.Proxy.findPlacement(joint, box.Name, box, "", "") + targetPlc = App.Placement(App.Vector(), App.Rotation()) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 0".format(operation)) + + # Step 1 : box with placement. Face + Vertex + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Face6", "Vertex7") + targetPlc = App.Placement(App.Vector(L, W, H), App.Rotation()) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 1".format(operation)) + + # Step 2 : box with placement. Edge + Vertex + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Edge8", "Vertex8") + targetPlc = App.Placement(App.Vector(L, W, 0), App.Rotation(0, 0, -90)) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 2".format(operation)) + + # Step 3 : box with placement. Vertex + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Vertex3", "Vertex3") + targetPlc = App.Placement(App.Vector(0, W, H), App.Rotation()) + _msg(" plc '{}'".format(plc)) + _msg(" targetPlc '{}'".format(targetPlc)) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 3".format(operation)) + + # Step 4 : box with placement. Face + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Face2", "Face2") + targetPlc = App.Placement(App.Vector(L, W / 2, H / 2), App.Rotation(0, -90, 180)) + _msg(" plc '{}'".format(plc)) + _msg(" targetPlc '{}'".format(targetPlc)) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 4".format(operation)) + + def test_solve_assembly(self): + """Test solving an assembly.""" + operation = "Solve assembly" + _msg(" Test '{}'".format(operation)) + + box = self.assembly.newObject("Part::Box", "Box") + box.Length = 10 + box.Width = 10 + box.Height = 10 + box.Placement = App.Placement(App.Vector(10, 20, 30), App.Rotation(15, 25, 35)) + + box2 = self.assembly.newObject("Part::Box", "Box") + box2.Length = 10 + box2.Width = 10 + box2.Height = 10 + box2.Placement = App.Placement(App.Vector(40, 50, 60), App.Rotation(45, 55, 65)) + + ground = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint") + JointObject.GroundedJoint(ground, box2) + + joint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + JointObject.Joint(joint, 0) + + current_selection = [] + current_selection.append( + { + "object": box2, + "part": box2, + "element_name": "Face6", + "vertex_name": "Vertex7", + } + ) + current_selection.append( + { + "object": box, + "part": box, + "element_name": "Face6", + "vertex_name": "Vertex7", + } + ) + + joint.Proxy.setJointConnectors(joint, current_selection) + + self.assertTrue(box.Placement.isSame(box2.Placement, 1e-6), "'{}'".format(operation)) diff --git a/src/Mod/Assembly/TestAssemblyWorkbench.py b/src/Mod/Assembly/TestAssemblyWorkbench.py index 000241de0c..a9d404fd54 100644 --- a/src/Mod/Assembly/TestAssemblyWorkbench.py +++ b/src/Mod/Assembly/TestAssemblyWorkbench.py @@ -26,6 +26,5 @@ import TestApp from AssemblyTests.TestCore import TestCore -# dummy usage to get flake8 and lgtm quiet -False if TestCore.__name__ else True -False if TestApp.__name__ else True +# Use the modules so that code checkers don't complain (flake8) +True if TestCore else False diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f01639d7bd..95758fd826 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -78,6 +78,9 @@ set(TestExecutables Tests_run ) +if(BUILD_ASSEMBLY) + list (APPEND TestExecutables Assembly_tests_run) +endif(BUILD_ASSEMBLY) if(BUILD_MATERIAL) list (APPEND TestExecutables Material_tests_run) endif(BUILD_MATERIAL) diff --git a/tests/src/Mod/Assembly/App/AssemblyObject.cpp b/tests/src/Mod/Assembly/App/AssemblyObject.cpp new file mode 100644 index 0000000000..2f0f86ef95 --- /dev/null +++ b/tests/src/Mod/Assembly/App/AssemblyObject.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gtest/gtest.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +class AssemblyObjectTest: public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + _docName = App::GetApplication().getUniqueDocumentName("test"); + auto _doc = App::GetApplication().newDocument(_docName.c_str(), "testUser"); + _assemblyObj = + static_cast(_doc->addObject("Assembly::AssemblyObject")); + _jointGroupObj = static_cast( + _assemblyObj->addObject("Assembly::JointGroup", "jointGroupTest")); + } + + void TearDown() override + { + App::GetApplication().closeDocument(_docName.c_str()); + } + + Assembly::AssemblyObject* getObject() + { + return _assemblyObj; + } + +private: + // TODO: use shared_ptr or something else here? + Assembly::AssemblyObject* _assemblyObj; + Assembly::JointGroup* _jointGroupObj; + std::string _docName; +}; + +TEST_F(AssemblyObjectTest, createAssemblyObject) // NOLINT +{ + // Arrange + + // Act + + // Assert +} diff --git a/tests/src/Mod/Assembly/App/CMakeLists.txt b/tests/src/Mod/Assembly/App/CMakeLists.txt new file mode 100644 index 0000000000..41c6eb9a84 --- /dev/null +++ b/tests/src/Mod/Assembly/App/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources( + Assembly_tests_run + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/AssemblyObject.cpp +) diff --git a/tests/src/Mod/Assembly/CMakeLists.txt b/tests/src/Mod/Assembly/CMakeLists.txt new file mode 100644 index 0000000000..0b6aaf602a --- /dev/null +++ b/tests/src/Mod/Assembly/CMakeLists.txt @@ -0,0 +1,15 @@ + +target_include_directories(Assembly_tests_run PUBLIC + ${EIGEN3_INCLUDE_DIR} + ${OCC_INCLUDE_DIR} + ${Python3_INCLUDE_DIRS} + ${XercesC_INCLUDE_DIRS} +) + +target_link_libraries(Assembly_tests_run + gtest_main + ${Google_Tests_LIBS} + Assembly +) + +add_subdirectory(App) diff --git a/tests/src/Mod/CMakeLists.txt b/tests/src/Mod/CMakeLists.txt index 597d6e878a..05c9bbe6e2 100644 --- a/tests/src/Mod/CMakeLists.txt +++ b/tests/src/Mod/CMakeLists.txt @@ -1,3 +1,6 @@ +if(BUILD_ASSEMBLY) + add_subdirectory(Assembly) +endif(BUILD_ASSEMBLY) if(BUILD_MATERIAL) add_subdirectory(Material) endif(BUILD_MATERIAL) From 2a5b22f0c873781559296246f15bbb8f59089716 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Fri, 26 Jan 2024 16:25:35 +0100 Subject: [PATCH 15/22] Assembly : Fixes for sketches in bodies --- src/Mod/Assembly/App/AssemblyObject.cpp | 6 +++--- src/Mod/Assembly/JointObject.py | 13 ++++++++++++ src/Mod/Assembly/UtilsAssembly.py | 27 ++++++++++++++++--------- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index fab9d80184..359d00ef4c 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -1643,16 +1643,16 @@ App::DocumentObject* AssemblyObject::getObjFromNameProp(App::DocumentObject* joi return containingPart; } - if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { + /*if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { App::Link* link = dynamic_cast(containingPart); containingPart = link->getLinkedObject(); if (!containingPart) { return nullptr; } - } + }*/ - for (auto obj : containingPart->getOutList()) { + for (auto obj : containingPart->getOutListRecursive()) { if (objName == obj->getNameInDocument()) { return obj; } diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index f186a7da89..d927806c45 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -81,6 +81,19 @@ def solveIfAllowed(assembly, storePrev=False): assembly.solve(storePrev) +# The joint object consists of 2 JCS (joint coordinate systems) and a Joint Type. +# A JCS is a placement that is computed (unless it is detached) from : +# - An Object name: this is the name of the solid. It can be any Part::Feature solid. +# Or a PartDesign Body. Or a App::Link to those. We use the name and not directly the DocumentObject +# because the object can be external. +# - A Part DocumentObject : This is the lowest level containing part. It can be either the Object itself if it +# stands alone. Or a App::Part. Or a App::Link to a App::Part. +# For example : +# Assembly.Assembly1.Part1.Part2.Box : Object is Box, part is 'Part1' +# Assembly.Assembly1.LinkToPart1.Part2.Box : Object is Box, part is 'LinkToPart1' +# - An element name: This can be either a face, an edge, a vertex or empty. Empty means that the Object placement will be used +# - A vertex name: For faces and edges, we need to specify which vertex of said face/edge to use +# From these a placement is computed. It is relative to the Object. class Joint: def __init__(self, joint, type_index): joint.Proxy = self diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 7e2630160a..ce7b09bb72 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -132,7 +132,11 @@ def getObject(full_name): linked_obj = obj.getLinkedObject() if linked_obj.TypeId == "PartDesign::Body": if i + 1 < len(names): - obj2 = doc.getObject(names[i + 1]) + obj2 = None + for obji in obj.OutList: + if obji.Name == names[i + 1]: + obj2 = obji + break if obj2 and isBodySubObject(obj2.TypeId): return obj2 return obj @@ -148,7 +152,11 @@ def getObject(full_name): elif obj.TypeId == "PartDesign::Body": if i + 1 < len(names): - obj2 = doc.getObject(names[i + 1]) + obj2 = None + for obji in obj.OutList: + if obji.Name == names[i + 1]: + obj2 = obji + break if obj2 and isBodySubObject(obj2.TypeId): return obj2 return obj @@ -174,6 +182,7 @@ def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): # full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1 # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1 # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.Sketch.Edge1" -> LinkOrBody + if selected_object is None: App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None") return None @@ -196,15 +205,15 @@ def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): return selected_object if obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): - if obj.hasObject(selected_object, True): + if selected_object in obj.OutListRecursive: return obj # Note here we may want to specify a specific behavior for Assembly::AssemblyObject. if obj.TypeId == "App::Part": - if obj.hasObject(selected_object, True): + if selected_object in obj.OutListRecursive: if not activeAssemblyOrPart: return obj - elif obj.hasObject(activeAssemblyOrPart, True) or obj == activeAssemblyOrPart: + elif activeAssemblyOrPart in obj.OutListRecursive or obj == activeAssemblyOrPart: continue else: return obj @@ -212,16 +221,16 @@ def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): elif obj.TypeId == "App::Link": linked_obj = obj.getLinkedObject() if linked_obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): - if linked_obj.hasObject(selected_object, True): + if selected_object in linked_obj.OutListRecursive: return obj if linked_obj.TypeId == "App::Part": # linked_obj_doc = linked_obj.Document # selected_obj_in_doc = doc.getObject(selected_object.Name) - if linked_obj.hasObject(selected_object, True): + if selected_object in linked_obj.OutListRecursive: if not activeAssemblyOrPart: return obj elif (linked_obj.Document == activeAssemblyOrPart.Document) and ( - linked_obj.hasObject(activeAssemblyOrPart, True) + activeAssemblyOrPart in linked_obj.OutListRecursive or linked_obj == activeAssemblyOrPart ): continue @@ -245,7 +254,7 @@ def getObjectInPart(objName, part): "App::DocumentObjectGroup", "PartDesign::Body", }: - for obji in part.OutList: + for obji in part.OutListRecursive: if obji.Name == objName: return obji From 56dc773b5ca99ae98026f44df6ec45c0d3500fa4 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Mon, 29 Jan 2024 15:57:34 +0100 Subject: [PATCH 16/22] Assembly : fix bugs --- src/Mod/Assembly/JointObject.py | 57 +++++++++++++++++++++++-------- src/Mod/Assembly/UtilsAssembly.py | 2 +- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index d927806c45..328a7651d7 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -1255,14 +1255,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.current_selection.append(selection_dict1) self.current_selection.append(selection_dict2) - elName = self.getObjSubNameFromObj(obj1, self.joint.Element1) - if obj1 != self.joint.Part1: - elName = obj1.Name + "." + elName + # Add the elements to the selection. Note we cannot do : + # Gui.Selection.addSelection(self.doc.Name, obj1.Name, elName) + # Because obj1 can be external in which case addSelection will fail. And + # Gui.Selection.addSelection(obj1.Document.Name, obj1.Name, elName) + # will not select in the assembly doc. + elName = self.getSubnameForSelection(obj1, self.joint.Part1, self.joint.Element1) Gui.Selection.addSelection(self.doc.Name, self.joint.Part1.Name, elName) - elName = self.getObjSubNameFromObj(obj2, self.joint.Element2) - if obj2 != self.joint.Part2: - elName = obj2.Name + "." + elName + elName = self.getSubnameForSelection(obj2, self.joint.Part2, self.joint.Element2) Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.Name, elName) self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance) @@ -1272,20 +1273,48 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType)) self.updateJointList() - def getObjSubNameFromObj(self, obj, elName): - if obj is None: + def getSubnameForSelection(self, obj, part, elName): + # We need the subname starting from the part. + # Example for : Assembly.Part1.LinkToPart2.Part3.Body.Tip.Face1 + # part is Part1 and obj is Body + # we should get : LinkToPart2.Part3.Body.Tip.Face1 + + if obj is None or part is None: return elName if obj.TypeId == "PartDesign::Body": - return obj.Tip.Name + "." + elName + elName = 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 + elName = linked_obj.Tip.Name + "." + elName + + if obj != part and obj in part.OutListRecursive: + bSub = "" + currentObj = part + + limit = 0 + while limit < 1000: + limit = limit + 1 + + if currentObj != part: + if bSub != "": + bSub = bSub + "." + bSub = bSub + currentObj.Name + + if currentObj == obj: + break + + if currentObj.TypeId == "App::Link": + currentObj = currentObj.getLinkedObject() + + for obji in currentObj.OutList: + if obji == obj or obj in obji.OutListRecursive: + currentObj = obji + break + + elName = bSub + "." + elName + return elName def updateJoint(self): # First we build the listwidget diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index ce7b09bb72..8e623dc737 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -133,7 +133,7 @@ def getObject(full_name): if linked_obj.TypeId == "PartDesign::Body": if i + 1 < len(names): obj2 = None - for obji in obj.OutList: + for obji in linked_obj.OutList: if obji.Name == names[i + 1]: obj2 = obji break From 098fdda6610adbae3a0c90ac065dc194cd4931f8 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Mon, 29 Jan 2024 18:20:48 +0100 Subject: [PATCH 17/22] Assembly: fixes --- src/Mod/Assembly/CommandCreateJoint.py | 19 ++++++++-- src/Mod/Assembly/CommandInsertLink.py | 51 +++++++++++++++++++++++++- src/Mod/Assembly/JointObject.py | 2 +- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 1c6ea545c8..cb710d6aea 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -225,6 +225,20 @@ class CommandCreateJointDistance: activateJoint(5) +def createGroundedJoint(obj): + assembly = UtilsAssembly.activeAssembly() + if not assembly: + return + + joint_group = UtilsAssembly.getJointGroup(assembly) + + obj.Label = obj.Label + " 🔒" + ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") + JointObject.GroundedJoint(ground, obj) + JointObject.ViewProviderGroundedJoint(ground.ViewObject) + return ground + + class CommandToggleGrounded: def __init__(self): pass @@ -294,10 +308,7 @@ class CommandToggleGrounded: continue # Create groundedJoint. - part_containing_obj.Label = part_containing_obj.Label + " 🔒" - ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") - JointObject.GroundedJoint(ground, part_containing_obj) - JointObject.ViewProviderGroundedJoint(ground.ViewObject) + createGroundedJoint(part_containing_obj) App.closeActiveTransaction() diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index 70e19b11df..c0793a9ae3 100644 --- a/src/Mod/Assembly/CommandInsertLink.py +++ b/src/Mod/Assembly/CommandInsertLink.py @@ -33,6 +33,7 @@ if App.GuiUp: import UtilsAssembly import Preferences +import CommandCreateJoint # translate = App.Qt.translate @@ -106,6 +107,7 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.translation = 0 self.partMoving = False self.totalTranslation = App.Vector() + self.groundedObj = None self.insertionStack = [] # used to handle cancellation of insertions. @@ -261,6 +263,49 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.form.partList.setItemSelected(item, False) + if len(self.insertionStack) == 1 and not UtilsAssembly.isAssemblyGrounded(): + self.handleFirstInsertion() + + def handleFirstInsertion(self): + pref = Preferences.preferences() + fixPart = False + fixPartPref = pref.GetInt("GroundFirstPart", 0) + if fixPartPref == 0: # unset + msgBox = QtWidgets.QMessageBox() + msgBox.setWindowTitle("Ground Part?") + msgBox.setText( + "Do you want to ground the first inserted part automatically?\nYou need at least one grounded part in your assembly." + ) + msgBox.setIcon(QtWidgets.QMessageBox.Question) + + yesButton = msgBox.addButton("Yes", QtWidgets.QMessageBox.YesRole) + noButton = msgBox.addButton("No", QtWidgets.QMessageBox.NoRole) + yesAlwaysButton = msgBox.addButton("Always", QtWidgets.QMessageBox.YesRole) + noAlwaysButton = msgBox.addButton("Never", QtWidgets.QMessageBox.NoRole) + + msgBox.exec_() + + clickedButton = msgBox.clickedButton() + if clickedButton == yesButton: + fixPart = True + elif clickedButton == yesAlwaysButton: + fixPart = True + pref.SetInt("GroundFirstPart", 1) + elif clickedButton == noAlwaysButton: + pref.SetInt("GroundFirstPart", 2) + + elif fixPartPref == 1: # Yes always + fixPart = True + + if fixPart: + # Create groundedJoint. + if len(self.insertionStack) != 1: + return + + self.groundedObj = self.insertionStack[0]["addedObject"] + self.groundedJoint = CommandCreateJoint.createGroundedJoint(self.groundedObj) + self.endMove() + def increment_counter(self, item): text = item.text() match = re.search(r"(\d+) inserted$", text) @@ -373,7 +418,11 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.endMove() self.totalTranslation -= stack_item["translation"] - UtilsAssembly.removeObjAndChilds(stack_item["addedObject"]) + obj = stack_item["addedObject"] + if self.groundedObj == obj: + self.groundedJoint.Document.removeObject(self.groundedJoint.Name) + UtilsAssembly.removeObjAndChilds(obj) + self.decrement_counter(item) del self.insertionStack[i] self.form.partList.setItemSelected(item, False) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 328a7651d7..ab1b0e963c 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -941,7 +941,7 @@ class ViewProviderGroundedJoint: # Remove grounded tag. if hasattr(feature.Object, "ObjectToGround"): obj = feature.Object.ObjectToGround - if obj.Label.endswith(" 🔒"): + if obj is not None and obj.Label.endswith(" 🔒"): obj.Label = obj.Label[:-2] return True # If False is returned the object won't be deleted From ea5a11b44efbaa5e65398168d97ef571eba4e07c Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Mon, 29 Jan 2024 21:02:55 +0100 Subject: [PATCH 18/22] Assembly: fix vertex preselection JCS bug --- src/Mod/Assembly/JointObject.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index ab1b0e963c..407222af86 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -1351,7 +1351,9 @@ class TaskAssemblyCreateJoint(QtCore.QObject): if ( not cursor_info or not self.preselection_dict - or cursor_info["SubName"] != self.preselection_dict["sub_name"] + # or cursor_info["SubName"] != self.preselection_dict["sub_name"] + # Removed because they are not equal when hovering a line endpoints. + # But we don't actually need to test because if there's no preselection then not cursor is None ): self.joint.ViewObject.Proxy.showPreviewJCS(False) return From 11fd6eb4da6e974b9c85c6187579289cd63215b1 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Tue, 30 Jan 2024 10:53:03 +0100 Subject: [PATCH 19/22] Assembly: Fix annoying incomplete initial selections --- src/Mod/Assembly/JointObject.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 407222af86..5d43552914 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -1130,7 +1130,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): objs_names, element_name = UtilsAssembly.getObjsNamesAndElement( sel.ObjectName, sub_name ) - if len(self.current_selection) >= 2 or self.assembly.Name not in objs_names: + if self.assembly.Name not in objs_names: Gui.Selection.removeSelection(sel.Object, sub_name) continue @@ -1154,8 +1154,9 @@ class TaskAssemblyCreateJoint(QtCore.QObject): and selected_object == self.current_selection[0]["object"] ): # do not select several feature of the same object. - Gui.Selection.removeSelection(sel.Object, sub_name) - continue + self.current_selection.clear() + Gui.Selection.clearSelection() + return selection_dict = { "object": selected_object, @@ -1168,7 +1169,12 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.current_selection.append(selection_dict) - self.updateJoint() + # do not accept initial selection if we don't have 2 selected features + if len(self.current_selection) != 2: + self.current_selection.clear() + Gui.Selection.clearSelection() + else: + self.updateJoint() def createJointObject(self): type_index = self.form.jointType.currentIndex() From 51b81c14260d0b059af0c32b2bac5bf00c31a04e Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Tue, 30 Jan 2024 15:14:32 +0100 Subject: [PATCH 20/22] Assembly: Esc pref --- src/Mod/Assembly/Gui/PreCompiled.h | 7 +++ .../Gui/Resources/preferences/Assembly.ui | 60 ++++++++++++++++++- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 16 +++++ src/Mod/Assembly/Gui/ViewProviderAssembly.h | 2 + src/Mod/Assembly/Preferences.py | 14 ++++- src/Mod/Assembly/UtilsAssembly.py | 8 +++ 6 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/Mod/Assembly/Gui/PreCompiled.h b/src/Mod/Assembly/Gui/PreCompiled.h index 02bfff4f01..79ef18d8f4 100644 --- a/src/Mod/Assembly/Gui/PreCompiled.h +++ b/src/Mod/Assembly/Gui/PreCompiled.h @@ -42,6 +42,13 @@ #include #endif +#include + +// all of Inventor +#ifndef __InventorAll__ +#include +#endif + #endif //_PreComp_ #endif // POINTSGUI_PRECOMPILED_H diff --git a/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui b/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui index 83a06b97c5..642cbe1da0 100644 --- a/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui +++ b/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui @@ -13,8 +13,57 @@ General - - + + + + + Allow to leave edit mode when pressing Esc button + + + Esc leave edit mode + + + true + + + LeaveEditWithEscape + + + Mod/Assembly + + + + + + + Ground first part: + + + + + + + When you insert the first part in the assembly, you can choose to ground the part automatically. + + + 0 + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + Qt::Vertical @@ -29,6 +78,13 @@ + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index ab7a4a1c46..b2ee2e1c2b 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #endif #include @@ -235,6 +236,21 @@ App::DocumentObject* ViewProviderAssembly::getActivePart() const return activeView->getActiveObject(PARTKEY); } +bool ViewProviderAssembly::keyPressed(bool pressed, int key) +{ + if (key == SoKeyboardEvent::ESCAPE) { + if (isInEditMode()) { + + ParameterGrp::handle hPgr = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + + return !hPgr->GetBool("LeaveEditWithEscape", true); + } + } + + return false; // handle all other key events +} + bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer) { // Initialize or end the dragging of parts diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index dc2dc8a4ff..5cc7bd684f 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -84,6 +84,8 @@ public: App::DocumentObject* getActivePart() const; + /// is called when the Provider is in edit and a key event ocours. Only ESC ends edit. + bool keyPressed(bool pressed, int key) override; /// 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 diff --git a/src/Mod/Assembly/Preferences.py b/src/Mod/Assembly/Preferences.py index 71c60323ef..8db2be359b 100644 --- a/src/Mod/Assembly/Preferences.py +++ b/src/Mod/Assembly/Preferences.py @@ -24,6 +24,8 @@ import FreeCAD import FreeCADGui +from UtilsAssembly import tr + def preferences(): return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly") @@ -34,7 +36,15 @@ class PreferencesPage: self.form = FreeCADGui.PySideUic.loadUi(":preferences/Assembly.ui") def saveSettings(self): - pass + pref = preferences() + pref.SetBool("LeaveEditWithEscape", self.form.checkBoxEnableEscape.isChecked()) + pref.SetInt("GroundFirstPart", self.form.groundFirstPart.currentIndex()) def loadSettings(self): - pass + pref = preferences() + self.form.checkBoxEnableEscape.setChecked(pref.GetBool("LeaveEditWithEscape", True)) + self.form.groundFirstPart.clear() + self.form.groundFirstPart.addItem(tr("Assembly", "Ask")) + self.form.groundFirstPart.addItem(tr("Assembly", "Always")) + self.form.groundFirstPart.addItem(tr("Assembly", "Never")) + self.form.groundFirstPart.setCurrentIndex(pref.GetInt("GroundFirstPart", 0)) diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 8e623dc737..dfb2e3cb7b 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -27,6 +27,10 @@ import Part if App.GuiUp: import FreeCADGui as Gui +import PySide.QtCore as QtCore +import PySide.QtGui as QtGui + + # translate = App.Qt.translate __title__ = "Assembly utilitary functions" @@ -34,6 +38,10 @@ __author__ = "Ondsel" __url__ = "https://www.freecad.org" +def tr(context, text, comment=None): + return QtCore.QCoreApplication.translate(context, text, comment) + + def activeAssembly(): doc = Gui.ActiveDocument From 926490adce62cbd2a137c9d181606a21b96563be Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Wed, 31 Jan 2024 16:05:32 +0100 Subject: [PATCH 21/22] Assembly fixes --- src/Mod/Assembly/CommandCreateJoint.py | 4 +- src/Mod/Assembly/CommandInsertLink.py | 4 +- src/Mod/Assembly/JointObject.py | 70 +++++++++++++++----------- src/Mod/Assembly/Preferences.py | 8 +-- src/Mod/Assembly/UtilsAssembly.py | 4 -- src/Tools/updatecrowdin.py | 5 ++ src/Tools/updatets.py | 5 ++ 7 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index cb710d6aea..e5d0509d18 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -76,7 +76,7 @@ class CommandCreateJointFixed: + "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", - "2 - If a part is active : Position sub parts by matching seleted coordinate systems. The second part selected will move.", + "2 - If a part is active : Position sub parts by matching selected coordinate systems. The second part selected will move.", ) + "

", "CmdType": "ForEdit", @@ -211,7 +211,7 @@ class CommandCreateJointDistance: "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointDistance", - "Create a Distance Joint: Depending on your selection this tool will apply different constraints.", + "Create a Distance Joint: Fix the distance between the selected objects.", ) + "

", "CmdType": "ForEdit", diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index c0793a9ae3..aeeb7776ff 100644 --- a/src/Mod/Assembly/CommandInsertLink.py +++ b/src/Mod/Assembly/CommandInsertLink.py @@ -59,7 +59,9 @@ class CommandInsertLink: + "

  • " + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert by left clicking items in the list.") + "
  • " - + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Undo by right clicking items in the list.") + + QT_TRANSLATE_NOOP( + "Assembly_InsertLink", "Remove by right clicking items in the list." + ) + "
  • " + QT_TRANSLATE_NOOP( "Assembly_InsertLink", diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 5d43552914..92c998f632 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -42,35 +42,46 @@ from pivy import coin import UtilsAssembly import Preferences +translate = App.Qt.translate + +TranslatedJointTypes = [ + translate("Assembly", "Fixed"), + translate("Assembly", "Revolute"), + translate("Assembly", "Cylindrical"), + translate("Assembly", "Slider"), + translate("Assembly", "Ball"), + translate("Assembly", "Distance"), +] + JointTypes = [ - QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Ball"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), + "Fixed", + "Revolute", + "Cylindrical", + "Slider", + "Ball", + "Distance", ] JointUsingDistance = [ - QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), + "Distance", ] JointUsingOffset = [ - QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), + "Fixed", + "Revolute", ] JointUsingRotation = [ - QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), + "Fixed", + "Slider", ] JointUsingReverse = [ - QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), + "Fixed", + "Revolute", + "Cylindrical", + "Slider", + "Distance", ] @@ -112,7 +123,7 @@ class Joint: "App::PropertyString", # Not PropertyLink because they don't support external objects "Object1", "Joint Connector 1", - QT_TRANSLATE_NOOP("App::Property", "The name of the first object of the joint"), + QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"), ) joint.addProperty( @@ -142,7 +153,7 @@ class Joint: "Joint Connector 1", QT_TRANSLATE_NOOP( "App::Property", - "This is the local coordinate system within the object1 that will be used to joint.", + "This is the local coordinate system within object1 that will be used for the joint.", ), ) @@ -152,7 +163,7 @@ class Joint: "Joint Connector 1", QT_TRANSLATE_NOOP( "App::Property", - "This prevent Placement1 from recomputing, enabling custom positioning of the placement.", + "This prevents Placement1 from recomputing, enabling custom positioning of the placement.", ), ) @@ -161,7 +172,7 @@ class Joint: "App::PropertyString", "Object2", "Joint Connector 2", - QT_TRANSLATE_NOOP("App::Property", "The name of the second object of the joint"), + QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"), ) joint.addProperty( @@ -191,7 +202,7 @@ class Joint: "Joint Connector 2", QT_TRANSLATE_NOOP( "App::Property", - "This is the local coordinate system within the object2 that will be used to joint.", + "This is the local coordinate system within object2 that will be used for the joint.", ), ) @@ -201,7 +212,7 @@ class Joint: "Joint Connector 2", QT_TRANSLATE_NOOP( "App::Property", - "This prevent Placement2 from recomputing, enabling custom positioning of the placement.", + "This prevents Placement2 from recomputing, enabling custom positioning of the placement.", ), ) @@ -241,7 +252,7 @@ class Joint: "Joint", QT_TRANSLATE_NOOP( "App::Property", - "This indicate if the joint is active.", + "This indicates if the joint is active.", ), ) joint.Activated = True @@ -1020,7 +1031,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): if self.activeType == "Part": self.form.setWindowTitle("Match parts") self.form.jointType.hide() - self.form.jointType.addItems(JointTypes) + self.form.jointType.addItems(TranslatedJointTypes) self.form.jointType.setCurrentIndex(jointTypeIndex) self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged) @@ -1189,7 +1200,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): ViewProviderJoint(self.joint.ViewObject) def onJointTypeChanged(self, index): - self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText()) + + self.joint.Proxy.setJointType(self.joint, JointTypes[self.form.jointType.currentIndex()]) self.toggleDistanceVisibility() self.toggleOffsetVisibility() self.toggleRotationVisibility() @@ -1208,7 +1220,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.joint.Proxy.flipOnePart(self.joint) def toggleDistanceVisibility(self): - if self.form.jointType.currentText() in JointUsingDistance: + if JointTypes[self.form.jointType.currentIndex()] in JointUsingDistance: self.form.distanceLabel.show() self.form.distanceSpinbox.show() else: @@ -1216,7 +1228,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.distanceSpinbox.hide() def toggleOffsetVisibility(self): - if self.form.jointType.currentText() in JointUsingOffset: + if JointTypes[self.form.jointType.currentIndex()] in JointUsingOffset: self.form.offsetLabel.show() self.form.offsetSpinbox.show() else: @@ -1224,7 +1236,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.offsetSpinbox.hide() def toggleRotationVisibility(self): - if self.form.jointType.currentText() in JointUsingRotation: + if JointTypes[self.form.jointType.currentIndex()] in JointUsingRotation: self.form.rotationLabel.show() self.form.rotationSpinbox.show() else: @@ -1232,7 +1244,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.rotationSpinbox.hide() def toggleReverseVisibility(self): - if self.form.jointType.currentText() in JointUsingReverse: + if JointTypes[self.form.jointType.currentIndex()] in JointUsingReverse: self.form.PushButtonReverse.show() else: self.form.PushButtonReverse.hide() diff --git a/src/Mod/Assembly/Preferences.py b/src/Mod/Assembly/Preferences.py index 8db2be359b..8f96362a4d 100644 --- a/src/Mod/Assembly/Preferences.py +++ b/src/Mod/Assembly/Preferences.py @@ -24,7 +24,7 @@ import FreeCAD import FreeCADGui -from UtilsAssembly import tr +translate = FreeCAD.Qt.translate def preferences(): @@ -44,7 +44,7 @@ class PreferencesPage: pref = preferences() self.form.checkBoxEnableEscape.setChecked(pref.GetBool("LeaveEditWithEscape", True)) self.form.groundFirstPart.clear() - self.form.groundFirstPart.addItem(tr("Assembly", "Ask")) - self.form.groundFirstPart.addItem(tr("Assembly", "Always")) - self.form.groundFirstPart.addItem(tr("Assembly", "Never")) + self.form.groundFirstPart.addItem(translate("Assembly", "Ask")) + self.form.groundFirstPart.addItem(translate("Assembly", "Always")) + self.form.groundFirstPart.addItem(translate("Assembly", "Never")) self.form.groundFirstPart.setCurrentIndex(pref.GetInt("GroundFirstPart", 0)) diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index dfb2e3cb7b..9a8a39570a 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -38,10 +38,6 @@ __author__ = "Ondsel" __url__ = "https://www.freecad.org" -def tr(context, text, comment=None): - return QtCore.QCoreApplication.translate(context, text, comment) - - def activeAssembly(): doc = Gui.ActiveDocument diff --git a/src/Tools/updatecrowdin.py b/src/Tools/updatecrowdin.py index 9a1b728b06..e83e0fe3e8 100755 --- a/src/Tools/updatecrowdin.py +++ b/src/Tools/updatecrowdin.py @@ -111,6 +111,11 @@ locations = [ ], ["App", "../App/Resources/translations", "../App/Resources/App.qrc"], ["Arch", "../Mod/Arch/Resources/translations", "../Mod/Arch/Resources/Arch.qrc"], + [ + "Assembly", + "../Mod/Assembly/Gui/Resources/translations", + "../Mod/Assembly/Gui/Resources/Assembly.qrc", + ], [ "draft", "../Mod/Draft/Resources/translations", diff --git a/src/Tools/updatets.py b/src/Tools/updatets.py index e2e670aac2..d528ed8801 100755 --- a/src/Tools/updatets.py +++ b/src/Tools/updatets.py @@ -67,6 +67,11 @@ directories = [ "workingdir": "./src/Mod/Arch/", "tsdir": "Resources/translations", }, + { + "tsname": "Assembly", + "workingdir": "./src/Mod/Assembly/", + "tsdir": "Gui/Resources/translations", + }, { "tsname": "Draft", "workingdir": "./src/Mod/Draft/", From ead9f41541fb23660c12cfbbd02ca581e0f39532 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Mon, 12 Feb 2024 17:41:16 +0100 Subject: [PATCH 22/22] Assembly: Small language tweaks by @chennes --- src/Mod/Assembly/CommandCreateAssembly.py | 2 +- src/Mod/Assembly/InitGui.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index 0f22d29a29..4a6e9e34b2 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -49,7 +49,7 @@ class CommandCreateAssembly: "Accel": "A", "ToolTip": QT_TRANSLATE_NOOP( "Assembly_CreateAssembly", - "Create an assembly object in the current document or if in the current active assembly if any. One root assembly per file max.", + "Create an assembly object in the current document, or in the current active assembly (if any). Limit of one root assembly per file.", ), "CmdType": "ForEdit", } diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index b49cba344c..723b877aa3 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -56,7 +56,6 @@ class AssemblyWorkbench(Workbench): self.__class__.ToolTip = "Assembly workbench" def Initialize(self): - print("Initializing Assembly workbench...") global AssemblyCommandGroup translate = FreeCAD.Qt.translate @@ -104,8 +103,6 @@ class AssemblyWorkbench(Workbench): cmdList + cmdListMenuOnly + ["Separator"] + cmdListJoints, ) - print("Assembly workbench loaded") - def Activated(self): # update the translation engine FreeCADGui.updateLocale()