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] 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