diff --git a/src/Gui/Icons/media-playback-start-back.svg b/src/Gui/Icons/media-playback-start-back.svg new file mode 100644 index 0000000000..fbc10e25f2 --- /dev/null +++ b/src/Gui/Icons/media-playback-start-back.svg @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + play + media + music + video + player + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/Icons/media-playback-step-back.svg b/src/Gui/Icons/media-playback-step-back.svg new file mode 100644 index 0000000000..8f407aa750 --- /dev/null +++ b/src/Gui/Icons/media-playback-step-back.svg @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + play + media + music + video + player + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/Icons/media-playback-step.svg b/src/Gui/Icons/media-playback-step.svg new file mode 100644 index 0000000000..5873e85c95 --- /dev/null +++ b/src/Gui/Icons/media-playback-step.svg @@ -0,0 +1,432 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + play + media + music + video + player + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc index 0cc5702e6f..694dfbd191 100644 --- a/src/Gui/Icons/resource.qrc +++ b/src/Gui/Icons/resource.qrc @@ -50,6 +50,9 @@ button_valid.svg button_invalid.svg media-playback-start.svg + media-playback-start-back.svg + media-playback-step.svg + media-playback-step-back.svg media-record.svg media-playback-stop.svg preferences-display.svg diff --git a/src/Mod/Assembly/App/AppAssembly.cpp b/src/Mod/Assembly/App/AppAssembly.cpp index a2679171a7..578c592ca3 100644 --- a/src/Mod/Assembly/App/AppAssembly.cpp +++ b/src/Mod/Assembly/App/AppAssembly.cpp @@ -33,6 +33,7 @@ #include "BomGroup.h" #include "JointGroup.h" #include "ViewGroup.h" +#include "SimulationGroup.h" namespace Assembly @@ -68,6 +69,7 @@ PyMOD_INIT_FUNC(AssemblyApp) Assembly::BomGroup ::init(); Assembly::JointGroup ::init(); Assembly::ViewGroup ::init(); + Assembly::SimulationGroup ::init(); PyMOD_Return(mod); } diff --git a/src/Mod/Assembly/App/AssemblyLinkPy.xml b/src/Mod/Assembly/App/AssemblyLinkPy.xml index 2b72b7af31..e21a050895 100644 --- a/src/Mod/Assembly/App/AssemblyLinkPy.xml +++ b/src/Mod/Assembly/App/AssemblyLinkPy.xml @@ -13,7 +13,12 @@ This class handles document objects in Assembly - + + + A list of all joints this assembly link has. + + + diff --git a/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp b/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp index 2a7123a213..5d9a9e78c7 100644 --- a/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp +++ b/src/Mod/Assembly/App/AssemblyLinkPyImp.cpp @@ -45,3 +45,15 @@ int AssemblyLinkPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) { return 0; } + +Py::List AssemblyLinkPy::getJoints() const +{ + Py::List ret; + std::vector list = getAssemblyLinkPtr()->getJoints(); + + for (auto It : list) { + ret.append(Py::Object(It->getPyObject(), true)); + } + + return ret; +} diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index cd5c454f92..ee82a21e4e 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -29,6 +29,9 @@ #include #endif +#include +#include + #include #include #include @@ -67,10 +70,15 @@ #include #include #include +#include +#include +#include #include #include #include #include +#include +#include #include "AssemblyLink.h" #include "AssemblyObject.h" @@ -78,6 +86,7 @@ #include "AssemblyUtils.h" #include "JointGroup.h" #include "ViewGroup.h" +#include "SimulationGroup.h" FC_LOG_LEVEL_INIT("Assembly", true, true, true) @@ -110,7 +119,9 @@ PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) AssemblyObject::AssemblyObject() : mbdAssembly(std::make_shared()) , bundleFixed(false) -{} +{ + mbdAssembly->externalSystem->freecadAssemblyObject = this; +} AssemblyObject::~AssemblyObject() = default; @@ -141,6 +152,7 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS) mbdAssembly = makeMbdAssembly(); objectPartMap.clear(); + motions.clear(); std::vector groundedObjs = fixGroundedParts(); if (groundedObjs.empty()) { @@ -159,7 +171,8 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS) } try { - mbdAssembly->runPreDrag(); // solve() is causing some issues with limits. + // mbdAssembly->runPreDrag(); // solve() is causing some issues with limits. + mbdAssembly->runKINEMATIC(); } catch (const std::exception& e) { FC_ERR("Solve failed: " << e.what()); @@ -177,6 +190,79 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS) return 0; } +int AssemblyObject::generateSimulation(App::DocumentObject* sim) +{ + mbdAssembly = makeMbdAssembly(); + objectPartMap.clear(); + + motions = getMotionsFromSimulation(sim); + + 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); + + create_mbdSimulationParameters(sim); + + + try { + mbdAssembly->runKINEMATIC(); + } + catch (...) { + Base::Console().Error("Generation of simulation failed\n"); + motions.clear(); + return -1; + } + + motions.clear(); + + return 0; +} + +std::vector AssemblyObject::getMotionsFromSimulation(App::DocumentObject* sim) +{ + if (!sim) { + return {}; + } + + auto* prop = dynamic_cast(sim->getPropertyByName("Group")); + if (!prop) { + return {}; + } + + return prop->getValue(); +} + +int Assembly::AssemblyObject::updateForFrame(size_t index, bool updateJCS) +{ + if (!mbdAssembly) { + return -1; + } + + auto nfrms = mbdAssembly->numberOfFrames(); + if (index >= nfrms) { + return -1; + } + + mbdAssembly->updateForFrame(index); + setNewPlacements(); + auto jointDocs = getJoints(updateJCS); + redrawJointPlacements(jointDocs); + return 0; +} + +size_t Assembly::AssemblyObject::numberOfFrames() +{ + return mbdAssembly->numberOfFrames(); +} + void AssemblyObject::preDrag(std::vector dragParts) { bundleFixed = true; @@ -480,6 +566,7 @@ void AssemblyObject::recomputeJointPlacements(std::vector std::shared_ptr AssemblyObject::makeMbdAssembly() { auto assembly = CREATE::With(); + assembly->externalSystem->freecadAssemblyObject = this; assembly->setName("OndselAssembly"); ParameterGrp::handle hPgr = App::GetApplication().GetParameterGroupByPath( @@ -521,6 +608,23 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::Docum return nullptr; } +template +T* AssemblyObject::getGroup() +{ + App::Document* doc = getDocument(); + + std::vector groups = doc->getObjectsOfType(T::getClassTypeId()); + if (groups.empty()) { + return nullptr; + } + for (auto group : groups) { + if (hasObject(group)) { + return dynamic_cast(group); + } + } + return nullptr; +} + JointGroup* AssemblyObject::getJointGroup() const { return Assembly::getJointGroup(this); @@ -964,6 +1068,27 @@ void AssemblyObject::jointParts(std::vector joints) } } +void Assembly::AssemblyObject::create_mbdSimulationParameters(App::DocumentObject* sim) +{ + auto mbdSim = mbdAssembly->simulationParameters; + if (!sim) { + return; + } + auto valueOf = [](DocumentObject* docObj, const char* propName) { + auto* prop = dynamic_cast(docObj->getPropertyByName(propName)); + if (!prop) { + return 0.0; + } + return prop->getValue(); + }; + mbdSim->settstart(valueOf(sim, "aTimeStart")); + mbdSim->settend(valueOf(sim, "bTimeEnd")); + mbdSim->sethout(valueOf(sim, "cTimeStepOutput")); + mbdSim->sethmin(1.0e-9); + mbdSim->sethmax(1.0); + mbdSim->seterrorTol(valueOf(sim, "fGlobalErrorTolerance")); +} + std::shared_ptr AssemblyObject::makeMbdJointOfType(App::DocumentObject* joint, JointType type) { @@ -1351,7 +1476,7 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) if (maxEnabled) { auto limit2 = ASMTRotationLimit::With(); - limit2->setName(joint->getFullName() + "-LimiRotMax"); + limit2->setName(joint->getFullName() + "-LimitRotMax"); limit2->setMarkerI(fullMarkerNameI); limit2->setMarkerJ(fullMarkerNameJ); limit2->settype("=<"); @@ -1361,6 +1486,90 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint) } } } + std::vector done; + // Add motions if needed + for (auto* motion : motions) { + if (std::find(done.begin(), done.end(), motion) != done.end()) { + continue; // don't process twice (can happen in case of cylindrical) + } + + auto* pJoint = dynamic_cast(motion->getPropertyByName("Joint")); + if (!pJoint) { + continue; + } + App::DocumentObject* motionJoint = pJoint->getValue(); + if (joint != motionJoint) { + continue; + } + + auto* pType = + dynamic_cast(motion->getPropertyByName("MotionType")); + auto* pFormula = dynamic_cast(motion->getPropertyByName("Formula")); + if (!pType || !pFormula) { + continue; + } + std::string formula = pFormula->getValue(); + if (formula == "") { + continue; + } + std::string motionType = pType->getValueAsString(); + + // check if there is a second motion as cylindrical can have both, + // in which case the solver needs a general motion. + for (auto* motion2 : motions) { + pJoint = dynamic_cast(motion2->getPropertyByName("Joint")); + if (!pJoint) { + continue; + } + motionJoint = pJoint->getValue(); + if (joint != motionJoint || motion2 == motion) { + continue; + } + + auto* pType2 = + dynamic_cast(motion2->getPropertyByName("MotionType")); + auto* pFormula2 = + dynamic_cast(motion2->getPropertyByName("Formula")); + if (!pType2 || !pFormula2) { + continue; + } + std::string formula2 = pFormula2->getValue(); + if (formula2 == "") { + continue; + } + std::string motionType2 = pType2->getValueAsString(); + if (motionType2 == motionType) { + continue; // only if both motions are different. ie one angular and one linear. + } + + auto ASMTmotion = CREATE::With(); + ASMTmotion->setName(joint->getFullName() + "-ScrewMotion"); + ASMTmotion->setMarkerI(fullMarkerNameI); + ASMTmotion->setMarkerJ(fullMarkerNameJ); + ASMTmotion->rIJI->atiput(2, motionType == "Angular" ? formula2 : formula); + ASMTmotion->angIJJ->atiput(2, motionType == "Angular" ? formula : formula2); + mbdAssembly->addMotion(ASMTmotion); + + done.push_back(motion2); + } + + if (motionType == "Angular") { + auto ASMTmotion = CREATE::With(); + ASMTmotion->setName(joint->getFullName() + "-AngularMotion"); + ASMTmotion->setMarkerI(fullMarkerNameI); + ASMTmotion->setMarkerJ(fullMarkerNameJ); + ASMTmotion->setRotationZ(formula); + mbdAssembly->addMotion(ASMTmotion); + } + else if (motionType == "Linear") { + auto ASMTmotion = CREATE::With(); + ASMTmotion->setName(joint->getFullName() + "-LinearMotion"); + ASMTmotion->setMarkerI(fullMarkerNameI); + ASMTmotion->setMarkerJ(fullMarkerNameJ); + ASMTmotion->setTranslationZ(formula); + mbdAssembly->addMotion(ASMTmotion); + } + } return {mbdJoint}; } @@ -1565,7 +1774,7 @@ AssemblyObject::MbDPartData AssemblyObject::getMbDData(App::DocumentObject* part MbDPartData data = {mbdPart, Base::Placement()}; objectPartMap[part] = data; // Store the association - // Associate other objects conneted with fixed joints + // Associate other objects connected with fixed joints if (bundleFixed) { auto addConnectedFixedParts = [&](App::DocumentObject* currentPart, auto& self) -> void { std::vector joints = getJointsOfPart(currentPart); diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index c310ab6da6..8ddf548a42 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -31,6 +31,9 @@ #include #include #include +#include "SimulationGroup.h" + +#include <3rdParty/OndselSolver/OndselSolver/enum.h> namespace MbD { @@ -90,6 +93,9 @@ public: 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, bool updateJCS = true); + int generateSimulation(App::DocumentObject* sim); + int updateForFrame(size_t index, bool updateJCS = true); + size_t numberOfFrames(); void preDrag(std::vector dragParts); void doDragStep(); void postDrag(); @@ -111,6 +117,7 @@ public: // Ondsel Solver interface std::shared_ptr makeMbdAssembly(); + void create_mbdSimulationParameters(App::DocumentObject* sim); std::shared_ptr makeMbdPart(std::string& name, Base::Placement plc = Base::Placement(), double mass = 1.0); std::shared_ptr getMbDPart(App::DocumentObject* obj); @@ -140,6 +147,9 @@ public: void jointParts(std::vector joints); JointGroup* getJointGroup() const; ViewGroup* getExplodedViewGroup() const; + template + T* getGroup(); + std::vector getJoints(bool updateJCS = true, bool delBadJoints = false, bool subJoints = true); std::vector getGroundedJoints(); @@ -178,12 +188,15 @@ public: std::vector getSubAssemblies(); void updateGroundedJointsPlacements(); + std::vector getMotionsFromSimulation(App::DocumentObject* sim); + private: std::shared_ptr mbdAssembly; std::unordered_map objectPartMap; std::vector> objMasses; std::vector draggedParts; + std::vector motions; std::vector> previousPositions; diff --git a/src/Mod/Assembly/App/AssemblyObjectPy.xml b/src/Mod/Assembly/App/AssemblyObjectPy.xml index 35f1ba366d..5318e168b1 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPy.xml +++ b/src/Mod/Assembly/App/AssemblyObjectPy.xml @@ -38,6 +38,52 @@ + + + + Generate the simulation. + + solve(simulationObject) -> int + + Args: + simulationObject: The simulation Object. + + 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. + + + + + + + Update entire assembly to frame number specified. + + updateForFrame(index) + + Args: index of frame. + + Returns: None + + + + + + + numberOfFrames() + + Args: None + + Returns: Number of frames + + + @@ -125,7 +171,12 @@ - + + + A list of all joints this assembly has. + + + diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp index 5bd41b3872..c292cb1270 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp +++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp @@ -68,6 +68,18 @@ PyObject* AssemblyObjectPy::solve(PyObject* args) return Py_BuildValue("i", ret); } +PyObject* AssemblyObjectPy::generateSimulation(PyObject* args) +{ + PyObject* pyobj; + + if (!PyArg_ParseTuple(args, "O", &pyobj)) { + return nullptr; + } + auto* obj = static_cast(pyobj)->getDocumentObjectPtr(); + int ret = this->getAssemblyObjectPtr()->generateSimulation(obj); + return Py_BuildValue("i", ret); +} + PyObject* AssemblyObjectPy::ensureIdentityPlacements(PyObject* args) { if (!PyArg_ParseTuple(args, "")) { @@ -77,6 +89,31 @@ PyObject* AssemblyObjectPy::ensureIdentityPlacements(PyObject* args) Py_Return; } +PyObject* AssemblyObjectPy::updateForFrame(PyObject* args) +{ + unsigned long index {}; + + if (!PyArg_ParseTuple(args, "k", &index)) { + throw Py::RuntimeError("updateForFrame requires an integer index"); + } + PY_TRY + { + this->getAssemblyObjectPtr()->updateForFrame(index); + } + PY_CATCH; + + Py_Return; +} + +PyObject* AssemblyObjectPy::numberOfFrames(PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + size_t ret = this->getAssemblyObjectPtr()->numberOfFrames(); + return Py_BuildValue("k", ret); +} + PyObject* AssemblyObjectPy::undoSolve(PyObject* args) { if (!PyArg_ParseTuple(args, "")) { @@ -151,3 +188,15 @@ PyObject* AssemblyObjectPy::exportAsASMT(PyObject* args) Py_Return; } + +Py::List AssemblyObjectPy::getJoints() const +{ + Py::List ret; + std::vector list = getAssemblyObjectPtr()->getJoints(); + + for (auto It : list) { + ret.append(Py::Object(It->getPyObject(), true)); + } + + return ret; +} diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt index 0d3b8dcd60..b945f612c1 100644 --- a/src/Mod/Assembly/App/CMakeLists.txt +++ b/src/Mod/Assembly/App/CMakeLists.txt @@ -28,6 +28,7 @@ generate_from_xml(BomObjectPy) generate_from_xml(BomGroupPy) generate_from_xml(JointGroupPy) generate_from_xml(ViewGroupPy) +generate_from_xml(SimulationGroupPy) SET(Python_SRCS AssemblyObjectPy.xml @@ -42,6 +43,8 @@ SET(Python_SRCS JointGroupPyImp.cpp ViewGroupPy.xml ViewGroupPyImp.cpp + SimulationGroupPy.xml + SimulationGroupPyImp.cpp ) SOURCE_GROUP("Python" FILES ${Python_SRCS}) @@ -68,6 +71,8 @@ SET(Assembly_SRCS JointGroup.h ViewGroup.cpp ViewGroup.h + SimulationGroup.cpp + SimulationGroup.h ${Module_SRCS} ${Python_SRCS} ) diff --git a/src/Mod/Assembly/App/SimulationGroup.cpp b/src/Mod/Assembly/App/SimulationGroup.cpp new file mode 100644 index 0000000000..d80197a414 --- /dev/null +++ b/src/Mod/Assembly/App/SimulationGroup.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2024 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#endif + +#include +#include +#include +#include +#include +#include + +#include "SimulationGroup.h" +#include "SimulationGroupPy.h" + +using namespace Assembly; + + +PROPERTY_SOURCE(Assembly::SimulationGroup, App::DocumentObjectGroup) + +SimulationGroup::SimulationGroup() +{} + +SimulationGroup::~SimulationGroup() = default; + +PyObject* SimulationGroup::getPyObject() +{ + if (PythonObject.is(Py::_None())) { + // ref counter is set to 1 + PythonObject = Py::Object(new SimulationGroupPy(this), true); + } + return Py::new_reference_to(PythonObject); +} diff --git a/src/Mod/Assembly/App/SimulationGroup.h b/src/Mod/Assembly/App/SimulationGroup.h new file mode 100644 index 0000000000..279305524c --- /dev/null +++ b/src/Mod/Assembly/App/SimulationGroup.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2024 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#ifndef ASSEMBLY_SimulationGroup_H +#define ASSEMBLY_SimulationGroup_H + +#include + +#include +#include + + +namespace Assembly +{ + +class AssemblyExport SimulationGroup: public App::DocumentObjectGroup +{ + PROPERTY_HEADER_WITH_OVERRIDE(Assembly::SimulationGroup); + +public: + SimulationGroup(); + ~SimulationGroup() override; + + PyObject* getPyObject() override; + + /// returns the type name of the ViewProvider + const char* getViewProviderName() const override + { + return "AssemblyGui::ViewProviderSimulationGroup"; + } +}; + + +} // namespace Assembly + + +#endif // ASSEMBLY_SimulationGroup_H diff --git a/src/Mod/Assembly/App/SimulationGroupPy.xml b/src/Mod/Assembly/App/SimulationGroupPy.xml new file mode 100644 index 0000000000..cab1c61da8 --- /dev/null +++ b/src/Mod/Assembly/App/SimulationGroupPy.xml @@ -0,0 +1,19 @@ + + + + + + This class is a group subclass for joints. + + + + + diff --git a/src/Mod/Assembly/App/SimulationGroupPyImp.cpp b/src/Mod/Assembly/App/SimulationGroupPyImp.cpp new file mode 100644 index 0000000000..1d08082cb8 --- /dev/null +++ b/src/Mod/Assembly/App/SimulationGroupPyImp.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2024 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +// inclusion of the generated files (generated out of SimulationGroup.xml) +#include "SimulationGroupPy.h" +#include "SimulationGroupPy.cpp" + +using namespace Assembly; + +// returns a string which represents the object e.g. when printed in python +std::string SimulationGroupPy::representation() const +{ + return {""}; +} + +PyObject* SimulationGroupPy::getCustomAttributes(const char* /*attr*/) const +{ + return nullptr; +} + +int SimulationGroupPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt index 3c48c61cff..e74cdbce46 100644 --- a/src/Mod/Assembly/CMakeLists.txt +++ b/src/Mod/Assembly/CMakeLists.txt @@ -13,6 +13,7 @@ set(Assembly_Scripts CommandSolveAssembly.py CommandCreateJoint.py CommandCreateView.py + CommandCreateSimulation.py CommandExportASMT.py TestAssemblyWorkbench.py JointObject.py diff --git a/src/Mod/Assembly/CommandCreateSimulation.py b/src/Mod/Assembly/CommandCreateSimulation.py new file mode 100644 index 0000000000..595ccc2295 --- /dev/null +++ b/src/Mod/Assembly/CommandCreateSimulation.py @@ -0,0 +1,1012 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /************************************************************************** +# * +# Copyright (c) 2024 Ondsel * +# * +# This file is part of FreeCAD. * +# * +# FreeCAD is free software: you can redistribute it and/or modify it * +# under the terms of the GNU Lesser General Public License as * +# published by the Free Software Foundation, either version 2.1 of the * +# License, or (at your option) any later version. * +# * +# FreeCAD is distributed in the hope that it will be useful, but * +# WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# Lesser General Public License for more details. * +# * +# You should have received a copy of the GNU Lesser General Public * +# License along with FreeCAD. If not, see * +# . * +# * +# **************************************************************************/ + +import re +import os +import time +import FreeCAD as App + +from pivy import coin +from Part import LineSegment, Compound + +from PySide.QtCore import QT_TRANSLATE_NOOP + +if App.GuiUp: + import FreeCADGui as Gui + from PySide import QtCore, QtGui, QtWidgets + from PySide.QtWidgets import ( + QPushButton, + QMenu, + QDialog, + QComboBox, + QLineEdit, + QGridLayout, + QLabel, + QDialogButtonBox, + ) + from PySide.QtCore import Qt, QPoint + from PySide.QtGui import QCursor, QIcon, QGuiApplication + +import UtilsAssembly +import Preferences + +translate = App.Qt.translate + +__title__ = "Assembly Command Create Simulation" +__author__ = "Ondsel" +__url__ = "https://www.freecad.org" + + +class CommandCreateSimulation: + def __init__(self): + pass + + def GetResources(self): + return { + "Pixmap": "Assembly_CreateSimulation", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateSimulation", "Create Simulation"), + "Accel": "S", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateSimulation", + "Create a simulation of the current assembly.", + ) + + "

