From 491923e41e2c04930ad75f899f222d6dfb4fa036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Thu, 13 Feb 2025 19:35:10 +0100 Subject: [PATCH 1/7] Fem: Implement basic python filter functionality and glyph example --- cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake | 11 +- src/Mod/Fem/App/AppFem.cpp | 2 + src/Mod/Fem/App/CMakeLists.txt | 3 + src/Mod/Fem/App/FemPostFilter.cpp | 78 +++- src/Mod/Fem/App/FemPostFilter.h | 13 +- src/Mod/Fem/App/FemPostFilterPy.xml | 56 +++ src/Mod/Fem/App/FemPostFilterPyImp.cpp | 193 ++++++++++ src/Mod/Fem/App/PropertyPostDataObject.cpp | 24 +- src/Mod/Fem/CMakeLists.txt | 3 + src/Mod/Fem/Gui/AppFemGui.cpp | 2 + src/Mod/Fem/Gui/CMakeLists.txt | 4 + src/Mod/Fem/Gui/Resources/Fem.qrc | 2 + .../Resources/icons/FEM_PostFilterGlyph.svg | 111 ++++++ src/Mod/Fem/Gui/Resources/ui/TaskPostGlyph.ui | 355 ++++++++++++++++++ src/Mod/Fem/Gui/TaskPostBoxes.cpp | 190 +++++----- src/Mod/Fem/Gui/TaskPostBoxes.h | 58 ++- .../Gui/ViewProviderFemPostBranchFilter.cpp | 4 +- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 55 ++- src/Mod/Fem/Gui/ViewProviderFemPostFilter.h | 26 +- .../Fem/Gui/ViewProviderFemPostFilterPy.xml | 24 ++ .../Gui/ViewProviderFemPostFilterPyImp.cpp | 71 ++++ .../Fem/Gui/ViewProviderFemPostFunction.cpp | 3 +- src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 14 +- src/Mod/Fem/Gui/ViewProviderFemPostObject.h | 16 +- .../Fem/Gui/ViewProviderFemPostPipeline.cpp | 3 +- src/Mod/Fem/Gui/Workbench.cpp | 3 + src/Mod/Fem/ObjectsFem.py | 14 +- src/Mod/Fem/femcommands/commands.py | 12 + src/Mod/Fem/femcommands/manager.py | 43 +++ src/Mod/Fem/femobjects/post_glyphfilter.py | 267 +++++++++++++ .../femtaskpanels/task_post_glyphfilter.py | 212 +++++++++++ src/Mod/Fem/femtest/app/test_object.py | 1 + .../femviewprovider/view_post_glyphfilter.py | 86 +++++ 33 files changed, 1793 insertions(+), 166 deletions(-) create mode 100644 src/Mod/Fem/App/FemPostFilterPy.xml create mode 100644 src/Mod/Fem/App/FemPostFilterPyImp.cpp create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterGlyph.svg create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostGlyph.ui create mode 100644 src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml create mode 100644 src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp create mode 100644 src/Mod/Fem/femobjects/post_glyphfilter.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_glyphfilter.py diff --git a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake index 8db55febb1..31899c8b3d 100644 --- a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake +++ b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake @@ -42,7 +42,7 @@ macro(SetupSalomeSMESH) endif() endforeach() else() - set(VTK_COMPONENTS "CommonCore;CommonDataModel;FiltersVerdict;IOXML;FiltersCore;FiltersGeneral;IOLegacy;FiltersExtraction;FiltersSources;FiltersGeometry") + set(VTK_COMPONENTS "CommonCore;CommonDataModel;FiltersVerdict;IOXML;FiltersCore;FiltersGeneral;IOLegacy;FiltersExtraction;FiltersSources;FiltersGeometry;WrappingPythonCore") list(APPEND VTK_COMPONENTS "IOMPIParallel;ParallelMPI;hdf5;FiltersParallelDIY2;RenderingCore;InteractionStyle;RenderingFreeType;RenderingOpenGL2") foreach(_module ${VTK_COMPONENTS}) list (FIND VTK_AVAILABLE_COMPONENTS ${_module} _index) @@ -63,6 +63,15 @@ macro(SetupSalomeSMESH) endif() set(BUILD_FEM_VTK ON) + + # check if PythonWrapperCore was found (vtk 9 only) + if (${VTK_WrappingPythonCore_FOUND}) + add_compile_definitions(BUILD_FEM_VTK_WRAPPER) + message(STATUS "VTK python wrapper: available") + else() + message(STATUS "VTK python wrapper: NOT available") + endif() + if(${VTK_MAJOR_VERSION} LESS 6) message( FATAL_ERROR "Found VTK version is <6, this is not compatible" ) endif() diff --git a/src/Mod/Fem/App/AppFem.cpp b/src/Mod/Fem/App/AppFem.cpp index df13a6cf96..d3ed832784 100644 --- a/src/Mod/Fem/App/AppFem.cpp +++ b/src/Mod/Fem/App/AppFem.cpp @@ -206,6 +206,8 @@ PyMOD_INIT_FUNC(Fem) Fem::FemPostSphereFunction ::init(); Fem::PropertyPostDataObject ::init(); + + Fem::PostFilterPython ::init(); #endif // clang-format on diff --git a/src/Mod/Fem/App/CMakeLists.txt b/src/Mod/Fem/App/CMakeLists.txt index 61d3f20be1..b58a53f5b2 100644 --- a/src/Mod/Fem/App/CMakeLists.txt +++ b/src/Mod/Fem/App/CMakeLists.txt @@ -67,11 +67,14 @@ if(BUILD_FEM_VTK) FemPostObjectPyImp.cpp FemPostPipelinePy.xml FemPostPipelinePyImp.cpp + FemPostFilterPy.xml + FemPostFilterPyImp.cpp FemPostBranchFilterPy.xml FemPostBranchFilterPyImp.cpp ) generate_from_xml(FemPostObjectPy) generate_from_xml(FemPostPipelinePy) + generate_from_xml(FemPostFilterPy) generate_from_xml(FemPostBranchFilterPy) endif(BUILD_FEM_VTK) SOURCE_GROUP("Python" FILES ${Python_SRCS}) diff --git a/src/Mod/Fem/App/FemPostFilter.cpp b/src/Mod/Fem/App/FemPostFilter.cpp index 338dec9e89..3501860a3e 100644 --- a/src/Mod/Fem/App/FemPostFilter.cpp +++ b/src/Mod/Fem/App/FemPostFilter.cpp @@ -31,10 +31,13 @@ #include #endif +#include #include #include #include "FemPostFilter.h" +#include "FemPostFilterPy.h" + #include "FemPostPipeline.h" #include "FemPostBranchFilter.h" @@ -52,22 +55,42 @@ FemPostFilter::FemPostFilter() "Data", App::Prop_ReadOnly, "The step used to calculate the data"); + + // the default pipeline: just a passthrough + // this is used to simplify the python filter handling, + // as those do not have filter pipelines setup till later + // in the document loading process. + auto filter = vtkPassThrough::New(); + auto pipeline = FemPostFilter::FilterPipeline(); + pipeline.algorithmStorage.push_back(filter); + pipeline.source = filter; + pipeline.target = filter; + addFilterPipeline(pipeline, "__passthrough__"); } FemPostFilter::~FemPostFilter() = default; + void FemPostFilter::addFilterPipeline(const FemPostFilter::FilterPipeline& p, std::string name) { m_pipelines[name] = p; + + if (m_activePipeline.empty()) { + m_activePipeline = name; + } } FemPostFilter::FilterPipeline& FemPostFilter::getFilterPipeline(std::string name) { - return m_pipelines[name]; + return m_pipelines.at(name); } void FemPostFilter::setActiveFilterPipeline(std::string name) { + if (m_pipelines.count(name) == 0) { + throw Base::ValueError("Not a filter pipline name"); + } + if (m_activePipeline != name && isValid()) { // disable all inputs of current pipeline @@ -129,6 +152,7 @@ void FemPostFilter::onChanged(const App::Property* prop) { if (prop == &Placement) { + if (Placement.getValue().isIdentity() && m_use_transform) { // remove transform from pipeline if (m_transform_location == TransformLocation::output) { @@ -191,7 +215,6 @@ DocumentObjectExecReturn* FemPostFilter::execute() Data.setValue(output->GetOutputDataObject(0)); } - return StdReturn; } @@ -203,8 +226,19 @@ vtkSmartPointer FemPostFilter::getInputData() } vtkAlgorithmOutput* output = active.source->GetInputConnection(0, 0); + if(!output) { + return nullptr; + } vtkAlgorithm* algo = output->GetProducer(); - algo->Update(); + if(!algo) { + return nullptr; + } + if (Frame.getValue()>0) { + algo->UpdateTimeStep(Frame.getValue()); + } + else { + algo->Update(); + } return vtkDataSet::SafeDownCast(algo->GetOutputDataObject(0)); } @@ -251,6 +285,44 @@ void FemPostFilter::setTransformLocation(TransformLocation loc) m_transform_location = loc; } +PyObject* FemPostFilter::getPyObject() +{ + if (PythonObject.is(Py::_None())) { + // ref counter is set to 1 + PythonObject = Py::Object(new FemPostFilterPy(this), true); + } + + return Py::new_reference_to(PythonObject); +} + + +// Python Filter feature --------------------------------------------------------- + +namespace App +{ +/// @cond DOXERR +PROPERTY_SOURCE_TEMPLATE(Fem::PostFilterPython, Fem::FemPostFilter) +template<> const char* Fem::PostFilterPython::getViewProviderName(void) const +{ + return "FemGui::ViewProviderPostFilterPython"; +} +template<> PyObject* Fem::PostFilterPython::getPyObject() +{ + if (PythonObject.is(Py::_None())) { + // ref counter is set to 1 + PythonObject = Py::Object(new App::FeaturePythonPyT(this), true); + } + return Py::new_reference_to(PythonObject); +} + +/// @endcond + +// explicit template instantiation +template class FemExport FeaturePythonT; +}// namespace App + + + // *************************************************************************** // in the following, the different filters sorted alphabetically // *************************************************************************** diff --git a/src/Mod/Fem/App/FemPostFilter.h b/src/Mod/Fem/App/FemPostFilter.h index d137b68bcf..d3becd9783 100644 --- a/src/Mod/Fem/App/FemPostFilter.h +++ b/src/Mod/Fem/App/FemPostFilter.h @@ -40,6 +40,7 @@ #include #include +#include #include "FemPostObject.h" @@ -53,6 +54,8 @@ enum class TransformLocation : size_t output }; +class FemPostFilterPy; + class FemExport FemPostFilter: public Fem::FemPostObject { PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostFilter); @@ -69,12 +72,15 @@ protected: std::vector> algorithmStorage; }; + //pipeline handling void addFilterPipeline(const FilterPipeline& p, std::string name); - void setActiveFilterPipeline(std::string name); FilterPipeline& getFilterPipeline(std::string name); + void setActiveFilterPipeline(std::string name); + // Transformation handling void setTransformLocation(TransformLocation loc); + friend class FemPostFilterPy; public: /// Constructor FemPostFilter(); @@ -88,16 +94,21 @@ public: vtkSmartPointer getFilterInput(); vtkSmartPointer getFilterOutput(); + PyObject* getPyObject() override; + private: // handling of multiple pipelines which can be the filter std::map m_pipelines; std::string m_activePipeline; bool m_use_transform = false; + bool m_running_setup = false; TransformLocation m_transform_location = TransformLocation::output; void pipelineChanged(); // inform parents that the pipeline changed }; +using PostFilterPython = App::FeaturePythonT; + class FemExport FemPostSmoothFilterExtension: public App::DocumentObjectExtension { EXTENSION_PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostSmoothFilterExtension); diff --git a/src/Mod/Fem/App/FemPostFilterPy.xml b/src/Mod/Fem/App/FemPostFilterPy.xml new file mode 100644 index 0000000000..44ee6cc84b --- /dev/null +++ b/src/Mod/Fem/App/FemPostFilterPy.xml @@ -0,0 +1,56 @@ + + + + + + The FemPostFilter class. + + + + Registers a new vtk filter pipeline for data processing. Arguments are (name, source algorithm, target algorithm). + + + + + Sets the filter pipeline that shall be used for data processing. Argument is the name of the filter pipeline to activate. + + + + + Returns the postprocessing group the filter is in (e.g. a pipeline or branch object). None is returned if not in any. + + + + + +Returns the dataset available at the filters input. +Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. + + + + + + +Returns the names of all vector fields available on this filters input. +Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. + + + + + + +Returns the names of all scalar fields available on this filters input. +Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. + + + " + + diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp new file mode 100644 index 0000000000..349b3bbaec --- /dev/null +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -0,0 +1,193 @@ +/*************************************************************************** + * Copyright (c) 2017 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" +#ifndef _PreComp_ +#include +#endif + +#include +#include + +// clang-format off +#include "FemPostGroupExtension.h" +#include "FemPostFilter.h" +#include "FemPostFilterPy.h" +#include "FemPostFilterPy.cpp" +// clang-format on + +#ifdef BUILD_FEM_VTK_WRAPPER + #include + #include +#endif //BUILD_FEM_VTK + +using namespace Fem; + +// returns a string which represents the object e.g. when printed in python +std::string FemPostFilterPy::representation() const +{ + std::stringstream str; + str << ""; + + return str.str(); +} + + +PyObject* FemPostFilterPy::addFilterPipeline(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + const char* name; + PyObject *source = nullptr; + PyObject *target = nullptr; + + if (PyArg_ParseTuple(args, "sOO", &name, &source, &target)) { + + // extract the algorithms + vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(source, "vtkAlgorithm"); + if (!obj) { + // error marker is set by PythonUtil + return nullptr; + } + auto source_algo = static_cast(obj); + + obj = vtkPythonUtil::GetPointerFromObject(target,"vtkAlgorithm"); + if (!obj) { + // error marker is set by PythonUtil + return nullptr; + } + auto target_algo = static_cast(obj); + + // add the pipeline + FemPostFilter::FilterPipeline pipe; + pipe.source = source_algo; + pipe.target = target_algo; + getFemPostFilterPtr()->addFilterPipeline(pipe, name); + } + Py_Return; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + +PyObject* FemPostFilterPy::setActiveFilterPipeline(PyObject* args) +{ + const char* name; + if (PyArg_ParseTuple(args, "s", &name)) { + getFemPostFilterPtr()->setActiveFilterPipeline(std::string(name)); + } + + Py_Return; +} + +PyObject* FemPostFilterPy::getParentPostGroup(PyObject* args) +{ + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + auto group = Fem::FemPostGroupExtension::getGroupOfObject(getFemPostFilterPtr()); + if (group) { + return group->getPyObject(); + } + + return Py_None; +} + +PyObject* FemPostFilterPy::getInputData(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // make a copy of the dataset + auto dataset = getFemPostFilterPtr()->getInputData(); + vtkDataSet* copy; + switch (dataset->GetDataObjectType()) { + case VTK_UNSTRUCTURED_GRID: + copy = vtkUnstructuredGrid::New(); + break; + default: + PyErr_SetString(PyExc_TypeError, "cannot return datatype object; not unstructured grid"); + Py_Return; + } + + // return the python wrapper + copy->DeepCopy(dataset); + PyObject* py_dataset = vtkPythonUtil::GetObjectFromPointer(copy); + + return Py::new_reference_to(py_dataset); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + +PyObject* FemPostFilterPy::getInputVectorFields(PyObject* args) +{ + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + std::vector vector_fields = getFemPostFilterPtr()->getInputVectorFields(); + + // convert to python list of strings + Py::List list; + for (std::string& field : vector_fields) { + list.append(Py::String(field)); + } + + return Py::new_reference_to(list); +} + + +PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) +{ + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + std::vector scalar_fields = getFemPostFilterPtr()->getInputScalarFields(); + + // convert to python list of strings + Py::List list; + for (std::string& field : scalar_fields) { + list.append(Py::String(field)); + } + + return Py::new_reference_to(list); +} + +PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const +{ + return nullptr; +} + +int FemPostFilterPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index b8d953fcca..a11a57d58b 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -42,6 +42,10 @@ #include #endif +#ifdef BUILD_FEM_VTK_WRAPPER +#include +#endif + #include #include #include @@ -162,12 +166,26 @@ int PropertyPostDataObject::getDataType() PyObject* PropertyPostDataObject::getPyObject() { - // TODO: fetch the vtk python object from the data set and return it - return Py::new_reference_to(Py::None()); +#ifdef BUILD_FEM_VTK_WRAPPER + //create a copy first + auto copy = static_cast(Copy()); + + // get the data python wrapper + PyObject* py_dataset = vtkPythonUtil::GetObjectFromPointer(copy->getValue()); + auto result = Py::new_reference_to(py_dataset); + delete copy; + + return result; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif } void PropertyPostDataObject::setPyObject(PyObject* /*value*/) -{} +{ + throw Base::NotImplementedError(); +} App::Property* PropertyPostDataObject::Copy() const { diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 1e30655868..1f7d602b07 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -202,6 +202,7 @@ SET(FemObjects_SRCS femobjects/mesh_netgen.py femobjects/mesh_region.py femobjects/mesh_result.py + femobjects/post_glyphfilter.py femobjects/result_mechanical.py femobjects/solver_calculix.py femobjects/solver_ccxtools.py @@ -604,6 +605,7 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/task_mesh_group.py femtaskpanels/task_mesh_region.py femtaskpanels/task_mesh_netgen.py + femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_result_mechanical.py femtaskpanels/task_solver_calculix.py femtaskpanels/task_solver_ccxtools.py @@ -654,6 +656,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_mesh_netgen.py femviewprovider/view_mesh_region.py femviewprovider/view_mesh_result.py + femviewprovider/view_post_glyphfilter.py femviewprovider/view_result_mechanical.py femviewprovider/view_solver_calculix.py femviewprovider/view_solver_ccxtools.py diff --git a/src/Mod/Fem/Gui/AppFemGui.cpp b/src/Mod/Fem/Gui/AppFemGui.cpp index 65364059eb..a711876d50 100644 --- a/src/Mod/Fem/Gui/AppFemGui.cpp +++ b/src/Mod/Fem/Gui/AppFemGui.cpp @@ -161,6 +161,8 @@ PyMOD_INIT_FUNC(FemGui) #ifdef FC_USE_VTK FemGui::ViewProviderFemPostObject ::init(); FemGui::ViewProviderFemPostPipeline ::init(); + FemGui::ViewProviderFemPostFilterPythonBase ::init(); + FemGui::ViewProviderPostFilterPython ::init(); FemGui::ViewProviderFemPostBranchFilter ::init(); FemGui::ViewProviderFemPostCalculator ::init(); FemGui::ViewProviderFemPostClip ::init(); diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 2a83b2f50e..a7449bf623 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -36,6 +36,7 @@ set(FemGui_LIBS generate_from_xml(ViewProviderFemConstraintPy) generate_from_xml(ViewProviderFemMeshPy) generate_from_xml(ViewProviderFemPostPipelinePy) +generate_from_xml(ViewProviderFemPostFilterPy) SET(Python_SRCS ViewProviderFemConstraintPy.xml @@ -44,6 +45,8 @@ SET(Python_SRCS ViewProviderFemMeshPyImp.cpp ViewProviderFemPostPipelinePy.xml ViewProviderFemPostPipelinePyImp.cpp + ViewProviderFemPostFilterPy.xml + ViewProviderFemPostFilterPyImp.cpp ) SOURCE_GROUP("Python" FILES ${Python_SRCS}) @@ -430,6 +433,7 @@ SET(FemGuiPythonUI_SRCS Resources/ui/ResultShow.ui Resources/ui/SolverCalculiX.ui Resources/ui/SolverCcxTools.ui + Resources/ui/TaskPostGlyph.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index f57b979456..7e15fdf17e 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -82,6 +82,7 @@ icons/FEM_PostFilterDataAtPoint.svg icons/FEM_PostFilterLinearizedStresses.svg icons/FEM_PostFilterWarp.svg + icons/FEM_PostFilterGlyph.svg icons/FEM_PostFrames.svg icons/FEM_PostBranchFilter.svg icons/FEM_PostPipelineFromResult.svg @@ -150,5 +151,6 @@ ui/ResultShow.ui ui/SolverCalculiX.ui ui/SolverCcxTools.ui + ui/TaskPostGlyph.ui diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterGlyph.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterGlyph.svg new file mode 100644 index 0000000000..980e51de21 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterGlyph.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + [Alexander Gryson] + + + fem-warp + 2017-03-11 + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/ + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostGlyph.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostGlyph.ui new file mode 100644 index 0000000000..303f3c368f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostGlyph.ui @@ -0,0 +1,355 @@ + + + TaskPostGlyph + + + + 0 + 0 + 440 + 428 + + + + Glyph settings + + + + + + + + The form of the glyph + + + Form + + + + + + + + 0 + 0 + + + + The form of the glyph + + + + Arrow + + + + + Cube + + + + + + + + Which vector field is used to orient the glyphs + + + Orientation + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + + + + 1 + 0 + + + + Sca&le + + + false + + + false + + + false + + + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + Data + + + + + + + + + + 1 + 0 + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + + None + + + + + + + + + + A constant multiplier the glyphs are scaled with + + + Factor + + + + + + + + + + 0 + 0 + + + + A constant multiplier the glyphs are scaled with + + + 99999999999.000000000000000 + + + 0.000000000000000 + + + QAbstractSpinBox::StepType::AdaptiveDecimalStepType + + + 1.000000000000000 + + + + + + + + + + + Changes the scale factor by +/- 50% of the set scale factor + + + 100 + + + 5 + + + 50 + + + Qt::Orientation::Horizontal + + + + + + + + + + 1 + 0 + + + + Which data field is used to scale the glyphs + + + + Not a vector + + + + + By magnitude + + + + + By components + + + + + + + + + + + Vertex Mas&king + + + false + + + false + + + + + + Which vertices are used as glyph locations + + + Mode + + + + + + + true + + + + 0 + 0 + + + + Defines the maximal number of vertices used for "Uniform Sampling" masking mode + + + 1 + + + 999999999 + + + + + + + true + + + Define the stride for "Every Nth" masking mode + + + Stride + + + + + + + true + + + Defines the maximal number of vertices used for "Uniform Sampling" masking mode + + + Max + + + + + + + true + + + + 0 + 0 + + + + Define the stride for "Every Nth" masking mode + + + 1 + + + 999999999 + + + + + + + + 0 + 0 + + + + Which vertices are used as glyph locations + + + + All + + + + + Every Nth + + + + + Uniform Sampling + + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index fefe174507..ea51bdf8bf 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -203,29 +203,33 @@ void DataAlongLineMarker::customEvent(QEvent*) // *************************************************************************** // main task dialog -TaskPostBox::TaskPostBox(Gui::ViewProviderDocumentObject* view, +TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, const QPixmap& icon, const QString& title, QWidget* parent) - : TaskBox(icon, title, true, parent) + : QWidget(parent) , m_object(view->getObject()) , m_view(view) -{} +{ + setWindowTitle(title); + setWindowIcon(icon); + m_icon = icon; +} -TaskPostBox::~TaskPostBox() = default; +TaskPostWidget::~TaskPostWidget() = default; -bool TaskPostBox::autoApply() +bool TaskPostWidget::autoApply() { return FemSettings().getPostAutoRecompute(); } -App::Document* TaskPostBox::getDocument() const +App::Document* TaskPostWidget::getDocument() const { App::DocumentObject* obj = getObject(); return (obj ? obj->getDocument() : nullptr); } -void TaskPostBox::recompute() +void TaskPostWidget::recompute() { if (autoApply()) { App::Document* doc = getDocument(); @@ -235,7 +239,7 @@ void TaskPostBox::recompute() } } -void TaskPostBox::updateEnumerationList(App::PropertyEnumeration& prop, QComboBox* box) +void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComboBox* box) { QStringList list; std::vector vec = prop.getEnumVector(); @@ -266,10 +270,19 @@ TaskDlgPost::~TaskDlgPost() = default; QDialogButtonBox::StandardButtons TaskDlgPost::getStandardButtons() const { - // check if we only have gui task boxes bool guionly = true; - for (auto it : m_boxes) { - guionly = guionly && it->isGuiTaskOnly(); + for (auto& widget : Content) { + if(auto task_box = dynamic_cast(widget)) { + + // get the task widget and check if it is a post widget + auto widget = task_box->groupLayout()->itemAt(0)->widget(); + if(auto post_widget = dynamic_cast(widget)) { + guionly = guionly && post_widget->isGuiTaskOnly(); + } else { + // unknown panel, we can only assume + guionly = false; + } + } } if (!guionly) { @@ -285,7 +298,7 @@ void TaskDlgPost::connectSlots() // Connect emitAddedFunction() with slotAddedFunction() QObject* sender = nullptr; int indexSignal = 0; - for (const auto dlg : m_boxes) { + for (const auto dlg : Content) { indexSignal = dlg->metaObject()->indexOfSignal( QMetaObject::normalizedSignature("emitAddedFunction()")); if (indexSignal >= 0) { @@ -295,7 +308,7 @@ void TaskDlgPost::connectSlots() } if (sender) { - for (const auto dlg : m_boxes) { + for (const auto dlg : Content) { int indexSlot = dlg->metaObject()->indexOfSlot( QMetaObject::normalizedSignature("slotAddedFunction()")); if (indexSlot >= 0) { @@ -308,12 +321,6 @@ void TaskDlgPost::connectSlots() } } -void TaskDlgPost::appendBox(TaskPostBox* box) -{ - m_boxes.push_back(box); - Content.push_back(box); -} - void TaskDlgPost::open() { // only open a new command if none is pending (e.g. if the object was newly created) @@ -326,8 +333,14 @@ void TaskDlgPost::open() void TaskDlgPost::clicked(int button) { if (button == QDialogButtonBox::Apply) { - for (auto box : m_boxes) { - box->apply(); + for (auto& widget : Content) { + if(auto task_box = dynamic_cast(widget)) { + // get the task widget and check if it is a post widget + auto widget = task_box->groupLayout()->itemAt(0)->widget(); + if(auto post_widget = dynamic_cast(widget)) { + post_widget->apply(); + } + } } recompute(); } @@ -336,8 +349,14 @@ void TaskDlgPost::clicked(int button) bool TaskDlgPost::accept() { try { - for (auto& box : m_boxes) { - box->applyPythonCode(); + for (auto& widget : Content) { + if(auto task_box = dynamic_cast(widget)) { + // get the task widget and check if it is a post widget + auto widget = task_box->groupLayout()->itemAt(0)->widget(); + if(auto post_widget = dynamic_cast(widget)) { + post_widget->applyPythonCode(); + } + } } } catch (const Base::Exception& e) { @@ -377,19 +396,16 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) // *************************************************************************** // box to set the coloring TaskPostDisplay::TaskPostDisplay(ViewProviderFemPostObject* view, QWidget* parent) - : TaskPostBox(view, - Gui::BitmapFactory().pixmap("FEM_ResultShow"), - tr("Result display options"), + : TaskPostWidget(view, + Gui::BitmapFactory().pixmap("FEM_ResultShow"), QString(), parent) , ui(new Ui_TaskPostDisplay) { - // we need a separate container widget to add all controls to - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Result display options")); // set title here as setupUi overrides the constructor title setupConnections(); - this->groupLayout()->addWidget(proxy); - // update all fields updateEnumerationList(getTypedView()->DisplayMode, ui->Representation); @@ -463,7 +479,7 @@ void TaskPostDisplay::applyPythonCode() // *************************************************************************** // functions TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("fem-post-geo-plane"), tr("Implicit function"), parent) @@ -472,7 +488,10 @@ TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* p FunctionWidget* w = getTypedView()->createControlWidget(); w->setParent(this); w->setViewProvider(getTypedView()); - this->groupLayout()->addWidget(w); + + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget(w); + setLayout(layout); } TaskPostFunction::~TaskPostFunction() = default; @@ -486,13 +505,12 @@ void TaskPostFunction::applyPythonCode() // *************************************************************************** // Frames TaskPostFrames::TaskPostFrames(ViewProviderFemPostObject* view, QWidget* parent) - : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFrames"), tr("Result Frames"), parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFrames"), QString(), parent) , ui(new Ui_TaskPostFrames) { - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); - this->groupLayout()->addWidget(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Result Frames")); setupConnections(); // populate the data @@ -548,16 +566,15 @@ void TaskPostFrames::applyPythonCode() // *************************************************************************** // Branch TaskPostBranch::TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostBranchFilter"), - tr("Branch behaviour"), + QString(), parent) , ui(new Ui_TaskPostBranch) { - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); - this->groupLayout()->addWidget(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Branch behaviour")); setupConnections(); // populate the data @@ -603,19 +620,17 @@ void TaskPostBranch::applyPythonCode() // data along line filter TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderFemPostDataAlongLine* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterDataAlongLine"), - tr("Data along a line options"), + QString(), parent) , ui(new Ui_TaskPostDataAlongLine) , marker(nullptr) { - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); - + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Data along a line options")); setupConnectionsStep1(); - this->groupLayout()->addWidget(proxy); QSize size = ui->point1X->sizeForText(QStringLiteral("000000000000")); ui->point1X->setMinimumWidth(size.width()); @@ -1025,21 +1040,19 @@ plt.show()\n"; // *************************************************************************** // data at point filter TaskPostDataAtPoint::TaskPostDataAtPoint(ViewProviderFemPostDataAtPoint* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterDataAtPoint"), - tr("Data at point options"), + QString(), parent) , viewer(nullptr) , connSelectPoint(QMetaObject::Connection()) , ui(new Ui_TaskPostDataAtPoint) { - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Data at point options")); setupConnections(); - this->groupLayout()->addWidget(proxy); - QSize size = ui->centerX->sizeForText(QStringLiteral("000000000000")); ui->centerX->setMinimumWidth(size.width()); ui->centerY->setMinimumWidth(size.width()); @@ -1382,9 +1395,9 @@ std::string TaskPostDataAtPoint::toString(double val) const TaskPostClip::TaskPostClip(ViewProviderFemPostClip* view, App::PropertyLink* function, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterClipRegion"), - tr("Clip region, choose implicit function"), + QString(), parent) , ui(new Ui_TaskPostClip) { @@ -1393,11 +1406,10 @@ TaskPostClip::TaskPostClip(ViewProviderFemPostClip* view, fwidget = nullptr; - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Clip region, choose implicit function")); setupConnections(); - this->groupLayout()->addWidget(proxy); // the layout for the container widget QVBoxLayout* layout = new QVBoxLayout(); @@ -1542,17 +1554,16 @@ void TaskPostClip::onInsideOutToggled(bool val) // *************************************************************************** // contours filter TaskPostContours::TaskPostContours(ViewProviderFemPostContours* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterContours"), - tr("Contours filter options"), + QString(), parent) , ui(new Ui_TaskPostContours) { - // load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Contours filter options")); QMetaObject::connectSlotsByName(this); - this->groupLayout()->addWidget(proxy); auto obj = getObject(); @@ -1697,9 +1708,9 @@ void TaskPostContours::onRelaxationChanged(double value) // *************************************************************************** // cut filter TaskPostCut::TaskPostCut(ViewProviderFemPostCut* view, App::PropertyLink* function, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterCutFunction"), - tr("Function cut, choose implicit function"), + QString(), parent) , ui(new Ui_TaskPostCut) { @@ -1708,11 +1719,10 @@ TaskPostCut::TaskPostCut(ViewProviderFemPostCut* view, App::PropertyLink* functi fwidget = nullptr; - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Function cut, choose implicit function")); setupConnections(); - this->groupLayout()->addWidget(proxy); // the layout for the container widget QVBoxLayout* layout = new QVBoxLayout(); @@ -1836,17 +1846,16 @@ void TaskPostCut::onFunctionBoxCurrentIndexChanged(int idx) // *************************************************************************** // scalar clip filter TaskPostScalarClip::TaskPostScalarClip(ViewProviderFemPostScalarClip* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterClipScalar"), - tr("Scalar clip options"), + QString(), parent) , ui(new Ui_TaskPostScalarClip) { - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Scalar clip options")); setupConnections(); - this->groupLayout()->addWidget(proxy); // load the default values updateEnumerationList(getTypedObject()->Scalars, ui->Scalar); @@ -1961,17 +1970,16 @@ void TaskPostScalarClip::onInsideOutToggled(bool val) // *************************************************************************** // warp vector filter TaskPostWarpVector::TaskPostWarpVector(ViewProviderFemPostWarpVector* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"), - tr("Warp options"), + QString(), parent) , ui(new Ui_TaskPostWarpVector) { - // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + // setup the ui + ui->setupUi(this); + setWindowTitle(tr("Warp options")); setupConnections(); - this->groupLayout()->addWidget(proxy); // load the default values for warp display updateEnumerationList(getTypedObject()->Vector, ui->Vector); @@ -2136,17 +2144,15 @@ static const std::vector calculatorOperators = { "log", "pow", "sqrt", "iHat", "jHat", "kHat", "cross", "dot", "mag", "norm"}; TaskPostCalculator::TaskPostCalculator(ViewProviderFemPostCalculator* view, QWidget* parent) - : TaskPostBox(view, + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterCalculator"), tr("Calculator options"), parent) , ui(new Ui_TaskPostCalculator) { // we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); + ui->setupUi(this); setupConnections(); - this->groupLayout()->addWidget(proxy); // load the default values auto obj = getObject(); diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index a13f9a6d7e..85ea7f21eb 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -131,18 +131,22 @@ protected: // *************************************************************************** // main task dialog -class TaskPostBox: public Gui::TaskView::TaskBox +class TaskPostWidget: public QWidget { - Q_OBJECT + // Q_OBJECT public: - TaskPostBox(Gui::ViewProviderDocumentObject* view, - const QPixmap& icon, - const QString& title, - QWidget* parent = nullptr); - ~TaskPostBox() override; + TaskPostWidget(Gui::ViewProviderDocumentObject* view, + const QPixmap& icon, + const QString& title = QString(), + QWidget* parent = nullptr); + ~TaskPostWidget() override; virtual void applyPythonCode() {}; + QPixmap getIcon() + { + return m_icon; + } virtual bool isGuiTaskOnly() { return false; @@ -184,6 +188,7 @@ protected: static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box); private: + QPixmap m_icon; App::DocumentObjectWeakPtrT m_object; Gui::ViewProviderWeakPtrT m_view; }; @@ -200,7 +205,6 @@ public: ~TaskDlgPost() override; void connectSlots(); - void appendBox(TaskPostBox* box); Gui::ViewProviderDocumentObject* getView() const { return *m_view; @@ -230,7 +234,6 @@ protected: protected: Gui::ViewProviderWeakPtrT m_view; - std::vector m_boxes; }; @@ -238,7 +241,7 @@ protected: // box to set the coloring class ViewProviderFemPostObject; -class TaskPostDisplay: public TaskPostBox +class TaskPostDisplay: public TaskPostWidget { Q_OBJECT @@ -261,7 +264,6 @@ private: void slotAddedFunction(); private: - QWidget* proxy; std::unique_ptr ui; }; @@ -270,7 +272,7 @@ private: // functions class ViewProviderFemPostFunction; -class TaskPostFunction: public TaskPostBox +class TaskPostFunction: public TaskPostWidget { Q_OBJECT @@ -283,7 +285,7 @@ public: // *************************************************************************** // frames -class TaskPostFrames: public TaskPostBox +class TaskPostFrames: public TaskPostWidget { Q_OBJECT @@ -297,7 +299,6 @@ private: void setupConnections(); void onSelectionChanged(); - QWidget* proxy; std::unique_ptr ui; }; @@ -311,12 +312,13 @@ private: // branch class ViewProviderFemPostBranchFilter; -class TaskPostBranch: public TaskPostBox +class TaskPostBranch: public TaskPostWidget { Q_OBJECT public: - explicit TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent = nullptr); + explicit TaskPostBranch(ViewProviderFemPostBranchFilter* view, + QWidget* parent = nullptr); ~TaskPostBranch() override; void applyPythonCode() override; @@ -326,7 +328,6 @@ private: void onModeIndexChanged(int); void onOutputIndexChanged(int); - QWidget* proxy; std::unique_ptr ui; }; @@ -334,7 +335,7 @@ private: // data along line filter class ViewProviderFemPostDataAlongLine; -class TaskPostDataAlongLine: public TaskPostBox +class TaskPostDataAlongLine: public TaskPostWidget { Q_OBJECT @@ -362,7 +363,6 @@ private: private: std::string Plot(); std::string ObjectVisible(); - QWidget* proxy; std::unique_ptr ui; DataAlongLineMarker* marker; }; @@ -372,7 +372,7 @@ private: // data at point filter class ViewProviderFemPostDataAtPoint; -class TaskPostDataAtPoint: public TaskPostBox +class TaskPostDataAtPoint: public TaskPostWidget { Q_OBJECT @@ -400,7 +400,6 @@ private: std::string toString(double val) const; void showValue(double value, const char* unit); std::string objectVisible(bool visible) const; - QWidget* proxy; std::unique_ptr ui; }; @@ -409,7 +408,7 @@ private: // clip filter class ViewProviderFemPostClip; -class TaskPostClip: public TaskPostBox +class TaskPostClip: public TaskPostWidget { Q_OBJECT @@ -435,7 +434,6 @@ private: void collectImplicitFunctions(); // App::PropertyLink* m_functionProperty; - QWidget* proxy; std::unique_ptr ui; FunctionWidget* fwidget; }; @@ -445,7 +443,7 @@ private: // contours filter class ViewProviderFemPostContours; -class TaskPostContours: public TaskPostBox +class TaskPostContours: public TaskPostWidget { Q_OBJECT @@ -464,7 +462,6 @@ private: void onRelaxationChanged(double v); private: - QWidget* proxy; std::unique_ptr ui; bool blockVectorUpdate = false; void updateFields(); @@ -475,7 +472,7 @@ private: // cut filter class ViewProviderFemPostCut; -class TaskPostCut: public TaskPostBox +class TaskPostCut: public TaskPostWidget { Q_OBJECT @@ -499,7 +496,6 @@ private: void collectImplicitFunctions(); // App::PropertyLink* m_functionProperty; - QWidget* proxy; std::unique_ptr ui; FunctionWidget* fwidget; }; @@ -509,7 +505,7 @@ private: // scalar clip filter class ViewProviderFemPostScalarClip; -class TaskPostScalarClip: public TaskPostBox +class TaskPostScalarClip: public TaskPostWidget { Q_OBJECT @@ -527,7 +523,6 @@ private: void onInsideOutToggled(bool val); private: - QWidget* proxy; std::unique_ptr ui; }; @@ -536,7 +531,7 @@ private: // warp vector filter class ViewProviderFemPostWarpVector; -class TaskPostWarpVector: public TaskPostBox +class TaskPostWarpVector: public TaskPostWidget { Q_OBJECT @@ -555,7 +550,6 @@ private: void onVectorCurrentIndexChanged(int idx); private: - QWidget* proxy; std::unique_ptr ui; }; @@ -564,7 +558,7 @@ private: // calculator filter class ViewProviderFemPostCalculator; -class TaskPostCalculator: public TaskPostBox +class TaskPostCalculator: public TaskPostWidget { Q_OBJECT diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostBranchFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostBranchFilter.cpp index 5faa50b58d..f3871c5873 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostBranchFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostBranchFilter.cpp @@ -25,6 +25,7 @@ #include "TaskPostBoxes.h" #include "ViewProviderFemPostBranchFilter.h" #include +#include using namespace FemGui; @@ -46,7 +47,8 @@ ViewProviderFemPostBranchFilter::~ViewProviderFemPostBranchFilter() void ViewProviderFemPostBranchFilter::setupTaskDialog(TaskDlgPost* dlg) { // add the branch ui - dlg->appendBox(new TaskPostBranch(this)); + auto panel = new TaskPostBranch(this); + dlg->addTaskBox(panel->windowIcon().pixmap(32), panel); // add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index fe0ad21fcf..7349f24870 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -30,10 +30,39 @@ #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" +#include "ViewProviderFemPostFilterPy.h" using namespace FemGui; +PROPERTY_SOURCE(FemGui::ViewProviderFemPostFilterPythonBase, FemGui::ViewProviderFemPostObject) + +ViewProviderFemPostFilterPythonBase::ViewProviderFemPostFilterPythonBase() {} + +ViewProviderFemPostFilterPythonBase::~ViewProviderFemPostFilterPythonBase() = default; + +std::vector ViewProviderFemPostFilterPythonBase::getDisplayModes() const +{ + return std::vector(); +} + +namespace Gui { +PROPERTY_SOURCE_TEMPLATE(FemGui::ViewProviderPostFilterPython, FemGui::ViewProviderFemPostFilterPythonBase) + +template<> PyObject* FemGui::ViewProviderPostFilterPython::getPyObject() +{ + if (!pyViewObject) { + pyViewObject = new ViewProviderFemPostFilterPy(this); + } + pyViewObject->IncRef(); + return pyViewObject; +} + +// explicit template instantiation +template class GuiExport ViewProviderFeaturePythonT; + +} + // *************************************************************************** // in the following, the different filters sorted alphabetically // *************************************************************************** @@ -54,7 +83,8 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - dlg->appendBox(new TaskPostDataAlongLine(this)); + auto panel = new TaskPostDataAlongLine(this); + dlg->addTaskBox(panel->getIcon(), panel); } @@ -102,7 +132,8 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - dlg->appendBox(new TaskPostDataAtPoint(this)); + auto panel = new TaskPostDataAtPoint(this); + dlg->addTaskBox(panel->getIcon(), panel); } @@ -123,8 +154,8 @@ void ViewProviderFemPostClip::setupTaskDialog(TaskDlgPost* dlg) // add the function box assert(dlg->getView() == this); - dlg->appendBox( - new TaskPostClip(this, &dlg->getView()->getObject()->Function)); + auto panel = new TaskPostClip(this, &dlg->getView()->getObject()->Function); + dlg->addTaskBox(panel->getIcon(), panel); // add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); @@ -146,7 +177,8 @@ void ViewProviderFemPostContours::setupTaskDialog(TaskDlgPost* dlg) { // the filter-specific task panel assert(dlg->getView() == this); - dlg->appendBox(new TaskPostContours(this)); + auto panel = new TaskPostContours(this); + dlg->addTaskBox(panel->getIcon(), panel); } @@ -165,8 +197,8 @@ void ViewProviderFemPostCut::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - dlg->appendBox( - new TaskPostCut(this, &dlg->getView()->getObject()->Function)); + auto panel = new TaskPostCut(this, &dlg->getView()->getObject()->Function); + dlg->addTaskBox(panel->getIcon(), panel); // add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); @@ -188,7 +220,8 @@ void ViewProviderFemPostScalarClip::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - dlg->appendBox(new TaskPostScalarClip(this)); + auto panel = new TaskPostScalarClip(this); + dlg->addTaskBox(panel->getIcon(), panel); // add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); @@ -210,7 +243,8 @@ void ViewProviderFemPostWarpVector::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - dlg->appendBox(new TaskPostWarpVector(this)); + auto panel = new TaskPostWarpVector(this); + dlg->addTaskBox(panel->getIcon(), panel); // add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); @@ -245,7 +279,8 @@ void ViewProviderFemPostCalculator::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - dlg->appendBox(new TaskPostCalculator(this)); + auto panel = new TaskPostCalculator(this); + dlg->addTaskBox(panel->getIcon(), panel); // add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h index e728e5fcd0..771ce5ef16 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h @@ -23,12 +23,36 @@ #ifndef FEM_VIEWPROVIDERFEMPOSTFILTER_H #define FEM_VIEWPROVIDERFEMPOSTFILTER_H +#include #include "ViewProviderFemPostObject.h" - namespace FemGui { +// *************************************************************************** +// Special classes to enable python filter view providers +// *************************************************************************** + +// Special class for the python view providers, which need some special behaviour +class FemGuiExport ViewProviderFemPostFilterPythonBase: public ViewProviderFemPostObject +{ + PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostFilterPythonBase); + +public: + /// constructor. + ViewProviderFemPostFilterPythonBase(); + ~ViewProviderFemPostFilterPythonBase() override; + + // we do not use default display modes but let the python implementation choose + // Python view provider needs to return a sublist of PostObject supporter DisplayModes + std::vector getDisplayModes() const override; +}; + + +// Viewprovider for the python filters +using ViewProviderPostFilterPython = Gui::ViewProviderFeaturePythonT; + + // *************************************************************************** // in the following, the different filters sorted alphabetically // *************************************************************************** diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml new file mode 100644 index 0000000000..c41959e24d --- /dev/null +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml @@ -0,0 +1,24 @@ + + + + + + ViewProviderFemPostPipeline class + + + + Returns the display option task panel for a post processing edit task dialog. + + + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp new file mode 100644 index 0000000000..c922d76840 --- /dev/null +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + * Copyright (c) 2025 Stefan Tröger * + * * + * 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" + +// clang-format off +#include +#include +#include "ViewProviderFemPostFilter.h" +#include "TaskPostBoxes.h" +// inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml) +#include "ViewProviderFemPostFilterPy.h" +#include "ViewProviderFemPostFilterPy.cpp" +#include +// clang-format on + + +using namespace FemGui; + +// returns a string which represents the object e.g. when printed in python +std::string ViewProviderFemPostFilterPy::representation() const +{ + return {""}; +} + +PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) +{ + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + auto panel = new TaskPostDisplay(getViewProviderFemPostObjectPtr()); + + Gui::PythonWrapper wrap; + if (wrap.loadCoreModule()) { + return Py::new_reference_to(wrap.fromQWidget(panel)); + } + + PyErr_SetString(PyExc_TypeError, "creating the panel failed"); + return nullptr; +} + +PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const +{ + return nullptr; +} + +int ViewProviderFemPostFilterPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFunction.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFunction.cpp index 200622da31..1c7132aa07 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFunction.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFunction.cpp @@ -343,7 +343,8 @@ bool ViewProviderFemPostFunction::setEdit(int ModNum) } else { postDlg = new TaskDlgPost(this); - postDlg->appendBox(new TaskPostFunction(this)); + auto panel = new TaskPostFunction(this); + postDlg->addTaskBox(panel->windowIcon().pixmap(32), panel); Gui::Control().showDialog(postDlg); } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 0eaeb5047b..7113155471 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -53,7 +53,6 @@ #endif #include -#include #include #include #include @@ -187,7 +186,7 @@ ViewProviderFemPostObject::ViewProviderFemPostObject() LineWidth.setConstraints(&sizeRange); PointSize.setConstraints(&sizeRange); - sPixmap = "fem-femmesh-from-shape"; + sPixmap = "FEM_PostPipelineFromResult"; // create the subnodes which do the visualization work m_transpType = new SoTransparencyType(); @@ -408,7 +407,9 @@ void ViewProviderFemPostObject::updateVtk() } m_currentAlgorithm->Update(); - updateProperties(); + if (!isRestoring()) { + updateProperties(); + } update3D(); } @@ -931,7 +932,9 @@ void ViewProviderFemPostObject::onChanged(const App::Property* prop) } if (prop == &Field && setupPipeline()) { - updateProperties(); + if(!isRestoring()) { + updateProperties(); + } WriteColorData(ResetColorBarRange); } else if (prop == &Component && setupPipeline()) { @@ -1016,7 +1019,8 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { assert(dlg->getView() == this); - dlg->appendBox(new TaskPostDisplay(this)); + auto panel = new TaskPostDisplay(this); + dlg->addTaskBox(panel->windowIcon().pixmap(32), panel); } void ViewProviderFemPostObject::unsetEdit(int ModNum) diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h index 0e2e74954e..46b6724a5d 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h @@ -114,20 +114,8 @@ public: bool canDelete(App::DocumentObject* obj) const override; virtual void onSelectionChanged(const Gui::SelectionChanges& sel); - /** @name Selection handling - * This group of methods do the selection handling. - * Here you can define how the selection for your ViewProvider - * works. - */ - //@{ - // /// indicates if the ViewProvider use the new Selection model - // virtual bool useNewSelectionModel(void) const {return true;} - // /// return a hit element to the selection path or 0 - // virtual std::string getElement(const SoDetail*) const; - // virtual SoDetail* getDetail(const char*) const; - // /// return the highlight lines for a given element or the whole shape - // virtual std::vector getSelectionShape(const char* Element) const; - // //@} + // setting up task dialogs + virtual void setupTaskDialog(TaskDlgPost* dlg); protected: void handleChangedPropertyName(Base::XMLReader& reader, diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp index 6e83a9f24d..a78af62cd7 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp @@ -221,7 +221,8 @@ void ViewProviderFemPostPipeline::setupTaskDialog(TaskDlgPost* dlg) // add the function box assert(dlg->getView() == this); ViewProviderFemPostObject::setupTaskDialog(dlg); - dlg->appendBox(new TaskPostFrames(this)); + auto panel = new TaskPostFrames(this); + dlg->addTaskBox(panel->windowIcon().pixmap(32), panel); } diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 66d46af435..0ed5cc1bc6 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -206,6 +206,9 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterCutFunction" << "FEM_PostFilterClipRegion" << "FEM_PostFilterContours" +#ifdef BUILD_FEM_VTK_WRAPPER + << "FEM_PostFilterGlyph" +#endif << "FEM_PostFilterDataAlongLine" << "FEM_PostFilterLinearizedStresses" << "FEM_PostFilterDataAtPoint" diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 72c407b3bc..e60c23c521 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -652,6 +652,19 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"): base_vtk_result.addObject(obj) return obj +def makePostVtkFilterGlyph(doc, base_vtk_result, name="Glyph"): + """makePostVtkFilterGlyph(document, [name]): + creates a FEM post processing filter that visualizes vector fields with glyphs + """ + obj = doc.addObject("Fem::PostFilterPython", name) + from femobjects import post_glyphfilter + + post_glyphfilter.PostGlyphFilter(obj) + base_vtk_result.addObject(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_glyphfilter + view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject) + return obj def makePostVtkResult(doc, result_data, name="VtkResult"): """makePostVtkResult(document, base_result, [name]): @@ -669,7 +682,6 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): obj.ViewObject.DisplayMode = "Surface" return obj - # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): """makeEquationDeformation(document, [base_solver], [name]): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 2025fbaabd..66217b3cf0 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1216,6 +1216,17 @@ class _SolverZ88(CommandManager): self.is_active = "with_analysis" self.do_activated = "add_obj_on_gui_expand_noset_edit" +class _PostFilterGlyph(CommandManager): + "The FEM_PostFilterGlyph command definition" + + def __init__(self): + super().__init__() + self.menutext = Qt.QT_TRANSLATE_NOOP("FEM_PostFilterGlyph", "Glyph filter") + self.accel = "F, G" + self.tooltip = Qt.QT_TRANSLATE_NOOP("FEM_PostFilterGlyph", "Post processing filter that adds glyphs to the mesh vertices for vertex data visualization") + self.is_active = "with_vtk_selresult" + self.do_activated = "add_filter" + # the string in add command will be the page name on FreeCAD wiki FreeCADGui.addCommand("FEM_Analysis", _Analysis()) @@ -1271,3 +1282,4 @@ FreeCADGui.addCommand("FEM_SolverElmer", _SolverElmer()) FreeCADGui.addCommand("FEM_SolverMystran", _SolverMystran()) FreeCADGui.addCommand("FEM_SolverRun", _SolverRun()) FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88()) +FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index 16529a94eb..81da4da431 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -89,6 +89,10 @@ class CommandManager: FreeCADGui.ActiveDocument is not None and self.result_selected() ) + elif self.is_active == "with_vtk_selresult": + active = ( + self.vtk_result_selected() + ) elif self.is_active == "with_part_feature": active = FreeCADGui.ActiveDocument is not None and self.part_feature_selected() elif self.is_active == "with_femmesh": @@ -144,6 +148,8 @@ class CommandManager: self.add_obj_on_gui_selobj_set_edit(self.__class__.__name__.lstrip("_")) elif self.do_activated == "add_obj_on_gui_selobj_expand_noset_edit": self.add_obj_on_gui_selobj_expand_noset_edit(self.__class__.__name__.lstrip("_")) + elif self.do_activated == "add_filter": + self.add_filter(self.__class__.__name__.lstrip("_")) # in all other cases Activated is implemented it the command class def results_present(self): @@ -169,6 +175,13 @@ class CommandManager: return True return False + def vtk_result_selected(self): + sel = FreeCADGui.Selection.getSelection() + if len(sel) == 1 and sel[0].isDerivedFrom("Fem::FemPostObject"): + self.selobj = sel[0] + return True + return False + def part_feature_selected(self): sel = FreeCADGui.Selection.getSelection() if len(sel) == 1 and sel[0].isDerivedFrom("Part::Feature"): @@ -363,3 +376,33 @@ class CommandManager: ) # expand selobj in tree view expandParentObject() + + def add_filter(self, filtertype): + # like add_obj_on_gui_selobj_noset_edit but the selection is kept + # and the selobj is expanded in the tree to see the added obj + + # Note: we know selobj is a FemPostObject as otherwise the command should not have been active + # We also assume the all filters are in PostGroups and not astray + group = None + if self.selobj.hasExtension("Fem::FemPostGroupExtension"): + group = self.selobj + else: + group = self.selobj.getParentPostGroup() + + FreeCAD.ActiveDocument.openTransaction(f"Create Fem{filtertype}") + FreeCADGui.addModule("ObjectsFem") + FreeCADGui.doCommand( + "ObjectsFem.make{}(" + "FreeCAD.ActiveDocument, FreeCAD.ActiveDocument.{})".format(filtertype, group.Name) + ) + # set display and selection style to assure the user sees the new object + FreeCADGui.doCommand("FreeCAD.ActiveDocument.ActiveObject.ViewObject.DisplayMode = \"Surface\""); + FreeCADGui.doCommand("FreeCAD.ActiveDocument.ActiveObject.ViewObject.SelectionStyle = \"BoundBox\""); + + # hide selected filter + FreeCADGui.doCommand( + "FreeCAD.ActiveDocument.{}.ViewObject.Visibility = False".format(self.selobj.Name) + ) + + # expand selobj in tree view + expandParentObject() diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py new file mode 100644 index 0000000000..b111e61e8d --- /dev/null +++ b/src/Mod/Fem/femobjects/post_glyphfilter.py @@ -0,0 +1,267 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post glyph filter" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_glyphfilter +# \ingroup FEM +# \brief Post processing filter creating glyphs for vector fields + +# IMPORTANT: Never import vtk directly. Often vtk is compiled with different QT +# version than FreeCAD, and "import vtk" crashes by importing qt components. +# Always import the filter and data modules only. +from vtkmodules.vtkFiltersCore import vtkMaskPoints +from vtkmodules.vtkFiltersCore import vtkGlyph3D +import vtkmodules.vtkFiltersSources as vtkSources + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): + """ + A post processing filter adding glyphs + """ + + Type = "Fem::PostFilterPython" + + def __init__(self, obj): + super().__init__(obj) + + for prop in self._get_properties(): + prop.add_to_object(obj) + + self.__setupFilterPipeline(obj) + + def _get_properties(self): + + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="Glyph", + group="Glyph", + doc="The form of the glyph", + value=["Arrow", "Cube"], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="OrientationData", + group="Glyph", + doc="Which vector field is used to orient the glyphs", + value=["None"], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="ScaleData", + group="Scale", + doc="Which data field is used to scale the glyphs", + value=["None"], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="VectorScaleMode", + group="Scale", + doc="If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components", + value=["Not a vector"], + ), + _PropHelper( + type="App::PropertyFloatConstraint", + name="ScaleFactor", + group="Scale", + doc="A constant multiplier the glyphs are scaled with", + value= (1, 0, 1e12, 1e-12), + ), + _PropHelper( + type="App::PropertyEnumeration", + name="MaskMode", + group="Masking", + doc="Which vertices are used as glyph locations", + value=["Use All", "Every Nth", "Uniform Samping"], + ), + _PropHelper( + type="App::PropertyIntegerConstraint", + name="Stride", + group="Masking", + doc="Define the stride for \"Every Nth\" masking mode", + value= (2, 1, 999999999, 1), + ), + _PropHelper( + type="App::PropertyIntegerConstraint", + name="MaxNumber", + group="Masking", + doc="Defines the maximal number of vertices used for \"Uniform Sampling\" masking mode", + value= (1000, 1, 999999999, 1), + ), + ] + return prop + + def __setupMaskingFilter(self, obj, masking): + + if obj.MaskMode == "Use All": + masking.RandomModeOff() + masking.SetOnRatio(1) + masking.SetMaximumNumberOfPoints(int(1e10)) + elif obj.MaskMode == "Every Nth": + masking.RandomModeOff() + masking.SetOnRatio(obj.Stride) + masking.SetMaximumNumberOfPoints(int(1e10)) + else: + masking.SetOnRatio(1) + masking.SetMaximumNumberOfPoints(obj.MaxNumber) + masking.RandomModeOn() + + def __setupGlyphFilter(self, obj, glyph): + + # scaling + if obj.ScaleData != "None": + + glyph.ScalingOn() + if obj.ScaleData in obj.getInputVectorFields(): + + # make sure the vector mode is set correctly + if obj.VectorScaleMode == "Not a vector": + obj.VectorScaleMode = ["Scale by magnitude", "Scale by components"] + obj.VectorScaleMode = "Scale by magnitude" + + if obj.VectorScaleMode == "Scale by magnitude": + glyph.SetScaleModeToScaleByVector() + else: + glyph.SetScaleModeToScaleByVectorComponents() + + glyph.SetInputArrayToProcess(2,0,0,0,obj.ScaleData) + + else: + # scalar scaling mode + if obj.VectorScaleMode != "Not a vector": + obj.VectorScaleMode = ["Not a vector"] + + glyph.SetInputArrayToProcess(2,0,0,0,obj.ScaleData) + glyph.SetScaleModeToScaleByScalar() + else: + glyph.ScalingOff() + + glyph.SetScaleFactor(obj.ScaleFactor) + + # Orientation + if obj.OrientationData != "None": + glyph.OrientOn() + glyph.SetInputArrayToProcess(1,0,0,0,obj.OrientationData) + else: + glyph.OrientOff() + + + def __setupFilterPipeline(self, obj): + + # store of all algorithms for later access + # its map filter_name : [source, mask, glyph] + self._algorithms = {} + + # create all vtkalgorithm combinations and set them as filter pipeline + sources = {"Arrow": vtkSources.vtkArrowSource, + "Cube": vtkSources.vtkCubeSource} + + for source_name in sources: + + source = sources[source_name]() + + masking = vtkMaskPoints() + self.__setupMaskingFilter(obj, masking) + + glyph = vtkGlyph3D() + glyph.SetSourceConnection(source.GetOutputPort(0)) + glyph.SetInputConnection(masking.GetOutputPort(0)) + self.__setupGlyphFilter(obj, glyph) + + self._algorithms[source_name] = [source, masking, glyph] + obj.addFilterPipeline(source_name, masking, glyph) + + obj.setActiveFilterPipeline(obj.Glyph) + + + def onDocumentRestored(self, obj): + # resetup the pipeline + self.__setupFilterPipeline(obj) + + def execute(self, obj): + # we check what new inputs + + vector_fields = obj.getInputVectorFields() + all_fields = (vector_fields + obj.getInputScalarFields()) + + vector_fields.sort() + all_fields.sort() + + current_orient = obj.OrientationData + enumeration = ["None"] + vector_fields + obj.OrientationData = enumeration + if current_orient in enumeration: + obj.OrientationData = current_orient + + current_scale = obj.ScaleData + enumeration = ["None"] + all_fields + obj.ScaleData = enumeration + if current_scale in enumeration: + obj.ScaleData = current_scale + + # make sure parent class execute is called! + return False + + + def onChanged(self, obj, prop): + + # check if we are setup already + if not hasattr(self, "_algorithms"): + return + + if prop == "Glyph": + obj.setActiveFilterPipeline(obj.Glyph) + + if prop == "MaskMode": + for filter in self._algorithms: + masking = self._algorithms[filter][1] + self.__setupMaskingFilter(obj, masking) + + if prop == "Stride": + # if mode is use all stride setting needs to stay at one + if obj.MaskMode == "Every Nth": + for filter in self._algorithms: + masking = self._algorithms[filter][1] + masking.SetOnRatio(obj.Stride) + + if prop == "MaxNumber": + if obj.MaskMode == "Uniform Sampling": + for filter in self._algorithms: + masking = self._algorithms[filter][1] + masking.SetMaximumNumberOfPoints(obj.MaxNumber) + + if prop == "OrientationData" or prop == "ScaleData": + for filter in self._algorithms: + glyph = self._algorithms[filter][2] + self.__setupGlyphFilter(obj, glyph) + + if prop == "ScaleFactor": + for filter in self._algorithms: + glyph = self._algorithms[filter][2] + glyph.SetScaleFactor(obj.ScaleFactor) + diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py new file mode 100644 index 0000000000..d90f4456ea --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -0,0 +1,212 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM glyph filter task panel for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_glyphfilter +# \ingroup FEM +# \brief task panel for post glyph filter object + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from femguiutils import selection_widgets +from . import base_femtaskpanel + + +class _TaskPanel(base_femtaskpanel._BaseTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # glyph parameter widget + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostGlyph.ui" + ) + self.__init_widget() + + # form made from param and selection widget + self.form = [self.widget, vobj.createDisplayTaskWidget()] + + # get the settings group + self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem") + + # Implement parent functions + # ########################## + + def getStandardButtons(self): + return QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + + def clicked(self, button): + # apply button hit? + if button == QtGui.QDialogButtonBox.Apply: + self.obj.Document.recompute() + + def accept(self): + #self.obj.CharacteristicLength = self.elelen + #self.obj.References = self.selection_widget.references + #self.selection_widget.finish_selection() + return super().accept() + + def reject(self): + #self.selection_widget.finish_selection() + return super().reject() + + + # Helper functions + # ################## + + def _recompute(self): + # only recompute if the user wants automatic recompute + if self.__settings_grp.GetBool("PostAutoRecompute", True): + self.obj.Document.recompute() + + def _enumPropertyToCombobox(self, obj, prop, cbox): + cbox.blockSignals(True) + cbox.clear() + entries = obj.getEnumerationsOfProperty(prop) + for entry in entries: + cbox.addItem(entry) + + cbox.setCurrentText(getattr(obj, prop)) + cbox.blockSignals(False) + + + # Setup functions + # ############### + + def __init_widget(self): + + # set current values to ui + self._enumPropertyToCombobox(self.obj, "Glyph", self.widget.FormComboBox) + self._enumPropertyToCombobox(self.obj, "OrientationData", self.widget.OrientationComboBox) + self._enumPropertyToCombobox(self.obj, "ScaleData", self.widget.ScaleComboBox) + self._enumPropertyToCombobox(self.obj, "VectorScaleMode", self.widget.VectorModeComboBox) + self._enumPropertyToCombobox(self.obj, "MaskMode", self.widget.MaskModeComboBox) + + self.widget.ScaleFactorBox.setValue(self.obj.ScaleFactor) + self.__slide_min = self.obj.ScaleFactor*0.5 + self.__slide_max = self.obj.ScaleFactor*1.5 + self.widget.ScaleSlider.setValue(50) + self.widget.StrideBox.setValue(self.obj.Stride) + self.widget.MaxBox.setValue(self.obj.MaxNumber) + self.__update_scaling_ui() + self.__update_masking_ui() + + # connect all signals + self.widget.FormComboBox.currentTextChanged.connect(self._form_changed) + self.widget.OrientationComboBox.currentTextChanged.connect(self._orientation_changed) + self.widget.ScaleComboBox.currentTextChanged.connect(self._scale_data_changed) + self.widget.VectorModeComboBox.currentTextChanged.connect(self._scale_vector_mode_changed) + self.widget.ScaleFactorBox.valueChanged.connect(self._scale_factor_changed) + self.widget.ScaleSlider.valueChanged.connect(self._scale_slider_changed) + self.widget.MaskModeComboBox.currentTextChanged.connect(self._mask_mode_changed) + self.widget.StrideBox.valueChanged.connect(self._stride_changed) + self.widget.MaxBox.valueChanged.connect(self._max_number_changed) + + + def __update_scaling_ui(self): + enabled = self.widget.ScaleComboBox.currentIndex() != 0 + self.widget.VectorModeComboBox.setEnabled(enabled) + self.widget.ScaleFactorBox.setEnabled(enabled) + self.widget.ScaleSlider.setEnabled(enabled) + + def __update_masking_ui(self): + enabled = self.widget.MaskModeComboBox.currentIndex() != 0 + self.widget.StrideBox.setEnabled(enabled) + self.widget.MaxBox.setEnabled(enabled) + + + # callbacks and logic + # ################### + + def _form_changed(self, value): + self.obj.Glyph = value + self._recompute() + + def _orientation_changed(self, value): + self.obj.OrientationData = value + self._recompute() + + def _scale_data_changed(self, value): + self.obj.ScaleData = value + self._enumPropertyToCombobox(self.obj, "VectorScaleMode", self.widget.VectorModeComboBox) + self.__update_scaling_ui() + self._recompute() + + def _scale_vector_mode_changed(self, value): + self.obj.VectorScaleMode = value + self._recompute() + + def _scale_factor_changed(self, value): + + # set slider + self.__slide_min = value*0.5 + self.__slide_max = value*1.5 + slider_value = (value - self.__slide_min) / (self.__slide_max - self.__slide_min) * 100. + self.widget.ScaleSlider.blockSignals(True) + self.widget.ScaleSlider.setValue(slider_value) + self.widget.ScaleSlider.blockSignals(False) + + self.obj.ScaleFactor = value + self._recompute() + + def _scale_slider_changed(self, value): + + # calculate value + # ( max - min ) + # factor = min + ( slider_value x ------------- ) + # 100 + # + f = self.__slide_min + (value * (self.__slide_max - self.__slide_min)/100) + + # sync factor spin box + self.widget.ScaleFactorBox.blockSignals(True) + self.widget.ScaleFactorBox.setValue(f) + self.widget.ScaleFactorBox.blockSignals(False) + + # set value + self.obj.ScaleFactor = f + self._recompute() + + + def _mask_mode_changed(self, value): + self.obj.MaskMode = value + self.__update_masking_ui() + self._recompute() + + def _stride_changed(self, value): + self.obj.Stride = value + self._recompute() + + def _max_number_changed(self, value): + self.obj.MaxNumber = value + self._recompute() + diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index cf9ed123ac..acf9bf9652 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -1154,6 +1154,7 @@ def create_all_fem_objects_doc(doc): ObjectsFem.makePostVtkFilterCutFunction(doc, vres) ObjectsFem.makePostVtkFilterWarp(doc, vres) ObjectsFem.makePostVtkFilterContours(doc, vres) + ObjectsFem.makePostVtkFilterGlyph(doc, vres) analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc)) analysis.addObject(ObjectsFem.makeSolverCalculiX(doc)) diff --git a/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py b/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py new file mode 100644 index 0000000000..4df2a7698b --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py @@ -0,0 +1,86 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing glyph filter ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_glyphfilter +# \ingroup FEM +# \brief view provider for post glyph filter object + +import FreeCAD +import FreeCADGui + +import FemGui +from PySide import QtGui +from femtaskpanels import task_post_glyphfilter + + +class VPPostGlyphFilter: + """ + A View Provider for the PostGlyphFilter object + """ + + def __init__(self, vobj): + vobj.Proxy = self + + def getIcon(self): + return ":/icons/FEM_PostFilterGlyph.svg" + + def getDisplayModes(self, obj): + # Mandatory, as the ViewProviderPostFilterPython does not add any + # display modes. We can choose here any that is supported by it: + # "Outline", "Nodes", "Surface", "Surface with Edges", + # "Wireframe", "Wireframe (surface only)", "Nodes (surface only)" + + # only surface makes sense for the glyphs + return ["Surface"] + + def setDisplayMode(self, mode): + # the post object viewprovider implements the different display modes + # via vtk filter, not via masking modes. Hence we need to make sure + # to always stay in the "Default" masking mode, no matter the display mode + return "Default" + + def setEdit(self, vobj, mode): + # make sure we see what we edit + vobj.show() + + # build up the task panel + taskd = task_post_glyphfilter._TaskPanel(vobj) + + #show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def unsetEdit(self, vobj, mode): + FreeCADGui.Control.closeDialog() + return True + + def dumps(self): + return None + + def loads(self, state): + return None From 0217a2176e5507c75eb939b2a23e731b32cf6903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 14 Apr 2025 20:48:58 +0200 Subject: [PATCH 2/7] Fem: make python filter build process more elegant --- cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake | 6 ++-- src/Mod/Fem/App/FemPostFilterPyImp.cpp | 6 ++-- src/Mod/Fem/App/PropertyPostDataObject.cpp | 19 +++++++++++-- src/Mod/Fem/CMakeLists.txt | 30 ++++++++++++++++++-- src/Mod/Fem/Gui/Workbench.cpp | 5 +++- src/Mod/Fem/ObjectsFem.py | 2 +- src/Mod/Fem/femcommands/commands.py | 6 ++-- src/Mod/Fem/femcommands/manager.py | 17 ++++++++--- 8 files changed, 72 insertions(+), 19 deletions(-) diff --git a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake index 31899c8b3d..978416d088 100644 --- a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake +++ b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake @@ -64,9 +64,11 @@ macro(SetupSalomeSMESH) set(BUILD_FEM_VTK ON) - # check if PythonWrapperCore was found (vtk 9 only) + # Check if PythonWrapperCore was found + # Note: vtk 9 only, as the implementations use the vtk modules introduced in 9.0 + # VTK_WrappingPythonCore_FOUND is named differently for versions <9.0 if (${VTK_WrappingPythonCore_FOUND}) - add_compile_definitions(BUILD_FEM_VTK_WRAPPER) + set(BUILD_FEM_VTK_PYTHON 1) message(STATUS "VTK python wrapper: available") else() message(STATUS "VTK python wrapper: NOT available") diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index 349b3bbaec..479e6d55d2 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -35,7 +35,7 @@ #include "FemPostFilterPy.cpp" // clang-format on -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON #include #include #endif //BUILD_FEM_VTK @@ -54,7 +54,7 @@ std::string FemPostFilterPy::representation() const PyObject* FemPostFilterPy::addFilterPipeline(PyObject* args) { -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON const char* name; PyObject *source = nullptr; PyObject *target = nullptr; @@ -116,7 +116,7 @@ PyObject* FemPostFilterPy::getParentPostGroup(PyObject* args) PyObject* FemPostFilterPy::getInputData(PyObject* args) { -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON // we take no arguments if (!PyArg_ParseTuple(args, "")) { return nullptr; diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index a11a57d58b..302c2dc704 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -42,7 +42,7 @@ #include #endif -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON #include #endif @@ -166,7 +166,7 @@ int PropertyPostDataObject::getDataType() PyObject* PropertyPostDataObject::getPyObject() { -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON //create a copy first auto copy = static_cast(Copy()); @@ -182,9 +182,22 @@ PyObject* PropertyPostDataObject::getPyObject() #endif } -void PropertyPostDataObject::setPyObject(PyObject* /*value*/) +void PropertyPostDataObject::setPyObject(PyObject* value) { +#ifdef FC_USE_VTK_PYTHON + vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(value, "vtkDataObject"); + if (!obj) { + throw Base::TypeError("Can only set vtkDataObject"); + } + auto dobj = static_cast(obj); + createDataObjectByExternalType(dobj); + + aboutToSetValue(); + m_dataObject->DeepCopy(dobj); + hasSetValue(); +#else throw Base::NotImplementedError(); +#endif } App::Property* PropertyPostDataObject::Copy() const diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 1f7d602b07..42258b391c 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -1,6 +1,12 @@ if(BUILD_FEM_VTK) add_definitions(-DFC_USE_VTK) + + # we may use VTK but do not have the python wrappers available + if(BUILD_FEM_VTK_PYTHON) + add_definitions(-DFC_USE_VTK_PYTHON) + endif(BUILD_FEM_VTK_PYTHON) + endif(BUILD_FEM_VTK) @@ -202,12 +208,18 @@ SET(FemObjects_SRCS femobjects/mesh_netgen.py femobjects/mesh_region.py femobjects/mesh_result.py - femobjects/post_glyphfilter.py femobjects/result_mechanical.py femobjects/solver_calculix.py femobjects/solver_ccxtools.py ) +if(BUILD_FEM_VTK_PYTHON) + SET(FemObjects_SRCS + ${FemObjects_SRCS} + femobjects/post_glyphfilter.py + ) +endif(BUILD_FEM_VTK_PYTHON) + SET(FemResult_SRCS femresult/__init__.py femresult/resulttools.py @@ -605,12 +617,18 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/task_mesh_group.py femtaskpanels/task_mesh_region.py femtaskpanels/task_mesh_netgen.py - femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_result_mechanical.py femtaskpanels/task_solver_calculix.py femtaskpanels/task_solver_ccxtools.py ) +if(BUILD_FEM_VTK_PYTHON) + SET(FemGuiTaskPanels_SRCS + ${FemGuiTaskPanels_SRCS} + femtaskpanels/task_post_glyphfilter.py + ) +endif(BUILD_FEM_VTK_PYTHON) + SET(FemGuiTests_SRCS femtest/gui/__init__.py femtest/gui/test_open.py @@ -656,12 +674,18 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_mesh_netgen.py femviewprovider/view_mesh_region.py femviewprovider/view_mesh_result.py - femviewprovider/view_post_glyphfilter.py femviewprovider/view_result_mechanical.py femviewprovider/view_solver_calculix.py femviewprovider/view_solver_ccxtools.py ) +if(BUILD_FEM_VTK_PYTHON) + SET(FemGuiViewProvider_SRCS + ${FemGuiViewProvider_SRCS} + femviewprovider/view_post_glyphfilter.py + ) +endif(BUILD_FEM_VTK_PYTHON) + SET(FemGuiPreferencePages_SRCS fempreferencepages/__init__.py fempreferencepages/dlg_settings_netgen.py diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 0ed5cc1bc6..3ae3219705 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -206,7 +206,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterCutFunction" << "FEM_PostFilterClipRegion" << "FEM_PostFilterContours" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON << "FEM_PostFilterGlyph" #endif << "FEM_PostFilterDataAlongLine" @@ -358,6 +358,9 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterCutFunction" << "FEM_PostFilterClipRegion" << "FEM_PostFilterContours" +#ifdef FC_USE_VTK_PYTHON + << "FEM_PostFilterGlyph" +#endif << "FEM_PostFilterDataAlongLine" << "FEM_PostFilterLinearizedStresses" << "FEM_PostFilterDataAtPoint" diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index e60c23c521..92ca918d40 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -652,7 +652,7 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"): base_vtk_result.addObject(obj) return obj -def makePostVtkFilterGlyph(doc, base_vtk_result, name="Glyph"): +def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): """makePostVtkFilterGlyph(document, [name]): creates a FEM post processing filter that visualizes vector fields with glyphs """ diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 66217b3cf0..5e260f7765 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1225,7 +1225,7 @@ class _PostFilterGlyph(CommandManager): self.accel = "F, G" self.tooltip = Qt.QT_TRANSLATE_NOOP("FEM_PostFilterGlyph", "Post processing filter that adds glyphs to the mesh vertices for vertex data visualization") self.is_active = "with_vtk_selresult" - self.do_activated = "add_filter" + self.do_activated = "add_filter_set_edit" # the string in add command will be the page name on FreeCAD wiki @@ -1282,4 +1282,6 @@ FreeCADGui.addCommand("FEM_SolverElmer", _SolverElmer()) FreeCADGui.addCommand("FEM_SolverMystran", _SolverMystran()) FreeCADGui.addCommand("FEM_SolverRun", _SolverRun()) FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88()) -FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) + +if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index 81da4da431..f82301d0c7 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -148,8 +148,8 @@ class CommandManager: self.add_obj_on_gui_selobj_set_edit(self.__class__.__name__.lstrip("_")) elif self.do_activated == "add_obj_on_gui_selobj_expand_noset_edit": self.add_obj_on_gui_selobj_expand_noset_edit(self.__class__.__name__.lstrip("_")) - elif self.do_activated == "add_filter": - self.add_filter(self.__class__.__name__.lstrip("_")) + elif self.do_activated == "add_filter_set_edit": + self.add_filter_set_edit(self.__class__.__name__.lstrip("_")) # in all other cases Activated is implemented it the command class def results_present(self): @@ -377,7 +377,7 @@ class CommandManager: # expand selobj in tree view expandParentObject() - def add_filter(self, filtertype): + def add_filter_set_edit(self, filtertype): # like add_obj_on_gui_selobj_noset_edit but the selection is kept # and the selobj is expanded in the tree to see the added obj @@ -404,5 +404,14 @@ class CommandManager: "FreeCAD.ActiveDocument.{}.ViewObject.Visibility = False".format(self.selobj.Name) ) - # expand selobj in tree view + # recompute, expand selobj in tree view expandParentObject() + FreeCADGui.doCommand( + "FreeCAD.ActiveDocument.ActiveObject.recompute()" + ) + + # set edit + FreeCADGui.Selection.clearSelection() + FreeCADGui.doCommand( + "FreeCADGui.ActiveDocument.setEdit(FreeCAD.ActiveDocument.ActiveObject.Name)" + ) From d51d5a3b0be30f9d2d4547821eba95ac381ceaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 14 Apr 2025 21:56:28 +0200 Subject: [PATCH 3/7] Fem: correctly handle python filters if vtk python is not available --- src/Mod/Fem/App/PropertyPostDataObject.cpp | 2 ++ src/Mod/Fem/ObjectsFem.py | 25 +++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index 302c2dc704..86821e1ac7 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -44,6 +44,8 @@ #ifdef FC_USE_VTK_PYTHON #include +#else +#include #endif #include diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 92ca918d40..84b5d84818 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -652,19 +652,20 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"): base_vtk_result.addObject(obj) return obj -def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): - """makePostVtkFilterGlyph(document, [name]): - creates a FEM post processing filter that visualizes vector fields with glyphs - """ - obj = doc.addObject("Fem::PostFilterPython", name) - from femobjects import post_glyphfilter +if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): + """makePostVtkFilterGlyph(document, [name]): + creates a FEM post processing filter that visualizes vector fields with glyphs + """ + obj = doc.addObject("Fem::PostFilterPython", name) + from femobjects import post_glyphfilter - post_glyphfilter.PostGlyphFilter(obj) - base_vtk_result.addObject(obj) - if FreeCAD.GuiUp: - from femviewprovider import view_post_glyphfilter - view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject) - return obj + post_glyphfilter.PostGlyphFilter(obj) + base_vtk_result.addObject(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_glyphfilter + view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject) + return obj def makePostVtkResult(doc, result_data, name="VtkResult"): """makePostVtkResult(document, base_result, [name]): From 96e9e7a17a74cba2fc1e24f426b2e1ba9d7e527e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Tue, 15 Apr 2025 09:12:09 +0200 Subject: [PATCH 4/7] FEM: Fix test with new python filter and other small fixes --- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 2 +- src/Mod/Fem/Gui/ViewProviderFemPostObject.h | 1 - src/Mod/Fem/ObjectsFem.py | 25 +++++++++---------- .../femtaskpanels/task_post_glyphfilter.py | 1 + src/Mod/Fem/femtest/app/support_utils.py | 2 +- src/Mod/Fem/femtest/app/test_object.py | 11 +++++--- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 7349f24870..be1bff14be 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -59,7 +59,7 @@ template<> PyObject* FemGui::ViewProviderPostFilterPython::getPyObject() } // explicit template instantiation -template class GuiExport ViewProviderFeaturePythonT; +template class FemGuiExport ViewProviderFeaturePythonT; } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h index 46b6724a5d..948ad96f13 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h @@ -122,7 +122,6 @@ protected: const char* typeName, const char* propName) override; - virtual void setupTaskDialog(TaskDlgPost* dlg); bool setupPipeline(); void updateVtk(); void setRangeOfColorBar(float min, float max); diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 84b5d84818..92ca918d40 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -652,20 +652,19 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"): base_vtk_result.addObject(obj) return obj -if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: - def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): - """makePostVtkFilterGlyph(document, [name]): - creates a FEM post processing filter that visualizes vector fields with glyphs - """ - obj = doc.addObject("Fem::PostFilterPython", name) - from femobjects import post_glyphfilter +def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): + """makePostVtkFilterGlyph(document, [name]): + creates a FEM post processing filter that visualizes vector fields with glyphs + """ + obj = doc.addObject("Fem::PostFilterPython", name) + from femobjects import post_glyphfilter - post_glyphfilter.PostGlyphFilter(obj) - base_vtk_result.addObject(obj) - if FreeCAD.GuiUp: - from femviewprovider import view_post_glyphfilter - view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject) - return obj + post_glyphfilter.PostGlyphFilter(obj) + base_vtk_result.addObject(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_glyphfilter + view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject) + return obj def makePostVtkResult(doc, result_data, name="VtkResult"): """makePostVtkResult(document, base_result, [name]): diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index d90f4456ea..85e050c18e 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -50,6 +50,7 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostGlyph.ui" ) + self.widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostFilterGlyph.svg")) self.__init_widget() # form made from param and selection widget diff --git a/src/Mod/Fem/femtest/app/support_utils.py b/src/Mod/Fem/femtest/app/support_utils.py index 10ff3b3433..7046ce712c 100644 --- a/src/Mod/Fem/femtest/app/support_utils.py +++ b/src/Mod/Fem/femtest/app/support_utils.py @@ -87,7 +87,7 @@ def get_defmake_count(fem_vtk_post=True): # we are not able to create VTK post objects new_lines = [] for li in lines_defmake: - if "PostVtk" not in li: + if "Post" not in li: new_lines.append(li) lines_defmake = new_lines return len(lines_defmake) diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index acf9bf9652..72544504be 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -81,9 +81,13 @@ class TestObjectCreate(unittest.TestCase): # result children: mesh result --> 1 # post pipeline children: region, scalar, cut, wrap --> 5 # analysis itself is not in analysis group --> 1 - # thus: -20 + # vtk python post objects: glyph --> 1 - self.assertEqual(len(doc.Analysis.Group), count_defmake - 20) + subtraction = 20 + if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + subtraction += 1 + + self.assertEqual(len(doc.Analysis.Group), count_defmake - subtraction) self.assertEqual(len(doc.Objects), count_defmake) fcc_print( @@ -1154,7 +1158,8 @@ def create_all_fem_objects_doc(doc): ObjectsFem.makePostVtkFilterCutFunction(doc, vres) ObjectsFem.makePostVtkFilterWarp(doc, vres) ObjectsFem.makePostVtkFilterContours(doc, vres) - ObjectsFem.makePostVtkFilterGlyph(doc, vres) + if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + ObjectsFem.makePostFilterGlyph(doc, vres) analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc)) analysis.addObject(ObjectsFem.makeSolverCalculiX(doc)) From 0304faa2bc60a976934e9b918b3564ca2906b4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 30 Apr 2025 19:08:22 +0200 Subject: [PATCH 5/7] FEM: Handle python vtk user installs that conflict with FreeCAD VTK --- src/Mod/Fem/App/AppFemPy.cpp | 41 +++ src/Mod/Fem/CMakeLists.txt | 1 + src/Mod/Fem/InitGui.py | 4 + src/Mod/Fem/femcommands/manager.py | 5 + .../Fem/femguiutils/vtk_module_handling.py | 236 ++++++++++++++++++ src/Mod/Fem/femobjects/post_glyphfilter.py | 6 + 6 files changed, 293 insertions(+) create mode 100644 src/Mod/Fem/femguiutils/vtk_module_handling.py diff --git a/src/Mod/Fem/App/AppFemPy.cpp b/src/Mod/Fem/App/AppFemPy.cpp index c87474fd08..5ff9e1a87e 100644 --- a/src/Mod/Fem/App/AppFemPy.cpp +++ b/src/Mod/Fem/App/AppFemPy.cpp @@ -40,6 +40,11 @@ #ifdef FC_USE_VTK #include "FemPostPipeline.h" #include "FemVTKTools.h" +#include +#endif + +#ifdef FC_USE_VTK_PYTHON +#include #endif @@ -75,6 +80,14 @@ public: &Module::writeResult, "write a CFD or FEM result (auto detect) to a file (file format " "detected from file suffix)"); + add_varargs_method("getVtkVersion", + &Module::getVtkVersion, + "Returns the VTK version freeCAD is linked against"); +#ifdef FC_USE_VTK_PYTHON + add_varargs_method("isVtkCompatible", + &Module::isVtkCompatible, + "Checks if the passed vtkObject is compatible with the c++ VTK version freecad uses"); +#endif #endif add_varargs_method("show", &Module::show, @@ -318,6 +331,34 @@ private: return Py::None(); } + + Py::Object getVtkVersion(const Py::Tuple& args) + { + if (!PyArg_ParseTuple(args.ptr(),"")) { + throw Py::Exception(); + } + + return Py::String(fcVtkVersion); + } + +#ifdef FC_USE_VTK_PYTHON + Py::Object isVtkCompatible(const Py::Tuple& args) + { + PyObject* pcObj = nullptr; + if (!PyArg_ParseTuple(args.ptr(),"O", &pcObj)) { + throw Py::Exception(); + } + + // if non is returned the vtk object was created by annother vtk library, and the + // python api used to create it cannot be used with FreeCAD + vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(pcObj, "vtkObject"); + if (!obj) { + PyErr_Clear(); + return Py::False(); + } + return Py::True(); + } +#endif #endif Py::Object show(const Py::Tuple& args) diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 42258b391c..e90422449e 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -639,6 +639,7 @@ SET(FemGuiUtils_SRCS femguiutils/disambiguate_solid_selection.py femguiutils/migrate_gui.py femguiutils/selection_widgets.py + femguiutils/vtk_module_handling.py ) SET(FemGuiViewProvider_SRCS diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 621a278f17..9b48177bba 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -80,6 +80,10 @@ class FemWorkbench(Workbench): False if FemGui.__name__ else True False if femcommands.commands.__name__ else True + # check vtk version to potentially find missmatchs + from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() + def GetClassName(self): # see https://forum.freecad.org/viewtopic.php?f=10&t=43300 return "FemGui::Workbench" diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index f82301d0c7..18614b8a22 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -34,6 +34,7 @@ import FreeCAD from femtools.femutils import expandParentObject from femtools.femutils import is_of_type +from femguiutils.vtk_module_handling import vtk_compatibility_abort if FreeCAD.GuiUp: from PySide import QtCore @@ -381,6 +382,10 @@ class CommandManager: # like add_obj_on_gui_selobj_noset_edit but the selection is kept # and the selobj is expanded in the tree to see the added obj + # check if we should use python fitler + if vtk_compatibility_abort(True): + return + # Note: we know selobj is a FemPostObject as otherwise the command should not have been active # We also assume the all filters are in PostGroups and not astray group = None diff --git a/src/Mod/Fem/femguiutils/vtk_module_handling.py b/src/Mod/Fem/femguiutils/vtk_module_handling.py new file mode 100644 index 0000000000..b9feae6638 --- /dev/null +++ b/src/Mod/Fem/femguiutils/vtk_module_handling.py @@ -0,0 +1,236 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +"""Methods to verify if the python VTK module is the correct one + +FreeCAD is linked with VTK libraries during its build process. To use the VTK +python module and pass objects between python and c++ the compiled module library +needs to be linked to the exact same vtk library as FreeCAD is. This is ensured by +installing VTK via linux app managers: All known distros install the python side +packages together with vtk libs. Libpack and other OS systems ensure this too. + +However, if a vtk python package is installed manually, e.g. by "pip install vtk", +it could be found instead of the system module. This python API brings its own +set of VTK libs, and hence object passing in FreeCAD fails. (Note: import and +pure vtk python code still works, only passing to c++ fails) + +This file provides functions that detect this situation and inform the user. +Additionally we try to find the correct module in the path and offer to use +it instead. + +Note that this problem occurs with all "compiled binary" python APIs, also +with PySide. It is the users responsibility to handle his python path and keep +it clean/working. The functions provided here are a workaround only. +""" + +__title__ = "FEM GUI vtk python module check" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +__user_input_received = False + +def vtk_module_compatible(): + # checks if the VTK library FreeCAD is build against is the one used by + # the python module + + # make sure we do not contaminate the modules with vtk to not trick + # the check later + unload = not _vtk_is_loaded() + + import Fem + from vtkmodules.vtkCommonCore import vtkVersion, vtkBitArray + + # simple version check + if Fem.getVtkVersion() != vtkVersion.GetVTKVersion(): + return False + + # check binary compatibility + result = Fem.isVtkCompatible(vtkBitArray()) + + if unload: + # cleanup our own import + _unload_vtk_modules() + + return result + + +def _vtk_is_loaded(): + import sys + return any("vtkmodules" in module for module in sys.modules) + + +def _unload_vtk_modules(): + # unloads all loaded vtk modules + # NOTE: does not remove any stored references in objects + + import sys + for module in sys.modules.copy(): + if "vtkmodules" in module: + del sys.modules[module] + + +def _find_compatible_module(): + # Check all python path folders if they contain a vtk module + + import Fem + import sys + + # remove module from runtime + _unload_vtk_modules() + + path = sys.path.copy() + + for folder in reversed(path): + try: + # use a single folder as path and try to load vtk + sys.path = [folder] + if vtk_module_compatible(): + # we do still unload, to let the user descide if he wants to use it + _unload_vtk_modules() + sys.path = path + return folder + + except: + continue + + # reset the correct path and indicate that we failed + sys.path = path + return None + + +def _load_vtk_from(folder): + + import sys + + path = sys.path + try: + sys.path = [folder] + import vtkmodules + finally: + sys.path = path + + +# If FreeCAD is build with VTK python support this function checks if the +# used python module is compatible with the c++ lib. Does inform the user +# if not so and offers the correct module, if available +# +# Note: Call this also from Python feature module, as on document load +# this can be loaded before initializing FEM workbench. +def vtk_module_handling(): + + import sys + import FreeCAD + + if not "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + # no VTK python api support in FreeCAD + return + + # only ask user once per session + global __user_input_received + if __user_input_received: + return + __user_input_received = True + + loaded = _vtk_is_loaded() + + # check if we are compatible + if not vtk_module_compatible(): + + if not FreeCAD.GuiUp: + FreeCAD.Console.PrintError("FEM: vtk python module is not compatible with internal vtk library") + return + + import FreeCAD, Fem + from vtkmodules.vtkCommonCore import vtkVersion + import inspect + from PySide import QtGui + + translate = FreeCAD.Qt.translate + + path = inspect.getfile(vtkVersion) + path = path[:path.find("vtkmodules")] + + message = translate("FEM", ("FreeCAD is linked to a different VTK library then the imported " + "VTK python module. This is incompatible and will lead to errors." + "\n\nWrong python module is imported from: \n{}")).format(path) + + buttons = QtGui.QMessageBox.Discard + + # check if there is any compatible vtk module + compatible_module = _find_compatible_module() + + if compatible_module: + # there is a compatible module of VTK available. + message += translate("FEM", "\n\nCorrect module found in: \n{}").format(compatible_module) + + if not loaded: + # vtk was not loaded beforehand, therefore we can realistically reload + message += translate("FEM", "\n\nShould this module be loaded instead?") + + buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No + + else: + message += translate("FEM", ("\n\nAs the wrong module was already loaded, a reload is not possible. " + "Restart FreeCAD to get the option for loading this module.")) + + else: + message += translate("FEM", "\n\nNo matching module was found in the current python path.") + + + # raise a dialog to the user + import FreeCADGui + button = QtGui.QMessageBox.critical( + FreeCADGui.getMainWindow(), + translate("FEM", "VTK module conflict"), + message, + buttons=buttons, + ) + + if button == QtGui.QMessageBox.Yes: + # try to reload the correct vtk module + _load_vtk_from(compatible_module) + + +# Returns if vtk python is incompatible and hence operations need to be aborted. +# If inform=True the user gets informed by dialog about incompatibilities +def vtk_compatibility_abort(inform=True): + + if not vtk_module_compatible(): + + if inform: + # raise a dialog to the user that this functionality is not available + import FreeCAD + import FreeCADGui + from PySide import QtGui + translate = FreeCAD.Qt.translate + + button = QtGui.QMessageBox.critical( + FreeCADGui.getMainWindow(), + translate("FEM", "VTK module conflict"), + translate("FEM", "This functionality is not available due to VTK python module conflict"), + buttons=QtGui.QMessageBox.Discard, + ) + + return True + + return False diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py index b111e61e8d..e86eb51872 100644 --- a/src/Mod/Fem/femobjects/post_glyphfilter.py +++ b/src/Mod/Fem/femobjects/post_glyphfilter.py @@ -29,6 +29,12 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing filter creating glyphs for vector fields +import FreeCAD + +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() + # IMPORTANT: Never import vtk directly. Often vtk is compiled with different QT # version than FreeCAD, and "import vtk" crashes by importing qt components. # Always import the filter and data modules only. From ce25de290e1eac24f23841ea3593d30059d0c8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 2 May 2025 09:25:48 +0200 Subject: [PATCH 6/7] FEM: Fix typos in python post processing code --- cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake | 2 +- src/Mod/Fem/App/AppFemPy.cpp | 6 +++--- src/Mod/Fem/App/FemPostFilterPy.xml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake index 978416d088..fa7a20daf3 100644 --- a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake +++ b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake @@ -65,7 +65,7 @@ macro(SetupSalomeSMESH) set(BUILD_FEM_VTK ON) # Check if PythonWrapperCore was found - # Note: vtk 9 only, as the implementations use the vtk modules introduced in 9.0 + # Note: VTK 9 only, as the implementations use the VTK modules introduced in 8.1 # VTK_WrappingPythonCore_FOUND is named differently for versions <9.0 if (${VTK_WrappingPythonCore_FOUND}) set(BUILD_FEM_VTK_PYTHON 1) diff --git a/src/Mod/Fem/App/AppFemPy.cpp b/src/Mod/Fem/App/AppFemPy.cpp index 5ff9e1a87e..fa93907637 100644 --- a/src/Mod/Fem/App/AppFemPy.cpp +++ b/src/Mod/Fem/App/AppFemPy.cpp @@ -82,11 +82,11 @@ public: "detected from file suffix)"); add_varargs_method("getVtkVersion", &Module::getVtkVersion, - "Returns the VTK version freeCAD is linked against"); + "Returns the VTK version FreeCAD is linked against"); #ifdef FC_USE_VTK_PYTHON add_varargs_method("isVtkCompatible", &Module::isVtkCompatible, - "Checks if the passed vtkObject is compatible with the c++ VTK version freecad uses"); + "Checks if the passed vtkObject is compatible with the c++ VTK version FreeCAD uses"); #endif #endif add_varargs_method("show", @@ -349,7 +349,7 @@ private: throw Py::Exception(); } - // if non is returned the vtk object was created by annother vtk library, and the + // if non is returned the VTK object was created by annother VTK library, and the // python api used to create it cannot be used with FreeCAD vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(pcObj, "vtkObject"); if (!obj) { diff --git a/src/Mod/Fem/App/FemPostFilterPy.xml b/src/Mod/Fem/App/FemPostFilterPy.xml index 44ee6cc84b..28d1823f69 100644 --- a/src/Mod/Fem/App/FemPostFilterPy.xml +++ b/src/Mod/Fem/App/FemPostFilterPy.xml @@ -31,7 +31,7 @@ -Returns the dataset available at the filters input. +Returns the dataset available at the filter's input. Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. @@ -39,7 +39,7 @@ Note: Can lead to a full recompute of the whole pipeline, hence best to call thi -Returns the names of all vector fields available on this filters input. +Returns the names of all vector fields available on this filter's input. Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. @@ -47,7 +47,7 @@ Note: Can lead to a full recompute of the whole pipeline, hence best to call thi -Returns the names of all scalar fields available on this filters input. +Returns the names of all scalar fields available on this filter's input. Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. From 3ef7ba2d9d2a921e55c987b92e4084a90e75b888 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 07:28:59 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/Fem/App/AppFemPy.cpp | 13 +-- src/Mod/Fem/App/FemPostFilter.cpp | 15 ++-- src/Mod/Fem/App/FemPostFilter.h | 3 +- src/Mod/Fem/App/FemPostFilterPyImp.cpp | 23 ++--- src/Mod/Fem/App/PropertyPostDataObject.cpp | 6 +- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 87 +++++++++---------- src/Mod/Fem/Gui/TaskPostBoxes.h | 3 +- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 20 +++-- src/Mod/Fem/Gui/ViewProviderFemPostFilter.h | 3 +- src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 2 +- src/Mod/Fem/InitGui.py | 1 + src/Mod/Fem/ObjectsFem.py | 4 + src/Mod/Fem/femcommands/commands.py | 6 +- src/Mod/Fem/femcommands/manager.py | 16 ++-- .../Fem/femguiutils/vtk_module_handling.py | 44 +++++++--- src/Mod/Fem/femobjects/post_glyphfilter.py | 28 +++--- .../femtaskpanels/task_post_glyphfilter.py | 30 +++---- .../femviewprovider/view_post_glyphfilter.py | 2 +- 18 files changed, 166 insertions(+), 140 deletions(-) diff --git a/src/Mod/Fem/App/AppFemPy.cpp b/src/Mod/Fem/App/AppFemPy.cpp index fa93907637..69ab7fc502 100644 --- a/src/Mod/Fem/App/AppFemPy.cpp +++ b/src/Mod/Fem/App/AppFemPy.cpp @@ -84,9 +84,10 @@ public: &Module::getVtkVersion, "Returns the VTK version FreeCAD is linked against"); #ifdef FC_USE_VTK_PYTHON - add_varargs_method("isVtkCompatible", - &Module::isVtkCompatible, - "Checks if the passed vtkObject is compatible with the c++ VTK version FreeCAD uses"); + add_varargs_method( + "isVtkCompatible", + &Module::isVtkCompatible, + "Checks if the passed vtkObject is compatible with the c++ VTK version FreeCAD uses"); #endif #endif add_varargs_method("show", @@ -334,7 +335,7 @@ private: Py::Object getVtkVersion(const Py::Tuple& args) { - if (!PyArg_ParseTuple(args.ptr(),"")) { + if (!PyArg_ParseTuple(args.ptr(), "")) { throw Py::Exception(); } @@ -345,13 +346,13 @@ private: Py::Object isVtkCompatible(const Py::Tuple& args) { PyObject* pcObj = nullptr; - if (!PyArg_ParseTuple(args.ptr(),"O", &pcObj)) { + if (!PyArg_ParseTuple(args.ptr(), "O", &pcObj)) { throw Py::Exception(); } // if non is returned the VTK object was created by annother VTK library, and the // python api used to create it cannot be used with FreeCAD - vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(pcObj, "vtkObject"); + vtkObjectBase* obj = vtkPythonUtil::GetPointerFromObject(pcObj, "vtkObject"); if (!obj) { PyErr_Clear(); return Py::False(); diff --git a/src/Mod/Fem/App/FemPostFilter.cpp b/src/Mod/Fem/App/FemPostFilter.cpp index 3501860a3e..24531c1790 100644 --- a/src/Mod/Fem/App/FemPostFilter.cpp +++ b/src/Mod/Fem/App/FemPostFilter.cpp @@ -226,14 +226,14 @@ vtkSmartPointer FemPostFilter::getInputData() } vtkAlgorithmOutput* output = active.source->GetInputConnection(0, 0); - if(!output) { + if (!output) { return nullptr; } vtkAlgorithm* algo = output->GetProducer(); - if(!algo) { + if (!algo) { return nullptr; } - if (Frame.getValue()>0) { + if (Frame.getValue() > 0) { algo->UpdateTimeStep(Frame.getValue()); } else { @@ -302,11 +302,13 @@ namespace App { /// @cond DOXERR PROPERTY_SOURCE_TEMPLATE(Fem::PostFilterPython, Fem::FemPostFilter) -template<> const char* Fem::PostFilterPython::getViewProviderName(void) const +template<> +const char* Fem::PostFilterPython::getViewProviderName(void) const { return "FemGui::ViewProviderPostFilterPython"; } -template<> PyObject* Fem::PostFilterPython::getPyObject() +template<> +PyObject* Fem::PostFilterPython::getPyObject() { if (PythonObject.is(Py::_None())) { // ref counter is set to 1 @@ -319,8 +321,7 @@ template<> PyObject* Fem::PostFilterPython::getPyObject() // explicit template instantiation template class FemExport FeaturePythonT; -}// namespace App - +} // namespace App // *************************************************************************** diff --git a/src/Mod/Fem/App/FemPostFilter.h b/src/Mod/Fem/App/FemPostFilter.h index d3becd9783..273bd63c0f 100644 --- a/src/Mod/Fem/App/FemPostFilter.h +++ b/src/Mod/Fem/App/FemPostFilter.h @@ -72,7 +72,7 @@ protected: std::vector> algorithmStorage; }; - //pipeline handling + // pipeline handling void addFilterPipeline(const FilterPipeline& p, std::string name); FilterPipeline& getFilterPipeline(std::string name); void setActiveFilterPipeline(std::string name); @@ -81,6 +81,7 @@ protected: void setTransformLocation(TransformLocation loc); friend class FemPostFilterPy; + public: /// Constructor FemPostFilter(); diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index 479e6d55d2..dddf9048e1 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -36,9 +36,9 @@ // clang-format on #ifdef FC_USE_VTK_PYTHON - #include - #include -#endif //BUILD_FEM_VTK +#include +#include +#endif // BUILD_FEM_VTK using namespace Fem; @@ -56,20 +56,20 @@ PyObject* FemPostFilterPy::addFilterPipeline(PyObject* args) { #ifdef FC_USE_VTK_PYTHON const char* name; - PyObject *source = nullptr; - PyObject *target = nullptr; + PyObject* source = nullptr; + PyObject* target = nullptr; if (PyArg_ParseTuple(args, "sOO", &name, &source, &target)) { // extract the algorithms - vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(source, "vtkAlgorithm"); + vtkObjectBase* obj = vtkPythonUtil::GetPointerFromObject(source, "vtkAlgorithm"); if (!obj) { // error marker is set by PythonUtil return nullptr; } auto source_algo = static_cast(obj); - obj = vtkPythonUtil::GetPointerFromObject(target,"vtkAlgorithm"); + obj = vtkPythonUtil::GetPointerFromObject(target, "vtkAlgorithm"); if (!obj) { // error marker is set by PythonUtil return nullptr; @@ -130,7 +130,8 @@ PyObject* FemPostFilterPy::getInputData(PyObject* args) copy = vtkUnstructuredGrid::New(); break; default: - PyErr_SetString(PyExc_TypeError, "cannot return datatype object; not unstructured grid"); + PyErr_SetString(PyExc_TypeError, + "cannot return datatype object; not unstructured grid"); Py_Return; } @@ -138,7 +139,7 @@ PyObject* FemPostFilterPy::getInputData(PyObject* args) copy->DeepCopy(dataset); PyObject* py_dataset = vtkPythonUtil::GetObjectFromPointer(copy); - return Py::new_reference_to(py_dataset); + return Py::new_reference_to(py_dataset); #else PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); Py_Return; @@ -160,7 +161,7 @@ PyObject* FemPostFilterPy::getInputVectorFields(PyObject* args) list.append(Py::String(field)); } - return Py::new_reference_to(list); + return Py::new_reference_to(list); } @@ -179,7 +180,7 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) list.append(Py::String(field)); } - return Py::new_reference_to(list); + return Py::new_reference_to(list); } PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index 86821e1ac7..786d58f783 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -169,12 +169,12 @@ int PropertyPostDataObject::getDataType() PyObject* PropertyPostDataObject::getPyObject() { #ifdef FC_USE_VTK_PYTHON - //create a copy first + // create a copy first auto copy = static_cast(Copy()); // get the data python wrapper PyObject* py_dataset = vtkPythonUtil::GetObjectFromPointer(copy->getValue()); - auto result = Py::new_reference_to(py_dataset); + auto result = Py::new_reference_to(py_dataset); delete copy; return result; @@ -187,7 +187,7 @@ PyObject* PropertyPostDataObject::getPyObject() void PropertyPostDataObject::setPyObject(PyObject* value) { #ifdef FC_USE_VTK_PYTHON - vtkObjectBase *obj = vtkPythonUtil::GetPointerFromObject(value, "vtkDataObject"); + vtkObjectBase* obj = vtkPythonUtil::GetPointerFromObject(value, "vtkDataObject"); if (!obj) { throw Base::TypeError("Can only set vtkDataObject"); } diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index ea51bdf8bf..db0ed493be 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -204,9 +204,9 @@ void DataAlongLineMarker::customEvent(QEvent*) // *************************************************************************** // main task dialog TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, - const QPixmap& icon, - const QString& title, - QWidget* parent) + const QPixmap& icon, + const QString& title, + QWidget* parent) : QWidget(parent) , m_object(view->getObject()) , m_view(view) @@ -272,13 +272,14 @@ QDialogButtonBox::StandardButtons TaskDlgPost::getStandardButtons() const { bool guionly = true; for (auto& widget : Content) { - if(auto task_box = dynamic_cast(widget)) { + if (auto task_box = dynamic_cast(widget)) { // get the task widget and check if it is a post widget auto widget = task_box->groupLayout()->itemAt(0)->widget(); - if(auto post_widget = dynamic_cast(widget)) { + if (auto post_widget = dynamic_cast(widget)) { guionly = guionly && post_widget->isGuiTaskOnly(); - } else { + } + else { // unknown panel, we can only assume guionly = false; } @@ -334,10 +335,10 @@ void TaskDlgPost::clicked(int button) { if (button == QDialogButtonBox::Apply) { for (auto& widget : Content) { - if(auto task_box = dynamic_cast(widget)) { + if (auto task_box = dynamic_cast(widget)) { // get the task widget and check if it is a post widget auto widget = task_box->groupLayout()->itemAt(0)->widget(); - if(auto post_widget = dynamic_cast(widget)) { + if (auto post_widget = dynamic_cast(widget)) { post_widget->apply(); } } @@ -350,10 +351,10 @@ bool TaskDlgPost::accept() { try { for (auto& widget : Content) { - if(auto task_box = dynamic_cast(widget)) { + if (auto task_box = dynamic_cast(widget)) { // get the task widget and check if it is a post widget auto widget = task_box->groupLayout()->itemAt(0)->widget(); - if(auto post_widget = dynamic_cast(widget)) { + if (auto post_widget = dynamic_cast(widget)) { post_widget->applyPythonCode(); } } @@ -396,14 +397,13 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) // *************************************************************************** // box to set the coloring TaskPostDisplay::TaskPostDisplay(ViewProviderFemPostObject* view, QWidget* parent) - : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_ResultShow"), QString(), - parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_ResultShow"), QString(), parent) , ui(new Ui_TaskPostDisplay) { // setup the ui ui->setupUi(this); - setWindowTitle(tr("Result display options")); // set title here as setupUi overrides the constructor title + setWindowTitle( + tr("Result display options")); // set title here as setupUi overrides the constructor title setupConnections(); // update all fields @@ -480,16 +480,16 @@ void TaskPostDisplay::applyPythonCode() // functions TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("fem-post-geo-plane"), - tr("Implicit function"), - parent) + Gui::BitmapFactory().pixmap("fem-post-geo-plane"), + tr("Implicit function"), + parent) { // we load the views widget FunctionWidget* w = getTypedView()->createControlWidget(); w->setParent(this); w->setViewProvider(getTypedView()); - QVBoxLayout *layout = new QVBoxLayout; + QVBoxLayout* layout = new QVBoxLayout; layout->addWidget(w); setLayout(layout); } @@ -566,10 +566,7 @@ void TaskPostFrames::applyPythonCode() // *************************************************************************** // Branch TaskPostBranch::TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent) - : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostBranchFilter"), - QString(), - parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostBranchFilter"), QString(), parent) , ui(new Ui_TaskPostBranch) { // setup the ui @@ -621,9 +618,9 @@ void TaskPostBranch::applyPythonCode() TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderFemPostDataAlongLine* view, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterDataAlongLine"), - QString(), - parent) + Gui::BitmapFactory().pixmap("FEM_PostFilterDataAlongLine"), + QString(), + parent) , ui(new Ui_TaskPostDataAlongLine) , marker(nullptr) { @@ -1041,9 +1038,9 @@ plt.show()\n"; // data at point filter TaskPostDataAtPoint::TaskPostDataAtPoint(ViewProviderFemPostDataAtPoint* view, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterDataAtPoint"), - QString(), - parent) + Gui::BitmapFactory().pixmap("FEM_PostFilterDataAtPoint"), + QString(), + parent) , viewer(nullptr) , connSelectPoint(QMetaObject::Connection()) , ui(new Ui_TaskPostDataAtPoint) @@ -1396,9 +1393,9 @@ TaskPostClip::TaskPostClip(ViewProviderFemPostClip* view, App::PropertyLink* function, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterClipRegion"), - QString(), - parent) + Gui::BitmapFactory().pixmap("FEM_PostFilterClipRegion"), + QString(), + parent) , ui(new Ui_TaskPostClip) { assert(function); @@ -1554,10 +1551,7 @@ void TaskPostClip::onInsideOutToggled(bool val) // *************************************************************************** // contours filter TaskPostContours::TaskPostContours(ViewProviderFemPostContours* view, QWidget* parent) - : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterContours"), - QString(), - parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterContours"), QString(), parent) , ui(new Ui_TaskPostContours) { // setup the ui @@ -1709,9 +1703,9 @@ void TaskPostContours::onRelaxationChanged(double value) // cut filter TaskPostCut::TaskPostCut(ViewProviderFemPostCut* view, App::PropertyLink* function, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterCutFunction"), - QString(), - parent) + Gui::BitmapFactory().pixmap("FEM_PostFilterCutFunction"), + QString(), + parent) , ui(new Ui_TaskPostCut) { assert(function); @@ -1847,9 +1841,9 @@ void TaskPostCut::onFunctionBoxCurrentIndexChanged(int idx) // scalar clip filter TaskPostScalarClip::TaskPostScalarClip(ViewProviderFemPostScalarClip* view, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterClipScalar"), - QString(), - parent) + Gui::BitmapFactory().pixmap("FEM_PostFilterClipScalar"), + QString(), + parent) , ui(new Ui_TaskPostScalarClip) { // setup the ui @@ -1970,10 +1964,7 @@ void TaskPostScalarClip::onInsideOutToggled(bool val) // *************************************************************************** // warp vector filter TaskPostWarpVector::TaskPostWarpVector(ViewProviderFemPostWarpVector* view, QWidget* parent) - : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"), - QString(), - parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"), QString(), parent) , ui(new Ui_TaskPostWarpVector) { // setup the ui @@ -2145,9 +2136,9 @@ static const std::vector calculatorOperators = { TaskPostCalculator::TaskPostCalculator(ViewProviderFemPostCalculator* view, QWidget* parent) : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostFilterCalculator"), - tr("Calculator options"), - parent) + Gui::BitmapFactory().pixmap("FEM_PostFilterCalculator"), + tr("Calculator options"), + parent) , ui(new Ui_TaskPostCalculator) { // we load the views widget diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index 85ea7f21eb..d37742dd27 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -317,8 +317,7 @@ class TaskPostBranch: public TaskPostWidget Q_OBJECT public: - explicit TaskPostBranch(ViewProviderFemPostBranchFilter* view, - QWidget* parent = nullptr); + explicit TaskPostBranch(ViewProviderFemPostBranchFilter* view, QWidget* parent = nullptr); ~TaskPostBranch() override; void applyPythonCode() override; diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index be1bff14be..4cbacb5cad 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -37,7 +37,8 @@ using namespace FemGui; PROPERTY_SOURCE(FemGui::ViewProviderFemPostFilterPythonBase, FemGui::ViewProviderFemPostObject) -ViewProviderFemPostFilterPythonBase::ViewProviderFemPostFilterPythonBase() {} +ViewProviderFemPostFilterPythonBase::ViewProviderFemPostFilterPythonBase() +{} ViewProviderFemPostFilterPythonBase::~ViewProviderFemPostFilterPythonBase() = default; @@ -46,10 +47,13 @@ std::vector ViewProviderFemPostFilterPythonBase::getDisplayModes() return std::vector(); } -namespace Gui { -PROPERTY_SOURCE_TEMPLATE(FemGui::ViewProviderPostFilterPython, FemGui::ViewProviderFemPostFilterPythonBase) +namespace Gui +{ +PROPERTY_SOURCE_TEMPLATE(FemGui::ViewProviderPostFilterPython, + FemGui::ViewProviderFemPostFilterPythonBase) -template<> PyObject* FemGui::ViewProviderPostFilterPython::getPyObject() +template<> +PyObject* FemGui::ViewProviderPostFilterPython::getPyObject() { if (!pyViewObject) { pyViewObject = new ViewProviderFemPostFilterPy(this); @@ -61,7 +65,7 @@ template<> PyObject* FemGui::ViewProviderPostFilterPython::getPyObject() // explicit template instantiation template class FemGuiExport ViewProviderFeaturePythonT; -} +} // namespace Gui // *************************************************************************** // in the following, the different filters sorted alphabetically @@ -154,7 +158,8 @@ void ViewProviderFemPostClip::setupTaskDialog(TaskDlgPost* dlg) // add the function box assert(dlg->getView() == this); - auto panel = new TaskPostClip(this, &dlg->getView()->getObject()->Function); + auto panel = + new TaskPostClip(this, &dlg->getView()->getObject()->Function); dlg->addTaskBox(panel->getIcon(), panel); // add the display options @@ -197,7 +202,8 @@ void ViewProviderFemPostCut::setupTaskDialog(TaskDlgPost* dlg) { // add the function box assert(dlg->getView() == this); - auto panel = new TaskPostCut(this, &dlg->getView()->getObject()->Function); + auto panel = + new TaskPostCut(this, &dlg->getView()->getObject()->Function); dlg->addTaskBox(panel->getIcon(), panel); // add the display options diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h index 771ce5ef16..82b7f16bf8 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h @@ -50,7 +50,8 @@ public: // Viewprovider for the python filters -using ViewProviderPostFilterPython = Gui::ViewProviderFeaturePythonT; +using ViewProviderPostFilterPython = + Gui::ViewProviderFeaturePythonT; // *************************************************************************** diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 7113155471..4cbb97b414 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -932,7 +932,7 @@ void ViewProviderFemPostObject::onChanged(const App::Property* prop) } if (prop == &Field && setupPipeline()) { - if(!isRestoring()) { + if (!isRestoring()) { updateProperties(); } WriteColorData(ResetColorBarRange); diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 9b48177bba..8ac271d379 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -82,6 +82,7 @@ class FemWorkbench(Workbench): # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() def GetClassName(self): diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 92ca918d40..c05ecc6108 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -652,6 +652,7 @@ def makePostVtkFilterContours(doc, base_vtk_result, name="VtkFilterContours"): base_vtk_result.addObject(obj) return obj + def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): """makePostVtkFilterGlyph(document, [name]): creates a FEM post processing filter that visualizes vector fields with glyphs @@ -663,9 +664,11 @@ def makePostFilterGlyph(doc, base_vtk_result, name="Glyph"): base_vtk_result.addObject(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_glyphfilter + view_post_glyphfilter.VPPostGlyphFilter(obj.ViewObject) return obj + def makePostVtkResult(doc, result_data, name="VtkResult"): """makePostVtkResult(document, base_result, [name]): creates a FEM post processing result data (vtk based) to hold FEM results @@ -682,6 +685,7 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): obj.ViewObject.DisplayMode = "Surface" return obj + # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): """makeEquationDeformation(document, [base_solver], [name]): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 5e260f7765..0c61dcff2b 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1216,6 +1216,7 @@ class _SolverZ88(CommandManager): self.is_active = "with_analysis" self.do_activated = "add_obj_on_gui_expand_noset_edit" + class _PostFilterGlyph(CommandManager): "The FEM_PostFilterGlyph command definition" @@ -1223,7 +1224,10 @@ class _PostFilterGlyph(CommandManager): super().__init__() self.menutext = Qt.QT_TRANSLATE_NOOP("FEM_PostFilterGlyph", "Glyph filter") self.accel = "F, G" - self.tooltip = Qt.QT_TRANSLATE_NOOP("FEM_PostFilterGlyph", "Post processing filter that adds glyphs to the mesh vertices for vertex data visualization") + self.tooltip = Qt.QT_TRANSLATE_NOOP( + "FEM_PostFilterGlyph", + "Post processing filter that adds glyphs to the mesh vertices for vertex data visualization", + ) self.is_active = "with_vtk_selresult" self.do_activated = "add_filter_set_edit" diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index 18614b8a22..44c5d2dc0a 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -91,9 +91,7 @@ class CommandManager: and self.result_selected() ) elif self.is_active == "with_vtk_selresult": - active = ( - self.vtk_result_selected() - ) + active = self.vtk_result_selected() elif self.is_active == "with_part_feature": active = FreeCADGui.ActiveDocument is not None and self.part_feature_selected() elif self.is_active == "with_femmesh": @@ -401,8 +399,12 @@ class CommandManager: "FreeCAD.ActiveDocument, FreeCAD.ActiveDocument.{})".format(filtertype, group.Name) ) # set display and selection style to assure the user sees the new object - FreeCADGui.doCommand("FreeCAD.ActiveDocument.ActiveObject.ViewObject.DisplayMode = \"Surface\""); - FreeCADGui.doCommand("FreeCAD.ActiveDocument.ActiveObject.ViewObject.SelectionStyle = \"BoundBox\""); + FreeCADGui.doCommand( + 'FreeCAD.ActiveDocument.ActiveObject.ViewObject.DisplayMode = "Surface"' + ) + FreeCADGui.doCommand( + 'FreeCAD.ActiveDocument.ActiveObject.ViewObject.SelectionStyle = "BoundBox"' + ) # hide selected filter FreeCADGui.doCommand( @@ -411,9 +413,7 @@ class CommandManager: # recompute, expand selobj in tree view expandParentObject() - FreeCADGui.doCommand( - "FreeCAD.ActiveDocument.ActiveObject.recompute()" - ) + FreeCADGui.doCommand("FreeCAD.ActiveDocument.ActiveObject.recompute()") # set edit FreeCADGui.Selection.clearSelection() diff --git a/src/Mod/Fem/femguiutils/vtk_module_handling.py b/src/Mod/Fem/femguiutils/vtk_module_handling.py index b9feae6638..dd5934af7e 100644 --- a/src/Mod/Fem/femguiutils/vtk_module_handling.py +++ b/src/Mod/Fem/femguiutils/vtk_module_handling.py @@ -49,6 +49,7 @@ __url__ = "https://www.freecad.org" __user_input_received = False + def vtk_module_compatible(): # checks if the VTK library FreeCAD is build against is the one used by # the python module @@ -76,6 +77,7 @@ def vtk_module_compatible(): def _vtk_is_loaded(): import sys + return any("vtkmodules" in module for module in sys.modules) @@ -84,6 +86,7 @@ def _unload_vtk_modules(): # NOTE: does not remove any stored references in objects import sys + for module in sys.modules.copy(): if "vtkmodules" in module: del sys.modules[module] @@ -157,7 +160,9 @@ def vtk_module_handling(): if not vtk_module_compatible(): if not FreeCAD.GuiUp: - FreeCAD.Console.PrintError("FEM: vtk python module is not compatible with internal vtk library") + FreeCAD.Console.PrintError( + "FEM: vtk python module is not compatible with internal vtk library" + ) return import FreeCAD, Fem @@ -168,11 +173,16 @@ def vtk_module_handling(): translate = FreeCAD.Qt.translate path = inspect.getfile(vtkVersion) - path = path[:path.find("vtkmodules")] + path = path[: path.find("vtkmodules")] - message = translate("FEM", ("FreeCAD is linked to a different VTK library then the imported " - "VTK python module. This is incompatible and will lead to errors." - "\n\nWrong python module is imported from: \n{}")).format(path) + message = translate( + "FEM", + ( + "FreeCAD is linked to a different VTK library then the imported " + "VTK python module. This is incompatible and will lead to errors." + "\n\nWrong python module is imported from: \n{}" + ), + ).format(path) buttons = QtGui.QMessageBox.Discard @@ -181,7 +191,9 @@ def vtk_module_handling(): if compatible_module: # there is a compatible module of VTK available. - message += translate("FEM", "\n\nCorrect module found in: \n{}").format(compatible_module) + message += translate("FEM", "\n\nCorrect module found in: \n{}").format( + compatible_module + ) if not loaded: # vtk was not loaded beforehand, therefore we can realistically reload @@ -190,15 +202,22 @@ def vtk_module_handling(): buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No else: - message += translate("FEM", ("\n\nAs the wrong module was already loaded, a reload is not possible. " - "Restart FreeCAD to get the option for loading this module.")) + message += translate( + "FEM", + ( + "\n\nAs the wrong module was already loaded, a reload is not possible. " + "Restart FreeCAD to get the option for loading this module." + ), + ) else: - message += translate("FEM", "\n\nNo matching module was found in the current python path.") - + message += translate( + "FEM", "\n\nNo matching module was found in the current python path." + ) # raise a dialog to the user import FreeCADGui + button = QtGui.QMessageBox.critical( FreeCADGui.getMainWindow(), translate("FEM", "VTK module conflict"), @@ -222,12 +241,15 @@ def vtk_compatibility_abort(inform=True): import FreeCAD import FreeCADGui from PySide import QtGui + translate = FreeCAD.Qt.translate button = QtGui.QMessageBox.critical( FreeCADGui.getMainWindow(), translate("FEM", "VTK module conflict"), - translate("FEM", "This functionality is not available due to VTK python module conflict"), + translate( + "FEM", "This functionality is not available due to VTK python module conflict" + ), buttons=QtGui.QMessageBox.Discard, ) diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py index e86eb51872..936c47e233 100644 --- a/src/Mod/Fem/femobjects/post_glyphfilter.py +++ b/src/Mod/Fem/femobjects/post_glyphfilter.py @@ -33,6 +33,7 @@ import FreeCAD # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() # IMPORTANT: Never import vtk directly. Often vtk is compiled with different QT @@ -43,8 +44,10 @@ from vtkmodules.vtkFiltersCore import vtkGlyph3D import vtkmodules.vtkFiltersSources as vtkSources from . import base_fempythonobject + _PropHelper = base_fempythonobject._PropHelper + class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): """ A post processing filter adding glyphs @@ -96,7 +99,7 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): name="ScaleFactor", group="Scale", doc="A constant multiplier the glyphs are scaled with", - value= (1, 0, 1e12, 1e-12), + value=(1, 0, 1e12, 1e-12), ), _PropHelper( type="App::PropertyEnumeration", @@ -109,15 +112,15 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): type="App::PropertyIntegerConstraint", name="Stride", group="Masking", - doc="Define the stride for \"Every Nth\" masking mode", - value= (2, 1, 999999999, 1), + doc='Define the stride for "Every Nth" masking mode', + value=(2, 1, 999999999, 1), ), _PropHelper( type="App::PropertyIntegerConstraint", name="MaxNumber", group="Masking", - doc="Defines the maximal number of vertices used for \"Uniform Sampling\" masking mode", - value= (1000, 1, 999999999, 1), + doc='Defines the maximal number of vertices used for "Uniform Sampling" masking mode', + value=(1000, 1, 999999999, 1), ), ] return prop @@ -155,14 +158,14 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): else: glyph.SetScaleModeToScaleByVectorComponents() - glyph.SetInputArrayToProcess(2,0,0,0,obj.ScaleData) + glyph.SetInputArrayToProcess(2, 0, 0, 0, obj.ScaleData) else: # scalar scaling mode if obj.VectorScaleMode != "Not a vector": obj.VectorScaleMode = ["Not a vector"] - glyph.SetInputArrayToProcess(2,0,0,0,obj.ScaleData) + glyph.SetInputArrayToProcess(2, 0, 0, 0, obj.ScaleData) glyph.SetScaleModeToScaleByScalar() else: glyph.ScalingOff() @@ -172,11 +175,10 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): # Orientation if obj.OrientationData != "None": glyph.OrientOn() - glyph.SetInputArrayToProcess(1,0,0,0,obj.OrientationData) + glyph.SetInputArrayToProcess(1, 0, 0, 0, obj.OrientationData) else: glyph.OrientOff() - def __setupFilterPipeline(self, obj): # store of all algorithms for later access @@ -184,8 +186,7 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): self._algorithms = {} # create all vtkalgorithm combinations and set them as filter pipeline - sources = {"Arrow": vtkSources.vtkArrowSource, - "Cube": vtkSources.vtkCubeSource} + sources = {"Arrow": vtkSources.vtkArrowSource, "Cube": vtkSources.vtkCubeSource} for source_name in sources: @@ -204,7 +205,6 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): obj.setActiveFilterPipeline(obj.Glyph) - def onDocumentRestored(self, obj): # resetup the pipeline self.__setupFilterPipeline(obj) @@ -213,7 +213,7 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): # we check what new inputs vector_fields = obj.getInputVectorFields() - all_fields = (vector_fields + obj.getInputScalarFields()) + all_fields = vector_fields + obj.getInputScalarFields() vector_fields.sort() all_fields.sort() @@ -233,7 +233,6 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): # make sure parent class execute is called! return False - def onChanged(self, obj, prop): # check if we are setup already @@ -270,4 +269,3 @@ class PostGlyphFilter(base_fempythonobject.BaseFemPythonObject): for filter in self._algorithms: glyph = self._algorithms[filter][2] glyph.SetScaleFactor(obj.ScaleFactor) - diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index 85e050c18e..a0658812e6 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -63,7 +63,9 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): # ########################## def getStandardButtons(self): - return QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + return ( + QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + ) def clicked(self, button): # apply button hit? @@ -71,16 +73,15 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.obj.Document.recompute() def accept(self): - #self.obj.CharacteristicLength = self.elelen - #self.obj.References = self.selection_widget.references - #self.selection_widget.finish_selection() + # self.obj.CharacteristicLength = self.elelen + # self.obj.References = self.selection_widget.references + # self.selection_widget.finish_selection() return super().accept() def reject(self): - #self.selection_widget.finish_selection() + # self.selection_widget.finish_selection() return super().reject() - # Helper functions # ################## @@ -99,7 +100,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): cbox.setCurrentText(getattr(obj, prop)) cbox.blockSignals(False) - # Setup functions # ############### @@ -113,8 +113,8 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self._enumPropertyToCombobox(self.obj, "MaskMode", self.widget.MaskModeComboBox) self.widget.ScaleFactorBox.setValue(self.obj.ScaleFactor) - self.__slide_min = self.obj.ScaleFactor*0.5 - self.__slide_max = self.obj.ScaleFactor*1.5 + self.__slide_min = self.obj.ScaleFactor * 0.5 + self.__slide_max = self.obj.ScaleFactor * 1.5 self.widget.ScaleSlider.setValue(50) self.widget.StrideBox.setValue(self.obj.Stride) self.widget.MaxBox.setValue(self.obj.MaxNumber) @@ -132,7 +132,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.widget.StrideBox.valueChanged.connect(self._stride_changed) self.widget.MaxBox.valueChanged.connect(self._max_number_changed) - def __update_scaling_ui(self): enabled = self.widget.ScaleComboBox.currentIndex() != 0 self.widget.VectorModeComboBox.setEnabled(enabled) @@ -144,7 +143,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.widget.StrideBox.setEnabled(enabled) self.widget.MaxBox.setEnabled(enabled) - # callbacks and logic # ################### @@ -169,9 +167,9 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): def _scale_factor_changed(self, value): # set slider - self.__slide_min = value*0.5 - self.__slide_max = value*1.5 - slider_value = (value - self.__slide_min) / (self.__slide_max - self.__slide_min) * 100. + self.__slide_min = value * 0.5 + self.__slide_max = value * 1.5 + slider_value = (value - self.__slide_min) / (self.__slide_max - self.__slide_min) * 100.0 self.widget.ScaleSlider.blockSignals(True) self.widget.ScaleSlider.setValue(slider_value) self.widget.ScaleSlider.blockSignals(False) @@ -186,7 +184,7 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): # factor = min + ( slider_value x ------------- ) # 100 # - f = self.__slide_min + (value * (self.__slide_max - self.__slide_min)/100) + f = self.__slide_min + (value * (self.__slide_max - self.__slide_min) / 100) # sync factor spin box self.widget.ScaleFactorBox.blockSignals(True) @@ -197,7 +195,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.obj.ScaleFactor = f self._recompute() - def _mask_mode_changed(self, value): self.obj.MaskMode = value self.__update_masking_ui() @@ -210,4 +207,3 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): def _max_number_changed(self, value): self.obj.MaxNumber = value self._recompute() - diff --git a/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py b/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py index 4df2a7698b..c35299f42a 100644 --- a/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py +++ b/src/Mod/Fem/femviewprovider/view_post_glyphfilter.py @@ -70,7 +70,7 @@ class VPPostGlyphFilter: # build up the task panel taskd = task_post_glyphfilter._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True