", + "CmdType": "ForEdit", + } + + def IsActive(self): + return ( + UtilsAssembly.isAssemblyCommandActive() + and UtilsAssembly.assembly_has_at_least_n_parts(1) + ) + + def Activated(self): + assembly = UtilsAssembly.activeAssembly() + if not assembly: + return + + self.panel = TaskAssemblyCreateSimulation() + Gui.Control.showDialog(self.panel) + + +######### Simulation Object ########### +class Simulation: + def __init__(self, feaPy): + feaPy.Proxy = self + feaPy.addExtension("App::GroupExtensionPython") + + if not hasattr(feaPy, "aTimeStart"): + feaPy.addProperty( + "App::PropertyTime", + "aTimeStart", + "Simulation", + QT_TRANSLATE_NOOP( + "App::Property", + "Simulation start time.", + ), + ) + + if not hasattr(feaPy, "bTimeEnd"): + feaPy.addProperty( + "App::PropertyTime", + "bTimeEnd", + "Simulation", + QT_TRANSLATE_NOOP( + "App::Property", + "Simulation end time.", + ), + ) + + if not hasattr(feaPy, "cTimeStepOutput"): + feaPy.addProperty( + "App::PropertyTime", + "cTimeStepOutput", + "Simulation", + QT_TRANSLATE_NOOP( + "App::Property", + "Simulation time step for output.", + ), + ) + + if not hasattr(feaPy, "fGlobalErrorTolerance"): + feaPy.addProperty( + "App::PropertyFloat", + "fGlobalErrorTolerance", + "Simulation", + QT_TRANSLATE_NOOP( + "App::Property", + "Integration global error tolerance.", + ), + ) + + if not hasattr(feaPy, "jFramesPerSecond"): + feaPy.addProperty( + "App::PropertyInteger", + "jFramesPerSecond", + "Simulation", + QT_TRANSLATE_NOOP( + "App::Property", + "Frames Per Second.", + ), + ) + + feaPy.aTimeStart = 0.0 + feaPy.bTimeEnd = 1.0 + feaPy.cTimeStepOutput = 1.0e-2 + feaPy.fGlobalErrorTolerance = 1.0e-6 + feaPy.jFramesPerSecond = 30 + + self.motionsChangedCallback = None + + def dumps(self): + return None + + def loads(self, state): + return None + + def onChanged(self, feaPy, prop): + if prop == "Group" and hasattr(self, "motionsChangedCallback"): + if self.motionsChangedCallback is not None: + self.motionsChangedCallback() + + def setMotionsChangedCallback(self, callback): + self.motionsChangedCallback = callback + + def execute(self, feaPy): + """Do something when doing a recomputation, this method is mandatory""" + # App.Console.PrintMessage("Recompute Python Box feature\n") + pass + + def getAssembly(self, feaPy): + assert feaPy.isDerivedFrom("App::FeaturePython"), "Type error" + for obj in feaPy.InList: + if obj.isDerivedFrom("Assembly::AssemblyObject"): + return obj + return None + + +class ViewProviderSimulation: + def __init__(self, vpDoc): + vpDoc.Proxy = self + self.Object = vpDoc.Object + self.setProperties(vpDoc) + + def setProperties(self, vpDoc): + if not hasattr(vpDoc, "Decimals"): + vpDoc.addProperty( + "App::PropertyInteger", + "Decimals", + "Space", + QT_TRANSLATE_NOOP( + "App::Property", "The number of decimals to use for calculated texts" + ), + ) + vpDoc.Decimals = 9 + + def attach(self, vpDoc): + """Setup the scene sub-graph of the view provider, this method is mandatory""" + self.app_obj = vpDoc.Object + + self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() + + vpDoc.addDisplayMode(self.display_mode, "Wireframe") + + def updateData(self, feaPy, prop): + """If a property of the handled feature has changed we have the chance to handle this here""" + pass + + def getDisplayModes(self, vpDoc): + """Return a list of display modes.""" + return ["Wireframe"] + + def getDefaultDisplayMode(self): + """Return the name of the default display mode. It must be defined in getDisplayModes.""" + return "Wireframe" + + def onChanged(self, vpDoc, prop): + """Here we can do something when a single property got changed""" + pass + + def getIcon(self): + return ":/icons/Assembly_CreateSimulation.svg" + + def dumps(self): + """When saving the document this object gets stored using Python's json module.\ + Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ + to return a tuple of all serializable objects or None.""" + return None + + def loads(self, state): + """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 claimChildren(self): + return self.app_obj.Group + + def doubleClicked(self, vpDoc): + task = Gui.Control.activeTaskDialog() + if task: + task.reject() + + assembly = vpDoc.Object.Proxy.getAssembly(vpDoc.Object) + + if assembly is None: + return False + + if UtilsAssembly.activeAssembly() != assembly: + Gui.ActiveDocument.setEdit(assembly) + + panel = TaskAssemblyCreateSimulation(vpDoc.Object) + Gui.Control.showDialog(panel) + + return True + + def onDelete(self, vobj, subelements): + for obj in self.claimChildren(): + obj.Document.removeObject(obj.Name) + return True + + +########### Motion Object ############# +MotionTypes = [ + "Angular", + "Linear", +] + + +class Motion: + def __init__(self, feaPy, motionType=MotionTypes[0], joint=None, formula=""): + feaPy.Proxy = self + + self.createProperties(feaPy) + + feaPy.MotionType = MotionTypes # sets the list + feaPy.MotionType = motionType # set the initial value + feaPy.Joint = joint + feaPy.Formula = formula + + def onDocumentRestored(self, feaPy): + self.createProperties(feaPy) + + def createProperties(self, feaPy): + if not hasattr(feaPy, "Joint"): + feaPy.addProperty( + "App::PropertyXLinkSubHidden", + "Joint", + "Motion", + QT_TRANSLATE_NOOP("App::Property", "The joint that is moved by the motion"), + ) + + if not hasattr(feaPy, "Formula"): + feaPy.addProperty( + "App::PropertyString", + "Formula", + "Motion", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the formula of the motion. For example '1.0*time'.", + ), + ) + + if not hasattr(feaPy, "MotionType"): + feaPy.addProperty( + "App::PropertyEnumeration", + "MotionType", + "Motion", + QT_TRANSLATE_NOOP("App::Property", "The type of the motion"), + ) + + def dumps(self): + return None + + def loads(self, state): + return None + + def onChanged(self, feaPy, prop): + pass + + def execute(self, feaPy): + """Do something when doing a recomputation, this method is mandatory""" + # App.Console.PrintMessage("Recompute Python Box feature\n") + pass + + def getSimulation(self, feaPy): + for obj in feaPy.InList: + if hasattr(obj, "Proxy"): + if hasattr(obj.Proxy, "setMotionsChangedCallback"): + return obj + return None + + def getAssembly(self, feaPy): + simulation = self.getSimulation(feaPy) + if simulation is not None: + return simulation.getAssembly() + return None + + +class ViewProviderMotion: + def __init__(self, vp): + vp.Proxy = self + self.updateLabel() + + def attach(self, vpDoc): + """Setup the scene sub-graph of the view provider, this method is mandatory""" + self.app_obj = vpDoc.Object + self.assembly = self.app_obj.Proxy.getAssembly(self.app_obj) + + self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() + + vpDoc.addDisplayMode(self.display_mode, "Wireframe") + + def updateData(self, feaPy, prop): + """If a property of the handled feature has changed we have the chance to handle this here""" + pass + + def getDisplayModes(self, vpDoc): + """Return a list of display modes.""" + return ["Wireframe"] + + def getDefaultDisplayMode(self): + """Return the name of the default display mode. It must be defined in getDisplayModes.""" + return "Wireframe" + + def onChanged(self, vpDoc, prop): + """Here we can do something when a single property got changed""" + # App.Console.PrintMessage("Change property: " + str(prop) + "\n") + pass + + def getIcon(self): + if self.app_obj.MotionType == "Angular": + return ":/icons/button_rotate.svg" + + return ":/icons/button_right.svg" + + def dumps(self): + """When saving the document this object gets stored using Python's json module.\ + Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ + to return a tuple of all serializable objects or None.""" + return None + + def loads(self, state): + """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, vpDoc): + if self.assembly is None: + return False + + if UtilsAssembly.activeAssembly() != self.assembly: + Gui.ActiveDocument.setEdit(self.assembly) + + self.openEditDialog() + + def openEditDialog(self): + joint = None + if self.app_obj.Joint is not None: + joint = self.app_obj.Joint[0] + + dialog = MotionEditDialog( + self.assembly, self.app_obj.MotionType, joint, self.app_obj.Formula + ) + if dialog.exec_(): + self.app_obj.MotionType = dialog.motionType + self.app_obj.Joint = dialog.joint + self.app_obj.Formula = dialog.formula + + self.updateLabel() + + def updateLabel(self): + if self.app_obj.Joint is None: + return + + typeStr = "Linear" if self.app_obj.MotionType == "Linear" else "Angular" + + self.app_obj.Label = "{label} ({type_})".format( + label=self.app_obj.Joint[0].Label, type_=translate("Assembly", typeStr) + ) + + +class MotionEditDialog: + def __init__(self, assembly, motionType=MotionTypes[0], joint=None, formula="5*time"): + self.assembly = assembly + self.motionType = motionType + self.joint = joint + self.formula = formula + + # Create a non-modal, frameless dialog + self.dialog = QDialog() + self.dialog.setWindowFlags(Qt.Popup) + self.initialPos = QCursor.pos() + self.dialog.setMinimumSize(500, 200) # Set a reasonable minimum size + + # Create the joints combobox + self.joint_combo = QComboBox(self.dialog) + self.setup_joint_combo() + + # Create the motion type combobox + self.motion_type_combo = QComboBox(self.dialog) + self.setup_motiontype_combo() + + def on_motion_type_changed(text): + self.motionType = text + + self.motion_type_combo.currentTextChanged.connect(on_motion_type_changed) + + def on_joint_changed(index): + self.joint = self.joint_combo.itemData(index) + self.setup_motiontype_combo() # Refresh the motion combo box based on the new joint type + + self.joint_combo.currentIndexChanged.connect(on_joint_changed) + + # Create the line edit for the formula + formula_edit = QLineEdit(self.dialog) + formula_edit.setText(self.formula) + formula_edit.setPlaceholderText(translate("Assembly", "Enter your formula...")) + + # Connect the line edit to update the Formula property + def on_formula_changed(text): + self.formula = text + + formula_edit.textChanged.connect(on_formula_changed) + + self.setupHelpSection() + + # Create Ok and Cancel buttons + button_box = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self.dialog + ) + button_box.accepted.connect(self.dialog.accept) + button_box.rejected.connect(self.dialog.reject) + + # Set up the layout of the dialog + layout = QGridLayout(self.dialog) + + # Add labels and widgets to the layout + layout.addWidget(QLabel("Joint:"), 0, 0) + layout.addWidget(self.joint_combo, 0, 1) + + layout.addWidget(QLabel("Motion Type:"), 1, 0) + layout.addWidget(self.motion_type_combo, 1, 1) + + layout.addWidget(QLabel("Formula:"), 2, 0) + layout.addWidget(formula_edit, 2, 1) + + # Add the help label above the buttons + layout.addWidget(self.help_label0, 3, 0, 1, 2) + layout.addWidget(self.help_label1, 4, 0, 1, 2) + layout.addWidget(self.help_label2, 5, 0, 1, 2) + layout.addWidget(self.help_label3, 6, 0, 1, 2) + layout.addWidget(self.help_label4, 7, 0, 1, 2) + layout.addWidget(self.help_label5, 8, 0, 1, 2) + layout.addWidget(self.help_label6, 9, 0, 1, 2) + layout.addWidget(self.help_label7, 10, 0, 1, 2) + # Add the help button and button box in the next row + layout.addWidget(self.help_button, 11, 0) + + layout.addWidget(button_box, 11, 1) + + self.positionDialog() + + def setupHelpSection(self): + + # Create the help QLabels and set them to be initially hidden + self.help_label0 = QLabel( + translate( + "Assembly", + "In capital are variables that you need to replace with actual values. More details about each example in it's tooltip.", + ), + self.dialog, + ) + self.help_label1 = QLabel(translate("Assembly", " - Linear: C + VEL*time"), self.dialog) + self.help_label2 = QLabel( + translate("Assembly", " - Quadratic: C + VEL*time + ACC*time^2"), self.dialog + ) + self.help_label3 = QLabel( + translate("Assembly", " - Harmonic: C + AMP*sin(VEL*time - PHASE)"), self.dialog + ) + self.help_label4 = QLabel( + translate("Assembly", " - Exponential: C*exp(time/TIMEC)"), self.dialog + ) + self.help_label5 = QLabel( + translate( + "Assembly", + " - Smooth Step: L1 + (L2 - L1)*((1/2) + (1/pi)*arctan(SLOPE*(time - T0)))", + ), + self.dialog, + ) + self.help_label6 = QLabel( + translate( + "Assembly", + " - Smooth Square Impulse: (H/pi)*(arctan(SLOPE*(time - T1)) - arctan(SLOPE*(time - T2)))", + ), + self.dialog, + ) + self.help_label7 = QLabel( + translate( + "Assembly", + " - Smooth Ramp Top Impulse: ((1/pi)*(arctan(1000*(time - T1)) - arctan(1000*(time - T2))))*(((H2 - H1)/(T2 - T1))*(time - T1) + H1)", + ), + self.dialog, + ) + + self.help_label1.setToolTip( + translate( + "Assembly", + """C is a constant offset. +VEL is a velocity or slope or gradient of the straight line.""", + ) + ) + self.help_label2.setToolTip( + translate( + "Assembly", + """C is a constant offset. +VEL is the velocity or slope or gradient of the straight line. +ACC is the acceleration or coefficient of the second order. The function is a parabola.""", + ) + ) + self.help_label3.setToolTip( + translate( + "Assembly", + """C is a constant offset. +AMP is the amplitude of the sine wave. +VEL is the angular velocity in radians per second. +PHASE is the phase of the sine wave.""", + ) + ) + self.help_label4.setToolTip( + translate( + "Assembly", + """C is a constant. +TIMEC is the time constant of the exponential function.""", + ) + ) + self.help_label5.setToolTip( + translate( + "Assembly", + """L1 is step level before time = T0. +L2 is step level after time = T0. +SLOPE defines the steepness of the transition between L1 and L2 about time = T0. Higher values gives sharper cornered steps. SLOPE = 1000 or greater are suitable.""", + ) + ) + self.help_label6.setToolTip( + translate( + "Assembly", + """H is the height of the impulse. +T1 is the start of the impulse. +T2 is the end of the impulse. +SLOPE defines the steepness of the transition between 0 and H about time = T1 and T2. Higher values gives sharper cornered impulses. SLOPE = 1000 or greater are suitable.""", + ) + ) + self.help_label7.setToolTip( + translate( + "Assembly", + """This is similar to the square impulse but the top has a sloping ramp. It is good for building a smooth piecewise linear function by adding a series of these. +T1 is the start of the impulse. +T2 is the end of the impulse. +H1 is the height at T1 at the beginning of the ramp. +H2 is the height at T2 at the end of the ramp. +SLOPE defines the steepness of the transition between 0 and H1 and H2 to 0 about time = T1 and T2 respectively. Higher values gives sharper cornered impulses. SLOPE = 1000 or greater are suitable.""", + ) + ) + + self.help_label0.setWordWrap(True) + self.help_label1.setWordWrap(True) + self.help_label2.setWordWrap(True) + self.help_label3.setWordWrap(True) + self.help_label4.setWordWrap(True) + self.help_label5.setWordWrap(True) + self.help_label6.setWordWrap(True) + self.help_label7.setWordWrap(True) + + width = 1000 + self.help_label0.setFixedWidth(width) + self.help_label1.setFixedWidth(width) + self.help_label2.setFixedWidth(width) + self.help_label3.setFixedWidth(width) + self.help_label4.setFixedWidth(width) + self.help_label5.setFixedWidth(width) + self.help_label6.setFixedWidth(width) + self.help_label7.setFixedWidth(width) + + self.help_label0.setVisible(False) + self.help_label1.setVisible(False) + self.help_label2.setVisible(False) + self.help_label3.setVisible(False) + self.help_label4.setVisible(False) + self.help_label5.setVisible(False) + self.help_label6.setVisible(False) + self.help_label7.setVisible(False) + + self.help_label1.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.help_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.help_label3.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.help_label4.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.help_label5.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.help_label6.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.help_label7.setTextInteractionFlags(Qt.TextSelectableByMouse) + # Create the Help button + self.help_button = QPushButton(translate("Assembly", "Help"), self.dialog) + + # Slot to toggle help visibility and button text + def toggle_help(): + show = not self.help_label1.isVisible() + self.help_label0.setVisible(show) + self.help_label1.setVisible(show) + self.help_label2.setVisible(show) + self.help_label3.setVisible(show) + self.help_label4.setVisible(show) + self.help_label5.setVisible(show) + self.help_label6.setVisible(show) + self.help_label7.setVisible(show) + + if show: + self.help_button.setText(translate("Assembly", "Hide help")) + else: + self.help_button.setText(translate("Assembly", "Help")) + + self.positionDialog() + + self.help_button.clicked.connect(toggle_help) + + def positionDialog(self): + self.dialog.adjustSize() + + # Get the screen where the mouse is located + screen = QGuiApplication.screenAt(self.initialPos) + screen_geometry = ( + screen.availableGeometry() + if screen + else QApplication.primaryScreen().availableGeometry() + ) + + # Calculate the position of the dialog to ensure it stays within the screen + dialog_position = self.initialPos + + # Adjust position to keep the dialog within the screen bounds + if dialog_position.x() + self.dialog.width() > screen_geometry.right(): + dialog_position.setX(screen_geometry.right() - self.dialog.width()) + if dialog_position.y() + self.dialog.height() > screen_geometry.bottom(): + dialog_position.setY(screen_geometry.bottom() - self.dialog.height()) + + # Ensure the dialog does not go above or to the left of the screen + if dialog_position.x() < screen_geometry.left(): + dialog_position.setX(screen_geometry.left()) + if dialog_position.y() < screen_geometry.top(): + dialog_position.setY(screen_geometry.top()) + + # Move the dialog to the final position + self.dialog.move(dialog_position) + + def setup_joint_combo(self): + # Function to set up the joint combo box based on the selected motion type + + self.joint_combo.clear() # Clear existing items + + jointTypes = ["Revolute", "Slider", "Cylindrical"] + + joints = UtilsAssembly.getJointsOfType(self.assembly, jointTypes) + + # Add joints to the combo box with labels and icons + for joint in joints: + joint_label = joint.Label + joint_icon = QIcon(joint.ViewObject.Icon) + self.joint_combo.addItem(joint_icon, joint_label, userData=joint) + + # Set the current value based on the object's Joint property + if self.joint in joints: + self.joint_combo.setCurrentText(self.joint.Label) + elif len(joints) > 0: + self.joint = joints[0] + + def setup_motiontype_combo(self): + self.motion_type_combo.clear() # Clear existing items + + if self.joint is None: + return + + if self.joint.JointType == "Revolute": + types = ["Angular"] + elif self.joint.JointType == "Slider": + types = ["Linear"] + else: + types = ["Angular", "Linear"] + + self.motion_type_combo.addItems(types) + + # Set current value based on the object's MotionType + if self.motionType in types: + self.motion_type_combo.setCurrentText(self.motionType) + else: + # self.motionType is no longer available, so we reset it to first entry + self.motionType = types[0] + + def exec_(self): + return self.dialog.exec() + + +######### Create Simulation Task ########### +class TaskAssemblyCreateSimulation(QtCore.QObject): + def __init__(self, simFeaturePy=None): + super().__init__() + Gui.Selection.clearSelection() + + self.assembly = UtilsAssembly.activeAssembly() + + self.initialPlcs = UtilsAssembly.saveAssemblyPartsPlacements(self.assembly) + + self.doc = self.assembly.Document + self.gui_doc = Gui.getDocument(self.doc) + + self.view = self.gui_doc.activeView() + + if not self.assembly or not self.view or not self.doc: + return + + self.runKinematicsTimer = QtCore.QTimer() + self.runKinematicsTimer.setSingleShot(True) + self.runKinematicsTimer.timeout.connect(self.displayLastFrame) + + self.animationTimer = QtCore.QTimer() + self.animationTimer.setInterval(50) # ms + self.animationTimer.timeout.connect(self.playAnimation) + + self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateSimulation.ui") + self.form.motionList.installEventFilter(self) + self.setSpinboxPrecision(self.form.TimeStartSpinBox, 9) + self.setSpinboxPrecision(self.form.TimeEndSpinBox, 9) + self.setSpinboxPrecision(self.form.TimeStepOutputSpinBox, 9) + self.setSpinboxPrecision(self.form.GlobalErrorToleranceSpinBox, 9, App.Units.Length) + self.form.motionList.itemDoubleClicked.connect(self.onItemDoubleClicked) + self.form.TimeStartSpinBox.valueChanged.connect(self.onTimeStartChanged) + self.form.TimeEndSpinBox.valueChanged.connect(self.onTimeEndChanged) + self.form.TimeStepOutputSpinBox.valueChanged.connect(self.onTimeStepOutputChanged) + self.form.GlobalErrorToleranceSpinBox.valueChanged.connect( + self.onGlobalErrorToleranceChanged + ) + self.form.RunKinematicsButton.clicked.connect(self.runKinematics) + self.form.frameSlider.valueChanged.connect(self.onFrameChanged) + self.form.FramesPerSecondSpinBox.valueChanged.connect(self.onFramesPerSecondChanged) + self.form.PlayBackwardButton.clicked.connect(self.animationTimerStartBackward) + self.form.PlayForwardButton.clicked.connect(self.animationTimerStartForward) + self.form.StepBackwardButton.clicked.connect(self.stepBackward) + self.form.StepForwardButton.clicked.connect(self.stepForward) + self.form.StopButton.clicked.connect(self.stopAnimation) + self.form.AddButton.clicked.connect(self.addMotionClicked) + self.form.RemoveButton.clicked.connect(self.deleteSelectedMotions) + self.form.groupBox_player.hide() + + if simFeaturePy: + self.simFeaturePy = simFeaturePy + App.setActiveTransaction("Edit " + simFeaturePy.Label + " Simulation") + self.onMotionsChanged() + else: + App.setActiveTransaction("Create Simulation") + self.createSimulationObject() + + self.setUiInitialValues() + + self.simFeaturePy.Proxy.setMotionsChangedCallback(self.onMotionsChanged) + + self.currentFrm = 1 + self.startFrm = 1 + self.endFrm = 100 + self.fps = 30 + self.deltaTime = 1.0 / self.fps + self.startTime = time.time() + self.index = 0 + + def setUiInitialValues(self): + self.form.TimeStartSpinBox.setProperty("rawValue", self.simFeaturePy.aTimeStart.Value) + self.form.TimeEndSpinBox.setProperty("rawValue", self.simFeaturePy.bTimeEnd.Value) + self.form.TimeStepOutputSpinBox.setProperty( + "rawValue", self.simFeaturePy.cTimeStepOutput.Value + ) + self.form.GlobalErrorToleranceSpinBox.setProperty( + "rawValue", self.simFeaturePy.fGlobalErrorTolerance + ) + self.setFrameValue(0) + self.form.FramesPerSecondSpinBox.setValue(self.simFeaturePy.jFramesPerSecond) + + def setSpinboxPrecision(self, spinbox, precision, unit=App.Units.TimeSpan): + q = App.Units.Quantity() + q.Unit = unit + q.Format = {"Precision": precision} + spinbox.setProperty("value", q) + + def accept(self): + self.deactivate() + UtilsAssembly.restoreAssemblyPartsPlacements(self.assembly, self.initialPlcs) + App.closeActiveTransaction() + return True + + def reject(self): + self.deactivate() + App.closeActiveTransaction(True) + return True + + def deactivate(self): + self.animationTimer.stop() + self.simFeaturePy.Proxy.setMotionsChangedCallback(None) + if Gui.Control.activeDialog(): + Gui.Control.closeDialog() + + def onTimeStartChanged(self, quantity): + self.simFeaturePy.aTimeStart = self.form.TimeStartSpinBox.property("rawValue") + + def onTimeEndChanged(self, quantity): + self.simFeaturePy.bTimeEnd = self.form.TimeEndSpinBox.property("rawValue") + + def onTimeStepOutputChanged(self, quantity): + self.simFeaturePy.cTimeStepOutput = self.form.TimeStepOutputSpinBox.property("rawValue") + + def onGlobalErrorToleranceChanged(self, quantity): + self.simFeaturePy.fGlobalErrorTolerance = self.form.GlobalErrorToleranceSpinBox.property( + "rawValue" + ) + + def onItemDoubleClicked(self, item): + row = self.form.motionList.row(item) + if row < len(self.simFeaturePy.Group): + motion = self.simFeaturePy.Group[row] + motion.ViewObject.Proxy.openEditDialog() + self.onMotionsChanged() + + def createSimulationObject(self): + sim_group = UtilsAssembly.getSimulationGroup(self.assembly) + self.simFeaturePy = sim_group.newObject("App::FeaturePython", "Simulation") + Simulation(self.simFeaturePy) + ViewProviderSimulation(self.simFeaturePy.ViewObject) + + def createMotionObject(self, motionType, joint, formula): + motion = self.assembly.newObject("App::FeaturePython", "Motion") + Motion(motion, motionType, joint, formula) + ViewProviderMotion(motion.ViewObject) + + listOfMotions = self.simFeaturePy.Group + listOfMotions.append(motion) + self.simFeaturePy.Group = listOfMotions + + def onMotionsChanged(self): + self.form.motionList.clear() + for motion in self.simFeaturePy.Group: + self.form.motionList.addItem(motion.Label) + + def runKinematics(self): + self.assembly.generateSimulation(self.simFeaturePy) + nFrms = self.assembly.numberOfFrames() + self.form.frameSlider.setMaximum(nFrms - 1) + self.setFrameValue(nFrms - 1) + self.form.groupBox_player.show() + + def onFrameChanged(self, val): + self.assembly.updateForFrame(val) + self.form.FrameLabel.setText(translate("Assembly", "Frame" + " " + str(val))) + time = float(val * self.simFeaturePy.cTimeStepOutput) + self.form.FrameTimeLabel.setText(f"{time:.2f} s") + + def onFramesPerSecondChanged(self): + self.simFeaturePy.jFramesPerSecond = self.form.FramesPerSecondSpinBox.value() + + def playBackward(self): + pass + + def animationTimerStartForward(self): + self.direction = 1 + self.animationTimerStart() + + def animationTimerStartBackward(self): + self.direction = -1 + self.animationTimerStart() + + def animationTimerStart(self): + self.animationTimer.stop() + self.currentFrm = self.form.frameSlider.value() + self.startFrm = 1 + self.endFrm = self.form.frameSlider.maximum() + if self.startFrm >= self.endFrm: + return + + self.fps = self.simFeaturePy.jFramesPerSecond + self.deltaTime = 1.0 / self.fps + self.startTime = time.time() + self.index = self.currentFrm + self.animationTimer.setInterval(self.deltaTime * 1000) # ms + self.animationTimer.start() + + def playAnimation(self): + range_ = self.endFrm - self.startFrm + offset = self.currentFrm - self.startFrm + count = int((time.time() - self.startTime) / self.deltaTime) + self.index = ((self.direction * count + offset) % range_) + self.startFrm + self.setFrameValue(self.index) + + def displayLastFrame(self): + nFrms = self.assembly.numberOfFrames() + self.setFrameValue(nFrms - 1) + + def stepBackward(self): + self.animationTimer.stop() + + nextFrm = self.form.frameSlider.value() - 1 + if nextFrm < 1: + nextFrm = self.form.frameSlider.maximum() # wraparound + self.setFrameValue(nextFrm) + + def stepForward(self): + self.animationTimer.stop() + + nextFrm = self.form.frameSlider.value() + 1 + if nextFrm > self.form.frameSlider.maximum(): + nextFrm = 1 # wraparound + self.setFrameValue(nextFrm) + + def setFrameValue(self, val): + if val < 1: + val = 1 + if val > self.form.frameSlider.maximum(): + val = self.form.frameSlider.maximum() + + self.form.frameSlider.setValue(val) + + def stopAnimation(self): + self.animationTimer.stop() + + def addMotionClicked(self): + dialog = MotionEditDialog(self.assembly) + if dialog.exec_(): + self.createMotionObject(dialog.motionType, dialog.joint, dialog.formula) + + # Taskbox keyboard event handler + def eventFilter(self, watched, event): + if self.form is not None and watched == self.form.motionList: + if event.type() == QtCore.QEvent.ShortcutOverride: + if event.key() == QtCore.Qt.Key_Delete: + event.accept() + return True # Indicate that the event has been handled + return False + + elif event.type() == QtCore.QEvent.KeyPress: + if event.key() == QtCore.Qt.Key_Delete: + self.deleteSelectedMotions() + return True # Consume the event + + return super().eventFilter(watched, event) + + def deleteSelectedMotions(self): + selected_indexes = self.form.motionList.selectedIndexes() + sorted_indexes = sorted(selected_indexes, key=lambda x: x.row(), reverse=True) + for index in sorted_indexes: + row = index.row() + if row < len(self.simFeaturePy.Group): + motion = self.simFeaturePy.Group[row] + # First remove the link from the viewObj + self.simFeaturePy.Group.remove(motion) + # Delete the object + motion.Document.removeObject(motion.Name) + + +if App.GuiUp: + Gui.addCommand("Assembly_CreateSimulation", CommandCreateSimulation()) diff --git a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp index 6b4ec250e9..d24c4ab266 100644 --- a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp +++ b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp @@ -33,6 +33,7 @@ #include "ViewProviderBomGroup.h" #include "ViewProviderJointGroup.h" #include "ViewProviderViewGroup.h" +#include "ViewProviderSimulationGroup.h" namespace AssemblyGui @@ -66,6 +67,7 @@ PyMOD_INIT_FUNC(AssemblyGui) AssemblyGui::ViewProviderBomGroup::init(); AssemblyGui::ViewProviderJointGroup::init(); AssemblyGui::ViewProviderViewGroup::init(); + AssemblyGui::ViewProviderSimulationGroup::init(); PyMOD_Return(mod); } diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt index 6924b340d8..e7a7b44591 100644 --- a/src/Mod/Assembly/Gui/CMakeLists.txt +++ b/src/Mod/Assembly/Gui/CMakeLists.txt @@ -28,6 +28,15 @@ SOURCE_GROUP("Resources" FILES ${AssemblyResource_SRCS}) generate_from_xml(ViewProviderAssemblyPy) +set(AssemblyGui_UIC_SRCS + TaskAssemblyCreateBom.ui + TaskAssemblyCreateJoint.ui + TaskAssemblyCreateSimulation.ui + TaskAssemblyCreateView.ui + TaskAssemblyInsertLink.ui + Assembly.ui +) + SET(Python_SRCS ViewProviderAssemblyPy.xml ViewProviderAssemblyPyImp.cpp @@ -51,6 +60,8 @@ SET(AssemblyGui_SRCS_Module ViewProviderJointGroup.h ViewProviderViewGroup.cpp ViewProviderViewGroup.h + ViewProviderSimulationGroup.cpp + ViewProviderSimulationGroup.h ${Assembly_QRC_SRCS} ) diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc index 22b93155a1..3cbbbf896f 100644 --- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -26,10 +26,13 @@ icons/Assembly_JointGroup.svg icons/Assembly_ExplodedView.svg icons/Assembly_ExplodedViewGroup.svg + icons/Assembly_CreateSimulation.svg + icons/Assembly_SimulationGroup.svg panels/TaskAssemblyCreateBom.ui panels/TaskAssemblyCreateJoint.ui panels/TaskAssemblyInsertLink.ui panels/TaskAssemblyCreateView.ui + panels/TaskAssemblyCreateSimulation.ui preferences/Assembly.ui icons/Assembly_CreateJointDistance.svg icons/AssemblyWorkbench.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateSimulation.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateSimulation.svg new file mode 100644 index 0000000000..b8e1e63d8b --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateSimulation.svg @@ -0,0 +1,1406 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + 2015-07-04 + https://www.freecad.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/icons/Assembly_SimulationGroup.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SimulationGroup.svg new file mode 100644 index 0000000000..f363db7540 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SimulationGroup.svg @@ -0,0 +1,1434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + 2015-07-04 + https://www.freecad.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/TaskAssemblyCreateSimulation.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateSimulation.ui new file mode 100644 index 0000000000..ac4f575bcb --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateSimulation.ui @@ -0,0 +1,339 @@ + + + TaskAssemblyCreateSimulation + + + + 0 + 0 + 400 + 602 + + + + Create Simulation + + + + + + Motions + + + + + + + 16777215 + 75 + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Add a prescribed motion + + + + + + + :/icons/list-add.svg:/icons/list-add.svg + + + + + + + + 0 + 0 + + + + Delete selected motions + + + + + + + :/icons/edit-delete.svg:/icons/edit-delete.svg + + + + + + + + + + + + Simulation settings + + + + + + Start + + + Start time of the simulation + + + + + + + Start time of the simulation + + + + + + + End + + + End time of the simulation + + + + + + + End time of the simulation + + + + + + + Step + + + Time Step + + + + + + + Time Step + + + + + + + Tolerance + + + Global Error Tolerance + + + + + + + Global Error Tolerance + + + + + + + + + + Generate + + + + + + + Animation player + + + + + + + + Frame + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + 0.00 s + + + + + + + + + + + Frames Per Second + + + + + + + + + + + + + + + 0 + 0 + + + + Step backward + + + + + + + :/icons/media-playback-step-back.svg:/icons/media-playback-step-back.svg + + + + + + + + 0 + 0 + + + + Play backward + + + + + + + :/icons/media-playback-start-back.svg:/icons/media-playback-start-back.svg + + + + + + + + 0 + 0 + + + + Stop + + + + + + + :/icons/media-playback-stop.svg:/icons/media-playback-stop.svg + + + + + + + + 0 + 0 + + + + Play forward + + + + + + + :/icons/media-playback-start.svg:/icons/media-playback-start.svg + + + + + + + + 0 + 0 + + + + Step forward + + + + + + + :/icons/media-playback-step.svg:/icons/media-playback-step.svg + + + + + + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + +
diff --git a/src/Mod/Assembly/Gui/ViewProviderSimulationGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderSimulationGroup.cpp new file mode 100644 index 0000000000..ccfac42aec --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderSimulationGroup.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2024 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#endif + +#include +#include +#include +#include + +#include "ViewProviderSimulationGroup.h" + + +using namespace AssemblyGui; + +PROPERTY_SOURCE(AssemblyGui::ViewProviderSimulationGroup, Gui::ViewProviderDocumentObjectGroup) + +ViewProviderSimulationGroup::ViewProviderSimulationGroup() +{} + +ViewProviderSimulationGroup::~ViewProviderSimulationGroup() = default; + +QIcon ViewProviderSimulationGroup::getIcon() const +{ + return Gui::BitmapFactory().pixmap("Assembly_SimulationGroup.svg"); +} diff --git a/src/Mod/Assembly/Gui/ViewProviderSimulationGroup.h b/src/Mod/Assembly/Gui/ViewProviderSimulationGroup.h new file mode 100644 index 0000000000..949e51ee74 --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderSimulationGroup.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2024 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderSimulationGroup_H +#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderSimulationGroup_H + +#include + +#include + + +namespace AssemblyGui +{ + +class AssemblyGuiExport ViewProviderSimulationGroup: public Gui::ViewProviderDocumentObjectGroup +{ + PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderSimulationGroup); + +public: + ViewProviderSimulationGroup(); + ~ViewProviderSimulationGroup() override; + + /// 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; + }; +}; + +} // namespace AssemblyGui + +#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderSimulationGroup_H diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index 64d266b362..7e90be47f8 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -63,7 +63,7 @@ class AssemblyWorkbench(Workbench): # load the builtin modules from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP - import CommandCreateAssembly, CommandInsertLink, CommandInsertNewPart, CommandCreateJoint, CommandSolveAssembly, CommandExportASMT, CommandCreateView, CommandCreateBom + import CommandCreateAssembly, CommandInsertLink, CommandInsertNewPart, CommandCreateJoint, CommandSolveAssembly, CommandExportASMT, CommandCreateView, CommandCreateSimulation, CommandCreateBom import Preferences FreeCADGui.addLanguagePath(":/translations") @@ -79,6 +79,7 @@ class AssemblyWorkbench(Workbench): "Assembly_Insert", "Assembly_SolveAssembly", "Assembly_CreateView", + "Assembly_CreateSimulation", "Assembly_CreateBom", ] diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index f3f24c927b..9474568aab 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -80,18 +80,44 @@ def isDocTemporary(doc): def assembly_has_at_least_n_parts(n): assembly = activeAssembly() - i = 0 if not assembly: 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"): - i = i + 1 - if i == n: - return True - return False + i = number_of_components_in(assembly) + return i >= n + + +def number_of_components_in(assembly): + if not assembly: + return 0 + i = 0 + for obj in assembly.Group: + if isLinkGroup(obj): + i = i + obj.ElementCount + continue + + if obj.isDerivedFrom("Assembly::AssemblyObject") or obj.isDerivedFrom( + "Assembly::AssemblyLink" + ): + i = i + number_of_components_in(obj) + continue + + if obj.isDerivedFrom("App::Link"): + obj = obj.getLinkedObject() + + if not obj.isDerivedFrom("App::GeoFeature"): + continue + + # if obj.isDerivedFrom("App::DatumElement") or obj.isDerivedFrom("App::LocalCoordinateSystem"): + if obj.isDerivedFrom("App::Origin"): + # after https://github.com/FreeCAD/FreeCAD/pull/16675 merges, + # replace the App::Origin test by the one above + continue + + i = i + 1 + + return i def isLink(obj): @@ -546,6 +572,20 @@ def color_from_unsigned(c): ] +def getJointsOfType(asm, jointTypes): + if not ( + asm.isDerivedFrom("Assembly::AssemblyObject") or asm.isDerivedFrom("Assembly::AssemblyLink") + ): + return [] + + joints = [] + allJoints = asm.Joints + for joint in allJoints: + if joint.JointType in jointTypes: + joints.append(joint) + return joints + + def getBomGroup(assembly): bom_group = None @@ -588,6 +628,20 @@ def getViewGroup(assembly): return view_group +def getSimulationGroup(assembly): + sim_group = None + + for obj in assembly.OutList: + if obj.TypeId == "Assembly::SimulationGroup": + sim_group = obj + break + + if not sim_group: + sim_group = assembly.newObject("Assembly::SimulationGroup", "Simulations") + + return sim_group + + def isAssemblyGrounded(): assembly = activeAssembly() if not assembly: