From 6e4fab1f50595fe0e26cb7a979756b549260d214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 31 Mar 2025 20:11:09 +0200 Subject: [PATCH 01/27] FEM: Draft architecture of post data extraction with histogram example --- src/Mod/Fem/App/FemPostFilterPy.xml | 7 + src/Mod/Fem/App/FemPostFilterPyImp.cpp | 23 + src/Mod/Fem/App/FemPostObjectPy.xml | 7 + src/Mod/Fem/App/FemPostObjectPyImp.cpp | 25 + src/Mod/Fem/App/FemPostPipeline.h | 6 + src/Mod/Fem/App/FemPostPipelinePy.xml | 7 + src/Mod/Fem/App/FemPostPipelinePyImp.cpp | 23 + src/Mod/Fem/App/PropertyPostDataObject.cpp | 20 +- src/Mod/Fem/CMakeLists.txt | 16 +- src/Mod/Fem/Gui/CMakeLists.txt | 6 + src/Mod/Fem/Gui/Resources/Fem.qrc | 6 + .../Fem/Gui/Resources/icons/FEM_PostField.svg | 60 +++ .../Gui/Resources/icons/FEM_PostHistogram.svg | 40 ++ .../Fem/Gui/Resources/icons/FEM_PostIndex.svg | 42 ++ .../Gui/Resources/icons/FEM_PostLineplot.svg | 41 ++ .../Gui/Resources/icons/FEM_PostPlotline.svg | 41 ++ .../Resources/icons/FEM_PostSpreadsheet.svg | 40 ++ .../Resources/ui/PostHistogramFieldAppEdit.ui | 66 +++ .../ui/PostHistogramFieldViewEdit.ui | 162 ++++++ .../Gui/Resources/ui/TaskPostExtraction.ui | 54 ++ .../Fem/Gui/Resources/ui/TaskPostHistogram.ui | 223 ++++++++ src/Mod/Fem/Gui/TaskPostBoxes.cpp | 20 +- src/Mod/Fem/Gui/TaskPostBoxes.h | 7 +- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 169 ++++++ src/Mod/Fem/Gui/TaskPostExtraction.h | 67 +++ src/Mod/Fem/Gui/TaskPostExtraction.ui | 135 +++++ .../Fem/Gui/ViewProviderFemPostFilterPy.xml | 5 + .../Gui/ViewProviderFemPostFilterPyImp.cpp | 19 + src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 8 +- src/Mod/Fem/Gui/Workbench.cpp | 12 +- src/Mod/Fem/ObjectsFem.py | 42 ++ src/Mod/Fem/femcommands/commands.py | 7 +- src/Mod/Fem/femguiutils/data_extraction.py | 139 +++++ src/Mod/Fem/femguiutils/extract_link_view.py | 490 ++++++++++++++++++ src/Mod/Fem/femguiutils/post_visualization.py | 162 ++++++ src/Mod/Fem/femguiutils/vtk_table_view.py | 138 +++++ .../Fem/femobjects/base_fempostextractors.py | 199 +++++++ .../femobjects/base_fempostvisualizations.py | 71 +++ .../Fem/femobjects/base_fempythonobject.py | 1 + src/Mod/Fem/femobjects/post_extract1D.py | 178 +++++++ src/Mod/Fem/femobjects/post_histogram.py | 142 +++++ src/Mod/Fem/femobjects/post_lineplot.py | 211 ++++++++ .../Fem/femtaskpanels/base_fempostpanel.py | 83 +++ .../Fem/femtaskpanels/task_post_extractor.py | 54 ++ .../femtaskpanels/task_post_glyphfilter.py | 4 +- .../Fem/femtaskpanels/task_post_histogram.py | 180 +++++++ .../femviewprovider/view_base_femobject.py | 17 + .../view_base_fempostvisualization.py | 91 ++++ .../Fem/femviewprovider/view_post_extract.py | 129 +++++ .../femviewprovider/view_post_histogram.py | 476 +++++++++++++++++ .../Fem/femviewprovider/view_post_lineplot.py | 71 +++ 51 files changed, 4228 insertions(+), 14 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui create mode 100644 src/Mod/Fem/Gui/TaskPostExtraction.cpp create mode 100644 src/Mod/Fem/Gui/TaskPostExtraction.h create mode 100644 src/Mod/Fem/Gui/TaskPostExtraction.ui create mode 100644 src/Mod/Fem/femguiutils/data_extraction.py create mode 100644 src/Mod/Fem/femguiutils/extract_link_view.py create mode 100644 src/Mod/Fem/femguiutils/post_visualization.py create mode 100644 src/Mod/Fem/femguiutils/vtk_table_view.py create mode 100644 src/Mod/Fem/femobjects/base_fempostextractors.py create mode 100644 src/Mod/Fem/femobjects/base_fempostvisualizations.py create mode 100644 src/Mod/Fem/femobjects/post_extract1D.py create mode 100644 src/Mod/Fem/femobjects/post_histogram.py create mode 100644 src/Mod/Fem/femobjects/post_lineplot.py create mode 100644 src/Mod/Fem/femtaskpanels/base_fempostpanel.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_extractor.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_histogram.py create mode 100644 src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_extract.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_histogram.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_lineplot.py diff --git a/src/Mod/Fem/App/FemPostFilterPy.xml b/src/Mod/Fem/App/FemPostFilterPy.xml index 28d1823f69..3fe0e4fd88 100644 --- a/src/Mod/Fem/App/FemPostFilterPy.xml +++ b/src/Mod/Fem/App/FemPostFilterPy.xml @@ -49,6 +49,13 @@ 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 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. + + + + + + +Returns the filters vtk algorithm currently used as output (the one generating the Data field). Note that the output algorithm may change depending on filter settings. " diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index dddf9048e1..097915f78e 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -38,6 +38,7 @@ #ifdef FC_USE_VTK_PYTHON #include #include +#include #endif // BUILD_FEM_VTK using namespace Fem; @@ -129,6 +130,9 @@ PyObject* FemPostFilterPy::getInputData(PyObject* args) case VTK_UNSTRUCTURED_GRID: copy = vtkUnstructuredGrid::New(); break; + case VTK_POLY_DATA: + copy = vtkPolyData::New(); + break; default: PyErr_SetString(PyExc_TypeError, "cannot return datatype object; not unstructured grid"); @@ -183,6 +187,25 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) return Py::new_reference_to(list); } +PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the algorithm + auto algorithm = getFemPostFilterPtr()->getFilterOutput(); + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); + + return Py::new_reference_to(py_algorithm); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/FemPostObjectPy.xml b/src/Mod/Fem/App/FemPostObjectPy.xml index cc4d4dacef..8f5603234b 100644 --- a/src/Mod/Fem/App/FemPostObjectPy.xml +++ b/src/Mod/Fem/App/FemPostObjectPy.xml @@ -23,6 +23,13 @@ filename: str File extension is automatically detected from data type. + + + getDataset() -> vtkDataSet + +Returns the current output dataset. For normal filters this is equal to the objects Data property output. However, a pipelines Data property could store multiple frames, and hence Data can be of type vtkCompositeData, which is not a vtkDataset. To simplify implementations this function always returns a vtkDataSet, and for a pipeline it will be the dataset of the currently selected frame. Note that the returned value could be None, if no data is set at all. + + diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index 81ee5119ac..27a1204bc0 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -29,6 +29,10 @@ #include "FemPostObjectPy.h" #include "FemPostObjectPy.cpp" +#ifdef BUILD_FEM_VTK_WRAPPER + #include + #include +#endif //BUILD_FEM_VTK using namespace Fem; @@ -55,6 +59,27 @@ PyObject* FemPostObjectPy::writeVTK(PyObject* args) Py_Return; } +PyObject* FemPostObjectPy::getDataSet(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the dataset + auto dataset = getFemPostObjectPtr()->getDataSet(); + if (dataset) { + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset); + return Py::new_reference_to(py_algorithm); + } + return Py_None; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostObjectPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/FemPostPipeline.h b/src/Mod/Fem/App/FemPostPipeline.h index 15d9149705..d590915cb4 100644 --- a/src/Mod/Fem/App/FemPostPipeline.h +++ b/src/Mod/Fem/App/FemPostPipeline.h @@ -118,6 +118,12 @@ public: unsigned int getFrameNumber(); std::vector getFrameValues(); + // output algorithm handling + vtkSmartPointer getOutputAlgorithm() + { + return m_source_algorithm; + } + protected: void onChanged(const App::Property* prop) override; bool allowObject(App::DocumentObject* obj) override; diff --git a/src/Mod/Fem/App/FemPostPipelinePy.xml b/src/Mod/Fem/App/FemPostPipelinePy.xml index c71981393b..ab15496be9 100644 --- a/src/Mod/Fem/App/FemPostPipelinePy.xml +++ b/src/Mod/Fem/App/FemPostPipelinePy.xml @@ -71,5 +71,12 @@ Load a single result object or create a multiframe result by loading multiple re Change name of data arrays + + + +Returns the pipeline vtk algorithm, which generates the data passed to the pipelines filters. Note that the output algorithm may change depending on pipeline settings. + + + " diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index be59cdefb2..3154800802 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -34,6 +34,10 @@ #include "FemPostPipelinePy.cpp" // clang-format on +#ifdef BUILD_FEM_VTK_WRAPPER + #include +#endif //BUILD_FEM_VTK + using namespace Fem; @@ -313,6 +317,25 @@ PyObject* FemPostPipelinePy::renameArrays(PyObject* args) Py_Return; } +PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the algorithm + auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm(); + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); + + return Py::new_reference_to(py_algorithm); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostPipelinePy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index 3ab5e1cbbf..83648350ad 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -32,8 +32,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -243,6 +246,9 @@ void PropertyPostDataObject::createDataObjectByExternalType(vtkSmartPointer::New(); break; + case VTK_TABLE: + m_dataObject = vtkSmartPointer::New(); + break; default: throw Base::TypeError("Unsupported VTK data type"); }; @@ -313,6 +319,9 @@ void PropertyPostDataObject::Save(Base::Writer& writer) const case VTK_MULTIBLOCK_DATA_SET: extension = "zip"; break; + case VTK_TABLE: + extension = ".vtt"; + break; default: break; }; @@ -382,13 +391,16 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const xmlWriter = vtkSmartPointer::New(); xmlWriter->SetInputDataObject(m_dataObject); xmlWriter->SetFileName(datafile.filePath().c_str()); - xmlWriter->SetDataModeToBinary(); + } + else if (m_dataObject->IsA("vtkTable")) { + xmlWriter = vtkSmartPointer::New(); + xmlWriter->SetInputDataObject(m_dataObject); + xmlWriter->SetFileName(fi.filePath().c_str()); } else { xmlWriter = vtkSmartPointer::New(); xmlWriter->SetInputDataObject(m_dataObject); xmlWriter->SetFileName(fi.filePath().c_str()); - xmlWriter->SetDataModeToBinary(); #ifdef VTK_CELL_ARRAY_V2 // Looks like an invalid data object that causes a crash with vtk9 @@ -399,6 +411,7 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const } #endif } + xmlWriter->SetDataModeToBinary(); if (xmlWriter->Write() != 1) { // Note: Do NOT throw an exception here because if the tmp. file could @@ -481,6 +494,9 @@ void PropertyPostDataObject::RestoreDocFile(Base::Reader& reader) else if (extension == "vti") { xmlReader = vtkSmartPointer::New(); } + else if (extension == "vtt") { + xmlReader = vtkSmartPointer::New(); + } else if (extension == "zip") { // first unzip the file into a datafolder diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index a0390b1035..e5df8521ea 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -36,7 +36,7 @@ SET(FemBaseModules_SRCS coding_conventions.md Init.py InitGui.py - ObjectsFem.py + # ObjectsFem.py TestFemApp.py CreateLabels.py ) @@ -182,6 +182,8 @@ SET(FemObjects_SRCS femobjects/base_femelement.py femobjects/base_femmeshelement.py femobjects/base_fempythonobject.py + femobjects/base_fempostextractors.py + femobjects/base_fempostvisualizations.py femobjects/constant_vacuumpermittivity.py femobjects/constraint_bodyheatsource.py femobjects/constraint_centrif.py @@ -217,6 +219,8 @@ if(BUILD_FEM_VTK_PYTHON) SET(FemObjects_SRCS ${FemObjects_SRCS} femobjects/post_glyphfilter.py + femobjects/post_extract1D.py + femobjects/post_histogram.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -597,6 +601,7 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/__init__.py femtaskpanels/base_femtaskpanel.py femtaskpanels/base_femlogtaskpanel.py + femtaskpanels/base_fempostpanel.py femtaskpanels/task_constraint_bodyheatsource.py femtaskpanels/task_constraint_centrif.py femtaskpanels/task_constraint_currentdensity.py @@ -628,6 +633,8 @@ if(BUILD_FEM_VTK_PYTHON) SET(FemGuiTaskPanels_SRCS ${FemGuiTaskPanels_SRCS} femtaskpanels/task_post_glyphfilter.py + femtaskpanels/task_post_histogram.py + femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -642,6 +649,10 @@ SET(FemGuiUtils_SRCS femguiutils/migrate_gui.py femguiutils/selection_widgets.py femguiutils/vtk_module_handling.py + femguiutils/vtk_table_view.py + femguiutils/data_extraction.py + femguiutils/extract_link_view.py + femguiutils/post_visualization.py ) SET(FemGuiViewProvider_SRCS @@ -651,6 +662,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_base_femmaterial.py femviewprovider/view_base_femmeshelement.py femviewprovider/view_base_femobject.py + femviewprovider/view_base_fempostvisualization.py femviewprovider/view_constant_vacuumpermittivity.py femviewprovider/view_constraint_bodyheatsource.py femviewprovider/view_constraint_centrif.py @@ -686,6 +698,8 @@ if(BUILD_FEM_VTK_PYTHON) SET(FemGuiViewProvider_SRCS ${FemGuiViewProvider_SRCS} femviewprovider/view_post_glyphfilter.py + femviewprovider/view_post_extract.py + femviewprovider/view_post_histogram.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 7abc3b4df0..f624778753 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -291,6 +291,8 @@ if(BUILD_FEM_VTK) SphereWidget.ui TaskPostBoxes.h TaskPostBoxes.cpp + TaskPostExtraction.h + TaskPostExtraction.cpp TaskPostCalculator.ui TaskPostClip.ui TaskPostContours.ui @@ -440,6 +442,10 @@ SET(FemGuiPythonUI_SRCS Resources/ui/SolverCalculiX.ui Resources/ui/SolverCcxTools.ui Resources/ui/TaskPostGlyph.ui + Resources/ui/TaskPostExtraction.ui + Resources/ui/TaskPostHistogram.ui + Resources/ui/PostHistogramFieldViewEdit.ui + Resources/ui/PostHistogramFieldAppEdit.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 7e15fdf17e..8777f6b4dc 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -86,6 +86,12 @@ icons/FEM_PostFrames.svg icons/FEM_PostBranchFilter.svg icons/FEM_PostPipelineFromResult.svg + icons/FEM_PostLineplot.svg + icons/FEM_PostPlotline.svg + icons/FEM_PostHistogram.svg + icons/FEM_PostSpreadsheet.svg + icons/FEM_PostField.svg + icons/FEM_PostIndex.svg icons/FEM_ResultShow.svg icons/FEM_ResultsPurge.svg diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg new file mode 100644 index 0000000000..5a42219430 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg @@ -0,0 +1,60 @@ + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg new file mode 100644 index 0000000000..4e6d52d4a1 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg new file mode 100644 index 0000000000..36c93c04ba --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg new file mode 100644 index 0000000000..637dac60be --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg new file mode 100644 index 0000000000..a788318bac --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg new file mode 100644 index 0000000000..6220e8e87f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui new file mode 100644 index 0000000000..a89c7ef39b --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -0,0 +1,66 @@ + + + Form + + + + 0 + 0 + 317 + 118 + + + + Form + + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One field for all frames + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui new file mode 100644 index 0000000000..bc26238b94 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -0,0 +1,162 @@ + + + PostHistogramEdit + + + + 0 + 0 + 293 + 126 + + + + Form + + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + Lines: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + Density of hatch pattern + + + 1 + + + + + + + Bars: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + Color of all lines (bar outline and hatches) + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + + + Legend: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+
+ + +
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui new file mode 100644 index 0000000000..8f082da23f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui @@ -0,0 +1,54 @@ + + + TaskPostExtraction + + + + 0 + 0 + 515 + 36 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Data Summary + + + + + + + Show Data + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui new file mode 100644 index 0000000000..70e2f3ecba --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui @@ -0,0 +1,223 @@ + + + TaskPostGlyph + + + + 0 + 0 + 343 + 498 + + + + Glyph settings + + + Qt::LayoutDirection::LeftToRight + + + + + + + + The form of the glyph + + + Bins + + + + + + + + 0 + 0 + + + + Qt::LayoutDirection::LeftToRight + + + 2 + + + 1000 + + + + + + + Which vector field is used to orient the glyphs + + + Type + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + Cumulative + + + + + + + + + Legend + + + + + + Qt::LayoutDirection::LeftToRight + + + Show + + + + + + + + 0 + 0 + + + + + + + + + + + + 1 + 0 + + + + Labels + + + false + + + false + + + false + + + + + + + + + Y Axis + + + + + + + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + X Axis + + + + + + + A constant multiplier the glyphs are scaled with + + + Title + + + + + + + + + + + + + Visuals + + + + + + 1.000000000000000 + + + 0.050000000000000 + + + + + + + Hatch Line Width + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Bar width + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 805977f6d2..d880f73d33 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -64,7 +64,6 @@ #include "ui_TaskPostFrames.h" #include "ui_TaskPostBranch.h" - #include "FemSettings.h" #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" @@ -72,6 +71,9 @@ #include "ViewProviderFemPostObject.h" #include "ViewProviderFemPostBranchFilter.h" +#include +#include +#include using namespace FemGui; using namespace Gui; @@ -214,9 +216,14 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, setWindowTitle(title); setWindowIcon(icon); m_icon = icon; + + m_connection = m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, this, boost::placeholders::_1, boost::placeholders::_2)); } -TaskPostWidget::~TaskPostWidget() = default; +TaskPostWidget::~TaskPostWidget() +{ + m_connection.disconnect(); +}; bool TaskPostWidget::autoApply() { @@ -256,6 +263,14 @@ void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComb box->setCurrentIndex(index); } +void TaskPostWidget::handlePropertyChange(const App::DocumentObject& obj, const App::Property& prop) +{ + if (auto postobj = m_object.get()) { + if (&prop == &postobj->Data) { + this->onPostDataChanged(postobj); + } + } +} // *************************************************************************** // simulation dialog for the TaskView @@ -475,7 +490,6 @@ void TaskPostDisplay::onTransparencyValueChanged(int i) void TaskPostDisplay::applyPythonCode() {} - // *************************************************************************** // functions TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index d37742dd27..9b24eb314f 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -42,6 +42,7 @@ class Ui_TaskPostWarpVector; class Ui_TaskPostCut; class Ui_TaskPostFrames; class Ui_TaskPostBranch; +class Ui_TaskPostExtraction; class SoFontStyle; class SoText2; @@ -187,10 +188,15 @@ protected: static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box); + // object update handling + void handlePropertyChange(const App::DocumentObject&, const App::Property&); + virtual void onPostDataChanged(Fem::FemPostObject*) {}; + private: QPixmap m_icon; App::DocumentObjectWeakPtrT m_object; Gui::ViewProviderWeakPtrT m_view; + boost::signals2::connection m_connection; }; @@ -267,7 +273,6 @@ private: std::unique_ptr ui; }; - // *************************************************************************** // functions class ViewProviderFemPostFunction; diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp new file mode 100644 index 0000000000..ef70109462 --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -0,0 +1,169 @@ +/*************************************************************************** + * Copyright (c) 2015 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" + +#ifndef _PreComp_ + +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ViewProviderFemPostObject.h" +#include "TaskPostExtraction.h" + +using namespace FemGui; +using namespace Gui; + + +// *************************************************************************** +// box to handle data extractions + +TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent) + : TaskPostWidget(view, + Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), + parent) +{ + // we load the python implementation, and try to get the widget from it, to add + // directly our widget + + setWindowTitle(tr("Data and extractions")); + + Base::PyGILStateLocker lock; + + Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true); + if (mod.isNull()) + throw Base::ImportError("Unable to import data extraction widget"); + + try { + Py::Callable method(mod.getAttr(std::string("DataExtraction"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(view->getPyObject())); + m_panel = Py::Object(method.apply(args)); + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } + + if (m_panel.hasAttr(std::string("widget"))) { + Py::Object pywidget(m_panel.getAttr(std::string("widget"))); + + Gui::PythonWrapper wrap; + if (wrap.loadCoreModule()) { + QObject* object = wrap.toQObject(pywidget); + if (object) { + QWidget* widget = qobject_cast(object); + if (widget) { + // finally we have the usable QWidget. Add to us! + + auto layout = new QVBoxLayout(); + layout->addWidget(widget); + setLayout(layout); + return; + } + } + } + } + // if we are here somethign went wrong! + throw Base::ImportError("Unable to import data extraction widget"); +}; + +TaskPostExtraction::~TaskPostExtraction() { + + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("widget"))) { + m_panel.setAttr(std::string("widget"), Py::None()); + } + m_panel = Py::None(); + } + catch (Py::AttributeError& e) { + e.clear(); + } +} + +void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("onPostDataChanged"))) { + Py::Callable method(m_panel.getAttr(std::string("onPostDataChanged"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(obj->getPyObject())); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +}; + +bool TaskPostExtraction::isGuiTaskOnly() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("isGuiTaskOnly"))) { + Py::Callable method(m_panel.getAttr(std::string("isGuiTaskOnly"))); + auto result = Py::Boolean(method.apply()); + return result.as_bool(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } + + return false; +}; + +void TaskPostExtraction::apply() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("apply"))) { + Py::Callable method(m_panel.getAttr(std::string("apply"))); + method.apply(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +#include "moc_TaskPostExtraction.cpp" diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.h b/src/Mod/Fem/Gui/TaskPostExtraction.h new file mode 100644 index 0000000000..5423a83d00 --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.h @@ -0,0 +1,67 @@ +/*************************************************************************** + * 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 * + * * + ***************************************************************************/ + +#ifndef GUI_TASKVIEW_TaskPostExtraction_H +#define GUI_TASKVIEW_TaskPostExtraction_H + +#include +#include +#include +#include + +#include + +#include "TaskPostBoxes.h" + +#include +#include + +class Ui_TaskPostExtraction; + + +namespace FemGui +{ + +// *************************************************************************** +// box to handle data extractions: It is implemented in python, the c++ +// code is used to access it and manage it for the c++ task panels +class TaskPostExtraction: public TaskPostWidget +{ + Q_OBJECT + +public: + explicit TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent = nullptr); + ~TaskPostExtraction(); + +protected: + bool isGuiTaskOnly() override; + void apply() override; + void onPostDataChanged(Fem::FemPostObject* obj) override; + +private: + Py::Object m_panel; +}; + + +} // namespace FemGui + +#endif // GUI_TASKVIEW_TaskPostExtraction_H diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/TaskPostExtraction.ui new file mode 100644 index 0000000000..7387ffb7de --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.ui @@ -0,0 +1,135 @@ + + + TaskPostExtraction + + + + 0 + 0 + 375 + 302 + + + + Form + + + + + + + + Data Summary + + + + + + + Show Data + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 20 + 10 + + + + + + + + + + + 0 + 0 + + + + Data used in: + + + Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + Add data to + + + + + + + + + 0 + 0 + + + + + Create and add + + + + + + + + + + true + + + + + 0 + 0 + 359 + 188 + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml index c41959e24d..9a41e8e972 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml @@ -20,5 +20,10 @@ Returns the display option task panel for a post processing edit task dialog. + + + Returns the data extraction 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 index c922d76840..7caff695eb 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -27,6 +27,7 @@ #include #include "ViewProviderFemPostFilter.h" #include "TaskPostBoxes.h" +#include "TaskPostExtraction.h" // inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml) #include "ViewProviderFemPostFilterPy.h" #include "ViewProviderFemPostFilterPy.cpp" @@ -60,6 +61,24 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) return nullptr; } +PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args) +{ + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + auto panel = new TaskPostExtraction(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; diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 0d44e6486e..bc4dd1d953 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -67,6 +67,7 @@ #include #include "TaskPostBoxes.h" +#include "TaskPostExtraction.h" #include "ViewProviderAnalysis.h" #include "ViewProviderFemPostObject.h" @@ -1019,8 +1020,11 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { assert(dlg->getView() == this); - auto panel = new TaskPostDisplay(this); - dlg->addTaskBox(panel->windowIcon().pixmap(32), panel); + auto disp_panel = new TaskPostDisplay(this); + dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel); + + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); } void ViewProviderFemPostObject::unsetEdit(int ModNum) diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 3ae3219705..acd7202acc 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -214,7 +214,11 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterDataAtPoint" << "FEM_PostFilterCalculator" << "Separator" - << "FEM_PostCreateFunctions"; + << "FEM_PostCreateFunctions" +#ifdef BUILD_FEM_VTK_WRAPPER + << "FEM_PostVisualization" +#endif + ; #endif Gui::ToolBarItem* utils = new Gui::ToolBarItem(root); @@ -366,7 +370,11 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterDataAtPoint" << "FEM_PostFilterCalculator" << "Separator" - << "FEM_PostCreateFunctions"; + << "FEM_PostCreateFunctions" +#ifdef BUILD_FEM_VTK_WRAPPER + << "FEM_PostVisualization" +#endif + ; #endif Gui::MenuItem* utils = new Gui::MenuItem; diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c05ecc6108..1dce3db5f7 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -686,6 +686,48 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): return obj +def makePostVtkLinePlot(doc, name="Lineplot"): + """makePostVtkLineplot(document, [name]): + creates a FEM post processing line plot + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLinePlot(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLinePlot(obj.ViewObject) + return + + +def makePostVtkHistogramFieldData(doc, name="FieldData1D"): + """makePostVtkFieldData1D(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogramFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) + return obj + + +def makePostVtkHistogram(doc, name="Histogram"): + """makePostVtkHistogram(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogram(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogram(obj.ViewObject) + 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 0c61dcff2b..98c620a96c 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -40,6 +40,7 @@ from .manager import CommandManager from femtools.femutils import expandParentObject from femtools.femutils import is_of_type from femsolver.settings import get_default_solver +from femguiutils import post_visualization # Python command definitions: # for C++ command definitions see src/Mod/Fem/Command.cpp @@ -1231,7 +1232,6 @@ class _PostFilterGlyph(CommandManager): self.is_active = "with_vtk_selresult" self.do_activated = "add_filter_set_edit" - # the string in add command will be the page name on FreeCAD wiki FreeCADGui.addCommand("FEM_Analysis", _Analysis()) FreeCADGui.addCommand("FEM_ClippingPlaneAdd", _ClippingPlaneAdd()) @@ -1289,3 +1289,8 @@ FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88()) if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) + + # setup all visualization commands (register by importing) + import femobjects.post_histogram + post_visualization.setup_commands("FEM_PostVisualization") + diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py new file mode 100644 index 0000000000..4eeffbcef4 --- /dev/null +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -0,0 +1,139 @@ +# *************************************************************************** +# * 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 ldata view and extraction widget" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package data_extraction +# \ingroup FEM +# \brief A widget for data extraction. Used in the PostObject task panel. + +from . import vtk_table_view + +from PySide import QtCore, QtGui + +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter +from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents + +import FreeCAD +import FreeCADGui + +from femtaskpanels.base_fempostpanel import _BasePostTaskPanel + +from . import extract_link_view +ExtractLinkView = extract_link_view.ExtractLinkView + +class DataExtraction(_BasePostTaskPanel): + # The class is not a widget itself, but provides a widget. It implements + # all required callbacks for the widget and the task dialog. + # Note: This object is created and used from c++! See PostTaskExtraction + + def __init__(self, vobj): + + super().__init__(vobj.Object) + + self.ViewObject = vobj + self.Object = vobj.Object + + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostExtraction.ui" + ) + + # connect all signals as required + self.widget.Data.clicked.connect(self.showData) + self.widget.Summary.clicked.connect(self.showSummary) + + # setup the data models + self.data_model = vtk_table_view.VtkTableModel() + self.summary_model = vtk_table_view.VtkTableSummaryModel() + + # generate the data + self.onPostDataChanged(self.Object) + + # setup the extraction widget + self._extraction_view = ExtractLinkView(self.Object, True, self) + self.widget.layout().addSpacing(self.widget.Data.size().height()/3) + self.widget.layout().addWidget(self._extraction_view) + self._extraction_view.repopulate() + + + @QtCore.Slot() + def showData(self): + + dialog = QtGui.QDialog(self.widget) + widget = vtk_table_view.VtkTableView(self.data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0,0,0,0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + @QtCore.Slot() + def showSummary(self): + + dialog = QtGui.QDialog(self.widget) + widget = vtk_table_view.VtkTableView(self.summary_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0,0,0,0) + + dialog.setLayout(layout) + dialog.resize(600, 900) + dialog.show() + + def isGuiTaskOnly(self): + # If all panels return true it omits the Apply button in the dialog + return True + + def onPostDataChanged(self, obj): + + algo = obj.getOutputAlgorithm() + if not algo: + self.data_model.setTable(vtkTable()) + + filter = vtkAttributeDataToTableFilter() + filter.SetInputConnection(0, algo.GetOutputPort(0)) + filter.Update() + table = filter.GetOutputDataObject(0) + + # add the points + points = algo.GetOutputDataObject(0).GetPoints().GetData() + table.InsertColumn(points, 0) + + # split the components + splitter = vtkSplitColumnComponents() + splitter.SetNamingModeToNamesWithParens() + splitter.SetInputData(0, table) + + splitter.Update() + table = splitter.GetOutputDataObject(0) + + self.data_model.setTable(table) + self.summary_model.setTable(table) + + def apply(self): + pass diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py new file mode 100644 index 0000000000..60baecd9a4 --- /dev/null +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -0,0 +1,490 @@ +# *************************************************************************** +# * 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 view for summarizing extractor links" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package data_extraction +# \ingroup FEM +# \brief A widget that shows summaries of all available links to extractors + +from PySide import QtGui, QtCore + +import femobjects.base_fempostextractors as extr +import femobjects.base_fempostvisualizations as vis + +import FreeCAD +import FreeCADGui + +from . import post_visualization as pv + +# a model showing available visualizations and possible extractions +# ################################################################# + +def build_new_visualization_tree_model(): + # model that shows all options to create new visualizations + + model = QtGui.QStandardItemModel() + + visualizations = pv.get_registered_visualizations() + for vis_name in visualizations: + vis_icon = FreeCADGui.getIcon(visualizations[vis_name].icon) + vis_item = QtGui.QStandardItem(vis_icon, f"New {vis_name}") + vis_item.setFlags(QtGui.Qt.ItemIsEnabled) + vis_item.setData(visualizations[vis_name]) + + for ext in visualizations[vis_name].extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_name) + ext_item = QtGui.QStandardItem(icon, f"with {name}") + ext_item.setData(ext) + vis_item.appendRow(ext_item) + model.appendRow(vis_item) + + return model + +def build_add_to_visualization_tree_model(): + # model that shows all possible visualization objects to add data to + + visualizations = pv.get_registered_visualizations() + model = QtGui.QStandardItemModel() + + for obj in FreeCAD.ActiveDocument.Objects: + if obj.isDerivedFrom("Fem::FemAnalysis"): + ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label) + ana_item.setFlags(QtGui.Qt.ItemIsEnabled) + + # check all children it it is a visualization + for child in obj.Group: + if vis.is_visualization_object(child): + + vis_item = QtGui.QStandardItem(child.ViewObject.Icon, child.Label) + vis_type = vis.get_visualization_type(child) + vis_item.setFlags(QtGui.Qt.ItemIsEnabled) + vis_item.setData(child) + ana_item.appendRow(vis_item) + + # add extractor items + for ext in visualizations[vis_type].extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_type) + ext_item = QtGui.QStandardItem(icon, f"Add {name}") + ext_item.setData(ext) + vis_item.appendRow(ext_item) + + if ana_item.rowCount(): + model.appendRow(ana_item) + + return model + +def build_post_object_item(post_object, extractions, vis_type): + + # definitely build a item and add the extractions + post_item = QtGui.QStandardItem(post_object.ViewObject.Icon, f"From {post_object.Label}") + post_item.setFlags(QtGui.Qt.ItemIsEnabled) + post_item.setData(post_object) + + # add extractor items + for ext in extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_type) + ext_item = QtGui.QStandardItem(icon, f"add {name}") + ext_item.setData(ext) + post_item.appendRow(ext_item) + + # if we are a post group, we need to add the children + if post_object.hasExtension("Fem::FemPostGroupExtension"): + + for child in post_object.Group: + if child.isDerivedFrom("Fem::FemPostObject"): + item = build_post_object_item(child, extractions, vis_type) + post_item.appendRow(item) + + return post_item + + +def build_add_from_data_tree_model(vis_type): + # model that shows all Post data objects from which data can be extracted + extractions = pv.get_registered_visualizations()[vis_type].extractions + + model = QtGui.QStandardItemModel() + for obj in FreeCAD.ActiveDocument.Objects: + if obj.isDerivedFrom("Fem::FemAnalysis"): + ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label) + ana_item.setFlags(QtGui.Qt.ItemIsEnabled) + + # check all children if it is a post object + for child in obj.Group: + if child.isDerivedFrom("Fem::FemPostObject"): + item = build_post_object_item(child, extractions, vis_type) + ana_item.appendRow(item) + + if ana_item.rowCount(): + model.appendRow(ana_item) + + return model + +class TreeChoiceButton(QtGui.QToolButton): + + selection = QtCore.Signal(object,object) + + def __init__(self, model): + super().__init__() + + self.model = model + self.setEnabled(bool(model.rowCount())) + + self.__skip_next_hide = False + + self.tree_view = QtGui.QTreeView(self) + self.tree_view.setModel(model) + + self.tree_view.setFrameShape(QtGui.QFrame.NoFrame) + self.tree_view.setHeaderHidden(True) + self.tree_view.setEditTriggers(QtGui.QTreeView.EditTriggers.NoEditTriggers) + self.tree_view.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows) + self.tree_view.expandAll() + self.tree_view.activated.connect(self.selectIndex) + + # set a complex menu + self.popup = QtGui.QWidgetAction(self) + self.popup.setDefaultWidget(self.tree_view) + self.setPopupMode(QtGui.QToolButton.InstantPopup) + self.addAction(self.popup); + + QtCore.Slot(QtCore.QModelIndex) + def selectIndex(self, index): + item = self.model.itemFromIndex(index) + + if item and not item.hasChildren(): + extraction = item.data() + parent = item.parent().data() + self.selection.emit(parent, extraction) + self.popup.trigger() + + def setModel(self, model): + self.model = model + self.tree_view.setModel(model) + self.tree_view.expandAll() + + # check if we should be disabled + self.setEnabled(bool(model.rowCount())) + + +# implementationof GUI and its functionality +# ########################################## + +class _ShowVisualization: + def __init__(self, st_object): + self._st_object = st_object + + def __call__(self): + if vis.is_visualization_object(self._st_object): + # show the visualization + self._st_object.ViewObject.Proxy.show_visualization() + else: + # for now just select the thing + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(self._st_object) + +class _ShowEditDialog: + def __init__(self, extractor, post_dialog, widget): + self._extractor = extractor + self._post_dialog = post_dialog + self._widget = widget + + widgets = self._extractor.ViewObject.Proxy.get_edit_widgets(self._post_dialog) + vbox = QtGui.QVBoxLayout() + + buttonBox = QtGui.QDialogButtonBox() + buttonBox.setCenterButtons(True) + buttonBox.setStandardButtons(self._post_dialog.getStandardButtons()) + vbox.addWidget(buttonBox) + + started = False + for widget in widgets: + + if started: + # add a seperator line + frame = QtGui.QFrame() + frame.setFrameShape(QtGui.QFrame.HLine); + vbox.addWidget(frame); + else: + started = True + + vbox.addWidget(widget) + + vbox.addStretch() + + self.dialog = QtGui.QDialog(self._widget) + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.dialog.close) + buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.apply) + self.dialog.setLayout(vbox) + + + def accept(self): + # recompute and close + self._extractor.Document.recompute() + self.dialog.close() + + def apply(self): + self._extractor.Document.recompute() + + def __call__(self): + # create the widgets, add it to dialog + self.dialog.show() + +class _DeleteExtractor: + def __init__(self, extractor, widget): + self._extractor = extractor + self._widget = widget + + def __call__(self): + # remove the document object + doc = self._extractor.Document + doc.removeObject(self._extractor.Name) + doc.recompute() + + # remove the widget + self._widget.deleteLater() + +class ExtractLinkView(QtGui.QWidget): + + def __init__(self, obj, is_source, post_dialog): + # initializes the view. + # obj: The object for which the links should be shown / summarized + # is_source: Bool, if the object is the data source (e.g. postobject), or the target (e.g. plots) + + super().__init__() + + self._object = obj + self._is_source = is_source + self._post_dialog = post_dialog + self._widgets = [] + + # build the layout: + self._scroll_view = QtGui.QScrollArea(self) + self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff) + self._scroll_view.setWidgetResizable(True) + + hbox = QtGui.QHBoxLayout() + label = QtGui.QLabel("Data used in:") + if not self._is_source: + label.setText("Data used from:") + + label.setAlignment(QtGui.Qt.AlignBottom) + hbox.addWidget(label) + hbox.addStretch() + + if self._is_source: + + self._add = TreeChoiceButton(build_add_to_visualization_tree_model()) + self._add.setText("Add data to") + self._add.selection.connect(self.addExtractionToVisualization) + hbox.addWidget(self._add) + + self._create = TreeChoiceButton(build_new_visualization_tree_model()) + self._create.setText("New") + self._create.selection.connect(self.newVisualization) + hbox.addWidget(self._create) + + else: + vis_type = vis.get_visualization_type(self._object) + self._add = TreeChoiceButton(build_add_from_data_tree_model(vis_type)) + self._add.setText("Add data from") + self._add.selection.connect(self.addExtractionToPostObject) + hbox.addWidget(self._add) + + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0,0,0,0) + vbox.addItem(hbox) + vbox.addWidget(self._scroll_view) + + self.setLayout(vbox) + + + + # add the content + self.repopulate() + + def _build_summary_widget(self, extractor): + + widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostExtractionSummaryWidget.ui" + ) + + # add the separation line + frame = QtGui.QFrame() + frame.setFrameShape(QtGui.QFrame.HLine); + widget.layout().addWidget(frame); + + if self._is_source: + st_object = extractor.getParentGroup() + else: + st_object = extractor.Source + + widget.RemoveButton.setIcon(QtGui.QIcon.fromTheme("delete")) + + widget.STButton.setIcon(st_object.ViewObject.Icon) + widget.STButton.setText(st_object.Label) + + widget.ExtractButton.setIcon(extractor.ViewObject.Icon) + + extr_label = extr.get_extraction_dimension(extractor) + extr_label += " " + extr.get_extraction_type(extractor) + widget.ExtractButton.setText(extr_label) + + # connect actions. We add functions to widget, as well as the data we need, + # and use those as callback. This way every widget knows which objects to use + widget.STButton.clicked.connect(_ShowVisualization(st_object)) + widget.ExtractButton.clicked.connect(_ShowEditDialog(extractor, self._post_dialog, widget)) + widget.RemoveButton.clicked.connect(_DeleteExtractor(extractor, widget)) + + return widget + + def repopulate(self): + # collect all links that are available and shows them + + # clear the view + for widget in self._widgets: + widget.hide() + widget.deleteLater() + + self._widgets = [] + + # rebuild the widgets + + if self._is_source: + candidates = self._object.InList + else: + candidates = self._object.OutList + + # get all widgets from the candidates + extractors = [] + for candidate in candidates: + if extr.is_extractor_object(candidate): + summary = self._build_summary_widget(candidate) + self._widgets.append(summary) + + # fill the scroll area + vbox = QtGui.QVBoxLayout() + for widget in self._widgets: + vbox.addWidget(widget) + + vbox.addStretch() + widget = QtGui.QWidget() + widget.setLayout(vbox) + + self._scroll_view.setWidget(widget) + + # also reset the add button model + if self._is_source: + self._add.setModel(build_add_to_visualization_tree_model()) + + def _find_parent_analysis(self, obj): + # iterate upwards, till we find a analysis + for parent in obj.InList: + if parent.isDerivedFrom("Fem::FemAnalysis"): + return parent + + analysis = self._find_parent_analysis(parent) + if analysis: + return analysis + + return None + + QtCore.Slot(object, object) # visualization data, extraction data + def newVisualization(self, vis_data, ext_data): + + doc = self._object.Document + + FreeCADGui.addModule(vis_data.module) + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create visualization + FreeCADGui.doCommand( + f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)" + ) + analysis = self._find_parent_analysis(self._object) + if analysis: + FreeCADGui.doCommand( + f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)" + ) + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" + ) + FreeCADGui.doCommand( + f"visualization.addObject(extraction)" + ) + + self._post_dialog._recompute() + self.repopulate() + + QtCore.Slot(object, object) # visualization object, extraction data + def addExtractionToVisualization(self, vis_obj, ext_data): + + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" + ) + FreeCADGui.doCommand( + f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)" + ) + + self._post_dialog._recompute() + self.repopulate() + + QtCore.Slot(object, object) # post object, extraction data + def addExtractionToPostObject(self, post_obj, ext_data): + + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}" + ) + FreeCADGui.doCommand( + f"App.ActiveDocument.{self._object.Name}.addObject(extraction)" + ) + + self._post_dialog._recompute() + self.repopulate() + diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py new file mode 100644 index 0000000000..d1bfc93898 --- /dev/null +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -0,0 +1,162 @@ +# *************************************************************************** +# * 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 visualization registry" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_visualization +# \ingroup FEM +# \brief A registry to collect visualizations for use in menus + +import copy +from dataclasses import dataclass + +from PySide import QtGui, QtCore + +import FreeCAD +import FreeCADGui +import FemGui + +# Registry to handle visulization commands +# ######################################## + +_registry = {} + +@dataclass +class _Extraction: + + name: str + icon: str + dimension: str + extracttype: str + module: str + factory: str + +@dataclass +class _Visualization: + + name: str + icon: str + module: str + factory: str + extractions: list[_Extraction] + +# Register a visualization by type, icon and factory function +def register_visualization(visualization_type, icon, module, factory): + if visualization_type in _registry: + raise ValueError("Visualization type already registered") + + _registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, []) + +def register_extractor(visualization_type, extraction_type, icon, dimension, etype, module, factory): + + if not visualization_type in _registry: + raise ValueError("visualization not registered yet") + + extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory) + _registry[visualization_type].extractions.append(extraction) + +def get_registered_visualizations(): + return copy.deepcopy(_registry) + + +def _to_command_name(name): + return "FEM_PostVisualization" + name + +class _VisualizationGroupCommand: + + def GetCommands(self): + visus = _registry.keys() + cmds = [_to_command_name(v) for v in visus] + return cmds + + def GetDefaultCommand(self): + return 0 + + def GetResources(self): + return { 'MenuText': 'Data Visualizations', 'ToolTip': 'Different visualizations to show post processing data in'} + + def IsActive(self): + if not FreeCAD.ActiveDocument: + return False + + return bool(FemGui.getActiveAnalysis()) + + +class _VisualizationCommand: + + def __init__(self, visualization_type): + self._visualization_type = visualization_type + + def GetResources(self): + + cmd = _to_command_name(self._visualization_type) + vis = _registry[self._visualization_type] + tooltip = f"Create a {self._visualization_type} post processing data visualization" + + return { + "Pixmap": vis.icon, + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, f"{self._visualization_type}"), + "Accel": "", + "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), + "CmdType": "AlterDoc" + } + + def IsActive(self): + # active analysis available + if not FreeCAD.ActiveDocument: + return False + + return bool(FemGui.getActiveAnalysis()) + + def Activated(self): + + vis = _registry[self._visualization_type] + FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}") + + FreeCADGui.addModule(vis.module) + FreeCADGui.addModule("FemGui") + + FreeCADGui.doCommand( + f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"FemGui.getActiveAnalysis().addObject(obj)" + ) + + FreeCADGui.Selection.clearSelection() + FreeCADGui.doCommand( + "FreeCADGui.ActiveDocument.setEdit(obj)" + ) + +def setup_commands(toplevel_name): + # creates all visualization commands and registers them. The + # toplevel group command will have the name provided to this function. + + # first all visualization and extraction commands + for vis in _registry: + FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis)) + + # build the group command! + FreeCADGui.addCommand("FEM_PostVisualization", _VisualizationGroupCommand()) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py new file mode 100644 index 0000000000..df06c51ee0 --- /dev/null +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -0,0 +1,138 @@ +# *************************************************************************** +# * 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 table view widget to visualize vtkTable" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package vtk_table_view +# \ingroup FEM +# \brief A Qt widget to show a vtkTable + +from PySide import QtGui +from PySide import QtCore + +class VtkTableModel(QtCore.QAbstractTableModel): + # Simple table model. Only supports single component columns + + def __init__(self): + super().__init__() + self._table = None + + def setTable(self, table): + self.beginResetModel() + self._table = table + self.endResetModel() + + def rowCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfRows() + + def columnCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfColumns() + + def data(self, index, role): + + if not self._table: + return None + + if role == QtCore.Qt.DisplayRole: + col = self._table.GetColumn(index.column()) + return col.GetTuple(index.row())[0] + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return self._table.GetColumnName(section) + + if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: + return section + +class VtkTableSummaryModel(QtCore.QAbstractTableModel): + # Simple model showing a summary of the table. + # Only supports single component columns + + def __init__(self): + super().__init__() + self._table = None + + def setTable(self, table): + self.beginResetModel() + self._table = table + self.endResetModel() + + def rowCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfColumns() + + def columnCount(self, index): + return 2 # min, max + + def data(self, index, role): + + if not self._table: + return None + + if role == QtCore.Qt.DisplayRole: + col = self._table.GetColumn(index.row()) + range = col.GetRange() + return range[index.column()] + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return ["Min","Max"][section] + + if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: + return self._table.GetColumnName(section) + + +class VtkTableView(QtGui.QWidget): + + def __init__(self, model): + super().__init__() + + self.model = model + self.table_view = QtGui.QTableView() + self.table_view.setModel(model) + + # fast initial resize and manual resizing still allowed! + header = self.table_view.horizontalHeader() + header.setResizeContentsPrecision(10) + self.table_view.resizeColumnsToContents() + + layout = QtGui.QVBoxLayout() + layout.setContentsMargins(0,0,0,0) + layout.addWidget(self.table_view) + self.setLayout(layout) + diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py new file mode 100644 index 0000000000..4ccef7018a --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -0,0 +1,199 @@ +# *************************************************************************** +# * 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 data exxtractor base objcts" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \brief base objects for data extractors + +from vtkmodules.vtkCommonDataModel import vtkTable + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +# helper functions +# ################ + +def is_extractor_object(obj): + if not hasattr(obj, "Proxy"): + return False + + return hasattr(obj.Proxy, "ExtractionType") + +def get_extraction_type(obj): + # returns the extractor type string, or throws exception if + # not a extractor + return obj.Proxy.ExtractionType + +def get_extraction_dimension(obj): + # returns the extractor dimension string, or throws exception if + # not a extractor + return obj.Proxy.ExtractionDimension + + +# Base class for all extractors with common source and table handling functionality +# Note: Never use directly, always subclass! This class does not create a +# ExtractionType/Dimension variable, hence will not work correctly. +class Extractor(base_fempythonobject.BaseFemPythonObject): + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + + prop = [ + _PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the extracted data", + value=vtkTable(), + ), + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Base", + doc="The data source from which the data is extracted", + value=None, + ), + ] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(obj) + + def onChanged(self, obj, prop): + + if prop == "Source": + # check if the source is a Post object + if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): + FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") + obj.Source = None + + + def get_vtk_table(self, obj): + if not obj.DataTable: + obj.DataTable = vtkTable() + + return obj.DataTable + + def component_options(self, num): + + match num: + case 2: + return ["X", "Y"] + case 3: + return ["X", "Y", "Z"] + case _: + return ["Not a vector"] + + +class Extractor1D(Extractor): + + ExtractionDimension = "1D" + + def __init__(self, obj): + super().__init__(obj) + + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="XField", + group="X Data", + doc="The field to use as X data", + value=[], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XComponent", + group="X Data", + doc="Which part of the X field vector to use for the X axis", + value=[], + ), + ] + + return super()._get_properties() + prop + + + def onChanged(self, obj, prop): + + super().onChanged(obj, prop) + + if prop == "XField" and obj.Source and obj.Source.getDataSet(): + point_data = obj.Source.getDataSet().GetPointData() + self._setup_x_component_property(obj, point_data) + + if prop == "Source": + if obj.Source: + dset = obj.Source.getDataSet() + if dset: + self._setup_x_properties(obj, dset) + else: + self._clear_x_properties(obj) + else: + self._clear_x_properties(obj) + + def _setup_x_component_property(self, obj, point_data): + + if obj.XField == "Index": + obj.XComponent = self.component_options(1) + elif obj.XField == "Position": + obj.XComponent = self.component_options(3) + else: + array = point_data.GetAbstractArray(obj.XField) + obj.XComponent = self.component_options(array.GetNumberOfComponents()) + + def _clear_x_properties(self, obj): + if hasattr(obj, "XComponent"): + obj.XComponent = [] + if hasattr(obj, "XField"): + obj.XField = [] + + def _setup_x_properties(self, obj, dataset): + # Set all X Data properties correctly for the given dataset + fields = ["Index", "Position"] + point_data = dataset.GetPointData() + + for i in range(point_data.GetNumberOfArrays()): + fields.append(point_data.GetArrayName(i)) + + current_field = obj.XField + obj.XField = fields + if current_field in fields: + obj.XField = current_field + + self._setup_x_component_property(obj, point_data) + + diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py new file mode 100644 index 0000000000..fae9c58b6c --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -0,0 +1,71 @@ +# *************************************************************************** +# * 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 data exxtractor base objcts" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \brief base objects for data extractors + +from vtkmodules.vtkCommonDataModel import vtkTable + +from . import base_fempythonobject + +# helper functions +# ################ + +def is_visualization_object(obj): + if not hasattr(obj, "Proxy"): + return False + + return hasattr(obj.Proxy, "VisualizationType") + +def get_visualization_type(obj): + # returns the extractor type string, or throws exception if + # not a extractor + return obj.Proxy.VisualizationType + + +# Base class for all visualizations +# Note: Never use directly, always subclass! This class does not create a +# Visualization variable, hence will not work correctly. +class PostVisualization(base_fempythonobject.BaseFemPythonObject): + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(obj) + + + def onDocumentRestored(self, obj): + self._setup_properties(obj) + + def _get_properties(self): + return [] diff --git a/src/Mod/Fem/femobjects/base_fempythonobject.py b/src/Mod/Fem/femobjects/base_fempythonobject.py index 45b8de4e7d..48003443c2 100644 --- a/src/Mod/Fem/femobjects/base_fempythonobject.py +++ b/src/Mod/Fem/femobjects/base_fempythonobject.py @@ -54,6 +54,7 @@ class _PropHelper: Helper class to manage property data inside proxy objects. Initialization keywords are the same used with PropertyContainer to add dynamics properties plus "value" for the initial value. + Note: Is used as base for a GUI version, be aware when refactoring """ def __init__(self, **kwds): diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py new file mode 100644 index 0000000000..5a9404e149 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -0,0 +1,178 @@ +# *************************************************************************** +# * 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 line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempostextractors +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonCore import vtkIntArray +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonDataModel import vtkDataObject +from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline + +class PostFieldData1D(base_fempostextractors.Extractor1D): + """ + A post processing extraction of one dimensional field data + """ + + ExtractionType = "Field" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop =[ _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc="Specify if the field shall be extracted for every available frame", + value=False, + ), + ] + return super()._get_properties() + prop + + def __array_to_table(self, obj, array, table): + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray(); + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def __array_from_dataset(self, obj, dataset): + # extracts the relevant array from the dataset and returns a copy + + match obj.XField: + case "Index": + num = dataset.GetPoints().GetNumberOfPoints() + array = vtkIntArray() + array.SetNumberOfTuples(num) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i,i) + + case "Position": + orig_array = dataset.GetPoints().GetData() + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.XField) + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + return array + + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + frames = False + if obj.ExtractFrames: + # check if we have timesteps + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + frames = True + else: + FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + + if not frames: + # get the dataset and extract the correct array + array = self.__array_from_dataset(obj, dataset) + if array.GetNumberOfComponents() > 1: + array.SetName(obj.XField + " (" + obj.XComponent + ")") + else: + array.SetName(obj.XField) + + self.__array_to_table(obj, array, table) + + else: + algo = obj.Source.getOutputAlgorithm() + for timestep in timesteps: + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self.__array_from_dataset(obj, dataset) + + if array.GetNumberOfComponents() > 1: + array.SetName(f"{obj.XField} ({obj.XComponent}) - {timestep}") + else: + array.SetName(f"{obj.XField} - {timestep}") + self.__array_to_table(obj, array, table) + + # set the final table + obj.Table = table + + +class PostIndexData1D(base_fempostextractors.Extractor1D): + """ + A post processing extraction of one dimensional index data + """ + + ExtractionType = "Index" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop =[ _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc="Specify if the data at the index should be extracted for each frame", + value=False, + ), + _PropHelper( + type="App::PropertyInteger", + name="XIndex", + group="X Data", + doc="Specify for which point index the data should be extracted", + value=0, + ), + ] + return super()._get_properties() + prop diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py new file mode 100644 index 0000000000..df238c6e08 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -0,0 +1,142 @@ +# *************************************************************************** +# * 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 line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract1D + +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable + + +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization("Histogram", + ":/icons/FEM_PostHistogram.svg", + "ObjectsFem", + "makePostVtkHistogram") + +post_visualization.register_extractor("Histogram", + "HistogramFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostVtkHistogramFieldData") + + +# Implementation +# ############## + +def is_histogram_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Histogram" + + +class PostHistogramFieldData(post_extract1D.PostFieldData1D): + """ + A 1D Field extraction for histograms. + """ + VisualizationType = "Histogram" + + + + +class PostHistogram(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as histograms + """ + + VisualizationType = "Histogram" + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtensionPython") + + def _get_properties(self): + prop = [ + _PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the plotted data, one column per histogram", + value=vtkTable(), + ), + ] + return super()._get_properties() + prop + + + def onChanged(self, obj, prop): + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if not is_histogram_extractor(child): + FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added") + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + def execute(self, obj): + + # during execution we collect all child data into our table + table = vtkTable() + for child in obj.Group: + + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + # TODO: check which array type it is and use that one + array = vtkDoubleArray() + array.DeepCopy(c_array) + array.SetName(f"{child.Source.Label}: {c_array.GetName()}") + table.AddColumn(array) + + obj.Table = table + return False + + + + + diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py new file mode 100644 index 0000000000..f8798fbc23 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -0,0 +1,211 @@ +# *************************************************************************** +# * 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 line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_lineplot +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +# helper function to extract plot object type +def _get_extraction_subtype(obj): + if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): + return obj.Proxy.Type + + return "unknown" + + +class PostLinePlot(base_fempythonobject.BaseFemPythonObject): + """ + A post processing extraction for plotting lines + """ + + Type = "App::FeaturePython" + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtension") + self._setup_properties(obj) + + def _setup_properties(self, obj): + + self.ExtractionType = "LinePlot" + + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.Name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + prop = [] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(self, obj): + + def onChanged(self, obj, prop): + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if _get_extraction_subtype(child) not in ["Line"]: + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + +class PostPlotLine(base_fempythonobject.BaseFemPythonObject): + + Type = "App::FeaturePython" + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + + self.ExtractionType = "Line" + + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.Name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + + prop = [ + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Line", + doc="The data source, the line uses", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XField", + group="X Data", + doc="The field to use as X data", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XComponent", + group="X Data", + doc="Which part of the X field vector to use for the X axis", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YField", + group="Y Data", + doc="The field to use as Y data for the line plot", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YComponent", + group="Y Data", + doc="Which part of the Y field vector to use for the X axis", + value=None, + ), + ] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(self, obj): + + def onChanged(self, obj, prop): + + if prop == "Source": + # check if the source is a Post object + if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): + FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") + obj.XField = [] + obj.YField = [] + obj.Source = None + + if prop == "XField": + if not obj.Source: + obj.XComponent = [] + return + + point_data = obj.Source.Data.GetPointData() + if not point_data.HasArray(obj.XField): + obj.XComponent = [] + return + + match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: + case 1: + obj.XComponent = ["Not a vector"] + case 2: + obj.XComponent = ["Magnitude", "X", "Y"] + case 3: + obj.XComponent = ["Magnitude", "X", "Y", "Z"] + + if prop == "YField": + if not obj.Source: + obj.YComponent = [] + return + + point_data = obj.Source.Data.GetPointData() + if not point_data.HasArray(obj.YField): + obj.YComponent = [] + return + + match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: + case 1: + obj.YComponent = ["Not a vector"] + case 2: + obj.YComponent = ["Magnitude", "X", "Y"] + case 3: + obj.YComponent = ["Magnitude", "X", "Y", "Z"] + + def onExecute(self, obj): + # we need to make sure that we show the correct fields to the user as option for data extraction + + fields = [] + if obj.Source: + point_data = obj.Source.Data.GetPointData() + fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] + + current_X = obj.XField + obj.XField = fields + if current_X in fields: + obj.XField = current_X + + current_Y = obj.YField + obj.YField = fields + if current_Y in fields: + obj.YField = current_Y + + return True + diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py new file mode 100644 index 0000000000..f90af0e260 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -0,0 +1,83 @@ +# *************************************************************************** +# * 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 task panel base for post object task panels" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostpanel +# \ingroup FEM +# \brief task panel base for post objects + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from femguiutils import selection_widgets +from . import base_femtaskpanel + + +class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): + """ + The TaskPanel for post objects, mimicing the c++ functionality + """ + + def __init__(self, obj): + super().__init__(obj) + + # 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() + + + # 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) + + + diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py new file mode 100644 index 0000000000..5a56077c3e --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -0,0 +1,54 @@ +# *************************************************************************** +# * 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 post extractor object task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_extractor +# \ingroup FEM +# \brief universal task dialog for extractor objects. + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from femguiutils import selection_widgets +from . import base_fempostpanel + + +class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties extractor objects. The actual UI is + provided by the viewproviders. This allows using a universal task panel + """ + + def __init__(self, obj): + super().__init__(obj) + + # form is used to display individual task panels + self.form = obj.ViewObject.Proxy.get_edit_widgets(self) + + + diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index a0658812e6..8804951067 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -35,10 +35,10 @@ import FreeCAD import FreeCADGui from femguiutils import selection_widgets -from . import base_femtaskpanel +from . import base_fempostpanel -class _TaskPanel(base_femtaskpanel._BaseTaskPanel): +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter """ diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py new file mode 100644 index 0000000000..593f177a94 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -0,0 +1,180 @@ +# *************************************************************************** +# * 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 histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + hbox = QtGui.QHBoxLayout() + self.data_widget.show_plot = QtGui.QPushButton() + self.data_widget.show_plot.setText("Show plot") + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText("Show data") + hbox.addWidget(self.data_widget.show_table) + vbox = QtGui.QVBoxLayout() + vbox.addItem(hbox) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle("Histogram data") + + + # histogram parameter widget + self.view_widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" + ) + self.view_widget.setWindowTitle("Histogram view settings") + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget, self.view_widget] + + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_plot.clicked.connect(self.showPlot) + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + self.view_widget.Bins.setValue(viewObj.Bins) + self._enumPropertyToCombobox(viewObj, "Type", self.view_widget.Type) + self.view_widget.Cumulative.setChecked(viewObj.Cumulative) + + self.view_widget.Title.setText(viewObj.Title) + self.view_widget.XLabel.setText(viewObj.XLabel) + self.view_widget.YLabel.setText(viewObj.YLabel) + + self.view_widget.LegendShow.setChecked(viewObj.Legend) + self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) + self.view_widget.BarWidth.setValue(viewObj.BarWidth) + self.view_widget.HatchWidth.setValue(viewObj.HatchLineWidth) + + # connect callbacks + self.view_widget.Bins.valueChanged.connect(self.binsChanged) + self.view_widget.Type.activated.connect(self.typeChanged) + self.view_widget.Cumulative.toggled.connect(self.comulativeChanged) + + self.view_widget.Title.editingFinished.connect(self.titleChanged) + self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged) + self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged) + + self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) + self.view_widget.LegendPos.activated.connect(self.legendPosChanged) + self.view_widget.BarWidth.valueChanged.connect(self.barWidthChanged) + self.view_widget.HatchWidth.valueChanged.connect(self.hatchWidthChanged) + + + QtCore.Slot() + def showPlot(self): + self.obj.ViewObject.Proxy.show_visualization() + + QtCore.Slot() + def showTable(self): + + # TODO: make data model update when object is recomputed + data_model = vtk_table_view.VtkTableModel() + data_model.setTable(self.obj.Table) + + dialog = QtGui.QDialog(self.data_widget) + widget = vtk_table_view.VtkTableView(data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0,0,0,0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + + QtCore.Slot(int) + def binsChanged(self, bins): + self.obj.ViewObject.Bins = bins + + QtCore.Slot(int) + def typeChanged(self, idx): + self.obj.ViewObject.Type = idx + + QtCore.Slot(bool) + def comulativeChanged(self, state): + self.obj.ViewObject.Cumulative = state + + QtCore.Slot() + def titleChanged(self): + self.obj.ViewObject.Title = self.view_widget.Title.text() + + QtCore.Slot() + def xLabelChanged(self): + self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() + + QtCore.Slot() + def yLabelChanged(self): + self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() + + QtCore.Slot(int) + def legendPosChanged(self, idx): + self.obj.ViewObject.LegendLocation = idx + + QtCore.Slot(bool) + def legendShowChanged(self, state): + self.obj.ViewObject.Legend = state + + QtCore.Slot(float) + def barWidthChanged(self, value): + self.obj.ViewObject.BarWidth = value + + QtCore.Slot(float) + def hatchWidthChanged(self, value): + self.obj.ViewObject.HatchLineWidth = value diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index dc7e6ba8ba..a86c1288a2 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -36,8 +36,25 @@ import FreeCADGui import FemGui # needed to display the icons in TreeView +from femobjects.base_fempythonobject import _PropHelper + False if FemGui.__name__ else True # flake8, dummy FemGui usage +class _GuiPropHelper(_PropHelper): + """ + Helper class to manage property data inside proxy objects. + Based on the App verison, but viewprovider addProperty does + not take keyword args, hence we use positional arguments here + """ + + def __init__(self, **kwds): + super().__init__(**kwds) + + def add_to_object(self, obj): + obj.addProperty(self.info["type"], self.info["name"], self.info["group"], self.info["doc"]) + obj.setPropertyStatus(self.name, "LockDynamic") + setattr(obj, self.name, self.value) + class VPBaseFemObject: """Proxy View Provider for FEM FeaturePythons base constraint.""" diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py new file mode 100644 index 0000000000..153537d669 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -0,0 +1,91 @@ +# *************************************************************************** +# * 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 visualization base ViewProvider" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_base_fempostvisualizations +# \ingroup FEM +# \brief view provider for post visualization object + +from PySide import QtGui, QtCore + +import Plot +import FreeCADGui + +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper + +class VPPostVisualization: + """ + A View Provider for visualization objects + """ + + def __init__(self, vobj): + vobj.Proxy = self + self._setup_properties(vobj) + + def _setup_properties(self, vobj): + pl = vobj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(vobj) + + def _get_properties(self): + return [] + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def isShow(self): + return True + + def doubleClicked(self,vobj): + + guidoc = FreeCADGui.getDocument(vobj.Object.Document) + + # check if another VP is in edit mode and close it then + if guidoc.getInEdit(): + FreeCADGui.Control.closeDialog() + guidoc.resetEdit() + + guidoc.setEdit(vobj.Object.Name) + return True + + def show_visualization(self): + # shows the visualization without going into edit mode + # to be implemented by subclasses + pass + + def get_kw_args(self, obj): + # returns a dictionary with all visualization options needed for plotting + # based on the view provider properties + return {} + + def dumps(self): + return None + + def loads(self, state): + return None diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_post_extract.py new file mode 100644 index 0000000000..c75dd4bc8b --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_extract.py @@ -0,0 +1,129 @@ + +# *************************************************************************** +# * 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 line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import FemGui +from PySide import QtGui + +import femobjects.base_fempostextractors as fpe +from femtaskpanels import task_post_extractor + +class VPPostExtractor: + """ + A View Provider for extraction of data + """ + + def __init__(self, vobj): + vobj.Proxy = self + self._setup_properties(vobj) + + def _setup_properties(self, vobj): + pl = vobj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(vobj) + + def _get_properties(self): + return [] + + def attach(self, vobj): + self.Object = vobj.Object # used on various places, claim childreens, get icon, etc. + self.ViewObject = vobj + + def isShow(self): + return True + + def onChanged(self, vobj, prop): + + # one of our view properties was changed. Lets inform our parent plot + # that this happend, as this is the one that needs to redraw + + if prop == "Proxy": + return + + group = vobj.Object.getParentGroup() + if not group: + return + + if (hasattr(group.ViewObject, "Proxy") and + hasattr(group.ViewObject.Proxy, "childViewPropertyChanged")): + + group.ViewObject.Proxy.childViewPropertyChanged(vobj, prop) + + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_extractor._ExtractorTaskPanel(vobj.Object) + + #show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def doubleClicked(self, vobj): + guidoc = FreeCADGui.getDocument(vobj.Object.Document) + + # check if another VP is in edit mode and close it then + if guidoc.getInEdit(): + FreeCADGui.Control.closeDialog() + guidoc.resetEdit() + + guidoc.setEdit(vobj.Object.Name) + + return True + + def get_kw_args(self): + # should return the plot keyword arguments that represent the properties + # of the object + return {} + + def get_edit_widgets(self, post_dialog): + # Returns a list of widgets for editing the object/viewprovider. + # The widget will be part of the provided post_dialog, and + # should use its functionality to inform of changes. + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_preview_widget(self, post_dialog): + # Returns a widget for editing the object/viewprovider. + # The widget will be part of the provided post_dialog, and + # should use its functionality to inform of changes. + raise FreeCAD.Base.FreeCADError("Not implemented") + + + def dumps(self): + return None + + def loads(self, state): + return None diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py new file mode 100644 index 0000000000..5a433f17bc --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -0,0 +1,476 @@ +# *************************************************************************** +# * 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 line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import Plot +import FemGui +from PySide import QtGui, QtCore + +import numpy as np +import matplotlib as mpl + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_post_extract +from . import view_base_fempostvisualization +from femtaskpanels import task_post_histogram + +_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + vobj = self._object.ViewObject + + self.widget.Legend.setText(vobj.Legend) + self._post_dialog._enumPropertyToCombobox(vobj, "Hatch", self.widget.Hatch) + self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) + self.widget.LineWidth.setValue(vobj.LineWidth) + self.widget.HatchDensity.setValue(vobj.HatchDensity) + self.widget.BarColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.BarColor])) + self.widget.LineColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.LineColor])) + + self.widget.Legend.editingFinished.connect(self.legendChanged) + self.widget.Hatch.activated.connect(self.hatchPatternChanged) + self.widget.LineStyle.activated.connect(self.lineStyleChanged) + self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged) + self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) + self.widget.LineColor.changed.connect(self.lineColorChanged) + self.widget.BarColor.changed.connect(self.barColorChanged) + + @QtCore.Slot() + def lineColorChanged(self): + color = self.widget.LineColor.property("color") + self._object.ViewObject.LineColor = color.getRgb() + + @QtCore.Slot() + def barColorChanged(self): + color = self.widget.BarColor.property("color") + self._object.ViewObject.BarColor = color.getRgb() + + @QtCore.Slot(float) + def lineWidthChanged(self, value): + self._object.ViewObject.LineWidth = value + + @QtCore.Slot(float) + def hatchDensityChanged(self, value): + self._object.ViewObject.HatchDensity = value + + @QtCore.Slot(int) + def hatchPatternChanged(self, index): + self._object.ViewObject.Hatch = index + + @QtCore.Slot(int) + def lineStyleChanged(self, index): + self._object.ViewObject.LineStyle = index + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Legend = self.widget.Legend.text() + + +class EditAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): + """ + A View Provider for extraction of 1D field data specialy for histograms + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.Proxy = self + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Legend", + group="HistogramPlot", + doc="The name used in the plots legend", + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="BarColor", + group="HistogramBar", + doc="The color the data bin area is drawn with", + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Hatch", + group="HistogramBar", + doc="The hatch pattern drawn in the bar", + value=['None', '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'], + ), + _GuiPropHelper( + type="App::PropertyIntegerConstraint", + name="HatchDensity", + group="HistogramBar", + doc="The line width of the hatch", + value=(1, 1, 99, 1), + ), + _GuiPropHelper( + type="App::PropertyColor", + name="LineColor", + group="HistogramLine", + doc="The color the data bin area is drawn with", + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="HistogramLine", + doc="The width of the bar, between 0 and 1 (1 being without gaps)", + value=(1, 0, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LineStyle", + group="HistogramLine", + doc="The style the line is drawn in", + value=['None', '-', '--', '-.', ':'], + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_edit_widgets(self, post_dialog): + return [ EditAppWidget(self.Object, post_dialog), + EditViewWidget(self.Object, post_dialog)] + + def get_preview_widget(self, post_dialog): + return QtGui.QComboBox() + + def get_kw_args(self): + # builds kw args from the properties + kwargs = {} + + # colors need a workaround, some error occurs with rgba tuple + kwargs["edgecolor"] = self.ViewObject.LineColor + kwargs["facecolor"] = self.ViewObject.BarColor + kwargs["linestyle"] = self.ViewObject.LineStyle + kwargs["linewidth"] = self.ViewObject.LineWidth + if self.ViewObject.Hatch != "None": + kwargs["hatch"] = self.ViewObject.Hatch*self.ViewObject.HatchDensity + + return kwargs + + +class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Histogram plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Cumulative", + group="Histogram", + doc="If be the bars shoud show the cumulative sum left to rigth", + value=False, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Type", + group="Histogram", + doc="The type of histogram plotted", + value=["bar","barstacked", "step", "stepfilled"], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="BarWidth", + group="Histogram", + doc="The width of the bar, between 0 and 1 (1 being without gaps)", + value=(0.9, 0, 1, 0.05), + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="HatchLineWidth", + group="Histogram", + doc="The line width of all drawn hatch patterns", + value=(1, 0, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyInteger", + name="Bins", + group="Histogram", + doc="The number of bins the data is split into", + value=10, + ), + _GuiPropHelper( + type="App::PropertyString", + name="Title", + group="Plot", + doc="The histogram plot title", + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc="The label shown for the histogram X axis", + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc="The label shown for the histogram Y axis", + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc="Determines if the legend is plotted", + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LegendLocation", + group="Plot", + doc="Determines if the legend is plotted", + value=['best','upper right','upper left','lower left','lower right','right', + 'center left','center right','lower center','upper center','center'], + ), + + ] + return prop + + def getIcon(self): + return ":/icons/FEM_PostHistogram.svg" + + def doubleClicked(self,vobj): + + self.show_visualization() + super().doubleClicked(vobj) + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_histogram._TaskPanel(vobj) + + #show it + FreeCADGui.Control.showDialog(taskd) + + return True + + + def show_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + self._plot = Plot.Plot() + self._dialog = QtGui.QDialog(Plot.getMainWindow()) + box = QtGui.QVBoxLayout() + box.addWidget(self._plot) + self._dialog.setLayout(box) + + self.drawPlot() + self._dialog.show() + + def get_kw_args(self, obj): + view = obj.ViewObject + if not view or not hasattr(view, "Proxy"): + return {} + if not hasattr(view.Proxy, "get_kw_args"): + return {} + return view.Proxy.get_kw_args() + + def drawPlot(self): + + if not hasattr(self, "_plot") or not self._plot: + return + + self._plot.axes.clear() + bins = self.ViewObject.Bins + + # we do not iterate the table, but iterate the children. This makes it possible + # to attribute the correct styles + full_args = {} + full_data = [] + labels = [] + for child in self.Object.Group: + + table = child.Table + kwargs = self.get_kw_args(child) + + # iterate over the table and plot all + color_factor = np.linspace(1,0.5,table.GetNumberOfColumns()) + legend_multiframe = table.GetNumberOfColumns() > 1 + for i in range(table.GetNumberOfColumns()): + + # add the kw args, with some slide change over color for multiple frames + for key in kwargs: + if not (key in full_args): + full_args[key] = [] + + if "color" in key: + value = np.array(kwargs[key])*color_factor[i] + full_args[key].append(mpl.colors.to_hex(value)) + else: + full_args[key].append(kwargs[key]) + + data = VTKArray(table.GetColumn(i)) + full_data.append(data) + + # legend labels + if child.ViewObject.Legend: + if not legend_multiframe: + labels.append(child.ViewObject.Legend) + else: + postfix = table.GetColumnName(i).split("-")[-1] + labels.append(child.ViewObject.Legend + " - " + postfix) + else: + legend_prefix = "" + if len(self.Object.Group) > 1: + legend_prefix = child.Source.Label + ": " + labels.append(legend_prefix + table.GetColumnName(i)) + + + full_args["hatch_linewidth"] = self.ViewObject.HatchLineWidth + full_args["rwidth"] = self.ViewObject.BarWidth + full_args["cumulative"] = self.ViewObject.Cumulative + full_args["histtype"] = self.ViewObject.Type + full_args["label"] = labels + + self._plot.axes.hist(full_data, bins, **full_args) + + if self.ViewObject.Title: + self._plot.axes.set_title(self.ViewObject.Title) + if self.ViewObject.XLabel: + self._plot.axes.set_xlabel(self.ViewObject.XLabel) + if self.ViewObject.YLabel: + self._plot.axes.set_ylabel(self.ViewObject.YLabel) + + if self.ViewObject.Legend and labels: + self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + + self._plot.update() + + + def updateData(self, obj, prop): + # we only react if the table changed, as then know that new data is available + if prop == "Table": + self.drawPlot() + + + def onChanged(self, vobj, prop): + + # for all property changes we need to redraw the plot + self.drawPlot() + + def childViewPropertyChanged(self, vobj, prop): + + # on of our extractors has a changed view property. + self.drawPlot() + + def dumps(self): + return None + + + def loads(self, state): + return None diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py new file mode 100644 index 0000000000..0ce5ec8954 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -0,0 +1,71 @@ + +# *************************************************************************** +# * 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 line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import FemGui +from PySide import QtGui + + +class VPPostLinePlot: + """ + A View Provider for the Post LinePlot object + """ + + def __init__(self, vobj): + vobj.Proxy = self + + def getIcon(self): + return ":/icons/FEM_PostLineplot.svg" + + 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 c1b11a19f7fffd18a52cf841a5ddd1985916245d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 16 Apr 2025 18:47:01 +0200 Subject: [PATCH 02/27] Fem: Implement lineplot visualization --- src/Mod/Fem/CMakeLists.txt | 6 +- src/Mod/Fem/Gui/CMakeLists.txt | 4 + .../Resources/ui/PostHistogramFieldAppEdit.ui | 12 + .../ui/PostHistogramFieldViewEdit.ui | 30 +- .../Resources/ui/PostLineplotFieldAppEdit.ui | 101 +++++ .../Resources/ui/PostLineplotFieldViewEdit.ui | 154 +++++++ .../Fem/Gui/Resources/ui/TaskPostHistogram.ui | 36 +- .../Fem/Gui/Resources/ui/TaskPostLineplot.ui | 181 ++++++++ src/Mod/Fem/ObjectsFem.py | 51 ++- src/Mod/Fem/femcommands/commands.py | 2 +- src/Mod/Fem/femguiutils/extract_link_view.py | 315 +++++++++---- .../Fem/femobjects/base_fempostextractors.py | 181 ++++++++ src/Mod/Fem/femobjects/post_extract1D.py | 49 +- src/Mod/Fem/femobjects/post_extract2D.py | 153 +++++++ src/Mod/Fem/femobjects/post_histogram.py | 8 +- src/Mod/Fem/femobjects/post_lineplot.py | 221 +++------ src/Mod/Fem/femobjects/post_table.py | 211 +++++++++ .../Fem/femtaskpanels/task_post_extractor.py | 9 +- .../Fem/femtaskpanels/task_post_lineplot.py | 163 +++++++ .../Fem/femviewprovider/view_post_extract.py | 13 +- .../femviewprovider/view_post_histogram.py | 12 +- .../Fem/femviewprovider/view_post_lineplot.py | 429 +++++++++++++++++- 22 files changed, 1995 insertions(+), 346 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui create mode 100644 src/Mod/Fem/femobjects/post_extract2D.py create mode 100644 src/Mod/Fem/femobjects/post_table.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_lineplot.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index e5df8521ea..0178bffe84 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -36,7 +36,7 @@ SET(FemBaseModules_SRCS coding_conventions.md Init.py InitGui.py - # ObjectsFem.py + ObjectsFem.py TestFemApp.py CreateLabels.py ) @@ -220,7 +220,9 @@ if(BUILD_FEM_VTK_PYTHON) ${FemObjects_SRCS} femobjects/post_glyphfilter.py femobjects/post_extract1D.py + femobjects/post_extract2D.py femobjects/post_histogram.py + femobjects/post_lineplot.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -634,6 +636,7 @@ if(BUILD_FEM_VTK_PYTHON) ${FemGuiTaskPanels_SRCS} femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_post_histogram.py + femtaskpanels/task_post_lineplot.py femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -700,6 +703,7 @@ if(BUILD_FEM_VTK_PYTHON) femviewprovider/view_post_glyphfilter.py femviewprovider/view_post_extract.py femviewprovider/view_post_histogram.py + femviewprovider/view_post_lineplot.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index f624778753..e1523956ea 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -444,8 +444,12 @@ SET(FemGuiPythonUI_SRCS Resources/ui/TaskPostGlyph.ui Resources/ui/TaskPostExtraction.ui Resources/ui/TaskPostHistogram.ui + Resources/ui/TaskPostLineplot.ui + Resources/ui/PostExtractionSummaryWidget.ui Resources/ui/PostHistogramFieldViewEdit.ui Resources/ui/PostHistogramFieldAppEdit.ui + Resources/ui/PostLineplotFieldViewEdit.ui + Resources/ui/PostLineplotFieldAppEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui index a89c7ef39b..d100b81ab3 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui index bc26238b94..744e5a6240 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + @@ -76,9 +88,6 @@ Lines: - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - @@ -102,9 +111,6 @@ Bars: - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - @@ -141,9 +147,6 @@ Legend: - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - @@ -157,6 +160,15 @@
Gui/Widgets.h
+ + Legend + BarColor + Hatch + HatchDensity + LineColor + LineStyle + LineWidth + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui new file mode 100644 index 0000000000..00b8ab4f55 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui @@ -0,0 +1,101 @@ + + + Form + + + + 0 + 0 + 296 + 186 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + X Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Y Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One Y field for each frames + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui new file mode 100644 index 0000000000..720eb96c6e --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui @@ -0,0 +1,154 @@ + + + PostHistogramEdit + + + + 0 + 0 + 335 + 124 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + Marker: + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + + + + Legend: + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + Line: + + + + + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+
+ + Legend + Color + LineStyle + LineWidth + MarkerStyle + MarkerSize + + + +
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui index 70e2f3ecba..a753071f9a 100644 --- a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui @@ -14,9 +14,21 @@ Glyph settings - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight + + 0 + + + 0 + + + 0 + + + 0 + @@ -38,7 +50,7 @@ - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight 2 @@ -94,7 +106,7 @@ - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight Show @@ -195,9 +207,6 @@ Hatch Line Width - - Qt::AlignmentFlag::AlignCenter - @@ -205,9 +214,6 @@ Bar width - - Qt::AlignmentFlag::AlignCenter - @@ -218,6 +224,18 @@ + + Bins + Type + Cumulative + LegendShow + LegendPos + Title + XLabel + YLabel + BarWidth + HatchWidth + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui new file mode 100644 index 0000000000..bec95e063f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui @@ -0,0 +1,181 @@ + + + TaskPostGlyph + + + + 0 + 0 + 302 + 302 + + + + Glyph settings + + + Qt::LeftToRight + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + The form of the glyph + + + Grid + + + + + + + Show + + + + + + + Qt::LeftToRight + + + Show + + + + + + + Legend + + + + + + + + 0 + 0 + + + + + + + + Which vector field is used to orient the glyphs + + + Scale + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + + + + 1 + 0 + + + + Labels + + + false + + + false + + + false + + + + + + + + + Y Axis + + + + + + + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + X Axis + + + + + + + A constant multiplier the glyphs are scaled with + + + Title + + + + + + + + + + + + + Grid + LegendShow + LegendPos + Scale + Title + XLabel + YLabel + + + + diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 1dce3db5f7..c01cc8f8e6 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -686,36 +686,34 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): return obj -def makePostVtkLinePlot(doc, name="Lineplot"): - """makePostVtkLineplot(document, [name]): +def makePostLineplot(doc, name="Lineplot"): + """makePostLineplot(document, [name]): creates a FEM post processing line plot """ obj = doc.addObject("App::FeaturePython", name) from femobjects import post_lineplot - post_lineplot.PostLinePlot(obj) + post_lineplot.PostLineplot(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot - view_post_lineplot.VPPostLinePlot(obj.ViewObject) - return - - -def makePostVtkHistogramFieldData(doc, name="FieldData1D"): - """makePostVtkFieldData1D(document, [name]): - creates a FEM post processing data extractor for 1D Field data - """ - obj = doc.addObject("App::FeaturePython", name) - from femobjects import post_histogram - - post_histogram.PostHistogramFieldData(obj) - if FreeCAD.GuiUp: - from femviewprovider import view_post_histogram - view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) + view_post_lineplot.VPPostLineplot(obj.ViewObject) return obj +def makePostLineplotFieldData(doc, name="FieldData2D"): + """makePostLineplotFieldData(document, [name]): + creates a FEM post processing data extractor for 2D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_lineplot -def makePostVtkHistogram(doc, name="Histogram"): - """makePostVtkHistogram(document, [name]): + post_lineplot.PostLineplotFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) + return obj + +def makePostHistogram(doc, name="Histogram"): + """makePostHistogram(document, [name]): creates a FEM post processing histogram plot """ obj = doc.addObject("App::FeaturePython", name) @@ -727,6 +725,19 @@ def makePostVtkHistogram(doc, name="Histogram"): view_post_histogram.VPPostHistogram(obj.ViewObject) return obj +def makePostHistogramFieldData(doc, name="FieldData1D"): + """makePostHistogramFieldData1D(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogramFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) + return obj + # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 98c620a96c..befcd48f30 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1292,5 +1292,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: # setup all visualization commands (register by importing) import femobjects.post_histogram + import femobjects.post_lineplot post_visualization.setup_commands("FEM_PostVisualization") - diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 60baecd9a4..9c984b537e 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -145,7 +145,10 @@ def build_add_from_data_tree_model(vis_type): return model -class TreeChoiceButton(QtGui.QToolButton): +# implementation of GUI and its functionality +# ########################################### + +class _TreeChoiceButton(QtGui.QToolButton): selection = QtCore.Signal(object,object) @@ -191,15 +194,157 @@ class TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) +class _SettingsPopup(QtGui.QGroupBox): -# implementationof GUI and its functionality -# ########################################## + close = QtCore.Signal() + + def __init__(self, setting): + + toplevel = QtGui.QApplication.topLevelWidgets() + for i in toplevel: + if i.metaObject().className() == "Gui::MainWindow": + main = i + break + + super().__init__(main) + + self.setFocusPolicy(QtGui.Qt.ClickFocus) + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(setting) + + buttonBox = QtGui.QDialogButtonBox() + buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + vbox.addWidget(buttonBox) + + buttonBox.accepted.connect(self.accept) + self.setLayout(vbox) + + @QtCore.Slot() + def accept(self): + self.close.emit() + + def showEvent(self, event): + self.setFocus() + + def keyPressEvent(self, event): + if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: + self.accept() + + + +class _SummaryWidget(QtGui.QWidget): + + delete = QtCore.Signal(object, object) # to delete: document object, summary widget + + def __init__(self, st_object, extractor, post_dialog): + super().__init__() -class _ShowVisualization: - def __init__(self, st_object): self._st_object = st_object + self._extractor = extractor + self._post_dialog = post_dialog - def __call__(self): + extr_label = extractor.Proxy.get_representive_fieldname(extractor) + extr_repr = extractor.ViewObject.Proxy.get_preview() + + # build the UI + + self.stButton = self._button(st_object.Label) + self.stButton.setIcon(st_object.ViewObject.Icon) + + self.extrButton = self._button(extr_label) + self.extrButton.setIcon(extractor.ViewObject.Icon) + + self.viewButton = self._button(extr_repr[1]) + size = self.viewButton.iconSize() + size.setWidth(size.width()*2) + self.viewButton.setIconSize(size) + self.viewButton.setIcon(extr_repr[0]) + + + self.rmButton = QtGui.QToolButton(self) + self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) + self.rmButton.setAutoRaise(True) + + # add the separation line + self.frame = QtGui.QFrame(self) + self.frame.setFrameShape(QtGui.QFrame.HLine); + + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + self.setSizePolicy(policy) + self.setMinimumSize(self.stButton.sizeHint()+self.frame.sizeHint()*3) + + # connect actions. We add functions to widget, as well as the data we need, + # and use those as callback. This way every widget knows which objects to use + self.stButton.clicked.connect(self.showVisualization) + self.extrButton.clicked.connect(self.editApp) + self.viewButton.clicked.connect(self.editView) + self.rmButton.clicked.connect(self.deleteTriggered) + + # make sure initial drawing happened + self._redraw() + + def _button(self, text): + btn = QtGui.QPushButton(self) + btn.full_text = text + + #size = btn.sizeHint() + #size.setWidth(size.width()*2) + btn.setMinimumSize(btn.sizeHint()) + + btn.setFlat(True) + btn.setText(text) + btn.setStyleSheet("text-align:left;padding:6px"); + btn.setToolTip(text) + + return btn + + def _redraw(self): + + btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton + btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() + fm = self.fontMetrics() + min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 + + pos = 0 + btns = [self.stButton, self.extrButton, self.viewButton] + btn_rel_size = [0.4, 0.4, 0.2] + btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] + for i, btn in enumerate(btns): + + btn_size = btn_total_size*btn_rel_size[i] + txt_size = btn_size - btn.iconSize().width() - btn_margin/2*3 + + # we elide only if there is enough space for a meaningful text + if txt_size >= min_text_width: + + text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) + btn.setText(text) + btn.setStyleSheet("text-align:left;padding:6px"); + else: + btn.setText("") + btn.setStyleSheet("text-align:center;"); + + rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) + btn.setGeometry(rect) + pos+=btn_size + + rmsize = self.stButton.height() + pos = self.size().width() - rmsize + self.rmButton.setGeometry(pos, 0, rmsize, rmsize) + + frame_hint = self.frame.sizeHint() + rect = QtCore.QRect(0, self.stButton.height()+frame_hint.height(), self.size().width(), frame_hint.height()) + self.frame.setGeometry(rect) + + def resizeEvent(self, event): + + # calculate the allowed text length + self._redraw() + super().resizeEvent(event) + + @QtCore.Slot() + def showVisualization(self): if vis.is_visualization_object(self._st_object): # show the visualization self._st_object.ViewObject.Proxy.show_visualization() @@ -208,67 +353,76 @@ class _ShowVisualization: FreeCADGui.Selection.clearSelection() FreeCADGui.Selection.addSelection(self._st_object) -class _ShowEditDialog: - def __init__(self, extractor, post_dialog, widget): - self._extractor = extractor - self._post_dialog = post_dialog - self._widget = widget + def _position_dialog(self, dialog): - widgets = self._extractor.ViewObject.Proxy.get_edit_widgets(self._post_dialog) - vbox = QtGui.QVBoxLayout() + main = dialog.parent() + list_widget = self.parent().parent().parent() + widget_rect = list_widget.geometry() + diag_size = dialog.sizeHint() + # default is towards main window center + if main.geometry().center().x() >= list_widget.mapToGlobal(widget_rect.center()).x(): + rigth_point = list_widget.mapToGlobal(widget_rect.topRight()) + dialog.setGeometry(QtCore.QRect(rigth_point, diag_size)) + else: + left_point = list_widget.mapToGlobal(widget_rect.topLeft()) + left_point -= QtCore.QPoint(diag_size.width(), 0) + dialog.setGeometry(QtCore.QRect(left_point, diag_size)) - buttonBox = QtGui.QDialogButtonBox() - buttonBox.setCenterButtons(True) - buttonBox.setStandardButtons(self._post_dialog.getStandardButtons()) - vbox.addWidget(buttonBox) + @QtCore.Slot() + def editApp(self): + if not hasattr(self, "appDialog"): + widget = self._extractor.ViewObject.Proxy.get_app_edit_widget(self._post_dialog) + self.appDialog = _SettingsPopup(widget) + self.appDialog.close.connect(self.appAccept) - started = False - for widget in widgets: + if not self.appDialog.isVisible(): + # position correctly and show + self._position_dialog(self.appDialog) + self.appDialog.show() + #self.appDialog.raise_() - if started: - # add a seperator line - frame = QtGui.QFrame() - frame.setFrameShape(QtGui.QFrame.HLine); - vbox.addWidget(frame); - else: - started = True + @QtCore.Slot() + def editView(self): - vbox.addWidget(widget) + if not hasattr(self, "viewDialog"): + widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog) + self.viewDialog = _SettingsPopup(widget) + self.viewDialog.close.connect(self.viewAccept) - vbox.addStretch() + if not self.viewDialog.isVisible(): + # position correctly and show + self._position_dialog(self.viewDialog) + self.viewDialog.show() + #self.viewDialog.raise_() - self.dialog = QtGui.QDialog(self._widget) - buttonBox.accepted.connect(self.accept) - buttonBox.rejected.connect(self.dialog.close) - buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.apply) - self.dialog.setLayout(vbox) + @QtCore.Slot() + def deleteTriggered(self): + self.delete.emit(self._extractor, self) + + @QtCore.Slot() + def viewAccept(self): + + self.viewDialog.hide() + + # update the preview + extr_repr = self._extractor.ViewObject.Proxy.get_preview() + self.viewButton.setIcon(extr_repr[0]) + self.viewButton.full_text = extr_repr[1] + self.viewButton.setToolTip(extr_repr[1]) + self._redraw() + + @QtCore.Slot() + def appAccept(self): + + self.appDialog.hide() + + # update the preview + extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) + self.extrButton.full_text = extr_label + self.extrButton.setToolTip(extr_label) + self._redraw() - def accept(self): - # recompute and close - self._extractor.Document.recompute() - self.dialog.close() - - def apply(self): - self._extractor.Document.recompute() - - def __call__(self): - # create the widgets, add it to dialog - self.dialog.show() - -class _DeleteExtractor: - def __init__(self, extractor, widget): - self._extractor = extractor - self._widget = widget - - def __call__(self): - # remove the document object - doc = self._extractor.Document - doc.removeObject(self._extractor.Name) - doc.recompute() - - # remove the widget - self._widget.deleteLater() class ExtractLinkView(QtGui.QWidget): @@ -300,19 +454,19 @@ class ExtractLinkView(QtGui.QWidget): if self._is_source: - self._add = TreeChoiceButton(build_add_to_visualization_tree_model()) + self._add = _TreeChoiceButton(build_add_to_visualization_tree_model()) self._add.setText("Add data to") self._add.selection.connect(self.addExtractionToVisualization) hbox.addWidget(self._add) - self._create = TreeChoiceButton(build_new_visualization_tree_model()) + self._create = _TreeChoiceButton(build_new_visualization_tree_model()) self._create.setText("New") self._create.selection.connect(self.newVisualization) hbox.addWidget(self._create) else: vis_type = vis.get_visualization_type(self._object) - self._add = TreeChoiceButton(build_add_from_data_tree_model(vis_type)) + self._add = _TreeChoiceButton(build_add_from_data_tree_model(vis_type)) self._add.setText("Add data from") self._add.selection.connect(self.addExtractionToPostObject) hbox.addWidget(self._add) @@ -324,46 +478,31 @@ class ExtractLinkView(QtGui.QWidget): self.setLayout(vbox) - - # add the content self.repopulate() def _build_summary_widget(self, extractor): - widget = FreeCADGui.PySideUic.loadUi( - FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostExtractionSummaryWidget.ui" - ) - - # add the separation line - frame = QtGui.QFrame() - frame.setFrameShape(QtGui.QFrame.HLine); - widget.layout().addWidget(frame); - if self._is_source: st_object = extractor.getParentGroup() else: st_object = extractor.Source - widget.RemoveButton.setIcon(QtGui.QIcon.fromTheme("delete")) - - widget.STButton.setIcon(st_object.ViewObject.Icon) - widget.STButton.setText(st_object.Label) - - widget.ExtractButton.setIcon(extractor.ViewObject.Icon) - - extr_label = extr.get_extraction_dimension(extractor) - extr_label += " " + extr.get_extraction_type(extractor) - widget.ExtractButton.setText(extr_label) - - # connect actions. We add functions to widget, as well as the data we need, - # and use those as callback. This way every widget knows which objects to use - widget.STButton.clicked.connect(_ShowVisualization(st_object)) - widget.ExtractButton.clicked.connect(_ShowEditDialog(extractor, self._post_dialog, widget)) - widget.RemoveButton.clicked.connect(_DeleteExtractor(extractor, widget)) + widget = _SummaryWidget(st_object, extractor, self._post_dialog) + widget.delete.connect(self._delete_extraction) return widget + def _delete_extraction(self, extractor, widget): + # remove the document object + doc = extractor.Document + doc.removeObject(extractor.Name) + doc.recompute() + + # remove the widget + self._widgets.remove(widget) + widget.deleteLater() + def repopulate(self): # collect all links that are available and shows them diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 4ccef7018a..33ef4b4935 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief base objects for data extractors +from vtkmodules.vtkCommonCore import vtkIntArray +from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from . import base_fempythonobject @@ -117,6 +119,10 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): case _: return ["Not a vector"] + def get_representive_fieldname(self): + # should return the representive field name, e.g. Position (X) + return "" + class Extractor1D(Extractor): @@ -196,4 +202,179 @@ class Extractor1D(Extractor): self._setup_x_component_property(obj, point_data) + def _x_array_component_to_table(self, obj, array, table): + # extracts the component out of the array according to XComponent setting + # Note: Uses the array name unchanged + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray(); + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def _x_array_from_dataset(self, obj, dataset): + # extracts the relevant array from the dataset and returns a copy + + match obj.XField: + case "Index": + num = dataset.GetPoints().GetNumberOfPoints() + array = vtkIntArray() + array.SetNumberOfTuples(num) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i,i) + + case "Position": + orig_array = dataset.GetPoints().GetData() + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.XField) + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + return array + + def get_representive_fieldname(self, obj): + # representive field is the x field + label = obj.XField + if not label: + return "" + + if len(obj.getEnumerationsOfProperty("XComponent")) > 1: + label += f" ({obj.XComponent})" + + return label + +class Extractor2D(Extractor1D): + + ExtractionDimension = "2D" + + def __init__(self, obj): + super().__init__(obj) + + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="YField", + group="Y Data", + doc="The field to use as Y data", + value=[], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YComponent", + group="Y Data", + doc="Which part of the Y field vector to use for the Y axis", + value=[], + ), + ] + + return super()._get_properties() + prop + + + def onChanged(self, obj, prop): + + super().onChanged(obj, prop) + + if prop == "YField" and obj.Source and obj.Source.getDataSet(): + point_data = obj.Source.getDataSet().GetPointData() + self._setup_y_component_property(obj, point_data) + + if prop == "Source": + if obj.Source: + dset = obj.Source.getDataSet() + if dset: + self._setup_y_properties(obj, dset) + else: + self._clear_y_properties(obj) + else: + self._clear_y_properties(obj) + + def _setup_y_component_property(self, obj, point_data): + + if obj.YField == "Position": + obj.YComponent = self.component_options(3) + else: + array = point_data.GetAbstractArray(obj.YField) + obj.YComponent = self.component_options(array.GetNumberOfComponents()) + + def _clear_y_properties(self, obj): + if hasattr(obj, "YComponent"): + obj.YComponent = [] + if hasattr(obj, "YField"): + obj.YField = [] + + def _setup_y_properties(self, obj, dataset): + # Set all X Data properties correctly for the given dataset + fields = ["Position"] + point_data = dataset.GetPointData() + + for i in range(point_data.GetNumberOfArrays()): + fields.append(point_data.GetArrayName(i)) + + current_field = obj.YField + obj.YField = fields + if current_field in fields: + obj.YField = current_field + + self._setup_y_component_property(obj, point_data) + + def _y_array_component_to_table(self, obj, array, table): + # extracts the component out of the array according to XComponent setting + + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray(); + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("YComponent").index(obj.YComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def _y_array_from_dataset(self, obj, dataset): + # extracts the relevant array from the dataset and returns a copy + + match obj.YField: + case "Index": + num = dataset.GetPoints().GetNumberOfPoints() + array = vtkIntArray() + array.SetNumberOfTuples(num) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i,i) + + case "Position": + orig_array = dataset.GetPoints().GetData() + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.YField) + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + return array + + def get_representive_fieldname(self, obj): + # representive field is the y field + label = obj.YField + if not label: + return "" + + if len(obj.getEnumerationsOfProperty("YComponent")) > 1: + label += f" ({obj.YComponent})" + + return label diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 5a9404e149..f7a450c181 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -33,10 +33,7 @@ from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper -from vtkmodules.vtkCommonCore import vtkDoubleArray -from vtkmodules.vtkCommonCore import vtkIntArray from vtkmodules.vtkCommonDataModel import vtkTable -from vtkmodules.vtkCommonDataModel import vtkDataObject from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline class PostFieldData1D(base_fempostextractors.Extractor1D): @@ -60,44 +57,6 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): ] return super()._get_properties() + prop - def __array_to_table(self, obj, array, table): - if array.GetNumberOfComponents() == 1: - table.AddColumn(array) - else: - component_array = vtkDoubleArray(); - component_array.SetNumberOfComponents(1) - component_array.SetNumberOfTuples(array.GetNumberOfTuples()) - c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) - component_array.CopyComponent(0, array, c_idx) - component_array.SetName(array.GetName()) - table.AddColumn(component_array) - - def __array_from_dataset(self, obj, dataset): - # extracts the relevant array from the dataset and returns a copy - - match obj.XField: - case "Index": - num = dataset.GetPoints().GetNumberOfPoints() - array = vtkIntArray() - array.SetNumberOfTuples(num) - array.SetNumberOfComponents(1) - for i in range(num): - array.SetValue(i,i) - - case "Position": - orig_array = dataset.GetPoints().GetData() - array = vtkDoubleArray() - array.DeepCopy(orig_array) - - case _: - point_data = dataset.GetPointData() - orig_array = point_data.GetAbstractArray(obj.XField) - array = vtkDoubleArray() - array.DeepCopy(orig_array) - - return array - - def execute(self, obj): # on execution we populate the vtk table @@ -124,26 +83,26 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): if not frames: # get the dataset and extract the correct array - array = self.__array_from_dataset(obj, dataset) + array = self._x_array_from_dataset(obj, dataset) if array.GetNumberOfComponents() > 1: array.SetName(obj.XField + " (" + obj.XComponent + ")") else: array.SetName(obj.XField) - self.__array_to_table(obj, array, table) + self._x_array_component_to_table(obj, array, table) else: algo = obj.Source.getOutputAlgorithm() for timestep in timesteps: algo.UpdateTimeStep(timestep) dataset = algo.GetOutputDataObject(0) - array = self.__array_from_dataset(obj, dataset) + array = self._x_array_from_dataset(obj, dataset) if array.GetNumberOfComponents() > 1: array.SetName(f"{obj.XField} ({obj.XComponent}) - {timestep}") else: array.SetName(f"{obj.XField} - {timestep}") - self.__array_to_table(obj, array, table) + self._x_array_component_to_table(obj, array, table) # set the final table obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py new file mode 100644 index 0000000000..b25b809655 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -0,0 +1,153 @@ +# *************************************************************************** +# * 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 extractors 2D" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempostextractors +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline + +class PostFieldData2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction of two dimensional field data + """ + + ExtractionType = "Field" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop =[ _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc="Specify if the field shall be extracted for every available frame", + value=False, + ), + ] + return super()._get_properties() + prop + + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + frames = False + if obj.ExtractFrames: + # check if we have timesteps + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + frames = True + else: + FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + + if not frames: + # get the dataset and extract the correct array + xarray = self._x_array_from_dataset(obj, dataset) + if xarray.GetNumberOfComponents() > 1: + xarray.SetName(obj.XField + " (" + obj.XComponent + ")") + else: + xarray.SetName(obj.XField) + + self._x_array_component_to_table(obj, xarray, table) + + yarray = self._y_array_from_dataset(obj, dataset) + if yarray.GetNumberOfComponents() > 1: + yarray.SetName(obj.YField + " (" + obj.YComponent + ")") + else: + yarray.SetName(obj.YField) + + self._y_array_component_to_table(obj, yarray, table) + + else: + algo = obj.Source.getOutputAlgorithm() + for timestep in timesteps: + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + + xarray = self._x_array_from_dataset(obj, dataset) + if xarray.GetNumberOfComponents() > 1: + xarray.SetName(f"X - {obj.XField} ({obj.XComponent}) - {timestep}") + else: + xarray.SetName(f"X - {obj.XField} - {timestep}") + self._x_array_component_to_table(obj, xarray, table) + + yarray = self._y_array_from_dataset(obj, dataset) + if yarray.GetNumberOfComponents() > 1: + yarray.SetName(f"{obj.YField} ({obj.YComponent}) - {timestep}") + else: + yarray.SetName(f"{obj.YField} - {timestep}") + self._y_array_component_to_table(obj, yarray, table) + + # set the final table + obj.Table = table + + +class PostIndexData2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction of one dimensional index data + """ + + ExtractionType = "Index" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop =[ _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc="Specify if the data at the index should be extracted for each frame", + value=False, + ), + _PropHelper( + type="App::PropertyInteger", + name="XIndex", + group="X Data", + doc="Specify for which point index the data should be extracted", + value=0, + ), + ] + return super()._get_properties() + prop diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index df238c6e08..8cbd72b41c 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -21,13 +21,13 @@ # * * # *************************************************************************** -__title__ = "FreeCAD post line plot" +__title__ = "FreeCAD post histogram" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" ## @package post_histogram # \ingroup FEM -# \brief Post processing plot displaying lines +# \brief Post processing plot displaying histograms from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -46,7 +46,7 @@ from femguiutils import post_visualization post_visualization.register_visualization("Histogram", ":/icons/FEM_PostHistogram.svg", "ObjectsFem", - "makePostVtkHistogram") + "makePostHistogram") post_visualization.register_extractor("Histogram", "HistogramFieldData", @@ -54,7 +54,7 @@ post_visualization.register_extractor("Histogram", "1D", "Field", "ObjectsFem", - "makePostVtkHistogramFieldData") + "makePostHistogramFieldData") # Implementation diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index f8798fbc23..272aaea74c 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -32,41 +32,76 @@ __url__ = "https://www.freecad.org" from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper -# helper function to extract plot object type -def _get_extraction_subtype(obj): - if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): - return obj.Proxy.Type +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract2D - return "unknown" +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable -class PostLinePlot(base_fempythonobject.BaseFemPythonObject): +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization("Lineplot", + ":/icons/FEM_PostLineplot.svg", + "ObjectsFem", + "makePostLineplot") + +post_visualization.register_extractor("Lineplot", + "LineplotFieldData", + ":/icons/FEM_PostField.svg", + "2D", + "Field", + "ObjectsFem", + "makePostLineplotFieldData") + + +# Implementation +# ############## + +def is_lineplot_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Lineplot" + + +class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ - A post processing extraction for plotting lines + A 2D Field extraction for lineplot. + """ + VisualizationType = "Lineplot" + + + +class PostLineplot(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as line plots """ - Type = "App::FeaturePython" + VisualizationType = "Lineplot" def __init__(self, obj): super().__init__(obj) - obj.addExtension("App::GroupExtension") - self._setup_properties(obj) - - def _setup_properties(self, obj): - - self.ExtractionType = "LinePlot" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) + obj.addExtension("App::GroupExtensionPython") def _get_properties(self): - prop = [] - return prop + prop = [ + _PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the plotted data, two columns per lineplot (x,y)", + value=vtkTable(), + ), + ] + return super()._get_properties() + prop - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): def onChanged(self, obj, prop): @@ -75,137 +110,29 @@ class PostLinePlot(base_fempythonobject.BaseFemPythonObject): children = obj.Group for child in obj.Group: - if _get_extraction_subtype(child) not in ["Line"]: + if not is_lineplot_extractor(child): + FreeCAD.Console.PrintWarning(f"{child.Label} is not a data lineplot data extraction object, cannot be added") children.remove(child) if len(obj.Group) != len(children): obj.Group = children + def execute(self, obj): -class PostPlotLine(base_fempythonobject.BaseFemPythonObject): + # during execution we collect all child data into our table + table = vtkTable() + for child in obj.Group: - Type = "App::FeaturePython" + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + # TODO: check which array type it is and use that one + array = vtkDoubleArray() + array.DeepCopy(c_array) + array.SetName(f"{child.Source.Label}: {c_array.GetName()}") + table.AddColumn(array) - def __init__(self, obj): - super().__init__(obj) - self._setup_properties(obj) + obj.Table = table + return False - def _setup_properties(self, obj): - - self.ExtractionType = "Line" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) - - def _get_properties(self): - - prop = [ - _PropHelper( - type="App::PropertyLink", - name="Source", - group="Line", - doc="The data source, the line uses", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XField", - group="X Data", - doc="The field to use as X data", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XComponent", - group="X Data", - doc="Which part of the X field vector to use for the X axis", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YField", - group="Y Data", - doc="The field to use as Y data for the line plot", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YComponent", - group="Y Data", - doc="Which part of the Y field vector to use for the X axis", - value=None, - ), - ] - return prop - - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): - - def onChanged(self, obj, prop): - - if prop == "Source": - # check if the source is a Post object - if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): - FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") - obj.XField = [] - obj.YField = [] - obj.Source = None - - if prop == "XField": - if not obj.Source: - obj.XComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.XField): - obj.XComponent = [] - return - - match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: - case 1: - obj.XComponent = ["Not a vector"] - case 2: - obj.XComponent = ["Magnitude", "X", "Y"] - case 3: - obj.XComponent = ["Magnitude", "X", "Y", "Z"] - - if prop == "YField": - if not obj.Source: - obj.YComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.YField): - obj.YComponent = [] - return - - match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: - case 1: - obj.YComponent = ["Not a vector"] - case 2: - obj.YComponent = ["Magnitude", "X", "Y"] - case 3: - obj.YComponent = ["Magnitude", "X", "Y", "Z"] - - def onExecute(self, obj): - # we need to make sure that we show the correct fields to the user as option for data extraction - - fields = [] - if obj.Source: - point_data = obj.Source.Data.GetPointData() - fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] - - current_X = obj.XField - obj.XField = fields - if current_X in fields: - obj.XField = current_X - - current_Y = obj.YField - obj.YField = fields - if current_Y in fields: - obj.YField = current_Y - - return True diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py new file mode 100644 index 0000000000..f8798fbc23 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_table.py @@ -0,0 +1,211 @@ +# *************************************************************************** +# * 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 line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_lineplot +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +# helper function to extract plot object type +def _get_extraction_subtype(obj): + if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): + return obj.Proxy.Type + + return "unknown" + + +class PostLinePlot(base_fempythonobject.BaseFemPythonObject): + """ + A post processing extraction for plotting lines + """ + + Type = "App::FeaturePython" + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtension") + self._setup_properties(obj) + + def _setup_properties(self, obj): + + self.ExtractionType = "LinePlot" + + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.Name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + prop = [] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(self, obj): + + def onChanged(self, obj, prop): + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if _get_extraction_subtype(child) not in ["Line"]: + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + +class PostPlotLine(base_fempythonobject.BaseFemPythonObject): + + Type = "App::FeaturePython" + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + + self.ExtractionType = "Line" + + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.Name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + + prop = [ + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Line", + doc="The data source, the line uses", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XField", + group="X Data", + doc="The field to use as X data", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XComponent", + group="X Data", + doc="Which part of the X field vector to use for the X axis", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YField", + group="Y Data", + doc="The field to use as Y data for the line plot", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YComponent", + group="Y Data", + doc="Which part of the Y field vector to use for the X axis", + value=None, + ), + ] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(self, obj): + + def onChanged(self, obj, prop): + + if prop == "Source": + # check if the source is a Post object + if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): + FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") + obj.XField = [] + obj.YField = [] + obj.Source = None + + if prop == "XField": + if not obj.Source: + obj.XComponent = [] + return + + point_data = obj.Source.Data.GetPointData() + if not point_data.HasArray(obj.XField): + obj.XComponent = [] + return + + match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: + case 1: + obj.XComponent = ["Not a vector"] + case 2: + obj.XComponent = ["Magnitude", "X", "Y"] + case 3: + obj.XComponent = ["Magnitude", "X", "Y", "Z"] + + if prop == "YField": + if not obj.Source: + obj.YComponent = [] + return + + point_data = obj.Source.Data.GetPointData() + if not point_data.HasArray(obj.YField): + obj.YComponent = [] + return + + match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: + case 1: + obj.YComponent = ["Not a vector"] + case 2: + obj.YComponent = ["Magnitude", "X", "Y"] + case 3: + obj.YComponent = ["Magnitude", "X", "Y", "Z"] + + def onExecute(self, obj): + # we need to make sure that we show the correct fields to the user as option for data extraction + + fields = [] + if obj.Source: + point_data = obj.Source.Data.GetPointData() + fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] + + current_X = obj.XField + obj.XField = fields + if current_X in fields: + obj.XField = current_X + + current_Y = obj.YField + obj.YField = fields + if current_Y in fields: + obj.YField = current_Y + + return True + diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py index 5a56077c3e..6d29a305e8 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -48,7 +48,14 @@ class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): super().__init__(obj) # form is used to display individual task panels - self.form = obj.ViewObject.Proxy.get_edit_widgets(self) + app = obj.ViewObject.Proxy.get_app_edit_widget(self) + app.setWindowTitle("Data extraction") + app.setWindowIcon(obj.ViewObject.Icon) + view = obj.ViewObject.Proxy.get_view_edit_widget(self) + view.setWindowTitle("Visualization settings") + view.setWindowIcon(obj.ViewObject.Icon) + + self.form = [app, view] diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py new file mode 100644 index 0000000000..650cdd70a1 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -0,0 +1,163 @@ +# *************************************************************************** +# * 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 lineplot plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_lineplot +# \ingroup FEM +# \brief task panel for post lineplot plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + hbox = QtGui.QHBoxLayout() + self.data_widget.show_plot = QtGui.QPushButton() + self.data_widget.show_plot.setText("Show plot") + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText("Show data") + hbox.addWidget(self.data_widget.show_table) + vbox = QtGui.QVBoxLayout() + vbox.addItem(hbox) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle("Lineplot data") + + + # lineplot parameter widget + self.view_widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" + ) + self.view_widget.setWindowTitle("Lineplot view settings") + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget, self.view_widget] + + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_plot.clicked.connect(self.showPlot) + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + self._enumPropertyToCombobox(viewObj, "Scale", self.view_widget.Scale) + self.view_widget.Grid.setChecked(viewObj.Grid) + + self.view_widget.Title.setText(viewObj.Title) + self.view_widget.XLabel.setText(viewObj.XLabel) + self.view_widget.YLabel.setText(viewObj.YLabel) + + self.view_widget.LegendShow.setChecked(viewObj.Legend) + self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) + + + # connect callbacks + self.view_widget.Scale.activated.connect(self.scaleChanged) + self.view_widget.Grid.toggled.connect(self.gridChanged) + + self.view_widget.Title.editingFinished.connect(self.titleChanged) + self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged) + self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged) + + self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) + self.view_widget.LegendPos.activated.connect(self.legendPosChanged) + + + QtCore.Slot() + def showPlot(self): + self.obj.ViewObject.Proxy.show_visualization() + + QtCore.Slot() + def showTable(self): + + # TODO: make data model update when object is recomputed + data_model = vtk_table_view.VtkTableModel() + data_model.setTable(self.obj.Table) + + dialog = QtGui.QDialog(self.data_widget) + widget = vtk_table_view.VtkTableView(data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0,0,0,0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + + QtCore.Slot(int) + def scaleChanged(self, idx): + self.obj.ViewObject.Scale = idx + + QtCore.Slot(bool) + def gridChanged(self, state): + self.obj.ViewObject.Grid = state + + QtCore.Slot() + def titleChanged(self): + self.obj.ViewObject.Title = self.view_widget.Title.text() + + QtCore.Slot() + def xLabelChanged(self): + self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() + + QtCore.Slot() + def yLabelChanged(self): + self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() + + QtCore.Slot(int) + def legendPosChanged(self, idx): + self.obj.ViewObject.LegendLocation = idx + + QtCore.Slot(bool) + def legendShowChanged(self, state): + self.obj.ViewObject.Legend = state diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_post_extract.py index c75dd4bc8b..b91c65cb45 100644 --- a/src/Mod/Fem/femviewprovider/view_post_extract.py +++ b/src/Mod/Fem/femviewprovider/view_post_extract.py @@ -109,18 +109,23 @@ class VPPostExtractor: # of the object return {} - def get_edit_widgets(self, post_dialog): - # Returns a list of widgets for editing the object/viewprovider. + def get_app_edit_widget(self, post_dialog): + # Returns a widgets for editing the object (not viewprovider!) # The widget will be part of the provided post_dialog, and # should use its functionality to inform of changes. raise FreeCAD.Base.FreeCADError("Not implemented") - def get_preview_widget(self, post_dialog): - # Returns a widget for editing the object/viewprovider. + def get_view_edit_widget(self, post_dialog): + # Returns a widgets for editing the viewprovider (not object!) # The widget will be part of the provided post_dialog, and # should use its functionality to inform of changes. raise FreeCAD.Base.FreeCADError("Not implemented") + def get_preview(self): + # Returns the preview tuple of icon and label: (QPixmap, str) + # Note: QPixmap in ratio 2:1 + raise FreeCAD.Base.FreeCADError("Not implemented") + def dumps(self): return None diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 5a433f17bc..8fe2fa6b4c 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -232,12 +232,14 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): def getIcon(self): return ":/icons/FEM_PostField.svg" - def get_edit_widgets(self, post_dialog): - return [ EditAppWidget(self.Object, post_dialog), - EditViewWidget(self.Object, post_dialog)] + def get_app_edit_widget(self, post_dialog): + return EditAppWidget(self.Object, post_dialog) - def get_preview_widget(self, post_dialog): - return QtGui.QComboBox() + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + return (QtGui.QPixmap(), self.ViewObject.Legend) def get_kw_args(self): # builds kw args from the properties diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 0ce5ec8954..54fe5439f3 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -1,4 +1,3 @@ - # *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * @@ -33,39 +32,445 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui +import Plot import FemGui -from PySide import QtGui +from PySide import QtGui, QtCore + +import io +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_post_extract +from . import view_base_fempostvisualization +from femtaskpanels import task_post_lineplot + +_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + vobj = self._object.ViewObject + + self.widget.Legend.setText(vobj.Legend) + self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) + self._post_dialog._enumPropertyToCombobox(vobj, "MarkerStyle", self.widget.MarkerStyle) + self.widget.LineWidth.setValue(vobj.LineWidth) + self.widget.MarkerSize.setValue(vobj.MarkerSize) + self.widget.Color.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.Color])) + + self.widget.Legend.editingFinished.connect(self.legendChanged) + self.widget.MarkerStyle.activated.connect(self.markerStyleChanged) + self.widget.LineStyle.activated.connect(self.lineStyleChanged) + self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged) + self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) + self.widget.Color.changed.connect(self.colorChanged) + + @QtCore.Slot() + def colorChanged(self): + color = self.widget.Color.property("color") + self._object.ViewObject.Color = color.getRgb() + + @QtCore.Slot(float) + def lineWidthChanged(self, value): + self._object.ViewObject.LineWidth = value + + @QtCore.Slot(float) + def markerSizeChanged(self, value): + self._object.ViewObject.MarkerSize = value + + @QtCore.Slot(int) + def markerStyleChanged(self, index): + self._object.ViewObject.MarkerStyle = index + + @QtCore.Slot(int) + def lineStyleChanged(self, index): + self._object.ViewObject.LineStyle = index + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Legend = self.widget.Legend.text() -class VPPostLinePlot: +class EditAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.XField) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.XField.activated.connect(self.xFieldChanged) + self.widget.XComponent.activated.connect(self.xComponentChanged) + self.widget.YField.activated.connect(self.yFieldChanged) + self.widget.YComponent.activated.connect(self.yComponentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def xFieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def xComponentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yFieldChanged(self, index): + self._object.YField = index + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yComponentChanged(self, index): + self._object.YComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): """ - A View Provider for the Post LinePlot object + A View Provider for extraction of 1D field data specialy for histograms """ def __init__(self, vobj): + super().__init__(vobj) vobj.Proxy = self + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Legend", + group="Lineplot", + doc="The name used in the plots legend", + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="Color", + group="Lineplot", + doc="The color the line and the markers are drawn with", + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LineStyle", + group="Lineplot", + doc="The style the line is drawn in", + value=['-', '--', '-.', ':', 'None'], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="Lineplot", + doc="The width the line is drawn with", + value=(1, 0.1, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="MarkerStyle", + group="Lineplot", + doc="The style the data markers are drawn with", + value=['None', '*', '+', 's', '.', 'o', 'x'], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="MarkerSize", + group="Lineplot", + doc="The size the data markers are drawn in", + value=(10, 0.1, 99, 0.1), + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + # Returns the preview tuple of icon and label: (QPixmap, str) + # Note: QPixmap in ratio 2:1 + + fig = plt.figure(figsize=(0.2,0.1), dpi=1000) + ax = plt.Axes(fig, [0., 0., 1., 1.]) + ax.set_axis_off() + fig.add_axes(ax) + kwargs = self.get_kw_args() + kwargs["markevery"] = [1] + ax.plot([0,0.5,1],[0.5,0.5,0.5], **kwargs) + data = io.BytesIO() + plt.savefig(data, bbox_inches=0, transparent=True) + plt.close() + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data.getvalue()) + + return (pixmap, self.ViewObject.Legend) + + + def get_kw_args(self): + # builds kw args from the properties + kwargs = {} + + # colors need a workaround, some error occurs with rgba tuple + kwargs["color"] = self.ViewObject.Color + kwargs["markeredgecolor"] = self.ViewObject.Color + kwargs["markerfacecolor"] = self.ViewObject.Color + kwargs["linestyle"] = self.ViewObject.LineStyle + kwargs["linewidth"] = self.ViewObject.LineWidth + kwargs["marker"] = self.ViewObject.MarkerStyle + kwargs["markersize"] = self.ViewObject.MarkerSize + return kwargs + + +class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Lineplot plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Grid", + group="Lineplot", + doc="If be the bars shoud show the cumulative sum left to rigth", + value=False, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Scale", + group="Lineplot", + doc="The scale the axis are drawn in", + value=["linear","semi-log x", "semi-log y", "log"], + ), + _GuiPropHelper( + type="App::PropertyString", + name="Title", + group="Plot", + doc="The histogram plot title", + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc="The label shown for the histogram X axis", + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc="The label shown for the histogram Y axis", + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc="Determines if the legend is plotted", + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LegendLocation", + group="Plot", + doc="Determines if the legend is plotted", + value=['best','upper right','upper left','lower left','lower right','right', + 'center left','center right','lower center','upper center','center'], + ), + + ] + return prop + def getIcon(self): return ":/icons/FEM_PostLineplot.svg" - def setEdit(self, vobj, mode): - # make sure we see what we edit - vobj.show() + def doubleClicked(self,vobj): + + self.show_visualization() + super().doubleClicked(vobj) + + def setEdit(self, vobj, mode): # build up the task panel - #taskd = task_post_glyphfilter._TaskPanel(vobj) + taskd = task_post_lineplot._TaskPanel(vobj) #show it - #FreeCADGui.Control.showDialog(taskd) + FreeCADGui.Control.showDialog(taskd) return True - def unsetEdit(self, vobj, mode): - FreeCADGui.Control.closeDialog() - return True + + def show_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + self._plot = Plot.Plot() + self._dialog = QtGui.QDialog(Plot.getMainWindow()) + box = QtGui.QVBoxLayout() + box.addWidget(self._plot) + self._dialog.setLayout(box) + + self.drawPlot() + self._dialog.show() + + def get_kw_args(self, obj): + view = obj.ViewObject + if not view or not hasattr(view, "Proxy"): + return {} + if not hasattr(view.Proxy, "get_kw_args"): + return {} + return view.Proxy.get_kw_args() + + def drawPlot(self): + + if not hasattr(self, "_plot") or not self._plot: + return + + self._plot.axes.clear() + + # we do not iterate the table, but iterate the children. This makes it possible + # to attribute the correct styles + for child in self.Object.Group: + + table = child.Table + kwargs = self.get_kw_args(child) + + # iterate over the table and plot all (note: column 0 is always X!) + color_factor = np.linspace(1,0.5,int(table.GetNumberOfColumns()/2)) + legend_multiframe = table.GetNumberOfColumns() > 2 + + for i in range(0,table.GetNumberOfColumns(),2): + + # add the kw args, with some slide change over color for multiple frames + tmp_args = {} + for key in kwargs: + if "color" in key: + value = np.array(kwargs[key])*color_factor[int(i/2)] + tmp_args[key] = mpl.colors.to_hex(value) + else: + tmp_args[key] = kwargs[key] + + xdata = VTKArray(table.GetColumn(i)) + ydata = VTKArray(table.GetColumn(i+1)) + + # legend labels + if child.ViewObject.Legend: + if not legend_multiframe: + label = child.ViewObject.Legend + else: + postfix = table.GetColumnName(i+1).split("-")[-1] + label = child.ViewObject.Legend + " - " + postfix + else: + legend_prefix = "" + if len(self.Object.Group) > 1: + legend_prefix = child.Source.Label + ": " + label = legend_prefix + table.GetColumnName(i+1) + + match self.ViewObject.Scale: + case "log": + self._plot.axes.loglog(xdata, ydata, **tmp_args, label=label) + case "semi-log x": + self._plot.axes.semilogx(xdata, ydata, **tmp_args, label=label) + case "semi-log y": + self._plot.axes.semilogy(xdata, ydata, **tmp_args, label=label) + case _: + self._plot.axes.plot(xdata, ydata, **tmp_args, label=label) + + if self.ViewObject.Title: + self._plot.axes.set_title(self.ViewObject.Title) + if self.ViewObject.XLabel: + self._plot.axes.set_xlabel(self.ViewObject.XLabel) + if self.ViewObject.YLabel: + self._plot.axes.set_ylabel(self.ViewObject.YLabel) + + if self.ViewObject.Legend and self.Object.Group: + self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + + self._plot.axes.grid(self.ViewObject.Grid) + + self._plot.update() + + + def updateData(self, obj, prop): + # we only react if the table changed, as then know that new data is available + if prop == "Table": + self.drawPlot() + + + def onChanged(self, vobj, prop): + + # for all property changes we need to redraw the plot + self.drawPlot() + + def childViewPropertyChanged(self, vobj, prop): + + # on of our extractors has a changed view property. + self.drawPlot() def dumps(self): return None + def loads(self, state): return None From d3fa7ad8f072761908194362cd19e96fcf6853ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sat, 19 Apr 2025 11:23:04 +0200 Subject: [PATCH 03/27] FEM: Add index over frames visualizations --- src/Mod/Fem/Gui/CMakeLists.txt | 3 +- .../ui/PostHistogramFieldViewEdit.ui | 41 ++--- .../Resources/ui/PostHistogramIndexAppEdit.ui | 81 +++++++++ .../Resources/ui/PostLineplotFieldAppEdit.ui | 4 +- .../Resources/ui/PostLineplotFieldViewEdit.ui | 81 +++++---- .../Resources/ui/PostLineplotIndexAppEdit.ui | 85 ++++++++++ src/Mod/Fem/Gui/TaskPostBoxes.cpp | 19 +++ src/Mod/Fem/Gui/TaskPostBoxes.h | 10 ++ src/Mod/Fem/Gui/TaskPostExtraction.cpp | 18 ++ src/Mod/Fem/Gui/TaskPostExtraction.h | 1 + src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 1 + src/Mod/Fem/ObjectsFem.py | 33 +++- src/Mod/Fem/femcommands/commands.py | 2 +- src/Mod/Fem/femguiutils/data_extraction.py | 11 ++ src/Mod/Fem/femguiutils/extract_link_view.py | 72 ++++---- .../Fem/femobjects/base_fempostextractors.py | 48 ++++-- src/Mod/Fem/femobjects/post_extract1D.py | 71 ++++++-- src/Mod/Fem/femobjects/post_extract2D.py | 89 ++++++++-- src/Mod/Fem/femobjects/post_histogram.py | 14 +- src/Mod/Fem/femobjects/post_lineplot.py | 14 ++ .../Fem/femtaskpanels/task_post_histogram.py | 3 + .../Fem/femtaskpanels/task_post_lineplot.py | 3 + .../femviewprovider/view_post_histogram.py | 154 ++++++++++++++++-- .../Fem/femviewprovider/view_post_lineplot.py | 122 ++++++++++++-- src/Mod/Plot/Plot.py | 7 +- 25 files changed, 804 insertions(+), 183 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index e1523956ea..7337afd833 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -445,11 +445,12 @@ SET(FemGuiPythonUI_SRCS Resources/ui/TaskPostExtraction.ui Resources/ui/TaskPostHistogram.ui Resources/ui/TaskPostLineplot.ui - Resources/ui/PostExtractionSummaryWidget.ui Resources/ui/PostHistogramFieldViewEdit.ui Resources/ui/PostHistogramFieldAppEdit.ui + Resources/ui/PostHistogramIndexAppEdit.ui Resources/ui/PostLineplotFieldViewEdit.ui Resources/ui/PostLineplotFieldAppEdit.ui + Resources/ui/PostLineplotIndexAppEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui index 744e5a6240..5fe4a7d3dc 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -6,8 +6,8 @@ 0 0 - 293 - 126 + 278 + 110 @@ -68,7 +68,7 @@ - + 0 0 @@ -93,7 +93,7 @@ - + 0 0 @@ -113,10 +113,20 @@ + + + + + + + Legend: + + + - + - + 0 0 @@ -127,7 +137,7 @@ - + 0 @@ -139,27 +149,10 @@ - - - - - - - Legend: - - -
- - - Gui::ColorButton - QPushButton -
Gui/Widgets.h
-
-
Legend BarColor diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui new file mode 100644 index 0000000000..e9dd2a2b3d --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui @@ -0,0 +1,81 @@ + + + Form + + + + 0 + 0 + 261 + 110 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui index 00b8ab4f55..b0d1830852 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui @@ -6,8 +6,8 @@ 0 0 - 296 - 186 + 271 + 174 diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui index 720eb96c6e..f197016d12 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui @@ -6,8 +6,8 @@ 0 0 - 335 - 124 + 274 + 114 @@ -28,25 +28,6 @@ - - - - - 0 - 0 - - - - Width of all lines (outline and hatch) - - - 99.000000000000000 - - - 0.100000000000000 - - - @@ -57,7 +38,7 @@ - + 0 0 @@ -107,18 +88,8 @@ - - - - 99.000000000000000 - - - 0.100000000000000 - - - - + 0 @@ -130,24 +101,50 @@ + + + + + 0 + 0 + + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + +
- - - Gui::ColorButton - QPushButton -
Gui/Widgets.h
-
-
Legend Color LineStyle - LineWidth MarkerStyle - MarkerSize diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui new file mode 100644 index 0000000000..ba4ab0ead3 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui @@ -0,0 +1,85 @@ + + + Form + + + + 0 + 0 + 310 + 108 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Y Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + 99999999 + + + + + + + Index + YField + YComponent + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index d880f73d33..1a2c244943 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -408,6 +408,21 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) } } +void TaskDlgPost::processCollapsedWidgets() { + + 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)) { + if(post_widget->initiallyCollapsed()) { + post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); + task_box->hideGroupBox(); + } + } + } + } +} // *************************************************************************** // box to set the coloring @@ -571,6 +586,10 @@ void TaskPostFrames::applyPythonCode() // we apply the views widgets python code } +bool TaskPostFrames::initiallyCollapsed() { + + return (ui->FrameTable->rowCount() == 0); +} // *************************************************************************** // in the following, the different filters sorted alphabetically diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index 9b24eb314f..3c60fe8ddf 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -156,6 +156,11 @@ public: // executed when the apply button is pressed in the task dialog virtual void apply() {}; + // returns if the widget shall be collapsed when opening the task dialog + virtual bool initiallyCollapsed() { + return false; + }; + protected: App::DocumentObject* getObject() const { @@ -235,6 +240,9 @@ public: /// returns for Close and Help button QDialogButtonBox::StandardButtons getStandardButtons() const override; + /// makes sure all widgets are collapsed, if they want to be + void processCollapsedWidgets(); + protected: void recompute(); @@ -300,6 +308,8 @@ public: void applyPythonCode() override; + bool initiallyCollapsed() override; + private: void setupConnections(); void onSelectionChanged(); diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index ef70109462..57f39a70a2 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -166,4 +166,22 @@ void TaskPostExtraction::apply() } } +bool TaskPostExtraction::initiallyCollapsed() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("initiallyCollapsed"))) { + Py::Callable method(m_panel.getAttr(std::string("initiallyCollapsed"))); + auto result = Py::Boolean(method.apply()); + return result.as_bool(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } + + return false; +} + #include "moc_TaskPostExtraction.cpp" diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.h b/src/Mod/Fem/Gui/TaskPostExtraction.h index 5423a83d00..5fe2518760 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.h +++ b/src/Mod/Fem/Gui/TaskPostExtraction.h @@ -56,6 +56,7 @@ protected: bool isGuiTaskOnly() override; void apply() override; void onPostDataChanged(Fem::FemPostObject* obj) override; + bool initiallyCollapsed() override; private: Py::Object m_panel; diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index bc4dd1d953..2a34070c72 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -1007,6 +1007,7 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) postDlg = new TaskDlgPost(this); setupTaskDialog(postDlg); postDlg->connectSlots(); + postDlg->processCollapsedWidgets(); Gui::Control().showDialog(postDlg); } diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c01cc8f8e6..d40558f4bd 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -699,6 +699,7 @@ def makePostLineplot(doc, name="Lineplot"): view_post_lineplot.VPPostLineplot(obj.ViewObject) return obj + def makePostLineplotFieldData(doc, name="FieldData2D"): """makePostLineplotFieldData(document, [name]): creates a FEM post processing data extractor for 2D Field data @@ -712,6 +713,21 @@ def makePostLineplotFieldData(doc, name="FieldData2D"): view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) return obj + +def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): + """makePostLineplotIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 2D index data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLineplotIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotIndexOverFrames(obj.ViewObject) + return obj + + def makePostHistogram(doc, name="Histogram"): """makePostHistogram(document, [name]): creates a FEM post processing histogram plot @@ -725,8 +741,9 @@ def makePostHistogram(doc, name="Histogram"): view_post_histogram.VPPostHistogram(obj.ViewObject) return obj + def makePostHistogramFieldData(doc, name="FieldData1D"): - """makePostHistogramFieldData1D(document, [name]): + """makePostHistogramFieldData(document, [name]): creates a FEM post processing data extractor for 1D Field data """ obj = doc.addObject("App::FeaturePython", name) @@ -739,6 +756,20 @@ def makePostHistogramFieldData(doc, name="FieldData1D"): return obj +def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostHistogramIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogramIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramIndexOverFrames(obj.ViewObject) + 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 befcd48f30..f4edc1586e 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1291,6 +1291,6 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) # setup all visualization commands (register by importing) - import femobjects.post_histogram import femobjects.post_lineplot + import femobjects.post_histogram post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 4eeffbcef4..23a1bb784b 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -40,11 +40,13 @@ from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents import FreeCAD import FreeCADGui +import femobjects.base_fempostextractors as extr from femtaskpanels.base_fempostpanel import _BasePostTaskPanel from . import extract_link_view ExtractLinkView = extract_link_view.ExtractLinkView + class DataExtraction(_BasePostTaskPanel): # The class is not a widget itself, but provides a widget. It implements # all required callbacks for the widget and the task dialog. @@ -137,3 +139,12 @@ class DataExtraction(_BasePostTaskPanel): def apply(self): pass + + def initiallyCollapsed(self): + # if we do not have any extractions to show we hide initially to remove clutter + + for obj in self.Object.InList: + if extr.is_extractor_object(obj): + return False + + return True diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 9c984b537e..e1611f8609 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -198,16 +198,10 @@ class _SettingsPopup(QtGui.QGroupBox): close = QtCore.Signal() - def __init__(self, setting): - - toplevel = QtGui.QApplication.topLevelWidgets() - for i in toplevel: - if i.metaObject().className() == "Gui::MainWindow": - main = i - break - - super().__init__(main) + def __init__(self, setting, parent): + super().__init__(parent) + self.setWindowFlags(QtGui.Qt.Popup) self.setFocusPolicy(QtGui.Qt.ClickFocus) vbox = QtGui.QVBoxLayout() @@ -217,20 +211,22 @@ class _SettingsPopup(QtGui.QGroupBox): buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) vbox.addWidget(buttonBox) - buttonBox.accepted.connect(self.accept) + buttonBox.accepted.connect(self.hide) self.setLayout(vbox) - @QtCore.Slot() - def accept(self): - self.close.emit() - def showEvent(self, event): + # required to get keyboard events self.setFocus() - def keyPressEvent(self, event): - if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: - self.accept() + def hideEvent(self, event): + # emit on hide: this happens for OK button as well as + # "click away" closing of the popup + self.close.emit() + def keyPressEvent(self, event): + # close on hitting enter + if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: + self.hide() class _SummaryWidget(QtGui.QWidget): @@ -284,14 +280,12 @@ class _SummaryWidget(QtGui.QWidget): # make sure initial drawing happened self._redraw() + def _button(self, text): btn = QtGui.QPushButton(self) btn.full_text = text - #size = btn.sizeHint() - #size.setWidth(size.width()*2) btn.setMinimumSize(btn.sizeHint()) - btn.setFlat(True) btn.setText(text) btn.setStyleSheet("text-align:left;padding:6px"); @@ -313,7 +307,7 @@ class _SummaryWidget(QtGui.QWidget): for i, btn in enumerate(btns): btn_size = btn_total_size*btn_rel_size[i] - txt_size = btn_size - btn.iconSize().width() - btn_margin/2*3 + txt_size = btn_size - btn.iconSize().width() - btn_margin # we elide only if there is enough space for a meaningful text if txt_size >= min_text_width: @@ -355,24 +349,28 @@ class _SummaryWidget(QtGui.QWidget): def _position_dialog(self, dialog): - main = dialog.parent() - list_widget = self.parent().parent().parent() - widget_rect = list_widget.geometry() - diag_size = dialog.sizeHint() - # default is towards main window center - if main.geometry().center().x() >= list_widget.mapToGlobal(widget_rect.center()).x(): - rigth_point = list_widget.mapToGlobal(widget_rect.topRight()) - dialog.setGeometry(QtCore.QRect(rigth_point, diag_size)) - else: - left_point = list_widget.mapToGlobal(widget_rect.topLeft()) - left_point -= QtCore.QPoint(diag_size.width(), 0) - dialog.setGeometry(QtCore.QRect(left_point, diag_size)) + # the scroll area does mess the mapping to global up, somehow + # the transformation from the widget ot the scroll area gives + # very weird values. Hence we build the coords of the widget + # ourself + + summary = dialog.parent() # == self + base_widget = summary.parent() + viewport = summary.parent() + scroll = viewport.parent() + + top_left = summary.geometry().topLeft() + base_widget.geometry().topLeft() + viewport.geometry().topLeft() + delta = (summary.width() - dialog.sizeHint().width())/2 + local_point = QtCore.QPoint(top_left.x()+delta, top_left.y()+summary.height()) + global_point = scroll.mapToGlobal(local_point) + + dialog.setGeometry(QtCore.QRect(global_point, dialog.sizeHint())) @QtCore.Slot() def editApp(self): if not hasattr(self, "appDialog"): widget = self._extractor.ViewObject.Proxy.get_app_edit_widget(self._post_dialog) - self.appDialog = _SettingsPopup(widget) + self.appDialog = _SettingsPopup(widget, self) self.appDialog.close.connect(self.appAccept) if not self.appDialog.isVisible(): @@ -386,7 +384,7 @@ class _SummaryWidget(QtGui.QWidget): if not hasattr(self, "viewDialog"): widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog) - self.viewDialog = _SettingsPopup(widget) + self.viewDialog = _SettingsPopup(widget, self) self.viewDialog.close.connect(self.viewAccept) if not self.viewDialog.isVisible(): @@ -402,8 +400,6 @@ class _SummaryWidget(QtGui.QWidget): @QtCore.Slot() def viewAccept(self): - self.viewDialog.hide() - # update the preview extr_repr = self._extractor.ViewObject.Proxy.get_preview() self.viewButton.setIcon(extr_repr[0]) @@ -414,8 +410,6 @@ class _SummaryWidget(QtGui.QWidget): @QtCore.Slot() def appAccept(self): - self.appDialog.hide() - # update the preview extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) self.extrButton.full_text = extr_label diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 33ef4b4935..9e4ddac24f 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -116,6 +116,8 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): return ["X", "Y"] case 3: return ["X", "Y", "Z"] + case 6: + return ["XX", "YY", "ZZ", "XY", "XZ", "YZ"] case _: return ["Not a vector"] @@ -217,11 +219,13 @@ class Extractor1D(Extractor): component_array.SetName(array.GetName()) table.AddColumn(component_array) - def _x_array_from_dataset(self, obj, dataset): + def _x_array_from_dataset(self, obj, dataset, copy=True): # extracts the relevant array from the dataset and returns a copy + # indices = None uses all indices, otherwise the values in this list match obj.XField: case "Index": + # index needs always to be build, ignore copy argument num = dataset.GetPoints().GetNumberOfPoints() array = vtkIntArray() array.SetNumberOfTuples(num) @@ -230,15 +234,22 @@ class Extractor1D(Extractor): array.SetValue(i,i) case "Position": + orig_array = dataset.GetPoints().GetData() - array = vtkDoubleArray() - array.DeepCopy(orig_array) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array case _: point_data = dataset.GetPointData() orig_array = point_data.GetAbstractArray(obj.XField) - array = vtkDoubleArray() - array.DeepCopy(orig_array) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array return array @@ -343,28 +354,29 @@ class Extractor2D(Extractor1D): component_array.SetName(array.GetName()) table.AddColumn(component_array) - def _y_array_from_dataset(self, obj, dataset): + def _y_array_from_dataset(self, obj, dataset, copy=True): # extracts the relevant array from the dataset and returns a copy + # indices = None uses all indices, otherwise the values in this list match obj.YField: - case "Index": - num = dataset.GetPoints().GetNumberOfPoints() - array = vtkIntArray() - array.SetNumberOfTuples(num) - array.SetNumberOfComponents(1) - for i in range(num): - array.SetValue(i,i) - case "Position": + orig_array = dataset.GetPoints().GetData() - array = vtkDoubleArray() - array.DeepCopy(orig_array) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array case _: point_data = dataset.GetPointData() orig_array = point_data.GetAbstractArray(obj.YField) - array = vtkDoubleArray() - array.DeepCopy(orig_array) + + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array return array diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index f7a450c181..3540b6706a 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -33,6 +33,7 @@ from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper +from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline @@ -108,7 +109,7 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): obj.Table = table -class PostIndexData1D(base_fempostextractors.Extractor1D): +class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): """ A post processing extraction of one dimensional index data """ @@ -119,19 +120,67 @@ class PostIndexData1D(base_fempostextractors.Extractor1D): super().__init__(obj) def _get_properties(self): - prop =[ _PropHelper( - type="App::PropertyBool", - name="ExtractFrames", - group="Multiframe", - doc="Specify if the data at the index should be extracted for each frame", - value=False, - ), - _PropHelper( + prop =[_PropHelper( type="App::PropertyInteger", - name="XIndex", + name="Index", group="X Data", - doc="Specify for which point index the data should be extracted", + doc="Specify for which index the data should be extracted", value=0, ), ] return super()._get_properties() + prop + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + # check if we have timesteps (required!) + abort = True + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + if len(timesteps) > 1: + abort = False + + if abort: + FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") + obj.Table = table + return + + algo = obj.Source.getOutputAlgorithm() + setup = False + frame_array = vtkDoubleArray() + + idx = obj.Index + for i, timestep in enumerate(timesteps): + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset, copy=False) + + if not setup: + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_array.SetTuple(i, idx, array) + + if frame_array.GetNumberOfComponents() > 1: + frame_array.SetName(f"{obj.XField} ({obj.XComponent})") + else: + frame_array.SetName(f"{obj.XField}") + + self._x_array_component_to_table(obj, frame_array, table) + + # set the final table + obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index b25b809655..60c9ac2df2 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -33,6 +33,7 @@ from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper +from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline @@ -124,9 +125,9 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): obj.Table = table -class PostIndexData2D(base_fempostextractors.Extractor2D): +class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): """ - A post processing extraction of one dimensional index data + A post processing extraction for two dimensional data with X always being the frames """ ExtractionType = "Index" @@ -135,19 +136,83 @@ class PostIndexData2D(base_fempostextractors.Extractor2D): super().__init__(obj) def _get_properties(self): - prop =[ _PropHelper( - type="App::PropertyBool", - name="ExtractFrames", - group="Multiframe", - doc="Specify if the data at the index should be extracted for each frame", - value=False, - ), - _PropHelper( + prop =[_PropHelper( type="App::PropertyInteger", - name="XIndex", - group="X Data", + name="Index", + group="Data", doc="Specify for which point index the data should be extracted", value=0, ), ] return super()._get_properties() + prop + + def _setup_x_component_property(self, obj, point_data): + # override to only allow "Frames" as X data + obj.XComponent = ["Not a vector"] + + def _setup_x_properties(self, obj, dataset): + # override to only allow "Frames" as X data + obj.XField = ["Frames"] + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + # check if we have timesteps (required!) + abort = True + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + if len(timesteps) > 1: + abort = False + + if abort: + FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") + obj.Table = table + return + + algo = obj.Source.getOutputAlgorithm() + + frame_x_array = vtkDoubleArray() + frame_x_array.SetNumberOfTuples(len(timesteps)) + frame_x_array.SetNumberOfComponents(1) + + + frame_y_array = vtkDoubleArray() + idx = obj.Index + setup = False + for i, timestep in enumerate(timesteps): + + frame_x_array.SetTuple1(i, timestep) + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._y_array_from_dataset(obj, dataset, copy=False) + if not setup: + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_y_array.SetTuple(i, idx, array) + + frame_x_array.SetName("Frames") + if frame_y_array.GetNumberOfComponents() > 1: + frame_y_array.SetName(f"{obj.YField} ({obj.YComponent})") + else: + frame_y_array.SetName(obj.YField) + + table.AddColumn(frame_x_array) + self._y_array_component_to_table(obj, frame_y_array, table) + + # set the final table + obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index 8cbd72b41c..bdcb4ad553 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -57,6 +57,14 @@ post_visualization.register_extractor("Histogram", "makePostHistogramFieldData") +post_visualization.register_extractor("Histogram", + "HistogramIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostHistogramIndexOverFrames") + # Implementation # ############## @@ -77,7 +85,11 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D): """ VisualizationType = "Histogram" - +class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for histogram. + """ + VisualizationType = "Histogram" class PostHistogram(base_fempostvisualizations.PostVisualization): diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 272aaea74c..06f844ebac 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -56,6 +56,14 @@ post_visualization.register_extractor("Lineplot", "ObjectsFem", "makePostLineplotFieldData") +post_visualization.register_extractor("Lineplot", + "LineplotIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "2D", + "Index", + "ObjectsFem", + "makePostLineplotIndexOverFrames") + # Implementation # ############## @@ -77,6 +85,12 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ VisualizationType = "Lineplot" +class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): + """ + A 2D index extraction for lineplot. + """ + VisualizationType = "Lineplot" + class PostLineplot(base_fempostvisualizations.PostVisualization): diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py index 593f177a94..79b4e4eeab 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_histogram.py +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -64,6 +64,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setLayout(vbox) self.data_widget.setWindowTitle("Histogram data") + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) # histogram parameter widget @@ -71,6 +72,8 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" ) self.view_widget.setWindowTitle("Histogram view settings") + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) + self.__init_widgets() # form made from param and selection widget diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py index 650cdd70a1..507e6b3cbf 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -64,6 +64,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setLayout(vbox) self.data_widget.setWindowTitle("Lineplot data") + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) # lineplot parameter widget @@ -71,6 +72,8 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" ) self.view_widget.setWindowTitle("Lineplot view settings") + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) + self.__init_widgets() # form made from param and selection widget diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 8fe2fa6b4c..777e3d15c0 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -36,6 +36,7 @@ import Plot import FemGui from PySide import QtGui, QtCore +import io import numpy as np import matplotlib as mpl @@ -73,25 +74,59 @@ class EditViewWidget(QtGui.QWidget): self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) self.widget.LineWidth.setValue(vobj.LineWidth) self.widget.HatchDensity.setValue(vobj.HatchDensity) - self.widget.BarColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.BarColor])) - self.widget.LineColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.LineColor])) + + # setup the color buttons (don't use FreeCADs color button, as this does not work in popups!) + self._setup_color_button(self.widget.BarColor, vobj.BarColor, self.barColorChanged) + self._setup_color_button(self.widget.LineColor, vobj.LineColor, self.lineColorChanged) self.widget.Legend.editingFinished.connect(self.legendChanged) self.widget.Hatch.activated.connect(self.hatchPatternChanged) self.widget.LineStyle.activated.connect(self.lineStyleChanged) self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged) self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) - self.widget.LineColor.changed.connect(self.lineColorChanged) - self.widget.BarColor.changed.connect(self.barColorChanged) - @QtCore.Slot() - def lineColorChanged(self): - color = self.widget.LineColor.property("color") + # sometimes wierd sizes occur with spinboxes + self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height()) + self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) + + + def _setup_color_button(self, button, fcColor, callback): + + barColor = QtGui.QColor(*[v*255 for v in fcColor]) + icon_size = button.iconSize() + icon_size.setWidth(icon_size.width()*2) + button.setIconSize(icon_size) + pixmap = QtGui.QPixmap(icon_size) + pixmap.fill(barColor) + button.setIcon(pixmap) + + action = QtGui.QWidgetAction(button) + diag = QtGui.QColorDialog(barColor, parent=button) + diag.accepted.connect(action.trigger) + diag.rejected.connect(action.trigger) + diag.colorSelected.connect(callback) + + action.setDefaultWidget(diag) + button.addAction(action) + button.setPopupMode(QtGui.QToolButton.InstantPopup) + + + @QtCore.Slot(QtGui.QColor) + def lineColorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.LineColor.iconSize()) + pixmap.fill(color) + self.widget.LineColor.setIcon(pixmap) + self._object.ViewObject.LineColor = color.getRgb() - @QtCore.Slot() - def barColorChanged(self): - color = self.widget.BarColor.property("color") + @QtCore.Slot(QtGui.QColor) + def barColorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.BarColor.iconSize()) + pixmap.fill(color) + self.widget.BarColor.setIcon(pixmap) + self._object.ViewObject.BarColor = color.getRgb() @QtCore.Slot(float) @@ -115,7 +150,7 @@ class EditViewWidget(QtGui.QWidget): self._object.ViewObject.Legend = self.widget.Legend.text() -class EditAppWidget(QtGui.QWidget): +class EditFieldAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): super().__init__() @@ -160,6 +195,54 @@ class EditAppWidget(QtGui.QWidget): self._object.ExtractFrames = extract self._post_dialog._recompute() +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + + # sometimes wierd sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): """ @@ -233,13 +316,30 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): return ":/icons/FEM_PostField.svg" def get_app_edit_widget(self, post_dialog): - return EditAppWidget(self.Object, post_dialog) + return EditFieldAppWidget(self.Object, post_dialog) def get_view_edit_widget(self, post_dialog): return EditViewWidget(self.Object, post_dialog) def get_preview(self): - return (QtGui.QPixmap(), self.ViewObject.Legend) + + fig = mpl.pyplot.figure(figsize=(0.4,0.2), dpi=500) + ax = mpl.pyplot.Axes(fig, [0., 0., 2, 1]) + ax.set_axis_off() + fig.add_axes(ax) + + kwargs = self.get_kw_args() + patch = mpl.patches.Rectangle(xy=(0,0), width=2, height=1, **kwargs) + ax.add_patch(patch) + + data = io.BytesIO() + mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) + mpl.pyplot.close() + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data.getvalue()) + + return (pixmap, self.ViewObject.Legend) def get_kw_args(self): # builds kw args from the properties @@ -256,6 +356,21 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): return kwargs +class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData): + """ + A View Provider for extraction of 1D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): """ A View Provider for Histogram plots @@ -365,15 +480,26 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: + main = Plot.getMainWindow() self._plot = Plot.Plot() - self._dialog = QtGui.QDialog(Plot.getMainWindow()) + self._plot.destroyed.connect(self.destroyed) + self._dialog = QtGui.QDialog(main) box = QtGui.QVBoxLayout() box.addWidget(self._plot) + self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep it square self._dialog.setLayout(box) self.drawPlot() self._dialog.show() + + def destroyed(self, obj): + print("*********************************************************") + print("**************** ******************") + print("**************** destroy ******************") + print("**************** ******************") + print("*********************************************************") + def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 54fe5439f3..f98a5bf1e4 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -39,7 +39,6 @@ from PySide import QtGui, QtCore import io import numpy as np import matplotlib as mpl -import matplotlib.pyplot as plt from vtkmodules.numpy_interface.dataset_adapter import VTKArray @@ -75,18 +74,47 @@ class EditViewWidget(QtGui.QWidget): self._post_dialog._enumPropertyToCombobox(vobj, "MarkerStyle", self.widget.MarkerStyle) self.widget.LineWidth.setValue(vobj.LineWidth) self.widget.MarkerSize.setValue(vobj.MarkerSize) - self.widget.Color.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.Color])) + + self._setup_color_button(self.widget.Color, vobj.Color, self.colorChanged) self.widget.Legend.editingFinished.connect(self.legendChanged) self.widget.MarkerStyle.activated.connect(self.markerStyleChanged) self.widget.LineStyle.activated.connect(self.lineStyleChanged) self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged) self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) - self.widget.Color.changed.connect(self.colorChanged) - @QtCore.Slot() - def colorChanged(self): - color = self.widget.Color.property("color") + # sometimes wierd sizes occur with spinboxes + self.widget.MarkerSize.setMaximumHeight(self.widget.MarkerStyle.sizeHint().height()) + self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) + + def _setup_color_button(self, button, fcColor, callback): + + barColor = QtGui.QColor(*[v*255 for v in fcColor]) + icon_size = button.iconSize() + icon_size.setWidth(icon_size.width()*2) + button.setIconSize(icon_size) + pixmap = QtGui.QPixmap(icon_size) + pixmap.fill(barColor) + button.setIcon(pixmap) + + action = QtGui.QWidgetAction(button) + diag = QtGui.QColorDialog(barColor, parent=button) + diag.accepted.connect(action.trigger) + diag.rejected.connect(action.trigger) + diag.colorSelected.connect(callback) + + action.setDefaultWidget(diag) + button.addAction(action) + button.setPopupMode(QtGui.QToolButton.InstantPopup) + + + @QtCore.Slot(QtGui.QColor) + def colorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.Color.iconSize()) + pixmap.fill(color) + self.widget.Color.setIcon(pixmap) + self._object.ViewObject.Color = color.getRgb() @QtCore.Slot(float) @@ -110,7 +138,7 @@ class EditViewWidget(QtGui.QWidget): self._object.ViewObject.Legend = self.widget.Legend.text() -class EditAppWidget(QtGui.QWidget): +class EditFieldAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): super().__init__() @@ -171,9 +199,58 @@ class EditAppWidget(QtGui.QWidget): self._post_dialog._recompute() +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.YField.activated.connect(self.yFieldChanged) + self.widget.YComponent.activated.connect(self.yComponentChanged) + + # sometimes wierd sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.YField.sizeHint().height()) + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yFieldChanged(self, index): + self._object.YField = index + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yComponentChanged(self, index): + self._object.YComponent = index + self._post_dialog._recompute() + + class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): """ - A View Provider for extraction of 1D field data specialy for histograms + A View Provider for extraction of 2D field data specialy for histograms """ def __init__(self, vobj): @@ -236,7 +313,7 @@ class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): return ":/icons/FEM_PostField.svg" def get_app_edit_widget(self, post_dialog): - return EditAppWidget(self.Object, post_dialog) + return EditFieldAppWidget(self.Object, post_dialog) def get_view_edit_widget(self, post_dialog): return EditViewWidget(self.Object, post_dialog) @@ -245,16 +322,16 @@ class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): # Returns the preview tuple of icon and label: (QPixmap, str) # Note: QPixmap in ratio 2:1 - fig = plt.figure(figsize=(0.2,0.1), dpi=1000) - ax = plt.Axes(fig, [0., 0., 1., 1.]) + fig = mpl.pyplot.figure(figsize=(0.2,0.1), dpi=1000) + ax = mpl.pyplot.Axes(fig, [0., 0., 1., 1.]) ax.set_axis_off() fig.add_axes(ax) kwargs = self.get_kw_args() kwargs["markevery"] = [1] ax.plot([0,0.5,1],[0.5,0.5,0.5], **kwargs) data = io.BytesIO() - plt.savefig(data, bbox_inches=0, transparent=True) - plt.close() + mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) + mpl.pyplot.close() pixmap = QtGui.QPixmap() pixmap.loadFromData(data.getvalue()) @@ -277,6 +354,21 @@ class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): return kwargs +class VPPostLineplotIndexOverFrames(VPPostLineplotFieldData): + """ + A View Provider for extraction of 2D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): """ A View Provider for Lineplot plots @@ -366,9 +458,11 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: self._plot = Plot.Plot() - self._dialog = QtGui.QDialog(Plot.getMainWindow()) + main = Plot.getMainWindow() + self._dialog = QtGui.QDialog(main) box = QtGui.QVBoxLayout() box.addWidget(self._plot) + self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep aspect ratio constant self._dialog.setLayout(box) self.drawPlot() diff --git a/src/Mod/Plot/Plot.py b/src/Mod/Plot/Plot.py index 4f5a360745..a3423ae93d 100644 --- a/src/Mod/Plot/Plot.py +++ b/src/Mod/Plot/Plot.py @@ -28,7 +28,7 @@ import sys try: import matplotlib - matplotlib.use("Qt5Agg") + matplotlib.use("QtAgg") # Force matplotlib to use PySide backend by temporarily unloading PyQt if "PyQt5.QtCore" in sys.modules: @@ -36,10 +36,11 @@ try: import matplotlib.pyplot as plt import PyQt5.QtCore else: + print("default matplotlib import") import matplotlib.pyplot as plt - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas - from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas + from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure except ImportError: From d86040dd58707b22b5babaa07fa045f4c6a62019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 20 Apr 2025 14:18:30 +0200 Subject: [PATCH 04/27] FEM: Add table post data visualization --- src/Mod/Fem/CMakeLists.txt | 4 + src/Mod/Fem/Gui/CMakeLists.txt | 1 + .../Resources/ui/PostHistogramFieldAppEdit.ui | 2 +- .../Resources/ui/PostTableFieldViewEdit.ui | 43 +++ src/Mod/Fem/ObjectsFem.py | 42 +++ src/Mod/Fem/femcommands/commands.py | 1 + src/Mod/Fem/femguiutils/extract_link_view.py | 12 +- src/Mod/Fem/femguiutils/vtk_table_view.py | 22 +- .../femobjects/base_fempostvisualizations.py | 124 +++++++- src/Mod/Fem/femobjects/post_extract1D.py | 6 +- src/Mod/Fem/femobjects/post_extract2D.py | 6 +- src/Mod/Fem/femobjects/post_histogram.py | 59 +--- src/Mod/Fem/femobjects/post_lineplot.py | 57 +--- src/Mod/Fem/femobjects/post_table.py | 231 ++++---------- .../Fem/femtaskpanels/base_fempostpanel.py | 7 + src/Mod/Fem/femtaskpanels/task_post_table.py | 94 ++++++ ...ract.py => view_base_fempostextractors.py} | 29 +- .../view_base_fempostvisualization.py | 49 ++- .../femviewprovider/view_post_histogram.py | 55 +--- .../Fem/femviewprovider/view_post_lineplot.py | 51 +--- .../Fem/femviewprovider/view_post_table.py | 287 ++++++++++++++++++ 21 files changed, 777 insertions(+), 405 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui create mode 100644 src/Mod/Fem/femtaskpanels/task_post_table.py rename src/Mod/Fem/femviewprovider/{view_post_extract.py => view_base_fempostextractors.py} (92%) create mode 100644 src/Mod/Fem/femviewprovider/view_post_table.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 0178bffe84..3ba5a83c3e 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -223,6 +223,7 @@ if(BUILD_FEM_VTK_PYTHON) femobjects/post_extract2D.py femobjects/post_histogram.py femobjects/post_lineplot.py + femobjects/post_table.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -637,6 +638,7 @@ if(BUILD_FEM_VTK_PYTHON) femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_post_histogram.py femtaskpanels/task_post_lineplot.py + femtaskpanels/task_post_table.py femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -666,6 +668,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_base_femmeshelement.py femviewprovider/view_base_femobject.py femviewprovider/view_base_fempostvisualization.py + femviewprovider/view_base_fempostextractors.py femviewprovider/view_constant_vacuumpermittivity.py femviewprovider/view_constraint_bodyheatsource.py femviewprovider/view_constraint_centrif.py @@ -704,6 +707,7 @@ if(BUILD_FEM_VTK_PYTHON) femviewprovider/view_post_extract.py femviewprovider/view_post_histogram.py femviewprovider/view_post_lineplot.py + femviewprovider/view_post_table.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 7337afd833..7729f8af55 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -451,6 +451,7 @@ SET(FemGuiPythonUI_SRCS Resources/ui/PostLineplotFieldViewEdit.ui Resources/ui/PostLineplotFieldAppEdit.ui Resources/ui/PostLineplotIndexAppEdit.ui + Resources/ui/PostTableFieldViewEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui index d100b81ab3..8e611e7790 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -65,7 +65,7 @@ - One field for all frames + One field for each frames diff --git a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui new file mode 100644 index 0000000000..ada74b69b4 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -0,0 +1,43 @@ + + + PostHistogramEdit + + + + 0 + 0 + 259 + 38 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Name: + + + + + + + + diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index d40558f4bd..c21c219dda 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -770,6 +770,48 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): return obj +def makePostTable(doc, name="Table"): + """makePostTable(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_table + + post_table.PostTable(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + view_post_table.VPPostTable(obj.ViewObject) + return obj + + +def makePostTableFieldData(doc, name="FieldData1D"): + """makePostTableFieldData(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_table + + post_table.PostTableFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + view_post_table.VPPostTableFieldData(obj.ViewObject) + return obj + + +def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostTableIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_table + + post_table.PostTableIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + view_post_table.VPPostTableIndexOverFrames(obj.ViewObject) + 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 f4edc1586e..50461484a2 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1293,4 +1293,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: # setup all visualization commands (register by importing) import femobjects.post_lineplot import femobjects.post_histogram + import femobjects.post_table post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index e1611f8609..acd409765c 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -252,11 +252,13 @@ class _SummaryWidget(QtGui.QWidget): self.extrButton.setIcon(extractor.ViewObject.Icon) self.viewButton = self._button(extr_repr[1]) - size = self.viewButton.iconSize() - size.setWidth(size.width()*2) - self.viewButton.setIconSize(size) - self.viewButton.setIcon(extr_repr[0]) - + if not extr_repr[0].isNull(): + size = self.viewButton.iconSize() + size.setWidth(size.width()*2) + self.viewButton.setIconSize(size) + self.viewButton.setIcon(extr_repr[0]) + else: + self.viewButton.setIconSize(QtCore.QSize(0,0)) self.rmButton = QtGui.QToolButton(self) self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index df06c51ee0..95ebf1007e 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -34,14 +34,23 @@ from PySide import QtCore class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns + # One can supply a header_names dict to replace the table column names + # in the header. It is a dict "column_idx (int)" to "new name"" or + # "orig_name (str)" to "new name" - def __init__(self): + def __init__(self, header_names = None): super().__init__() self._table = None + if header_names: + self._header = header_names + else: + self._header = {} - def setTable(self, table): + def setTable(self, table, header_names = None): self.beginResetModel() self._table = table + if header_names: + self._header = header_names self.endResetModel() def rowCount(self, index): @@ -70,7 +79,14 @@ class VtkTableModel(QtCore.QAbstractTableModel): def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: - return self._table.GetColumnName(section) + if section in self._header: + return self._header[section] + + name = self._table.GetColumnName(section) + if name in self._header: + return self._header[name] + + return name if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return section diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index fae9c58b6c..b16f41451f 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -21,42 +21,70 @@ # * * # *************************************************************************** -__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts" +__title__ = "FreeCAD FEM postprocessing data visualization base object" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" ## @package base_fempostextractors # \ingroup FEM -# \brief base objects for data extractors +# \brief base objects for data visualizations from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonCore import vtkDoubleArray from . import base_fempythonobject +from . import base_fempostextractors # helper functions # ################ def is_visualization_object(obj): + if not obj: + return False + if not hasattr(obj, "Proxy"): return False return hasattr(obj.Proxy, "VisualizationType") + def get_visualization_type(obj): # returns the extractor type string, or throws exception if # not a extractor return obj.Proxy.VisualizationType +def is_visualization_extractor_type(obj, vistype): + + # must be extractor + if not base_fempostextractors.is_extractor_object(obj): + return False + + # must be visualization object + if not is_visualization_object(obj): + return False + + # must be correct type + if get_visualization_type(obj) != vistype: + return False + + return True + + + # Base class for all visualizations +# It collects all data from its extraction objects into a table. # Note: Never use directly, always subclass! This class does not create a # Visualization variable, hence will not work correctly. class PostVisualization(base_fempythonobject.BaseFemPythonObject): + def __init__(self, obj): super().__init__(obj) + obj.addExtension("App::GroupExtensionPython") self._setup_properties(obj) + def _setup_properties(self, obj): pl = obj.PropertiesList for prop in self._get_properties(): @@ -64,8 +92,96 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): prop.add_to_object(obj) + def _get_properties(self): + # override if subclass wants to add additional properties + + prop = [ + base_fempostextractors._PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the data for visualization", + value=vtkTable(), + ), + ] + return prop + + def onDocumentRestored(self, obj): + # if a new property was added we handle it by setup + # Override if subclass needs to handle changed property type + self._setup_properties(obj) - def _get_properties(self): - return [] + + def onChanged(self, obj, prop): + # Ensure only correct child object types are in the group + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if not is_visualization_extractor_type(child, self.VisualizationType): + FreeCAD.Console.PrintWarning(f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added") + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + + def execute(self, obj): + # Collect all extractor child data into our table + # Note: Each childs table can have different number of rows. We need + # to pad the date for our table in this case + + rows = self.getLongestColumnLength(obj) + table = vtkTable() + for child in obj.Group: + + # If child has no Source, its table should be empty. However, + # it would theoretical be possible that child source was set + # to none without recompute, and the visualization was manually + # recomputed afterwards + if not child.Source and (c_table.GetNumberOfColumns() > 0): + FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored") + continue + + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + array = vtkDoubleArray() + + if c_array.GetNumberOfTuples() == rows: + # simple deep copy is enough + array.DeepCopy(c_array) + + else: + array.SetNumberOfComponents(c_array.GetNumberOfComponents()) + array.SetNumberOfTuples(rows) + array.Fill(0) # so that all non-used entries are set to 0 + for i in range(c_array.GetNumberOfTuples()): + array.SetTuple(i, c_array.GetTuple(i)) + + array.SetName(f"{child.Source.Name}: {c_array.GetName()}") + table.AddColumn(array) + + + obj.Table = table + return False + + + def getLongestColumnLength(self, obj): + # iterate all extractor children and get the column lengths + + length = 0 + for child in obj.Group: + if base_fempostextractors.is_extractor_object(child): + table = child.Table + if table.GetNumberOfColumns() > 0: + # we assume all columns of an extractor have same length + num = table.GetColumn(0).GetNumberOfTuples() + if num > length: + length = num + + return length diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 3540b6706a..425a594fae 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines +import FreeCAD + from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -176,9 +178,9 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): frame_array.SetTuple(i, idx, array) if frame_array.GetNumberOfComponents() > 1: - frame_array.SetName(f"{obj.XField} ({obj.XComponent})") + frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") else: - frame_array.SetName(f"{obj.XField}") + frame_array.SetName(f"{obj.XField} @Idx {obj.Index}") self._x_array_component_to_table(obj, frame_array, table) diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 60c9ac2df2..0b9e5c528e 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines +import FreeCAD + from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -207,9 +209,9 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): frame_x_array.SetName("Frames") if frame_y_array.GetNumberOfComponents() > 1: - frame_y_array.SetName(f"{obj.YField} ({obj.YComponent})") + frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}") else: - frame_y_array.SetName(obj.YField) + frame_y_array.SetName(f"{obj.YField} @Idx {obj.Index}") table.AddColumn(frame_x_array) self._y_array_component_to_table(obj, frame_y_array, table) diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index bdcb4ad553..0a6277f5fe 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -29,17 +29,11 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying histograms -from . import base_fempythonobject -_PropHelper = base_fempythonobject._PropHelper from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract1D -from vtkmodules.vtkCommonCore import vtkDoubleArray -from vtkmodules.vtkCommonDataModel import vtkTable - - from femguiutils import post_visualization # register visualization and extractors @@ -85,6 +79,7 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D): """ VisualizationType = "Histogram" + class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): """ A 1D index extraction for histogram. @@ -96,57 +91,11 @@ class PostHistogram(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as histograms """ - VisualizationType = "Histogram" - def __init__(self, obj): - super().__init__(obj) - obj.addExtension("App::GroupExtensionPython") - - def _get_properties(self): - prop = [ - _PropHelper( - type="Fem::PropertyPostDataObject", - name="Table", - group="Base", - doc="The data table that stores the plotted data, one column per histogram", - value=vtkTable(), - ), - ] - return super()._get_properties() + prop - - - def onChanged(self, obj, prop): - - if prop == "Group": - # check if all objects are allowed - - children = obj.Group - for child in obj.Group: - if not is_histogram_extractor(child): - FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added") - children.remove(child) - - if len(obj.Group) != len(children): - obj.Group = children - - def execute(self, obj): - - # during execution we collect all child data into our table - table = vtkTable() - for child in obj.Group: - - c_table = child.Table - for i in range(c_table.GetNumberOfColumns()): - c_array = c_table.GetColumn(i) - # TODO: check which array type it is and use that one - array = vtkDoubleArray() - array.DeepCopy(c_array) - array.SetName(f"{child.Source.Label}: {c_array.GetName()}") - table.AddColumn(array) - - obj.Table = table - return False + + + diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 06f844ebac..486241b367 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -29,17 +29,10 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines -from . import base_fempythonobject -_PropHelper = base_fempythonobject._PropHelper - from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract2D -from vtkmodules.vtkCommonCore import vtkDoubleArray -from vtkmodules.vtkCommonDataModel import vtkTable - - from femguiutils import post_visualization # register visualization and extractors @@ -85,6 +78,7 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ VisualizationType = "Lineplot" + class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): """ A 2D index extraction for lineplot. @@ -97,56 +91,7 @@ class PostLineplot(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as line plots """ - VisualizationType = "Lineplot" - def __init__(self, obj): - super().__init__(obj) - obj.addExtension("App::GroupExtensionPython") - - def _get_properties(self): - prop = [ - _PropHelper( - type="Fem::PropertyPostDataObject", - name="Table", - group="Base", - doc="The data table that stores the plotted data, two columns per lineplot (x,y)", - value=vtkTable(), - ), - ] - return super()._get_properties() + prop - - - def onChanged(self, obj, prop): - - if prop == "Group": - # check if all objects are allowed - - children = obj.Group - for child in obj.Group: - if not is_lineplot_extractor(child): - FreeCAD.Console.PrintWarning(f"{child.Label} is not a data lineplot data extraction object, cannot be added") - children.remove(child) - - if len(obj.Group) != len(children): - obj.Group = children - - def execute(self, obj): - - # during execution we collect all child data into our table - table = vtkTable() - for child in obj.Group: - - c_table = child.Table - for i in range(c_table.GetNumberOfColumns()): - c_array = c_table.GetColumn(i) - # TODO: check which array type it is and use that one - array = vtkDoubleArray() - array.DeepCopy(c_array) - array.SetName(f"{child.Source.Label}: {c_array.GetName()}") - table.AddColumn(array) - - obj.Table = table - return False diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index f8798fbc23..3d7d7be689 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -21,191 +21,76 @@ # * * # *************************************************************************** -__title__ = "FreeCAD post line plot" +__title__ = "FreeCAD post table" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" -## @package post_lineplot +## @package post_table # \ingroup FEM -# \brief Post processing plot displaying lines +# \brief Post processing plot displaying tables -from . import base_fempythonobject -_PropHelper = base_fempythonobject._PropHelper - -# helper function to extract plot object type -def _get_extraction_subtype(obj): - if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): - return obj.Proxy.Type - - return "unknown" +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract1D -class PostLinePlot(base_fempythonobject.BaseFemPythonObject): +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization("Table", + ":/icons/FEM_PostSpreadsheet.svg", + "ObjectsFem", + "makePostTable") + +post_visualization.register_extractor("Table", + "TableFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostTableFieldData") + + +post_visualization.register_extractor("Table", + "TableIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostTableIndexOverFrames") + +# Implementation +# ############## + +def is_table_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Table" + + +class PostTableFieldData(post_extract1D.PostFieldData1D): """ - A post processing extraction for plotting lines + A 1D Field extraction for tables. """ - - Type = "App::FeaturePython" - - def __init__(self, obj): - super().__init__(obj) - obj.addExtension("App::GroupExtension") - self._setup_properties(obj) - - def _setup_properties(self, obj): - - self.ExtractionType = "LinePlot" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) - - def _get_properties(self): - prop = [] - return prop - - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): - - def onChanged(self, obj, prop): - - if prop == "Group": - # check if all objects are allowed - - children = obj.Group - for child in obj.Group: - if _get_extraction_subtype(child) not in ["Line"]: - children.remove(child) - - if len(obj.Group) != len(children): - obj.Group = children + VisualizationType = "Table" -class PostPlotLine(base_fempythonobject.BaseFemPythonObject): +class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for table. + """ + VisualizationType = "Table" - Type = "App::FeaturePython" - def __init__(self, obj): - super().__init__(obj) - self._setup_properties(obj) +class PostTable(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as tables + """ + VisualizationType = "Table" - def _setup_properties(self, obj): - - self.ExtractionType = "Line" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) - - def _get_properties(self): - - prop = [ - _PropHelper( - type="App::PropertyLink", - name="Source", - group="Line", - doc="The data source, the line uses", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XField", - group="X Data", - doc="The field to use as X data", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XComponent", - group="X Data", - doc="Which part of the X field vector to use for the X axis", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YField", - group="Y Data", - doc="The field to use as Y data for the line plot", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YComponent", - group="Y Data", - doc="Which part of the Y field vector to use for the X axis", - value=None, - ), - ] - return prop - - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): - - def onChanged(self, obj, prop): - - if prop == "Source": - # check if the source is a Post object - if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): - FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") - obj.XField = [] - obj.YField = [] - obj.Source = None - - if prop == "XField": - if not obj.Source: - obj.XComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.XField): - obj.XComponent = [] - return - - match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: - case 1: - obj.XComponent = ["Not a vector"] - case 2: - obj.XComponent = ["Magnitude", "X", "Y"] - case 3: - obj.XComponent = ["Magnitude", "X", "Y", "Z"] - - if prop == "YField": - if not obj.Source: - obj.YComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.YField): - obj.YComponent = [] - return - - match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: - case 1: - obj.YComponent = ["Not a vector"] - case 2: - obj.YComponent = ["Magnitude", "X", "Y"] - case 3: - obj.YComponent = ["Magnitude", "X", "Y", "Z"] - - def onExecute(self, obj): - # we need to make sure that we show the correct fields to the user as option for data extraction - - fields = [] - if obj.Source: - point_data = obj.Source.Data.GetPointData() - fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] - - current_X = obj.XField - obj.XField = fields - if current_X in fields: - obj.XField = current_X - - current_Y = obj.YField - obj.YField = fields - if current_Y in fields: - obj.YField = current_Y - - return True diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index f90af0e260..3e26ac1ce5 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -60,6 +60,13 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): if button == QtGui.QDialogButtonBox.Apply: self.obj.Document.recompute() + def accept(self): + print("accept") + return super().accept() + + def reject(self): + print("reject") + return super().reject() # Helper functions # ################ diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py new file mode 100644 index 0000000000..e17f584c01 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -0,0 +1,94 @@ +# *************************************************************************** +# * 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 histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText("Show table") + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(self.data_widget.show_table) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle("Table data") + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg")) + + + # histogram parameter widget + #self.view_widget = FreeCADGui.PySideUic.loadUi( + # FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostTable.ui" + #) + #self.view_widget.setWindowTitle("Table view settings") + #self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostTable.svg")) + + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget] + + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + + @QtCore.Slot() + def showTable(self): + self.obj.ViewObject.Proxy.show_visualization() + diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py similarity index 92% rename from src/Mod/Fem/femviewprovider/view_post_extract.py rename to src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index b91c65cb45..46313ba890 100644 --- a/src/Mod/Fem/femviewprovider/view_post_extract.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -66,7 +66,7 @@ class VPPostExtractor: def onChanged(self, vobj, prop): - # one of our view properties was changed. Lets inform our parent plot + # one of our view properties was changed. Lets inform our parent visualization # that this happend, as this is the one that needs to redraw if prop == "Proxy": @@ -92,6 +92,10 @@ class VPPostExtractor: return True + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True + def doubleClicked(self, vobj): guidoc = FreeCADGui.getDocument(vobj.Object.Document) @@ -104,10 +108,20 @@ class VPPostExtractor: return True + def dumps(self): + return None + + def loads(self, state): + return None + + + # To be implemented by subclasses: + # ################################ + def get_kw_args(self): - # should return the plot keyword arguments that represent the properties - # of the object - return {} + # Returns the matplotlib plot keyword arguments that represent the + # properties of the object. + raise FreeCAD.Base.FreeCADError("Not implemented") def get_app_edit_widget(self, post_dialog): # Returns a widgets for editing the object (not viewprovider!) @@ -125,10 +139,3 @@ class VPPostExtractor: # Returns the preview tuple of icon and label: (QPixmap, str) # Note: QPixmap in ratio 2:1 raise FreeCAD.Base.FreeCADError("Not implemented") - - - def dumps(self): - return None - - def loads(self, state): - return None diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 153537d669..20714b67c5 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -45,6 +45,8 @@ class VPPostVisualization: def __init__(self, vobj): vobj.Proxy = self self._setup_properties(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + def _setup_properties(self, vobj): pl = vobj.PropertiesList @@ -52,16 +54,21 @@ class VPPostVisualization: if not prop.name in pl: prop.add_to_object(vobj) + def _get_properties(self): return [] + def attach(self, vobj): self.Object = vobj.Object self.ViewObject = vobj + def isShow(self): + # Mark ourself as visible in the tree return True + def doubleClicked(self,vobj): guidoc = FreeCADGui.getDocument(vobj.Object.Document) @@ -71,21 +78,47 @@ class VPPostVisualization: FreeCADGui.Control.closeDialog() guidoc.resetEdit() + # open task dialog guidoc.setEdit(vobj.Object.Name) + + # show visualization + self.show_visualization() + return True - def show_visualization(self): - # shows the visualization without going into edit mode - # to be implemented by subclasses - pass + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True - def get_kw_args(self, obj): - # returns a dictionary with all visualization options needed for plotting - # based on the view provider properties - return {} + def updateData(self, obj, prop): + # If the data changed we need to update the visualization + if prop == "Table": + self.update_visualization() + + def onChanged(self, vobj, prop): + # for all property changes we need to update the visualization + self.update_visualization() + + def childViewPropertyChanged(self, vobj, prop): + # One of the extractors view properties has changed, we need to + # update the visualization + self.update_visualization() def dumps(self): return None def loads(self, state): return None + + + # To be implemented by subclasses: + # ################################ + + def update_visualization(self): + # The visualization data or any relevant view property has changed, + # and the visualization itself needs to update to reflect that + raise FreeCAD.Base.FreeCADError("Not implemented") + + def show_visualization(self): + # Shows the visualization without going into edit mode + raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 777e3d15c0..2a6d817e70 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -42,7 +42,7 @@ import matplotlib as mpl from vtkmodules.numpy_interface.dataset_adapter import VTKArray -from . import view_post_extract +from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_histogram @@ -244,7 +244,7 @@ class EditIndexAppWidget(QtGui.QWidget): self._post_dialog._recompute() -class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): +class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): """ A View Provider for extraction of 1D field data specialy for histograms """ @@ -378,7 +378,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + def _get_properties(self): @@ -458,13 +458,10 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): ] return prop + def getIcon(self): return ":/icons/FEM_PostHistogram.svg" - def doubleClicked(self,vobj): - - self.show_visualization() - super().doubleClicked(vobj) def setEdit(self, vobj, mode): @@ -482,24 +479,12 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: main = Plot.getMainWindow() self._plot = Plot.Plot() - self._plot.destroyed.connect(self.destroyed) - self._dialog = QtGui.QDialog(main) - box = QtGui.QVBoxLayout() - box.addWidget(self._plot) - self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep it square - self._dialog.setLayout(box) + self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square + self.update_visualization() - self.drawPlot() - self._dialog.show() + self._plot.show() - def destroyed(self, obj): - print("*********************************************************") - print("**************** ******************") - print("**************** destroy ******************") - print("**************** ******************") - print("*********************************************************") - def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): @@ -508,7 +493,8 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def drawPlot(self): + + def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: return @@ -579,26 +565,3 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.update() - - def updateData(self, obj, prop): - # we only react if the table changed, as then know that new data is available - if prop == "Table": - self.drawPlot() - - - def onChanged(self, vobj, prop): - - # for all property changes we need to redraw the plot - self.drawPlot() - - def childViewPropertyChanged(self, vobj, prop): - - # on of our extractors has a changed view property. - self.drawPlot() - - def dumps(self): - return None - - - def loads(self, state): - return None diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index f98a5bf1e4..29c0165275 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -42,9 +42,10 @@ import matplotlib as mpl from vtkmodules.numpy_interface.dataset_adapter import VTKArray -from . import view_post_extract +from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_lineplot +from femguiutils import post_visualization as pv _GuiPropHelper = view_base_fempostvisualization._GuiPropHelper @@ -248,7 +249,7 @@ class EditIndexAppWidget(QtGui.QWidget): self._post_dialog._recompute() -class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): +class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): """ A View Provider for extraction of 2D field data specialy for histograms """ @@ -376,7 +377,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + def _get_properties(self): @@ -435,13 +436,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): ] return prop + def getIcon(self): return ":/icons/FEM_PostLineplot.svg" - def doubleClicked(self,vobj): - - self.show_visualization() - super().doubleClicked(vobj) def setEdit(self, vobj, mode): @@ -457,16 +455,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: - self._plot = Plot.Plot() main = Plot.getMainWindow() - self._dialog = QtGui.QDialog(main) - box = QtGui.QVBoxLayout() - box.addWidget(self._plot) - self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep aspect ratio constant - self._dialog.setLayout(box) + self._plot = Plot.Plot() + self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self.update_visualization() + + self._plot.show() - self.drawPlot() - self._dialog.show() def get_kw_args(self, obj): view = obj.ViewObject @@ -476,7 +471,8 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def drawPlot(self): + + def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: return @@ -545,26 +541,3 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): self._plot.update() - - def updateData(self, obj, prop): - # we only react if the table changed, as then know that new data is available - if prop == "Table": - self.drawPlot() - - - def onChanged(self, vobj, prop): - - # for all property changes we need to redraw the plot - self.drawPlot() - - def childViewPropertyChanged(self, vobj, prop): - - # on of our extractors has a changed view property. - self.drawPlot() - - def dumps(self): - return None - - - def loads(self, state): - return None diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py new file mode 100644 index 0000000000..443667e31a --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -0,0 +1,287 @@ +# *************************************************************************** +# * 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 table ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_table +# \ingroup FEM +# \brief view provider for post table object + +import FreeCAD +import FreeCADGui + +import Plot +import FemGui +from PySide import QtGui, QtCore + +import io +import numpy as np +import matplotlib as mpl + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +from femtaskpanels import task_post_table +from femguiutils import vtk_table_view as vtv + +_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostTableFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + self.widget.Name.setText(self._object.ViewObject.Name) + self.widget.Name.editingFinished.connect(self.legendChanged) + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Name = self.widget.Name.text() + + +class EditFieldAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up (we reuse histogram, as we need the exact same) + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up (we reuse histogram, as we need the exact same) + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + + # sometimes wierd sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + +class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): + """ + A View Provider for extraction of 1D field data specialy for tables + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Name", + group="Table", + doc="The name used in the table header. Default name is used if empty", + value="", + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditFieldAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + name = "----" + if self.ViewObject.Name: + name = self.ViewObject.Name + return (QtGui.QPixmap(), name) + + +class VPPostTableIndexOverFrames(VPPostTableFieldData): + """ + A View Provider for extraction of 1D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + +class VPPostTable(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Table plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + + + def getIcon(self): + return ":/icons/FEM_PostSpreadsheet.svg" + + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_table._TaskPanel(vobj) + + #show it + FreeCADGui.Control.showDialog(taskd) + + return True + + + def show_visualization(self): + + if not hasattr(self, "_tableview") or not self._tableview: + self._tableModel = vtv.VtkTableModel() + self._tableview = vtv.VtkTableView(self._tableModel) + self.update_visualization() + + self._tableview.show() + + + def update_visualization(self): + + if not hasattr(self, "_tableModel") or not self._tableModel: + return + + # we collect the header names from the viewproviders + table = self.Object.Table + header = {} + for child in self.Object.Group: + + if not child.Source: + continue + + new_name = child.ViewObject.Name + if new_name: + # this child uses a custom name. We try to find all + # columns that are from this child and use custom header for it + for i in range(table.GetNumberOfColumns()): + if child.Source.Name in table.GetColumnName(i): + header[table.GetColumnName(i)] = new_name + + self._tableModel.setTable(self.Object.Table, header) + + From 54a35a6c4d362d9d0df0ece33efee71a631fb007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 11:22:31 +0200 Subject: [PATCH 05/27] FEM: Add extraction task panel to data plot filters --- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 9 +++++++++ src/Mod/Fem/femobjects/base_fempostvisualizations.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 4cbacb5cad..4ea5f3e36a 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -31,6 +31,7 @@ #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" #include "ViewProviderFemPostFilterPy.h" +#include "TaskPostExtraction.h" using namespace FemGui; @@ -89,6 +90,10 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); auto panel = new TaskPostDataAlongLine(this); dlg->addTaskBox(panel->getIcon(), panel); + + // and the extraction + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); } @@ -138,6 +143,10 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); auto panel = new TaskPostDataAtPoint(this); dlg->addTaskBox(panel->getIcon(), panel); + + // and the extraction + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); } diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index b16f41451f..396694a652 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -143,7 +143,7 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): # it would theoretical be possible that child source was set # to none without recompute, and the visualization was manually # recomputed afterwards - if not child.Source and (c_table.GetNumberOfColumns() > 0): + if not child.Source and (child.Table.GetNumberOfColumns() > 0): FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored") continue From 8672a680e0440b30646404f52678b2a4f943b298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 12:41:37 +0200 Subject: [PATCH 06/27] FEM: Post data visualization bug fixes and quality of life updates --- src/Mod/Fem/femguiutils/extract_link_view.py | 103 +++++++++++++----- .../Fem/femtaskpanels/base_fempostpanel.py | 8 -- .../view_base_fempostextractors.py | 10 ++ .../view_base_fempostvisualization.py | 10 ++ .../femviewprovider/view_post_histogram.py | 15 ++- .../Fem/femviewprovider/view_post_lineplot.py | 22 +++- .../Fem/femviewprovider/view_post_table.py | 8 ++ 7 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index acd409765c..619ea358f1 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -245,9 +245,6 @@ class _SummaryWidget(QtGui.QWidget): # build the UI - self.stButton = self._button(st_object.Label) - self.stButton.setIcon(st_object.ViewObject.Icon) - self.extrButton = self._button(extr_label) self.extrButton.setIcon(extractor.ViewObject.Icon) @@ -260,6 +257,19 @@ class _SummaryWidget(QtGui.QWidget): else: self.viewButton.setIconSize(QtCore.QSize(0,0)) + if st_object: + self.stButton = self._button(st_object.Label) + self.stButton.setIcon(st_object.ViewObject.Icon) + + else: + # that happens if the source of the extractor was deleted and now + # that property is set to None + self.extrButton.hide() + self.viewButton.hide() + + self.warning = QtGui.QLabel(self) + self.warning.full_text = f"{extractor.Label}: Data source not available" + self.rmButton = QtGui.QToolButton(self) self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) self.rmButton.setAutoRaise(True) @@ -270,13 +280,15 @@ class _SummaryWidget(QtGui.QWidget): policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) self.setSizePolicy(policy) - self.setMinimumSize(self.stButton.sizeHint()+self.frame.sizeHint()*3) + self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) # connect actions. We add functions to widget, as well as the data we need, # and use those as callback. This way every widget knows which objects to use - self.stButton.clicked.connect(self.showVisualization) - self.extrButton.clicked.connect(self.editApp) - self.viewButton.clicked.connect(self.editView) + if st_object: + self.stButton.clicked.connect(self.showVisualization) + self.extrButton.clicked.connect(self.editApp) + self.viewButton.clicked.connect(self.editView) + self.rmButton.clicked.connect(self.deleteTriggered) # make sure initial drawing happened @@ -300,37 +312,47 @@ class _SummaryWidget(QtGui.QWidget): btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() fm = self.fontMetrics() - min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 - pos = 0 - btns = [self.stButton, self.extrButton, self.viewButton] - btn_rel_size = [0.4, 0.4, 0.2] - btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] - for i, btn in enumerate(btns): + if self._st_object: - btn_size = btn_total_size*btn_rel_size[i] - txt_size = btn_size - btn.iconSize().width() - btn_margin + min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 - # we elide only if there is enough space for a meaningful text - if txt_size >= min_text_width: + pos = 0 + btns = [self.stButton, self.extrButton, self.viewButton] + btn_rel_size = [0.4, 0.4, 0.2] + btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] + for i, btn in enumerate(btns): - text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) - btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px"); - else: - btn.setText("") - btn.setStyleSheet("text-align:center;"); + btn_size = btn_total_size*btn_rel_size[i] + txt_size = btn_size - btn.iconSize().width() - btn_margin - rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) - btn.setGeometry(rect) - pos+=btn_size + # we elide only if there is enough space for a meaningful text + if txt_size >= min_text_width: - rmsize = self.stButton.height() + text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) + btn.setText(text) + btn.setStyleSheet("text-align:left;padding:6px"); + else: + btn.setText("") + btn.setStyleSheet("text-align:center;"); + + rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) + btn.setGeometry(rect) + pos+=btn_size + + else: + warning_txt = fm.elidedText(self.warning.full_text, QtGui.Qt.ElideRight, btn_total_size) + self.warning.setText(warning_txt) + rect = QtCore.QRect(0,0, btn_total_size, self.extrButton.sizeHint().height()) + self.warning.setGeometry(rect) + + + rmsize = self.extrButton.sizeHint().height() pos = self.size().width() - rmsize self.rmButton.setGeometry(pos, 0, rmsize, rmsize) frame_hint = self.frame.sizeHint() - rect = QtCore.QRect(0, self.stButton.height()+frame_hint.height(), self.size().width(), frame_hint.height()) + rect = QtCore.QRect(0, self.extrButton.sizeHint().height()+frame_hint.height(), self.size().width(), frame_hint.height()) self.frame.setGeometry(rect) def resizeEvent(self, event): @@ -563,6 +585,7 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)" ) + analysis = self._find_parent_analysis(self._object) if analysis: FreeCADGui.doCommand( @@ -576,10 +599,18 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" ) + # default values: color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = visualization.ViewObject.Proxy.get_next_default_color()" + ) + FreeCADGui.doCommand( f"visualization.addObject(extraction)" ) + self._post_dialog._recompute() self.repopulate() @@ -596,6 +627,14 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" ) + + # default values: color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = (Gui.ActiveDocument.{vis_obj.Name}.Proxy.get_next_default_color())" + ) + FreeCADGui.doCommand( f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)" ) @@ -616,6 +655,14 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}" ) + + # default values for color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = Gui.ActiveDocument.{self._object.Name}.Proxy.get_next_default_color()" + ) + FreeCADGui.doCommand( f"App.ActiveDocument.{self._object.Name}.addObject(extraction)" ) diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 3e26ac1ce5..a9edb902ec 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -60,14 +60,6 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): if button == QtGui.QDialogButtonBox.Apply: self.obj.Document.recompute() - def accept(self): - print("accept") - return super().accept() - - def reject(self): - print("reject") - return super().reject() - # Helper functions # ################ diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index 46313ba890..143cd8fba5 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -118,6 +118,16 @@ class VPPostExtractor: # To be implemented by subclasses: # ################################ + def get_default_color_property(self): + # Returns the property name to set the default color to. + # Return None if no such property + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_default_field_properties(self): + # Returns the property name to which the default field name should be set + # ret: [FieldProperty, ComponentProperty] + raise FreeCAD.Base.FreeCADError("Not implemented") + def get_kw_args(self): # Returns the matplotlib plot keyword arguments that represent the # properties of the object. diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 20714b67c5..b3d244b1ef 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -32,6 +32,7 @@ __url__ = "https://www.freecad.org" from PySide import QtGui, QtCore import Plot +import FreeCAD import FreeCADGui from . import view_base_femobject @@ -68,6 +69,8 @@ class VPPostVisualization: # Mark ourself as visible in the tree return True + def getDisplayModes(self, obj): + return ["Dialog"] def doubleClicked(self,vobj): @@ -122,3 +125,10 @@ class VPPostVisualization: def show_visualization(self): # Shows the visualization without going into edit mode raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_next_default_color(self): + # Returns the next default color a new object should use + # Returns color in FreeCAD proeprty notation (r,g,b,a) + # If the relevant extractors do not have color properties, this + # can stay unimplemented + raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 2a6d817e70..69aed4101f 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -289,7 +289,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="LineColor", group="HistogramLine", doc="The color the data bin area is drawn with", - value=(0, 85, 255, 255), + value=(0, 0, 0, 1), # black ), _GuiPropHelper( type="App::PropertyFloatConstraint", @@ -355,6 +355,9 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): return kwargs + def get_default_color_property(self): + return "BarColor" + class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData): """ @@ -477,8 +480,10 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: - main = Plot.getMainWindow() + main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setParent(main) + self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square self.update_visualization() @@ -565,3 +570,9 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.update() + def get_next_default_color(self): + # we use the next color in order. We do not check (yet) if this + # color is already taken + i = len(self.Object.Group) + cmap = mpl.pyplot.get_cmap("tab10") + return cmap(i) diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 29c0165275..dfd7f2b5b4 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -354,6 +354,9 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): kwargs["markersize"] = self.ViewObject.MarkerSize return kwargs + def get_default_color_property(self): + return "Color" + class VPPostLineplotIndexOverFrames(VPPostLineplotFieldData): """ @@ -387,7 +390,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="Grid", group="Lineplot", doc="If be the bars shoud show the cumulative sum left to rigth", - value=False, + value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", @@ -455,8 +458,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: - main = Plot.getMainWindow() + main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setParent(main) + self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio self.update_visualization() @@ -481,6 +486,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): # we do not iterate the table, but iterate the children. This makes it possible # to attribute the correct styles + plotted = False for child in self.Object.Group: table = child.Table @@ -492,6 +498,8 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): for i in range(0,table.GetNumberOfColumns(),2): + plotted = True + # add the kw args, with some slide change over color for multiple frames tmp_args = {} for key in kwargs: @@ -534,10 +542,16 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if self.ViewObject.YLabel: self._plot.axes.set_ylabel(self.ViewObject.YLabel) - if self.ViewObject.Legend and self.Object.Group: + if self.ViewObject.Legend and plotted: self._plot.axes.legend(loc = self.ViewObject.LegendLocation) self._plot.axes.grid(self.ViewObject.Grid) - self._plot.update() + def get_next_default_color(self): + # we use the next color in order. We do not check (yet) if this + # color is already taken + i = len(self.Object.Group) + cmap = mpl.pyplot.get_cmap("tab10") + return cmap(i) + diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 443667e31a..7b07cc785f 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -211,6 +211,9 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): name = self.ViewObject.Name return (QtGui.QPixmap(), name) + def get_default_color_property(self): + return None + class VPPostTableIndexOverFrames(VPPostTableFieldData): """ @@ -254,8 +257,13 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_tableview") or not self._tableview: + main = FreeCADGui.getMainWindow() self._tableModel = vtv.VtkTableModel() self._tableview = vtv.VtkTableView(self._tableModel) + self._tableview.setParent(main) + self._tableview.setWindowFlags(QtGui.Qt.Dialog) + self._tableview.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self.update_visualization() self._tableview.show() From aa6bb428cc484c33f4969484918eefc1cd2e0a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 18:23:32 +0200 Subject: [PATCH 07/27] FEM: Allow export of post processing data tables to CSV files or to cliboard to paste into spreadsheet programs --- src/Mod/Fem/femguiutils/vtk_table_view.py | 86 ++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index 95ebf1007e..912aa2aba4 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -32,6 +32,11 @@ __url__ = "https://www.freecad.org" from PySide import QtGui from PySide import QtCore +import FreeCAD +import FreeCADGui + +from vtkmodules.vtkIOCore import vtkDelimitedTextWriter + class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns # One can supply a header_names dict to replace the table column names @@ -91,6 +96,9 @@ class VtkTableModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return section + def getTable(self): + return self._table + class VtkTableSummaryModel(QtCore.QAbstractTableModel): # Simple model showing a summary of the table. # Only supports single component columns @@ -132,6 +140,9 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return self._table.GetColumnName(section) + def getTable(self): + return self._table + class VtkTableView(QtGui.QWidget): @@ -139,16 +150,87 @@ class VtkTableView(QtGui.QWidget): super().__init__() self.model = model + + layout = QtGui.QVBoxLayout() + layout.setContentsMargins(0,0,0,0) + layout.setSpacing(0) + + # start with the toolbar + self.toolbar = QtGui.QToolBar() + csv_action = QtGui.QAction(self) + csv_action.triggered.connect(self.exportCsv) + csv_action.setIcon(FreeCADGui.getIcon("Std_Export")) + csv_action.setToolTip("Export to CSV") + self.toolbar.addAction(csv_action) + + copy_action = QtGui.QAction(self) + copy_action.triggered.connect(self.copyToClipboard) + copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) + shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) + copy_action.setToolTip(f"Copy to clipboard ({shortcut.toString()})") + copy_action.setShortcut(shortcut) + self.toolbar.addAction(copy_action) + + layout.addWidget(self.toolbar) + + # now the table view self.table_view = QtGui.QTableView() self.table_view.setModel(model) + self.model.modelReset.connect(self.modelReset) # fast initial resize and manual resizing still allowed! header = self.table_view.horizontalHeader() header.setResizeContentsPrecision(10) self.table_view.resizeColumnsToContents() - layout = QtGui.QVBoxLayout() - layout.setContentsMargins(0,0,0,0) layout.addWidget(self.table_view) self.setLayout(layout) + @QtCore.Slot() + def modelReset(self): + # The model is reset, make sure the header visibility is working + # This is needed in case new data was added + self.table_view.resizeColumnsToContents() + + @QtCore.Slot(bool) + def exportCsv(self, state): + + file_path, filter = QtGui.QFileDialog.getSaveFileName(None, "Save as csv file", "", "CSV (*.csv)") + if not file_path: + FreeCAD.Console.PrintMessage("CSV file export aborted: no filename selected") + return + + writer = vtkDelimitedTextWriter() + writer.SetFileName(file_path) + writer.SetInputData(self.model.getTable()); + writer.Write(); + + @QtCore.Slot() + def copyToClipboard(self): + + sel_model = self.table_view.selectionModel() + selection = sel_model.selectedIndexes() + + if len(selection) < 1: + return + + copy_table = "" + previous = selection.pop(0) + for current in selection: + + data = self.model.data(previous, QtCore.Qt.DisplayRole); + copy_table += str(data) + + if current.row() != previous.row(): + copy_table += '\n' + else: + copy_table += '\t' + + previous = current + + copy_table += str(self.model.data(selection[-1], QtCore.Qt.DisplayRole)) + copy_table += '\n' + + clipboard = QtGui.QApplication.instance().clipboard() + clipboard.setText(copy_table) + From 318d0645f022e31bc351b5ab64014395c07fd381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 19:29:22 +0200 Subject: [PATCH 08/27] FEM: Usability and UI improvements for data extraction Update icons for post data extraction Improve translatability of post data extraction Fix post data extraction commit handling --- src/Mod/Fem/Gui/Resources/Fem.qrc | 1 - .../Fem/Gui/Resources/icons/FEM_PostField.svg | 94 ++++++++++++++----- .../Gui/Resources/icons/FEM_PostHistogram.svg | 45 +++++++-- .../Fem/Gui/Resources/icons/FEM_PostIndex.svg | 20 ++-- .../Gui/Resources/icons/FEM_PostLineplot.svg | 21 +++-- .../Gui/Resources/icons/FEM_PostPlotline.svg | 41 -------- .../Resources/icons/FEM_PostSpreadsheet.svg | 38 ++++++-- src/Mod/Fem/femguiutils/extract_link_view.py | 24 ++--- src/Mod/Fem/femguiutils/post_visualization.py | 5 +- src/Mod/Fem/femguiutils/vtk_table_view.py | 10 +- .../Fem/femobjects/base_fempostextractors.py | 14 +-- src/Mod/Fem/femobjects/post_extract1D.py | 7 +- src/Mod/Fem/femobjects/post_extract2D.py | 7 +- .../Fem/femtaskpanels/base_fempostpanel.py | 8 ++ .../Fem/femtaskpanels/task_post_histogram.py | 10 +- .../Fem/femtaskpanels/task_post_lineplot.py | 10 +- src/Mod/Fem/femtaskpanels/task_post_table.py | 14 +-- .../femviewprovider/view_post_histogram.py | 36 +++---- .../Fem/femviewprovider/view_post_lineplot.py | 30 +++--- .../Fem/femviewprovider/view_post_table.py | 3 +- 20 files changed, 260 insertions(+), 178 deletions(-) delete mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index 8777f6b4dc..351dad3e48 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -87,7 +87,6 @@ icons/FEM_PostBranchFilter.svg icons/FEM_PostPipelineFromResult.svg icons/FEM_PostLineplot.svg - icons/FEM_PostPlotline.svg icons/FEM_PostHistogram.svg icons/FEM_PostSpreadsheet.svg icons/FEM_PostField.svg diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg index 5a42219430..a93343fd25 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostField.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -25,36 +25,78 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:zoom="34.537747" - inkscape:cx="8.8743484" - inkscape:cy="9.6850556" + inkscape:cx="8.8598715" + inkscape:cy="9.6561018" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> - - + + + + + + - - + cx="4.0927677" + cy="12.27616" + rx="2.7138755" + ry="2.7138758" /> + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg index 4e6d52d4a1..333e138d83 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostHistogram.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,17 +24,46 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="97.6875" - inkscape:cx="8" - inkscape:cy="8" + inkscape:zoom="48.84375" + inkscape:cx="9.4996801" + inkscape:cy="6.1932182" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> - + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg index 36c93c04ba..9198dcdba0 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostIndex.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -25,18 +25,18 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:zoom="34.537747" - inkscape:cx="8.8453946" + inkscape:cx="7.7885798" inkscape:cy="9.6561018" - inkscape:window-width="3132" - inkscape:window-height="1772" + inkscape:window-width="1960" + inkscape:window-height="1308" inkscape:window-x="0" inkscape:window-y="0" - inkscape:window-maximized="1" + inkscape:window-maximized="0" inkscape:current-layer="svg1" /> + style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none" + id="path1" + cx="7.9564848" + cy="7.9564848" + r="4.2860532" /> diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg index 637dac60be..6e90515778 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostLineplot.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,9 +24,9 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="97.6875" - inkscape:cx="8" - inkscape:cy="8" + inkscape:zoom="45.254834" + inkscape:cx="1.6793786" + inkscape:cy="9.534893" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" @@ -34,8 +34,13 @@ inkscape:window-maximized="1" inkscape:current-layer="svg1" /> + id="rect1" + style="fill:#e5007e;fill-opacity:1;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" + d="M 0.5234375,0.5390625 V 14.324219 15.548828 H 2.0332031 15.248047 V 14.324219 H 2.0332031 V 0.5390625 Z" + sodipodi:nodetypes="ccccccccc" /> + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg deleted file mode 100644 index a788318bac..0000000000 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg index 6220e8e87f..b8453c0756 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostSpreadsheet.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,17 +24,41 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="97.6875" - inkscape:cx="8" - inkscape:cy="8" + inkscape:zoom="48.84375" + inkscape:cx="2.9277031" + inkscape:cy="8.4248241" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> + + style="fill:#e5007e;stroke:#260013;stroke-width:0.694259;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none" + d="M 1.013422,5.2704734 H 15.11965" + id="path2" /> + + + + diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 619ea358f1..ba0f151375 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -39,6 +39,8 @@ import FreeCADGui from . import post_visualization as pv +translate = FreeCAD.Qt.translate + # a model showing available visualizations and possible extractions # ################################################################# @@ -50,14 +52,14 @@ def build_new_visualization_tree_model(): visualizations = pv.get_registered_visualizations() for vis_name in visualizations: vis_icon = FreeCADGui.getIcon(visualizations[vis_name].icon) - vis_item = QtGui.QStandardItem(vis_icon, f"New {vis_name}") + vis_item = QtGui.QStandardItem(vis_icon, translate("FEM", "New {}").format(vis_name)) vis_item.setFlags(QtGui.Qt.ItemIsEnabled) vis_item.setData(visualizations[vis_name]) for ext in visualizations[vis_name].extractions: icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_name) - ext_item = QtGui.QStandardItem(icon, f"with {name}") + ext_item = QtGui.QStandardItem(icon, translate("FEM", "with {}").format(name)) ext_item.setData(ext) vis_item.appendRow(ext_item) model.appendRow(vis_item) @@ -89,7 +91,7 @@ def build_add_to_visualization_tree_model(): for ext in visualizations[vis_type].extractions: icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) - ext_item = QtGui.QStandardItem(icon, f"Add {name}") + ext_item = QtGui.QStandardItem(icon, translate("FEM", "Add {}").format(name)) ext_item.setData(ext) vis_item.appendRow(ext_item) @@ -101,7 +103,7 @@ def build_add_to_visualization_tree_model(): def build_post_object_item(post_object, extractions, vis_type): # definitely build a item and add the extractions - post_item = QtGui.QStandardItem(post_object.ViewObject.Icon, f"From {post_object.Label}") + post_item = QtGui.QStandardItem(post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label)) post_item.setFlags(QtGui.Qt.ItemIsEnabled) post_item.setData(post_object) @@ -109,7 +111,7 @@ def build_post_object_item(post_object, extractions, vis_type): for ext in extractions: icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) - ext_item = QtGui.QStandardItem(icon, f"add {name}") + ext_item = QtGui.QStandardItem(icon, translate("FEM", "add {}").format(name)) ext_item.setData(ext) post_item.appendRow(ext_item) @@ -268,7 +270,7 @@ class _SummaryWidget(QtGui.QWidget): self.viewButton.hide() self.warning = QtGui.QLabel(self) - self.warning.full_text = f"{extractor.Label}: Data source not available" + self.warning.full_text = translate("FEM", "{}: Data source not available").format(extractor.Label) self.rmButton = QtGui.QToolButton(self) self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) @@ -462,9 +464,9 @@ class ExtractLinkView(QtGui.QWidget): self._scroll_view.setWidgetResizable(True) hbox = QtGui.QHBoxLayout() - label = QtGui.QLabel("Data used in:") + label = QtGui.QLabel(translate("FEM", "Data used in:")) if not self._is_source: - label.setText("Data used from:") + label.setText(translate("FEM", "Data used from:")) label.setAlignment(QtGui.Qt.AlignBottom) hbox.addWidget(label) @@ -473,19 +475,19 @@ class ExtractLinkView(QtGui.QWidget): if self._is_source: self._add = _TreeChoiceButton(build_add_to_visualization_tree_model()) - self._add.setText("Add data to") + self._add.setText(translate("FEM", "Add data to")) self._add.selection.connect(self.addExtractionToVisualization) hbox.addWidget(self._add) self._create = _TreeChoiceButton(build_new_visualization_tree_model()) - self._create.setText("New") + self._create.setText(translate("FEM", "New")) self._create.selection.connect(self.newVisualization) hbox.addWidget(self._create) else: vis_type = vis.get_visualization_type(self._object) self._add = _TreeChoiceButton(build_add_from_data_tree_model(vis_type)) - self._add.setText("Add data from") + self._add.setText(translate("FEM", "Add data from")) self._add.selection.connect(self.addExtractionToPostObject) hbox.addWidget(self._add) diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index d1bfc93898..ab895a9a8c 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -95,7 +95,8 @@ class _VisualizationGroupCommand: return 0 def GetResources(self): - return { 'MenuText': 'Data Visualizations', 'ToolTip': 'Different visualizations to show post processing data in'} + return { 'MenuText': QtCore.QT_TRANSLATE_NOOP("FEM", 'Data Visualizations'), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("FEM", 'Different visualizations to show post processing data in')} def IsActive(self): if not FreeCAD.ActiveDocument: @@ -117,7 +118,7 @@ class _VisualizationCommand: return { "Pixmap": vis.icon, - "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, f"{self._visualization_type}"), + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), "Accel": "", "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), "CmdType": "AlterDoc" diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index 912aa2aba4..c1263150ac 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -37,6 +37,8 @@ import FreeCADGui from vtkmodules.vtkIOCore import vtkDelimitedTextWriter +translate = FreeCAD.Qt.translate + class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns # One can supply a header_names dict to replace the table column names @@ -160,14 +162,14 @@ class VtkTableView(QtGui.QWidget): csv_action = QtGui.QAction(self) csv_action.triggered.connect(self.exportCsv) csv_action.setIcon(FreeCADGui.getIcon("Std_Export")) - csv_action.setToolTip("Export to CSV") + csv_action.setToolTip(translate("FEM", "Export to CSV")) self.toolbar.addAction(csv_action) copy_action = QtGui.QAction(self) copy_action.triggered.connect(self.copyToClipboard) copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) - copy_action.setToolTip(f"Copy to clipboard ({shortcut.toString()})") + copy_action.setToolTip(translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString()))) copy_action.setShortcut(shortcut) self.toolbar.addAction(copy_action) @@ -195,9 +197,9 @@ class VtkTableView(QtGui.QWidget): @QtCore.Slot(bool) def exportCsv(self, state): - file_path, filter = QtGui.QFileDialog.getSaveFileName(None, "Save as csv file", "", "CSV (*.csv)") + file_path, filter = QtGui.QFileDialog.getSaveFileName(None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)") if not file_path: - FreeCAD.Console.PrintMessage("CSV file export aborted: no filename selected") + FreeCAD.Console.PrintMessage(translate("FEM", "CSV file export aborted: no filename selected")) return writer = vtkDelimitedTextWriter() diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 9e4ddac24f..e42d2adf1b 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -33,6 +33,8 @@ from vtkmodules.vtkCommonCore import vtkIntArray from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable +from PySide.QtCore import QT_TRANSLATE_NOOP + from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -78,14 +80,14 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): type="Fem::PropertyPostDataObject", name="Table", group="Base", - doc="The data table that stores the extracted data", + doc=QT_TRANSLATE_NOOP("FEM", "The data table that stores the extracted data"), value=vtkTable(), ), _PropHelper( type="App::PropertyLink", name="Source", group="Base", - doc="The data source from which the data is extracted", + doc=QT_TRANSLATE_NOOP("FEM", "The data source from which the data is extracted"), value=None, ), ] @@ -140,14 +142,14 @@ class Extractor1D(Extractor): type="App::PropertyEnumeration", name="XField", group="X Data", - doc="The field to use as X data", + doc=QT_TRANSLATE_NOOP("FEM", "The field to use as X data"), value=[], ), _PropHelper( type="App::PropertyEnumeration", name="XComponent", group="X Data", - doc="Which part of the X field vector to use for the X axis", + doc=QT_TRANSLATE_NOOP("FEM", "Which part of the X field vector to use for the X axis"), value=[], ), ] @@ -278,14 +280,14 @@ class Extractor2D(Extractor1D): type="App::PropertyEnumeration", name="YField", group="Y Data", - doc="The field to use as Y data", + doc=QT_TRANSLATE_NOOP("FEM", "The field to use as Y data"), value=[], ), _PropHelper( type="App::PropertyEnumeration", name="YComponent", group="Y Data", - doc="Which part of the Y field vector to use for the Y axis", + doc=QT_TRANSLATE_NOOP("FEM", "Which part of the Y field vector to use for the Y axis"), value=[], ), ] diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 425a594fae..987dfdabdd 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -39,6 +39,9 @@ from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline +from PySide.QtCore import QT_TRANSLATE_NOOP + + class PostFieldData1D(base_fempostextractors.Extractor1D): """ A post processing extraction of one dimensional field data @@ -54,7 +57,7 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc="Specify if the field shall be extracted for every available frame", + doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), value=False, ), ] @@ -126,7 +129,7 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): type="App::PropertyInteger", name="Index", group="X Data", - doc="Specify for which index the data should be extracted", + doc=QT_TRANSLATE_NOOP("FEM", "Specify for which index the data should be extracted"), value=0, ), ] diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 0b9e5c528e..baa5f3d8c2 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -39,6 +39,9 @@ from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline +from PySide.QtCore import QT_TRANSLATE_NOOP + + class PostFieldData2D(base_fempostextractors.Extractor2D): """ A post processing extraction of two dimensional field data @@ -54,7 +57,7 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc="Specify if the field shall be extracted for every available frame", + doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), value=False, ), ] @@ -142,7 +145,7 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): type="App::PropertyInteger", name="Index", group="Data", - doc="Specify for which point index the data should be extracted", + doc=QT_TRANSLATE_NOOP("FEM", "Specify for which point index the data should be extracted"), value=0, ), ] diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index a9edb902ec..08c067de7f 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -37,6 +37,8 @@ import FreeCADGui from femguiutils import selection_widgets from . import base_femtaskpanel +translate = FreeCAD.Qt.translate + class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): """ @@ -60,6 +62,12 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): if button == QtGui.QDialogButtonBox.Apply: self.obj.Document.recompute() + def open(self): + # open a new transaction if non is open + if not FreeCAD.getActiveTransaction(): + FreeCAD.ActiveDocument.openTransaction(translate("FEM", "Edit {}").format(self.obj.Label)) + + # Helper functions # ################ diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py index 79b4e4eeab..496fc1792a 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_histogram.py +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -38,6 +38,8 @@ from . import base_fempostpanel from femguiutils import extract_link_view as elv from femguiutils import vtk_table_view +translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -50,10 +52,10 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget = QtGui.QWidget() hbox = QtGui.QHBoxLayout() self.data_widget.show_plot = QtGui.QPushButton() - self.data_widget.show_plot.setText("Show plot") + self.data_widget.show_plot.setText(translate("FEM", "Show plot")) hbox.addWidget(self.data_widget.show_plot) self.data_widget.show_table = QtGui.QPushButton() - self.data_widget.show_table.setText("Show data") + self.data_widget.show_table.setText(translate("FEM", "Show data")) hbox.addWidget(self.data_widget.show_table) vbox = QtGui.QVBoxLayout() vbox.addItem(hbox) @@ -63,7 +65,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): vbox.addWidget(extracts) self.data_widget.setLayout(vbox) - self.data_widget.setWindowTitle("Histogram data") + self.data_widget.setWindowTitle(translate("FEM", "Histogram data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) @@ -71,7 +73,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" ) - self.view_widget.setWindowTitle("Histogram view settings") + self.view_widget.setWindowTitle(translate("FEM", "Histogram view settings")) self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) self.__init_widgets() diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py index 507e6b3cbf..474d84b80b 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -38,6 +38,8 @@ from . import base_fempostpanel from femguiutils import extract_link_view as elv from femguiutils import vtk_table_view +translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -50,10 +52,10 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget = QtGui.QWidget() hbox = QtGui.QHBoxLayout() self.data_widget.show_plot = QtGui.QPushButton() - self.data_widget.show_plot.setText("Show plot") + self.data_widget.show_plot.setText(translate("FEM", "Show plot")) hbox.addWidget(self.data_widget.show_plot) self.data_widget.show_table = QtGui.QPushButton() - self.data_widget.show_table.setText("Show data") + self.data_widget.show_table.setText(translate("FEM", "Show data")) hbox.addWidget(self.data_widget.show_table) vbox = QtGui.QVBoxLayout() vbox.addItem(hbox) @@ -63,7 +65,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): vbox.addWidget(extracts) self.data_widget.setLayout(vbox) - self.data_widget.setWindowTitle("Lineplot data") + self.data_widget.setWindowTitle(translate("FEM", "Lineplot data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) @@ -71,7 +73,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" ) - self.view_widget.setWindowTitle("Lineplot view settings") + self.view_widget.setWindowTitle(translate("FEM", "Lineplot view settings")) self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) self.__init_widgets() diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py index e17f584c01..b75549eeb9 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_table.py +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -38,6 +38,8 @@ from . import base_fempostpanel from femguiutils import extract_link_view as elv from femguiutils import vtk_table_view +translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -49,7 +51,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # data widget self.data_widget = QtGui.QWidget() self.data_widget.show_table = QtGui.QPushButton() - self.data_widget.show_table.setText("Show table") + self.data_widget.show_table.setText(translate("FEM", "Show table")) vbox = QtGui.QVBoxLayout() vbox.addWidget(self.data_widget.show_table) @@ -59,17 +61,9 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): vbox.addWidget(extracts) self.data_widget.setLayout(vbox) - self.data_widget.setWindowTitle("Table data") + self.data_widget.setWindowTitle(translate("FEM", "Table data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg")) - - # histogram parameter widget - #self.view_widget = FreeCADGui.PySideUic.loadUi( - # FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostTable.ui" - #) - #self.view_widget.setWindowTitle("Table view settings") - #self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostTable.svg")) - self.__init_widgets() # form made from param and selection widget diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 69aed4101f..fdbd975300 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -35,6 +35,7 @@ import FreeCADGui import Plot import FemGui from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP import io import numpy as np @@ -48,6 +49,7 @@ from femtaskpanels import task_post_histogram _GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + class EditViewWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -260,49 +262,49 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Legend", group="HistogramPlot", - doc="The name used in the plots legend", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), value="", ), _GuiPropHelper( type="App::PropertyColor", name="BarColor", group="HistogramBar", - doc="The color the data bin area is drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), value=(0, 85, 255, 255), ), _GuiPropHelper( type="App::PropertyEnumeration", name="Hatch", group="HistogramBar", - doc="The hatch pattern drawn in the bar", + doc=QT_TRANSLATE_NOOP("FEM", "The hatch pattern drawn in the bar"), value=['None', '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'], ), _GuiPropHelper( type="App::PropertyIntegerConstraint", name="HatchDensity", group="HistogramBar", - doc="The line width of the hatch", + doc=QT_TRANSLATE_NOOP("FEM", "The line width of the hatch)"), value=(1, 1, 99, 1), ), _GuiPropHelper( type="App::PropertyColor", name="LineColor", group="HistogramLine", - doc="The color the data bin area is drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), value=(0, 0, 0, 1), # black ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="LineWidth", group="HistogramLine", - doc="The width of the bar, between 0 and 1 (1 being without gaps)", + doc=QT_TRANSLATE_NOOP("FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"), value=(1, 0, 99, 0.1), ), _GuiPropHelper( type="App::PropertyEnumeration", name="LineStyle", group="HistogramLine", - doc="The style the line is drawn in", + doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), value=['None', '-', '--', '-.', ':'], ), ] @@ -390,70 +392,70 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Cumulative", group="Histogram", - doc="If be the bars shoud show the cumulative sum left to rigth", + doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), value=False, ), _GuiPropHelper( type="App::PropertyEnumeration", name="Type", group="Histogram", - doc="The type of histogram plotted", + doc=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"), value=["bar","barstacked", "step", "stepfilled"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="BarWidth", group="Histogram", - doc="The width of the bar, between 0 and 1 (1 being without gaps)", + doc=QT_TRANSLATE_NOOP("FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"), value=(0.9, 0, 1, 0.05), ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="HatchLineWidth", group="Histogram", - doc="The line width of all drawn hatch patterns", + doc=QT_TRANSLATE_NOOP("FEM", "The line width of all drawn hatch patterns"), value=(1, 0, 99, 0.1), ), _GuiPropHelper( type="App::PropertyInteger", name="Bins", group="Histogram", - doc="The number of bins the data is split into", + doc=QT_TRANSLATE_NOOP("FEM", "The number of bins the data is split into"), value=10, ), _GuiPropHelper( type="App::PropertyString", name="Title", group="Plot", - doc="The histogram plot title", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), value="", ), _GuiPropHelper( type="App::PropertyString", name="XLabel", group="Plot", - doc="The label shown for the histogram X axis", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"), value="", ), _GuiPropHelper( type="App::PropertyString", name="YLabel", group="Plot", - doc="The label shown for the histogram Y axis", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), value="", ), _GuiPropHelper( type="App::PropertyBool", name="Legend", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", name="LegendLocation", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), value=['best','upper right','upper left','lower left','lower right','right', 'center left','center right','lower center','upper center','center'], ), diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index dfd7f2b5b4..9d2d821e66 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -35,6 +35,8 @@ import FreeCADGui import Plot import FemGui from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + import io import numpy as np @@ -265,43 +267,43 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Legend", group="Lineplot", - doc="The name used in the plots legend", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), value="", ), _GuiPropHelper( type="App::PropertyColor", name="Color", group="Lineplot", - doc="The color the line and the markers are drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The color the line and the markers are drawn with"), value=(0, 85, 255, 255), ), _GuiPropHelper( type="App::PropertyEnumeration", name="LineStyle", group="Lineplot", - doc="The style the line is drawn in", + doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), value=['-', '--', '-.', ':', 'None'], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="LineWidth", group="Lineplot", - doc="The width the line is drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The width the line is drawn with"), value=(1, 0.1, 99, 0.1), ), _GuiPropHelper( type="App::PropertyEnumeration", name="MarkerStyle", group="Lineplot", - doc="The style the data markers are drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The style the data markers are drawn with"), value=['None', '*', '+', 's', '.', 'o', 'x'], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="MarkerSize", group="Lineplot", - doc="The size the data markers are drawn in", - value=(10, 0.1, 99, 0.1), + doc=QT_TRANSLATE_NOOP("FEM", "The size the data markers are drawn in"), + value=(5, 0.1, 99, 0.1), ), ] return super()._get_properties() + prop @@ -389,49 +391,49 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Grid", group="Lineplot", - doc="If be the bars shoud show the cumulative sum left to rigth", + doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", name="Scale", group="Lineplot", - doc="The scale the axis are drawn in", + doc=QT_TRANSLATE_NOOP("FEM", "The scale the axis are drawn in"), value=["linear","semi-log x", "semi-log y", "log"], ), _GuiPropHelper( type="App::PropertyString", name="Title", group="Plot", - doc="The histogram plot title", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), value="", ), _GuiPropHelper( type="App::PropertyString", name="XLabel", group="Plot", - doc="The label shown for the histogram X axis", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"), value="", ), _GuiPropHelper( type="App::PropertyString", name="YLabel", group="Plot", - doc="The label shown for the histogram Y axis", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), value="", ), _GuiPropHelper( type="App::PropertyBool", name="Legend", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", name="LegendLocation", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), value=['best','upper right','upper left','lower left','lower right','right', 'center left','center right','lower center','upper center','center'], ), diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 7b07cc785f..ac20fd2f01 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -35,6 +35,7 @@ import FreeCADGui import Plot import FemGui from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP import io import numpy as np @@ -186,7 +187,7 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Name", group="Table", - doc="The name used in the table header. Default name is used if empty", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the table header. Default name is used if empty"), value="", ), ] From 974f41fc9693c00206e973bfc43447c05399c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Tue, 6 May 2025 16:37:03 +0200 Subject: [PATCH 09/27] FEM: Adopt post extraction code to updated main --- src/Mod/Fem/App/FemPostFilterPyImp.cpp | 2 +- src/Mod/Fem/App/FemPostObjectPyImp.cpp | 4 +-- src/Mod/Fem/App/FemPostPipelinePyImp.cpp | 4 +-- src/Mod/Fem/CMakeLists.txt | 33 +++++++++++----------- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 10 +++---- src/Mod/Fem/Gui/Workbench.cpp | 4 +-- src/Mod/Fem/InitGui.py | 6 ++-- src/Mod/Fem/femcommands/commands.py | 3 +- src/Mod/Fem/femcommands/manager.py | 4 +-- src/Mod/Fem/femobjects/post_glyphfilter.py | 1 - src/Mod/Fem/femobjects/post_histogram.py | 3 ++ src/Mod/Fem/femobjects/post_lineplot.py | 4 +++ src/Mod/Fem/femobjects/post_table.py | 5 +++- 13 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index 097915f78e..2e09c9e3f0 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -189,7 +189,7 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) PyObject* FemPostFilterPy::getOutputAlgorithm(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/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index 27a1204bc0..a35e0b569a 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -29,7 +29,7 @@ #include "FemPostObjectPy.h" #include "FemPostObjectPy.cpp" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON #include #include #endif //BUILD_FEM_VTK @@ -61,7 +61,7 @@ PyObject* FemPostObjectPy::writeVTK(PyObject* args) PyObject* FemPostObjectPy::getDataSet(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/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index 3154800802..388aed35de 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -34,7 +34,7 @@ #include "FemPostPipelinePy.cpp" // clang-format on -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON #include #endif //BUILD_FEM_VTK @@ -319,7 +319,7 @@ PyObject* FemPostPipelinePy::renameArrays(PyObject* args) PyObject* FemPostPipelinePy::getOutputAlgorithm(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/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 3ba5a83c3e..aca1b443f0 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -182,8 +182,6 @@ SET(FemObjects_SRCS femobjects/base_femelement.py femobjects/base_femmeshelement.py femobjects/base_fempythonobject.py - femobjects/base_fempostextractors.py - femobjects/base_fempostvisualizations.py femobjects/constant_vacuumpermittivity.py femobjects/constraint_bodyheatsource.py femobjects/constraint_centrif.py @@ -216,8 +214,9 @@ SET(FemObjects_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemObjects_SRCS - ${FemObjects_SRCS} + list(APPEND FemObjects_SRCS + femobjects/base_fempostextractors.py + femobjects/base_fempostvisualizations.py femobjects/post_glyphfilter.py femobjects/post_extract1D.py femobjects/post_extract2D.py @@ -633,8 +632,7 @@ SET(FemGuiTaskPanels_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemGuiTaskPanels_SRCS - ${FemGuiTaskPanels_SRCS} + list(APPEND FemGuiTaskPanels_SRCS femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_post_histogram.py femtaskpanels/task_post_lineplot.py @@ -653,13 +651,18 @@ SET(FemGuiUtils_SRCS femguiutils/disambiguate_solid_selection.py femguiutils/migrate_gui.py femguiutils/selection_widgets.py - femguiutils/vtk_module_handling.py - femguiutils/vtk_table_view.py - femguiutils/data_extraction.py - femguiutils/extract_link_view.py - femguiutils/post_visualization.py ) +if(BUILD_FEM_VTK_PYTHON) + list(APPEND FemGuiUtils_SRCS + femguiutils/vtk_module_handling.py + femguiutils/vtk_table_view.py + femguiutils/data_extraction.py + femguiutils/extract_link_view.py + femguiutils/post_visualization.py + ) +endif(BUILD_FEM_VTK_PYTHON) + SET(FemGuiViewProvider_SRCS femviewprovider/__init__.py femviewprovider/view_base_femconstraint.py @@ -667,8 +670,6 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_base_femmaterial.py femviewprovider/view_base_femmeshelement.py femviewprovider/view_base_femobject.py - femviewprovider/view_base_fempostvisualization.py - femviewprovider/view_base_fempostextractors.py femviewprovider/view_constant_vacuumpermittivity.py femviewprovider/view_constraint_bodyheatsource.py femviewprovider/view_constraint_centrif.py @@ -701,10 +702,10 @@ SET(FemGuiViewProvider_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemGuiViewProvider_SRCS - ${FemGuiViewProvider_SRCS} + list(APPEND FemGuiViewProvider_SRCS + femviewprovider/view_base_fempostextractors.py + femviewprovider/view_base_fempostvisualization.py femviewprovider/view_post_glyphfilter.py - femviewprovider/view_post_extract.py femviewprovider/view_post_histogram.py femviewprovider/view_post_lineplot.py femviewprovider/view_post_table.py diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 57f39a70a2..45ace5bb15 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -76,7 +76,7 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } if (m_panel.hasAttr(std::string("widget"))) { @@ -129,7 +129,7 @@ void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } }; @@ -145,7 +145,7 @@ bool TaskPostExtraction::isGuiTaskOnly() } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } return false; @@ -162,7 +162,7 @@ void TaskPostExtraction::apply() } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } } @@ -178,7 +178,7 @@ bool TaskPostExtraction::initiallyCollapsed() } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } return false; diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index acd7202acc..44c1186c6f 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -215,7 +215,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterCalculator" << "Separator" << "FEM_PostCreateFunctions" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif ; @@ -371,7 +371,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterCalculator" << "Separator" << "FEM_PostCreateFunctions" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif ; diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 8ac271d379..35c835f81e 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -81,9 +81,9 @@ class FemWorkbench(Workbench): 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() + if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + 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 diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 50461484a2..8c85fc7270 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -40,7 +40,6 @@ from .manager import CommandManager from femtools.femutils import expandParentObject from femtools.femutils import is_of_type from femsolver.settings import get_default_solver -from femguiutils import post_visualization # Python command definitions: # for C++ command definitions see src/Mod/Fem/Command.cpp @@ -1294,4 +1293,6 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: import femobjects.post_lineplot import femobjects.post_histogram import femobjects.post_table + + from femguiutils import post_visualization post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index d57764e54b..50fbae0548 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -34,7 +34,6 @@ 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 @@ -380,7 +379,8 @@ 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 filter + # check if we should use python fitler + from femguiutils.vtk_module_handling import vtk_compatibility_abort if vtk_compatibility_abort(True): return diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py index a783835656..51c7d480c6 100644 --- a/src/Mod/Fem/femobjects/post_glyphfilter.py +++ b/src/Mod/Fem/femobjects/post_glyphfilter.py @@ -33,7 +33,6 @@ 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 diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index 0a6277f5fe..fcbb1ce2e7 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -29,6 +29,9 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying histograms +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() from . import base_fempostextractors from . import base_fempostvisualizations diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 486241b367..8d4b725128 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -29,6 +29,10 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() + from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract2D diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index 3d7d7be689..0a64dc733d 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -29,11 +29,14 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying tables +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() + from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract1D - from femguiutils import post_visualization # register visualization and extractors From 89c71c511ae900e585bd10aedda8450889a9d0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Thu, 8 May 2025 16:55:07 +0200 Subject: [PATCH 10/27] FEM: Data extraction objects are FEM::FeaturePython This allows them to be drag and droped in an analysis --- src/Mod/Fem/ObjectsFem.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c21c219dda..1ca11e2566 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -690,7 +690,7 @@ def makePostLineplot(doc, name="Lineplot"): """makePostLineplot(document, [name]): creates a FEM post processing line plot """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_lineplot post_lineplot.PostLineplot(obj) @@ -704,7 +704,7 @@ def makePostLineplotFieldData(doc, name="FieldData2D"): """makePostLineplotFieldData(document, [name]): creates a FEM post processing data extractor for 2D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_lineplot post_lineplot.PostLineplotFieldData(obj) @@ -718,7 +718,7 @@ def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): """makePostLineplotIndexOverFrames(document, [name]): creates a FEM post processing data extractor for 2D index data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_lineplot post_lineplot.PostLineplotIndexOverFrames(obj) @@ -732,7 +732,7 @@ def makePostHistogram(doc, name="Histogram"): """makePostHistogram(document, [name]): creates a FEM post processing histogram plot """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_histogram post_histogram.PostHistogram(obj) @@ -746,7 +746,7 @@ def makePostHistogramFieldData(doc, name="FieldData1D"): """makePostHistogramFieldData(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_histogram post_histogram.PostHistogramFieldData(obj) @@ -760,7 +760,7 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): """makePostHistogramIndexOverFrames(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_histogram post_histogram.PostHistogramIndexOverFrames(obj) @@ -774,7 +774,7 @@ def makePostTable(doc, name="Table"): """makePostTable(document, [name]): creates a FEM post processing histogram plot """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_table post_table.PostTable(obj) @@ -788,7 +788,7 @@ def makePostTableFieldData(doc, name="FieldData1D"): """makePostTableFieldData(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_table post_table.PostTableFieldData(obj) @@ -802,7 +802,7 @@ def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): """makePostTableIndexOverFrames(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_table post_table.PostTableIndexOverFrames(obj) From 6ba5c0d0a5bdb0b0fab33188b34de4a354ed0ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Thu, 8 May 2025 19:30:01 +0200 Subject: [PATCH 11/27] FEM: port DataAlongLine filter to use arc length. This makes it easier for the new data extraction to also plot data over line length. --- src/Mod/Fem/App/FemPostFilter.cpp | 17 +++++++---------- src/Mod/Fem/App/FemPostFilter.h | 2 ++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Mod/Fem/App/FemPostFilter.cpp b/src/Mod/Fem/App/FemPostFilter.cpp index d7d054f083..79da85c5a1 100644 --- a/src/Mod/Fem/App/FemPostFilter.cpp +++ b/src/Mod/Fem/App/FemPostFilter.cpp @@ -382,18 +382,21 @@ FemPostDataAlongLineFilter::FemPostDataAlongLineFilter() m_line->SetPoint2(vec2.x, vec2.y, vec2.z); m_line->SetResolution(Resolution.getValue()); + m_arclength = vtkSmartPointer::New(); + m_arclength->SetInputConnection(m_line->GetOutputPort(0)); + auto passthrough = vtkSmartPointer::New(); m_probe = vtkSmartPointer::New(); m_probe->SetSourceConnection(passthrough->GetOutputPort(0)); - m_probe->SetInputConnection(m_line->GetOutputPort()); - m_probe->SetValidPointMaskArrayName("ValidPointArray"); + m_probe->SetInputConnection(m_arclength->GetOutputPort()); m_probe->SetPassPointArrays(1); m_probe->SetPassCellArrays(1); m_probe->ComputeToleranceOff(); m_probe->SetTolerance(0.01); clip.source = passthrough; + clip.algorithmStorage.push_back(m_arclength); clip.target = m_probe; addFilterPipeline(clip, "DataAlongLine"); @@ -488,12 +491,7 @@ void FemPostDataAlongLineFilter::GetAxisData() return; } - vtkDataArray* tcoords = dset->GetPointData()->GetTCoords("Texture Coordinates"); - - const Base::Vector3d& vec1 = Point1.getValue(); - const Base::Vector3d& vec2 = Point2.getValue(); - const Base::Vector3d diff = vec1 - vec2; - double Len = diff.Length(); + vtkDataArray* alength = dset->GetPointData()->GetArray("arc_length"); for (vtkIdType i = 0; i < dset->GetNumberOfPoints(); ++i) { double value = 0; @@ -517,8 +515,7 @@ void FemPostDataAlongLineFilter::GetAxisData() } values.push_back(value); - double tcoord = tcoords->GetComponent(i, 0); - coords.push_back(tcoord * Len); + coords.push_back(alength->GetTuple1(i)); } YAxisData.setValues(values); diff --git a/src/Mod/Fem/App/FemPostFilter.h b/src/Mod/Fem/App/FemPostFilter.h index 273bd63c0f..38499d01d3 100644 --- a/src/Mod/Fem/App/FemPostFilter.h +++ b/src/Mod/Fem/App/FemPostFilter.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -181,6 +182,7 @@ protected: private: vtkSmartPointer m_line; + vtkSmartPointer m_arclength; vtkSmartPointer m_probe; }; From 64d864c944735f682955274aa28abda181cec143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 09:42:16 +0200 Subject: [PATCH 12/27] FEM: Add data extraction objects to FEM test suite --- src/Mod/Fem/femtest/app/test_object.py | 27 ++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index 93d331ea94..7b34db84e6 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -79,12 +79,16 @@ class TestObjectCreate(unittest.TestCase): # gmsh mesh children: group, region, boundary layer --> 3 # result children: mesh result --> 1 # analysis itself is not in analysis group --> 1 - # vtk post pipeline children: region, scalar, cut, wrap, glyph --> 5 - # vtk python post objects: glyph --> 1 + # vtk post pipeline children: region, scalar, cut, wrap, contour --> 5 + # vtk python post objects: glyph, 6x data extraction --> 7 subtraction = 15 if vtk_objects_used: - subtraction += 6 + subtraction += 12 + if not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__): + # remove the 3 data visualization objects that would be in the Analysis + # if they would be available (Lineplot, histogram, table) + subtraction += 3 self.assertEqual(len(doc.Analysis.Group), count_defmake - subtraction) @@ -92,7 +96,9 @@ class TestObjectCreate(unittest.TestCase): # have been counted, but will not be executed to create objects failed = 0 if vtk_objects_used and not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__): - failed += 1 + # the 7 objects also counted in subtraction, +3 additional objects that are + # added directly to the analysis + failed += 10 self.assertEqual(len(doc.Objects), count_defmake - failed) @@ -1167,6 +1173,19 @@ def create_all_fem_objects_doc(doc): if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: ObjectsFem.makePostFilterGlyph(doc, vres) + # data extraction objects + lp = analysis.addObject(ObjectsFem.makePostLineplot(doc))[0] + lp.addObject(ObjectsFem.makePostLineplotFieldData(doc)) + lp.addObject(ObjectsFem.makePostLineplotIndexOverFrames(doc)) + + hp = analysis.addObject(ObjectsFem.makePostHistogram(doc))[0] + hp.addObject(ObjectsFem.makePostHistogramFieldData(doc)) + hp.addObject(ObjectsFem.makePostHistogramIndexOverFrames(doc)) + + tb = analysis.addObject(ObjectsFem.makePostTable(doc))[0] + tb.addObject(ObjectsFem.makePostTableFieldData(doc)) + tb.addObject(ObjectsFem.makePostTableIndexOverFrames(doc)) + analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc)) analysis.addObject(ObjectsFem.makeSolverCalculiX(doc)) sol = analysis.addObject(ObjectsFem.makeSolverElmer(doc))[0] From d50a98b9cfb584db997e57109964ebc5645d402c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 10:02:50 +0200 Subject: [PATCH 13/27] FEM: Ensure post task dialogs work without VTK python build --- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 8 +++++++- src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp | 7 +++++++ src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 4ea5f3e36a..f6c60491b8 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -31,8 +31,10 @@ #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" #include "ViewProviderFemPostFilterPy.h" -#include "TaskPostExtraction.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif using namespace FemGui; @@ -91,9 +93,11 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) auto panel = new TaskPostDataAlongLine(this); dlg->addTaskBox(panel->getIcon(), panel); +#ifdef FC_USE_VTK_PYTHON // and the extraction auto extr_panel = new TaskPostExtraction(this); dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } @@ -144,9 +148,11 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) auto panel = new TaskPostDataAtPoint(this); dlg->addTaskBox(panel->getIcon(), panel); +#ifdef FC_USE_VTK_PYTHON // and the extraction auto extr_panel = new TaskPostExtraction(this); dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp index 7caff695eb..5683ce2467 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -27,7 +27,9 @@ #include #include "ViewProviderFemPostFilter.h" #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON #include "TaskPostExtraction.h" +#endif // inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml) #include "ViewProviderFemPostFilterPy.h" #include "ViewProviderFemPostFilterPy.cpp" @@ -63,6 +65,7 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args) { +#ifdef FC_USE_VTK_PYTHON // we take no arguments if (!PyArg_ParseTuple(args, "")) { return nullptr; @@ -77,6 +80,10 @@ PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args PyErr_SetString(PyExc_TypeError, "creating the panel failed"); return nullptr; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif } PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 2a34070c72..b3dcbf4c7b 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -67,7 +67,9 @@ #include #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON #include "TaskPostExtraction.h" +#endif #include "ViewProviderAnalysis.h" #include "ViewProviderFemPostObject.h" @@ -1024,8 +1026,10 @@ void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) auto disp_panel = new TaskPostDisplay(this); dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel); +#ifdef FC_USE_VTK_PYTHON auto extr_panel = new TaskPostExtraction(this); dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } void ViewProviderFemPostObject::unsetEdit(int ModNum) From 8cec9ad7f92f3c38d48af55ab67b60876233ecd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 11:19:49 +0200 Subject: [PATCH 14/27] FEM: Adopt data extraction for VTK <9.3: different table filter Additionally remove unneeded includes in c++ code remaining from earlier experiments --- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 4 ---- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 5 ----- src/Mod/Fem/femguiutils/data_extraction.py | 17 +++++++++++++++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 1a2c244943..804cde7f14 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -71,10 +71,6 @@ #include "ViewProviderFemPostObject.h" #include "ViewProviderFemPostBranchFilter.h" -#include -#include -#include - using namespace FemGui; using namespace Gui; diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 45ace5bb15..4765b14c0e 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -37,11 +37,6 @@ #include #include -#include -#include -#include -#include - #include "ViewProviderFemPostObject.h" #include "TaskPostExtraction.h" diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 23a1bb784b..5b80f64d36 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -33,10 +33,17 @@ from . import vtk_table_view from PySide import QtCore, QtGui +from vtkmodules.vtkCommonCore import vtkVersion from vtkmodules.vtkCommonDataModel import vtkTable -from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents +if vtkVersion.GetVTKMajorVersion() > 9 and \ + vtkVersion.GetVTKMinorVersion() > 3: + from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter +else: + from vtkmodules.vtkInfovisCore import vtkDataObjectToTable + + import FreeCAD import FreeCADGui @@ -117,7 +124,13 @@ class DataExtraction(_BasePostTaskPanel): if not algo: self.data_model.setTable(vtkTable()) - filter = vtkAttributeDataToTableFilter() + if vtkVersion.GetVTKMajorVersion() > 9 and \ + vtkVersion.GetVTKMinorVersion() > 3: + filter = vtkAttributeDataToTableFilter() + else: + filter = vtkDataObjectToTable() + filter.SetFieldType(vtkDataObjectToTable.POINT_DATA) + filter.SetInputConnection(0, algo.GetOutputPort(0)) filter.Update() table = filter.GetOutputDataObject(0) From fbf31f8657ec663330cfd26f77675c2d6016745b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 12:09:47 +0200 Subject: [PATCH 15/27] FEM: Data extraction ui works better with stylesheets --- src/Mod/Fem/femguiutils/extract_link_view.py | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index ba0f151375..64816dda48 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -196,7 +196,7 @@ class _TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) -class _SettingsPopup(QtGui.QGroupBox): +class _SettingsPopup(QtGui.QDialog): close = QtCore.Signal() @@ -211,9 +211,9 @@ class _SettingsPopup(QtGui.QGroupBox): buttonBox = QtGui.QDialogButtonBox() buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + buttonBox.accepted.connect(self.hide) vbox.addWidget(buttonBox) - buttonBox.accepted.connect(self.hide) self.setLayout(vbox) def showEvent(self, event): @@ -301,7 +301,7 @@ class _SummaryWidget(QtGui.QWidget): btn = QtGui.QPushButton(self) btn.full_text = text - btn.setMinimumSize(btn.sizeHint()) + btn.setMinimumWidth(0) btn.setFlat(True) btn.setText(text) btn.setStyleSheet("text-align:left;padding:6px"); @@ -311,7 +311,13 @@ class _SummaryWidget(QtGui.QWidget): def _redraw(self): - btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton + btn_spacing = 3 + btn_height = self.extrButton.sizeHint().height() + + # total size notes: + # - 5 spacing = 2x between buttons + 3 spacings to remove button + # - remove btn_height as this is used as remove button square size + btn_total_size = self.size().width() - btn_height - 5*btn_spacing btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() fm = self.fontMetrics() @@ -338,23 +344,23 @@ class _SummaryWidget(QtGui.QWidget): btn.setText("") btn.setStyleSheet("text-align:center;"); - rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) + rect = QtCore.QRect(pos, 0, btn_size, btn_height) btn.setGeometry(rect) - pos+=btn_size + pos += btn_size + btn_spacing else: warning_txt = fm.elidedText(self.warning.full_text, QtGui.Qt.ElideRight, btn_total_size) self.warning.setText(warning_txt) - rect = QtCore.QRect(0,0, btn_total_size, self.extrButton.sizeHint().height()) + rect = QtCore.QRect(0,0, btn_total_size, btn_height) self.warning.setGeometry(rect) - rmsize = self.extrButton.sizeHint().height() + rmsize = btn_height pos = self.size().width() - rmsize self.rmButton.setGeometry(pos, 0, rmsize, rmsize) frame_hint = self.frame.sizeHint() - rect = QtCore.QRect(0, self.extrButton.sizeHint().height()+frame_hint.height(), self.size().width(), frame_hint.height()) + rect = QtCore.QRect(0, btn_height+frame_hint.height(), self.size().width(), frame_hint.height()) self.frame.setGeometry(rect) def resizeEvent(self, event): From 8dff03ff794fb4c2b7661c064c01a7c073fafc01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 12:48:07 +0200 Subject: [PATCH 16/27] FEM: Ensure tests run without GUI with data extraction code --- src/Mod/Fem/femguiutils/post_visualization.py | 13 ++++++++++--- src/Mod/Fem/femguiutils/vtk_module_handling.py | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index ab895a9a8c..9975db127b 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -29,14 +29,16 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief A registry to collect visualizations for use in menus +# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui +# directly to support cmd line use. + import copy from dataclasses import dataclass -from PySide import QtGui, QtCore +from PySide import QtCore import FreeCAD -import FreeCADGui -import FemGui + # Registry to handle visulization commands # ######################################## @@ -102,6 +104,7 @@ class _VisualizationGroupCommand: if not FreeCAD.ActiveDocument: return False + import FemGui return bool(FemGui.getActiveAnalysis()) @@ -129,9 +132,11 @@ class _VisualizationCommand: if not FreeCAD.ActiveDocument: return False + import FemGui return bool(FemGui.getActiveAnalysis()) def Activated(self): + import FreeCADGui vis = _registry[self._visualization_type] FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}") @@ -155,6 +160,8 @@ def setup_commands(toplevel_name): # creates all visualization commands and registers them. The # toplevel group command will have the name provided to this function. + import FreeCADGui + # first all visualization and extraction commands for vis in _registry: FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis)) diff --git a/src/Mod/Fem/femguiutils/vtk_module_handling.py b/src/Mod/Fem/femguiutils/vtk_module_handling.py index 6c834b0820..9ae5cc2fb9 100644 --- a/src/Mod/Fem/femguiutils/vtk_module_handling.py +++ b/src/Mod/Fem/femguiutils/vtk_module_handling.py @@ -47,6 +47,10 @@ __title__ = "FEM GUI vtk python module check" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" + +# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui +# directly to support cmd line use. + __user_input_received = False From dcfc9327111572b73ecfab0091a405d865a06edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 13:49:57 +0200 Subject: [PATCH 17/27] FEM: Fix impact of stylesheet min button widht --- src/Mod/Fem/femguiutils/extract_link_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 64816dda48..db524361ca 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -304,7 +304,7 @@ class _SummaryWidget(QtGui.QWidget): btn.setMinimumWidth(0) btn.setFlat(True) btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px"); + btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); btn.setToolTip(text) return btn @@ -339,10 +339,10 @@ class _SummaryWidget(QtGui.QWidget): text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px"); + btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); else: btn.setText("") - btn.setStyleSheet("text-align:center;"); + btn.setStyleSheet("text-align:center;min-width:20px;"); rect = QtCore.QRect(pos, 0, btn_size, btn_height) btn.setGeometry(rect) From 8786b072d9f784e0b12ea5704cf4d5f791fb6e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 11 May 2025 19:30:44 +0200 Subject: [PATCH 18/27] FEM: Update data extraction dialog titles and spelling errors --- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 2 +- src/Mod/Fem/femguiutils/data_extraction.py | 2 ++ src/Mod/Fem/femviewprovider/view_post_histogram.py | 1 + src/Mod/Fem/femviewprovider/view_post_lineplot.py | 1 + src/Mod/Fem/femviewprovider/view_post_table.py | 1 + 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 4765b14c0e..54027dde38 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -93,7 +93,7 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* } } } - // if we are here somethign went wrong! + // if we are here something went wrong! throw Base::ImportError("Unable to import data extraction widget"); }; diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 5b80f64d36..8cfb845d17 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -92,6 +92,7 @@ class DataExtraction(_BasePostTaskPanel): def showData(self): dialog = QtGui.QDialog(self.widget) + dialog.setWindowTitle(f"Data of {self.Object.Label}") widget = vtk_table_view.VtkTableView(self.data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) @@ -105,6 +106,7 @@ class DataExtraction(_BasePostTaskPanel): def showSummary(self): dialog = QtGui.QDialog(self.widget) + dialog.setWindowTitle(f"Data summary of {self.Object.Label}") widget = vtk_table_view.VtkTableView(self.summary_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index fdbd975300..9b976d62b9 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -484,6 +484,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 9d2d821e66..fb17a2eaaf 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -462,6 +462,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index ac20fd2f01..cf8da7fdab 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -261,6 +261,7 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): main = FreeCADGui.getMainWindow() self._tableModel = vtv.VtkTableModel() self._tableview = vtv.VtkTableView(self._tableModel) + self._tableview.setWindowTitle(self.Object.Label) self._tableview.setParent(main) self._tableview.setWindowFlags(QtGui.Qt.Dialog) self._tableview.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio From 997fa8a193fdc67194fb2fd5f51057af0c621938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 11 May 2025 20:57:41 +0200 Subject: [PATCH 19/27] FEM: Remove VTK 9.4 only function And make sure filters task dialogs can be used if something in python fails --- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 12 ++++++++---- src/Mod/Fem/femguiutils/data_extraction.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 54027dde38..abce2a30cf 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -59,11 +59,14 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* Base::PyGILStateLocker lock; - Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true); - if (mod.isNull()) - throw Base::ImportError("Unable to import data extraction widget"); try { + Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true); + if (mod.isNull()) { + Base::Console().error("Unable to import data extraction widget\n"); + return; + } + Py::Callable method(mod.getAttr(std::string("DataExtraction"))); Py::Tuple args(1); args.setItem(0, Py::Object(view->getPyObject())); @@ -93,8 +96,9 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* } } } + // if we are here something went wrong! - throw Base::ImportError("Unable to import data extraction widget"); + Base::Console().error("Unable to import data extraction widget\n"); }; TaskPostExtraction::~TaskPostExtraction() { diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 8cfb845d17..cb55295703 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -139,7 +139,7 @@ class DataExtraction(_BasePostTaskPanel): # add the points points = algo.GetOutputDataObject(0).GetPoints().GetData() - table.InsertColumn(points, 0) + table.AddColumn(points) # split the components splitter = vtkSplitColumnComponents() From ecf2dba626ce63643aa1da09b0d68383668d945d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 14 May 2025 16:57:52 +0200 Subject: [PATCH 20/27] FEM: Adopt data extraction code to ubuntu LTS --- src/Mod/Fem/femguiutils/extract_link_view.py | 197 ++++++++++--------- 1 file changed, 105 insertions(+), 92 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index db524361ca..568724ceb9 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -60,6 +60,7 @@ def build_new_visualization_tree_model(): icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_name) ext_item = QtGui.QStandardItem(icon, translate("FEM", "with {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) vis_item.appendRow(ext_item) model.appendRow(vis_item) @@ -92,6 +93,7 @@ def build_add_to_visualization_tree_model(): icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) ext_item = QtGui.QStandardItem(icon, translate("FEM", "Add {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) vis_item.appendRow(ext_item) @@ -112,6 +114,7 @@ def build_post_object_item(post_object, extractions, vis_type): icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) ext_item = QtGui.QStandardItem(icon, translate("FEM", "add {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) post_item.appendRow(ext_item) @@ -150,6 +153,54 @@ def build_add_from_data_tree_model(vis_type): # implementation of GUI and its functionality # ########################################### +class _ElideToolButton(QtGui.QToolButton): + # tool button that elides its text, and left align icon and text + + def __init__(self, icon, text, parent): + super().__init__(parent) + + self._text = text + self._icon = icon + + def setCustomText(self, text): + self._text = text + self.repaint() + + def setCustomIcon(self, icon): + self._icon = icon + self.repaint() + + def sizeHint(self): + button_size = super().sizeHint() + return QtCore.QSize(self.iconSize().width()+10, button_size.height()) + + def paintEvent(self, event): + + # draw notmal button, without text and icon + super().paintEvent(event) + + # add icon and elided text + painter = QtGui.QPainter() + painter.begin(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing, True) + painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) + + margin = (self.height() - self.iconSize().height()) / 2 + match type(self._icon): + case QtGui.QPixmap: + painter.drawPixmap(margin, margin, self._icon.scaled(self.iconSize())) + case QtGui.QIcon: + self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + + fm = self.fontMetrics() + text_size = self.width() - self.iconSize().width() - 3*margin + text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, text_size) + if text: + painter.drawText(2*margin+self.iconSize().width(), margin + fm.ascent(), text) + + painter.end() + + class _TreeChoiceButton(QtGui.QToolButton): selection = QtCore.Signal(object,object) @@ -167,10 +218,13 @@ class _TreeChoiceButton(QtGui.QToolButton): self.tree_view.setFrameShape(QtGui.QFrame.NoFrame) self.tree_view.setHeaderHidden(True) - self.tree_view.setEditTriggers(QtGui.QTreeView.EditTriggers.NoEditTriggers) self.tree_view.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows) self.tree_view.expandAll() - self.tree_view.activated.connect(self.selectIndex) + self.tree_view.clicked.connect(self.selectIndex) + + style = self.style() + if not style.styleHint(QtGui.QStyle.SH_ItemView_ActivateItemOnSingleClick): + self.tree_view.activated.connect(self.selectIndex) # set a complex menu self.popup = QtGui.QWidgetAction(self) @@ -180,6 +234,7 @@ class _TreeChoiceButton(QtGui.QToolButton): QtCore.Slot(QtCore.QModelIndex) def selectIndex(self, index): + print("select triggered") item = self.model.itemFromIndex(index) if item and not item.hasChildren(): @@ -214,7 +269,13 @@ class _SettingsPopup(QtGui.QDialog): buttonBox.accepted.connect(self.hide) vbox.addWidget(buttonBox) - self.setLayout(vbox) + widget = QtGui.QFrame() + widget.setLayout(vbox) + + vbox2 = QtGui.QVBoxLayout() + vbox2.setContentsMargins(0,0,0,0) + vbox2.addWidget(widget) + self.setLayout(vbox2) def showEvent(self, event): # required to get keyboard events @@ -246,22 +307,22 @@ class _SummaryWidget(QtGui.QWidget): extr_repr = extractor.ViewObject.Proxy.get_preview() # build the UI + hbox = QtGui.QHBoxLayout() + hbox.setContentsMargins(6,0,6,0) + hbox.setSpacing(5) - self.extrButton = self._button(extr_label) - self.extrButton.setIcon(extractor.ViewObject.Icon) - - self.viewButton = self._button(extr_repr[1]) + self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) + self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) if not extr_repr[0].isNull(): size = self.viewButton.iconSize() size.setWidth(size.width()*2) self.viewButton.setIconSize(size) - self.viewButton.setIcon(extr_repr[0]) else: self.viewButton.setIconSize(QtCore.QSize(0,0)) if st_object: - self.stButton = self._button(st_object.Label) - self.stButton.setIcon(st_object.ViewObject.Icon) + self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) + hbox.addWidget(self.stButton) else: # that happens if the source of the extractor was deleted and now @@ -271,18 +332,30 @@ class _SummaryWidget(QtGui.QWidget): self.warning = QtGui.QLabel(self) self.warning.full_text = translate("FEM", "{}: Data source not available").format(extractor.Label) + hbox.addWidget(self.warning) self.rmButton = QtGui.QToolButton(self) - self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) + self.rmButton.setIcon(FreeCADGui.getIcon("delete.svg")) self.rmButton.setAutoRaise(True) + hbox.addWidget(self.extrButton) + hbox.addWidget(self.viewButton) + hbox.addSpacing(15) + hbox.addWidget(self.rmButton) + # add the separation line + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0,0,0,0) + vbox.setSpacing(5) + vbox.addItem(hbox) self.frame = QtGui.QFrame(self) self.frame.setFrameShape(QtGui.QFrame.HLine); + vbox.addWidget(self.frame) - policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.setSizePolicy(policy) - self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + #self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + self.setLayout(vbox) # connect actions. We add functions to widget, as well as the data we need, # and use those as callback. This way every widget knows which objects to use @@ -294,80 +367,21 @@ class _SummaryWidget(QtGui.QWidget): self.rmButton.clicked.connect(self.deleteTriggered) # make sure initial drawing happened - self._redraw() + #self._redraw() - def _button(self, text): - btn = QtGui.QPushButton(self) - btn.full_text = text + def _button(self, icon, text, stretch=2): + btn = _ElideToolButton(icon, text, self) btn.setMinimumWidth(0) - btn.setFlat(True) - btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); + btn.setAutoRaise(True) btn.setToolTip(text) + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + policy.setHorizontalStretch(stretch) + btn.setSizePolicy(policy) return btn - def _redraw(self): - - btn_spacing = 3 - btn_height = self.extrButton.sizeHint().height() - - # total size notes: - # - 5 spacing = 2x between buttons + 3 spacings to remove button - # - remove btn_height as this is used as remove button square size - btn_total_size = self.size().width() - btn_height - 5*btn_spacing - btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() - fm = self.fontMetrics() - - if self._st_object: - - min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 - - pos = 0 - btns = [self.stButton, self.extrButton, self.viewButton] - btn_rel_size = [0.4, 0.4, 0.2] - btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] - for i, btn in enumerate(btns): - - btn_size = btn_total_size*btn_rel_size[i] - txt_size = btn_size - btn.iconSize().width() - btn_margin - - # we elide only if there is enough space for a meaningful text - if txt_size >= min_text_width: - - text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) - btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); - else: - btn.setText("") - btn.setStyleSheet("text-align:center;min-width:20px;"); - - rect = QtCore.QRect(pos, 0, btn_size, btn_height) - btn.setGeometry(rect) - pos += btn_size + btn_spacing - - else: - warning_txt = fm.elidedText(self.warning.full_text, QtGui.Qt.ElideRight, btn_total_size) - self.warning.setText(warning_txt) - rect = QtCore.QRect(0,0, btn_total_size, btn_height) - self.warning.setGeometry(rect) - - - rmsize = btn_height - pos = self.size().width() - rmsize - self.rmButton.setGeometry(pos, 0, rmsize, rmsize) - - frame_hint = self.frame.sizeHint() - rect = QtCore.QRect(0, btn_height+frame_hint.height(), self.size().width(), frame_hint.height()) - self.frame.setGeometry(rect) - - def resizeEvent(self, event): - - # calculate the allowed text length - self._redraw() - super().resizeEvent(event) @QtCore.Slot() def showVisualization(self): @@ -434,19 +448,17 @@ class _SummaryWidget(QtGui.QWidget): # update the preview extr_repr = self._extractor.ViewObject.Proxy.get_preview() - self.viewButton.setIcon(extr_repr[0]) - self.viewButton.full_text = extr_repr[1] + self.viewButton.setCustomIcon(extr_repr[0]) + self.viewButton.setCustomText(extr_repr[1]) self.viewButton.setToolTip(extr_repr[1]) - self._redraw() @QtCore.Slot() def appAccept(self): # update the preview extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) - self.extrButton.full_text = extr_label + self.extrButton.setCustomText = extr_label self.extrButton.setToolTip(extr_label) - self._redraw() @@ -468,8 +480,15 @@ class ExtractLinkView(QtGui.QWidget): self._scroll_view = QtGui.QScrollArea(self) self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff) self._scroll_view.setWidgetResizable(True) + self._scroll_widget = QtGui.QWidget(self._scroll_view) + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0,6,0,0) + vbox.addStretch() + self._scroll_widget.setLayout(vbox) + self._scroll_view.setWidget(self._scroll_widget) hbox = QtGui.QHBoxLayout() + hbox.setSpacing(6) label = QtGui.QLabel(translate("FEM", "Data used in:")) if not self._is_source: label.setText(translate("FEM", "Data used from:")) @@ -554,15 +573,9 @@ class ExtractLinkView(QtGui.QWidget): self._widgets.append(summary) # fill the scroll area - vbox = QtGui.QVBoxLayout() + vbox = self._scroll_widget.layout() for widget in self._widgets: - vbox.addWidget(widget) - - vbox.addStretch() - widget = QtGui.QWidget() - widget.setLayout(vbox) - - self._scroll_view.setWidget(widget) + vbox.insertWidget(0, widget) # also reset the add button model if self._is_source: From f5533a5f53ee5c9d8f9bd3c414357b619a021b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 14 May 2025 17:44:26 +0200 Subject: [PATCH 21/27] FEM: Data extraction code version conflicts resolved: PySide, mpl, VTK --- .../Resources/ui/PostTableFieldViewEdit.ui | 11 ++- src/Mod/Fem/femguiutils/extract_link_view.py | 72 ++++++++++++------- .../femviewprovider/view_post_histogram.py | 36 ++++++---- .../Fem/femviewprovider/view_post_lineplot.py | 1 + .../Fem/femviewprovider/view_post_table.py | 2 +- 5 files changed, 83 insertions(+), 39 deletions(-) diff --git a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui index ada74b69b4..6b3000248a 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -6,7 +6,7 @@ 0 0 - 259 + 279 38 @@ -27,7 +27,14 @@ 0 - + + + + 0 + 0 + + + diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 568724ceb9..37e8e82c62 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -172,7 +172,9 @@ class _ElideToolButton(QtGui.QToolButton): def sizeHint(self): button_size = super().sizeHint() - return QtCore.QSize(self.iconSize().width()+10, button_size.height()) + icn_size = self.iconSize() + min_margin = max((button_size - icn_size).height(), 6) + return QtCore.QSize(self.iconSize().width()+10, icn_size.height() + min_margin) def paintEvent(self, event): @@ -186,17 +188,39 @@ class _ElideToolButton(QtGui.QToolButton): painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) margin = (self.height() - self.iconSize().height()) / 2 - match type(self._icon): - case QtGui.QPixmap: - painter.drawPixmap(margin, margin, self._icon.scaled(self.iconSize())) - case QtGui.QIcon: - self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + icn_width = self.iconSize().width() + if self._icon.isNull(): + icn_width = 0; + fm = self.fontMetrics() - text_size = self.width() - self.iconSize().width() - 3*margin - text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, text_size) - if text: - painter.drawText(2*margin+self.iconSize().width(), margin + fm.ascent(), text) + txt_size = self.width() - icn_width - 2*margin + if not self._icon.isNull(): + # we add the margin between icon and text + txt_size -= margin + + txt_min = fm.boundingRect("...").width() + + # should we center the icon? + xpos = margin + if not self._icon.isNull() and txt_size < txt_min: + # center icon + xpos = self.width()/2 - self.iconSize().width()/2 + + if not self._icon.isNull(): + match type(self._icon): + case QtGui.QPixmap: + painter.drawPixmap(xpos, margin, self._icon.scaled(self.iconSize())) + xpos += self.iconSize().width() + case QtGui.QIcon: + self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + xpos += self.iconSize().width() + + xpos += margin # the margin to the text + + if txt_size >= txt_min: + text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, txt_size) + painter.drawText(xpos, margin + fm.ascent(), text) painter.end() @@ -234,7 +258,6 @@ class _TreeChoiceButton(QtGui.QToolButton): QtCore.Slot(QtCore.QModelIndex) def selectIndex(self, index): - print("select triggered") item = self.model.itemFromIndex(index) if item and not item.hasChildren(): @@ -251,13 +274,14 @@ class _TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) -class _SettingsPopup(QtGui.QDialog): +class _SettingsPopup(QtGui.QMenu): close = QtCore.Signal() def __init__(self, setting, parent): super().__init__(parent) + self._setting = setting self.setWindowFlags(QtGui.Qt.Popup) self.setFocusPolicy(QtGui.Qt.ClickFocus) @@ -277,6 +301,9 @@ class _SettingsPopup(QtGui.QDialog): vbox2.addWidget(widget) self.setLayout(vbox2) + def size(self): + return self._setting.sizeHint() + def showEvent(self, event): # required to get keyboard events self.setFocus() @@ -309,16 +336,15 @@ class _SummaryWidget(QtGui.QWidget): # build the UI hbox = QtGui.QHBoxLayout() hbox.setContentsMargins(6,0,6,0) - hbox.setSpacing(5) + hbox.setSpacing(2) self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) - if not extr_repr[0].isNull(): - size = self.viewButton.iconSize() - size.setWidth(size.width()*2) - self.viewButton.setIconSize(size) - else: - self.viewButton.setIconSize(QtCore.QSize(0,0)) + + size = self.viewButton.iconSize() + size.setWidth(size.width()*2) + self.viewButton.setIconSize(size) + if st_object: self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) @@ -406,7 +432,7 @@ class _SummaryWidget(QtGui.QWidget): scroll = viewport.parent() top_left = summary.geometry().topLeft() + base_widget.geometry().topLeft() + viewport.geometry().topLeft() - delta = (summary.width() - dialog.sizeHint().width())/2 + delta = (summary.width() - dialog.size().width())/2 local_point = QtCore.QPoint(top_left.x()+delta, top_left.y()+summary.height()) global_point = scroll.mapToGlobal(local_point) @@ -423,7 +449,6 @@ class _SummaryWidget(QtGui.QWidget): # position correctly and show self._position_dialog(self.appDialog) self.appDialog.show() - #self.appDialog.raise_() @QtCore.Slot() def editView(self): @@ -437,7 +462,6 @@ class _SummaryWidget(QtGui.QWidget): # position correctly and show self._position_dialog(self.viewDialog) self.viewDialog.show() - #self.viewDialog.raise_() @QtCore.Slot() def deleteTriggered(self): @@ -457,7 +481,7 @@ class _SummaryWidget(QtGui.QWidget): # update the preview extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) - self.extrButton.setCustomText = extr_label + self.extrButton.setCustomText(extr_label) self.extrButton.setToolTip(extr_label) @@ -574,7 +598,7 @@ class ExtractLinkView(QtGui.QWidget): # fill the scroll area vbox = self._scroll_widget.layout() - for widget in self._widgets: + for widget in reversed(self._widgets): vbox.insertWidget(0, widget) # also reset the add button model diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 9b976d62b9..1079808fcd 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -40,6 +40,7 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import io import numpy as np import matplotlib as mpl +from packaging.version import Version from vtkmodules.numpy_interface.dataset_adapter import VTKArray @@ -104,6 +105,7 @@ class EditViewWidget(QtGui.QWidget): action = QtGui.QWidgetAction(button) diag = QtGui.QColorDialog(barColor, parent=button) + diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) diag.accepted.connect(action.trigger) diag.rejected.connect(action.trigger) diag.colorSelected.connect(callback) @@ -512,7 +514,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): # we do not iterate the table, but iterate the children. This makes it possible # to attribute the correct styles - full_args = {} + full_args = [] full_data = [] labels = [] for child in self.Object.Group: @@ -526,15 +528,14 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): for i in range(table.GetNumberOfColumns()): # add the kw args, with some slide change over color for multiple frames + args = kwargs.copy() for key in kwargs: - if not (key in full_args): - full_args[key] = [] if "color" in key: value = np.array(kwargs[key])*color_factor[i] - full_args[key].append(mpl.colors.to_hex(value)) - else: - full_args[key].append(kwargs[key]) + args[key] = mpl.colors.to_hex(value) + + full_args.append(args) data = VTKArray(table.GetColumn(i)) full_data.append(data) @@ -552,15 +553,26 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): legend_prefix = child.Source.Label + ": " labels.append(legend_prefix + table.GetColumnName(i)) + args = {} + args["rwidth"] = self.ViewObject.BarWidth + args["cumulative"] = self.ViewObject.Cumulative + args["histtype"] = self.ViewObject.Type + args["label"] = labels + if Version(mpl.__version__) >= Version("3.10.0"): + args["hatch_linewidth"] = self.ViewObject.HatchLineWidth - full_args["hatch_linewidth"] = self.ViewObject.HatchLineWidth - full_args["rwidth"] = self.ViewObject.BarWidth - full_args["cumulative"] = self.ViewObject.Cumulative - full_args["histtype"] = self.ViewObject.Type - full_args["label"] = labels + n, b, patches = self._plot.axes.hist(full_data, bins, **args) - self._plot.axes.hist(full_data, bins, **full_args) + # set the patches view properties. + if len(full_args) == 1: + for patch in patches: + patch.set(**full_args[0]) + elif len(full_args) > 1: + for i, args in enumerate(full_args): + for patch in patches[i]: + patch.set(**full_args[i]) + # axes decoration if self.ViewObject.Title: self._plot.axes.set_title(self.ViewObject.Title) if self.ViewObject.XLabel: diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index fb17a2eaaf..f2b03743d1 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -102,6 +102,7 @@ class EditViewWidget(QtGui.QWidget): action = QtGui.QWidgetAction(button) diag = QtGui.QColorDialog(barColor, parent=button) + diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) diag.accepted.connect(action.trigger) diag.rejected.connect(action.trigger) diag.colorSelected.connect(callback) diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index cf8da7fdab..75458ef9d7 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -207,7 +207,7 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): return EditViewWidget(self.Object, post_dialog) def get_preview(self): - name = "----" + name = QT_TRANSLATE_NOOP("FEM", "default") if self.ViewObject.Name: name = self.ViewObject.Name return (QtGui.QPixmap(), name) From cb4ab225a6affa35b99c5a82df1dfc5f0f77a67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 10:27:27 +0200 Subject: [PATCH 22/27] FEM: Plot single frame index data as point --- .../Resources/ui/PostHistogramIndexAppEdit.ui | 3 ++ src/Mod/Fem/femobjects/post_extract1D.py | 38 ++++++++------- src/Mod/Fem/femobjects/post_extract2D.py | 48 +++++++++++-------- src/Mod/Fem/femobjects/post_lineplot.py | 2 +- .../Fem/femviewprovider/view_post_lineplot.py | 4 ++ 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui index e9dd2a2b3d..496f42229b 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui @@ -70,6 +70,9 @@ 0 + + 999999999 +
diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 987dfdabdd..1ffb946acd 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -149,36 +149,40 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): obj.Table = table return - # check if we have timesteps (required!) - abort = True + # check if we have timesteps + timesteps = [] info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - if len(timesteps) > 1: - abort = False - if abort: - FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") - obj.Table = table - return algo = obj.Source.getOutputAlgorithm() - setup = False frame_array = vtkDoubleArray() - idx = obj.Index - for i, timestep in enumerate(timesteps): - algo.UpdateTimeStep(timestep) + if timesteps: + setup = False + for i, timestep in enumerate(timesteps): + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset, copy=False) + + if not setup: + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_array.SetTuple(i, idx, array) + else: + algo.Update() dataset = algo.GetOutputDataObject(0) array = self._x_array_from_dataset(obj, dataset, copy=False) - if not setup: - frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) - frame_array.SetNumberOfTuples(len(timesteps)) - setup = True + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(1) + frame_array.SetTuple(0, idx, array) - frame_array.SetTuple(i, idx, array) if frame_array.GetNumberOfComponents() > 1: frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index baa5f3d8c2..3cb17d8ffc 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -174,41 +174,49 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): return # check if we have timesteps (required!) - abort = True + timesteps = [] info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - if len(timesteps) > 1: - abort = False - if abort: - FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") - obj.Table = table - return algo = obj.Source.getOutputAlgorithm() frame_x_array = vtkDoubleArray() - frame_x_array.SetNumberOfTuples(len(timesteps)) - frame_x_array.SetNumberOfComponents(1) - - frame_y_array = vtkDoubleArray() idx = obj.Index - setup = False - for i, timestep in enumerate(timesteps): - frame_x_array.SetTuple1(i, timestep) + if timesteps: + setup = False + frame_x_array.SetNumberOfTuples(len(timesteps)) + frame_x_array.SetNumberOfComponents(1) + for i, timestep in enumerate(timesteps): - algo.UpdateTimeStep(timestep) + frame_x_array.SetTuple1(i, timestep) + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._y_array_from_dataset(obj, dataset, copy=False) + if not setup: + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_y_array.SetTuple(i, idx, array) + + else: + frame_x_array.SetNumberOfTuples(1) + frame_x_array.SetNumberOfComponents(1) + frame_x_array.SetTuple1(0,0) + + algo.Update() dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) - if not setup: - frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) - frame_y_array.SetNumberOfTuples(len(timesteps)) - setup = True - frame_y_array.SetTuple(i, idx, array) + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(1) + frame_y_array.SetTuple(0, idx, array) + frame_x_array.SetName("Frames") if frame_y_array.GetNumberOfComponents() > 1: diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 8d4b725128..e3483b0bf5 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -1,4 +1,4 @@ -# *************************************************************************** +2# *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * # * This file is part of the FreeCAD CAx development system. * diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index f2b03743d1..0a61fd771d 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -516,6 +516,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): xdata = VTKArray(table.GetColumn(i)) ydata = VTKArray(table.GetColumn(i+1)) + # ensure points are visible if it is a single datapoint + if len(xdata) == 1 and tmp_args["marker"] == "None": + tmp_args["marker"] = "o" + # legend labels if child.ViewObject.Legend: if not legend_multiframe: From 66f8674e6344e811e1c8b56d4ddb06415b29b3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 10:37:53 +0200 Subject: [PATCH 23/27] FEM: Prevent invalid index for data extraction --- src/Mod/Fem/femobjects/post_extract1D.py | 8 ++++++++ src/Mod/Fem/femobjects/post_extract2D.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 1ffb946acd..f04d90bfad 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -168,6 +168,10 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): dataset = algo.GetOutputDataObject(0) array = self._x_array_from_dataset(obj, dataset, copy=False) + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if not setup: frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_array.SetNumberOfTuples(len(timesteps)) @@ -179,6 +183,10 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): dataset = algo.GetOutputDataObject(0) array = self._x_array_from_dataset(obj, dataset, copy=False) + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_array.SetNumberOfTuples(1) frame_array.SetTuple(0, idx, array) diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 3cb17d8ffc..ed7d0faf47 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -197,6 +197,11 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): algo.UpdateTimeStep(timestep) dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) + + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if not setup: frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_y_array.SetNumberOfTuples(len(timesteps)) @@ -213,6 +218,10 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_y_array.SetNumberOfTuples(1) frame_y_array.SetTuple(0, idx, array) From 3e3e6a2c7b079d9bb6d3857f3d8fab09afe83a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 10:56:40 +0200 Subject: [PATCH 24/27] FEM: Include code quality improvements from review --- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 20 ++++++++++--------- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 16 ++++++--------- src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 8 ++++---- src/Mod/Fem/femcommands/manager.py | 2 +- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 804cde7f14..88cf26f8f9 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -407,16 +407,18 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) void TaskDlgPost::processCollapsedWidgets() { 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)) { - if(post_widget->initiallyCollapsed()) { - post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); - task_box->hideGroupBox(); - } - } + auto* task_box = dynamic_cast(widget); + if (!task_box) { + continue; } + // get the task widget and check if it is a post widget + auto* taskwidget = task_box->groupLayout()->itemAt(0)->widget(); + auto* post_widget = dynamic_cast(taskwidget); + if (!post_widget || !post_widget->initiallyCollapsed()) { + continue; + } + post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); + task_box->hideGroupBox(); } } diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index abce2a30cf..4de2401c7c 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -82,17 +82,13 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* Gui::PythonWrapper wrap; if (wrap.loadCoreModule()) { - QObject* object = wrap.toQObject(pywidget); - if (object) { - QWidget* widget = qobject_cast(object); - if (widget) { - // finally we have the usable QWidget. Add to us! + if (auto* widget = qobject_cast(wrap.toQObject(pywidget))) { + // finally we have the usable QWidget. Add to us! - auto layout = new QVBoxLayout(); - layout->addWidget(widget); - setLayout(layout); - return; - } + auto layout = new QVBoxLayout(); + layout->addWidget(widget); + setLayout(layout); + return; } } } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index b3dcbf4c7b..9205e2d708 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -1023,12 +1023,12 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { assert(dlg->getView() == this); - auto disp_panel = new TaskPostDisplay(this); - dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel); + auto dispPanel = new TaskPostDisplay(this); + dlg->addTaskBox(dispPanel->windowIcon().pixmap(32), dispPanel); #ifdef FC_USE_VTK_PYTHON - auto extr_panel = new TaskPostExtraction(this); - dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); + auto extrPanel = new TaskPostExtraction(this); + dlg->addTaskBox(extrPanel->windowIcon().pixmap(32), extrPanel); #endif } diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index 50fbae0548..f653d053a0 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -379,7 +379,7 @@ 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 + # check if we should use python filter from femguiutils.vtk_module_handling import vtk_compatibility_abort if vtk_compatibility_abort(True): return From 7557d4969733079b7ad4c12ae04c75f00b1ed1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 13:03:26 +0200 Subject: [PATCH 25/27] FEM: Extraction code CodeQL updated and typo fix --- src/Mod/AddonManager | 2 +- src/Mod/Fem/femguiutils/extract_link_view.py | 3 -- src/Mod/Fem/femguiutils/post_visualization.py | 4 +- src/Mod/Fem/femguiutils/vtk_table_view.py | 8 ++++ .../Fem/femobjects/base_fempostextractors.py | 2 +- .../femobjects/base_fempostvisualizations.py | 4 +- src/Mod/Fem/femobjects/post_extract1D.py | 5 +-- src/Mod/Fem/femobjects/post_extract2D.py | 5 +-- src/Mod/Fem/femobjects/post_lineplot.py | 2 +- .../Fem/femtaskpanels/base_fempostpanel.py | 2 - .../Fem/femtaskpanels/task_post_extractor.py | 4 -- .../femtaskpanels/task_post_glyphfilter.py | 43 ------------------- src/Mod/Fem/femtaskpanels/task_post_table.py | 4 -- .../view_base_fempostextractors.py | 1 - 14 files changed, 19 insertions(+), 70 deletions(-) diff --git a/src/Mod/AddonManager b/src/Mod/AddonManager index 69a6e0dc7b..34d433a02c 160000 --- a/src/Mod/AddonManager +++ b/src/Mod/AddonManager @@ -1 +1 @@ -Subproject commit 69a6e0dc7b8f5fe17547f4d1234df1617b78c45e +Subproject commit 34d433a02c7ec5c73bec9c57d0a27ea70b36c90d diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 37e8e82c62..a6f6067080 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -590,7 +590,6 @@ class ExtractLinkView(QtGui.QWidget): candidates = self._object.OutList # get all widgets from the candidates - extractors = [] for candidate in candidates: if extr.is_extractor_object(candidate): summary = self._build_summary_widget(candidate) @@ -620,8 +619,6 @@ class ExtractLinkView(QtGui.QWidget): QtCore.Slot(object, object) # visualization data, extraction data def newVisualization(self, vis_data, ext_data): - doc = self._object.Document - FreeCADGui.addModule(vis_data.module) FreeCADGui.addModule(ext_data.module) FreeCADGui.addModule("FemGui") diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index 9975db127b..6724fda779 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -40,8 +40,8 @@ from PySide import QtCore import FreeCAD -# Registry to handle visulization commands -# ######################################## +# Registry to handle visualization commands +# ######################################### _registry = {} diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index c1263150ac..f126640d24 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -83,6 +83,8 @@ class VtkTableModel(QtCore.QAbstractTableModel): col = self._table.GetColumn(index.column()) return col.GetTuple(index.row())[0] + return None + def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: @@ -98,6 +100,8 @@ class VtkTableModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return section + return None + def getTable(self): return self._table @@ -134,6 +138,8 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): range = col.GetRange() return range[index.column()] + return None + def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: @@ -142,6 +148,8 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return self._table.GetColumnName(section) + return None + def getTable(self): return self._table diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index e42d2adf1b..48bd9c4951 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -123,7 +123,7 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): case _: return ["Not a vector"] - def get_representive_fieldname(self): + def get_representive_fieldname(self, obj): # should return the representive field name, e.g. Position (X) return "" diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index 396694a652..ffaa94ee8f 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -160,8 +160,8 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): array.SetNumberOfComponents(c_array.GetNumberOfComponents()) array.SetNumberOfTuples(rows) array.Fill(0) # so that all non-used entries are set to 0 - for i in range(c_array.GetNumberOfTuples()): - array.SetTuple(i, c_array.GetTuple(i)) + for j in range(c_array.GetNumberOfTuples()): + array.SetTuple(j, c_array.GetTuple(j)) array.SetName(f"{child.Source.Name}: {c_array.GetName()}") table.AddColumn(array) diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index f04d90bfad..8fcec6e2da 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -77,17 +77,16 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): obj.Table = table return - frames = False + timesteps=[] if obj.ExtractFrames: # check if we have timesteps info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - frames = True else: FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") - if not frames: + if not timesteps: # get the dataset and extract the correct array array = self._x_array_from_dataset(obj, dataset) if array.GetNumberOfComponents() > 1: diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index ed7d0faf47..86827d7a7e 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -78,17 +78,16 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): obj.Table = table return - frames = False + timesteps = [] if obj.ExtractFrames: # check if we have timesteps info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - frames = True else: FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") - if not frames: + if not timesteps: # get the dataset and extract the correct array xarray = self._x_array_from_dataset(obj, dataset) if xarray.GetNumberOfComponents() > 1: diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index e3483b0bf5..8d4b725128 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -1,4 +1,4 @@ -2# *************************************************************************** +# *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * # * This file is part of the FreeCAD CAx development system. * diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 08c067de7f..5efa1aa12a 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -32,9 +32,7 @@ __url__ = "https://www.freecad.org" from PySide import QtCore, QtGui import FreeCAD -import FreeCADGui -from femguiutils import selection_widgets from . import base_femtaskpanel translate = FreeCAD.Qt.translate diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py index 6d29a305e8..56fdd998b5 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -31,10 +31,6 @@ __url__ = "https://www.freecad.org" from PySide import QtCore, QtGui -import FreeCAD -import FreeCADGui - -from femguiutils import selection_widgets from . import base_fempostpanel diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index 8804951067..a9a13139e9 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -56,49 +56,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # 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 # ############### diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py index b75549eeb9..1eda150016 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_table.py +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -36,7 +36,6 @@ import FreeCADGui from . import base_fempostpanel from femguiutils import extract_link_view as elv -from femguiutils import vtk_table_view translate = FreeCAD.Qt.translate @@ -78,9 +77,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # connect data widget self.data_widget.show_table.clicked.connect(self.showTable) - # set current values to view widget - viewObj = self.obj.ViewObject - @QtCore.Slot() def showTable(self): diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index 143cd8fba5..338aabb905 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -33,7 +33,6 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui -import FemGui from PySide import QtGui import femobjects.base_fempostextractors as fpe From fa9aba2749f7d9b0f4f470a6e0361f0ecd9ad8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 16 Jun 2025 20:23:18 +0200 Subject: [PATCH 26/27] FEM: Data extraction lint updates --- .../femviewprovider/view_base_fempostextractors.py | 1 - .../femviewprovider/view_base_fempostvisualization.py | 6 ------ src/Mod/Fem/femviewprovider/view_post_histogram.py | 4 ++-- src/Mod/Fem/femviewprovider/view_post_lineplot.py | 5 ++--- src/Mod/Fem/femviewprovider/view_post_table.py | 11 ++--------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index 338aabb905..bb4aca0934 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -35,7 +35,6 @@ import FreeCADGui from PySide import QtGui -import femobjects.base_fempostextractors as fpe from femtaskpanels import task_post_extractor class VPPostExtractor: diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index b3d244b1ef..9b357a4d0c 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -29,15 +29,9 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief view provider for post visualization object -from PySide import QtGui, QtCore - -import Plot import FreeCAD import FreeCADGui -from . import view_base_femobject -_GuiPropHelper = view_base_femobject._GuiPropHelper - class VPPostVisualization: """ A View Provider for visualization objects diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 1079808fcd..337ad62f86 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -33,7 +33,6 @@ import FreeCAD import FreeCADGui import Plot -import FemGui from PySide import QtGui, QtCore from PySide.QtCore import QT_TRANSLATE_NOOP @@ -48,7 +47,8 @@ from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_histogram -_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper class EditViewWidget(QtGui.QWidget): diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 0a61fd771d..3066e009b7 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -33,7 +33,6 @@ import FreeCAD import FreeCADGui import Plot -import FemGui from PySide import QtGui, QtCore from PySide.QtCore import QT_TRANSLATE_NOOP @@ -47,9 +46,9 @@ from vtkmodules.numpy_interface.dataset_adapter import VTKArray from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_lineplot -from femguiutils import post_visualization as pv -_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper class EditViewWidget(QtGui.QWidget): diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 75458ef9d7..536a3665e2 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -32,23 +32,16 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui -import Plot -import FemGui from PySide import QtGui, QtCore from PySide.QtCore import QT_TRANSLATE_NOOP -import io -import numpy as np -import matplotlib as mpl - -from vtkmodules.numpy_interface.dataset_adapter import VTKArray - from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_table from femguiutils import vtk_table_view as vtv -_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper class EditViewWidget(QtGui.QWidget): From da4d5c919ff4642b0799503c8c4fc57708dd6551 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:26:48 +0000 Subject: [PATCH 27/27] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/Fem/App/FemPostFilterPyImp.cpp | 2 +- src/Mod/Fem/App/FemPostObjectPyImp.cpp | 8 +- src/Mod/Fem/App/FemPostPipelinePyImp.cpp | 6 +- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 14 ++- src/Mod/Fem/Gui/TaskPostBoxes.h | 3 +- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 17 ++- src/Mod/Fem/Gui/Workbench.cpp | 4 +- src/Mod/Fem/InitGui.py | 1 + src/Mod/Fem/ObjectsFem.py | 9 ++ src/Mod/Fem/femcommands/commands.py | 2 + src/Mod/Fem/femcommands/manager.py | 1 + src/Mod/Fem/femguiutils/data_extraction.py | 14 +-- src/Mod/Fem/femguiutils/extract_link_view.py | 113 +++++++++--------- src/Mod/Fem/femguiutils/post_visualization.py | 45 ++++--- src/Mod/Fem/femguiutils/vtk_table_view.py | 37 +++--- .../Fem/femobjects/base_fempostextractors.py | 24 ++-- .../femobjects/base_fempostvisualizations.py | 20 ++-- src/Mod/Fem/femobjects/post_extract1D.py | 35 ++++-- src/Mod/Fem/femobjects/post_extract2D.py | 36 ++++-- src/Mod/Fem/femobjects/post_glyphfilter.py | 1 + src/Mod/Fem/femobjects/post_histogram.py | 53 ++++---- src/Mod/Fem/femobjects/post_lineplot.py | 48 ++++---- src/Mod/Fem/femobjects/post_table.py | 46 +++---- .../Fem/femtaskpanels/base_fempostpanel.py | 12 +- .../Fem/femtaskpanels/task_post_extractor.py | 3 - .../femtaskpanels/task_post_glyphfilter.py | 1 - .../Fem/femtaskpanels/task_post_histogram.py | 19 ++- .../Fem/femtaskpanels/task_post_lineplot.py | 17 ++- src/Mod/Fem/femtaskpanels/task_post_table.py | 4 +- .../femviewprovider/view_base_femobject.py | 1 + .../view_base_fempostextractors.py | 11 +- .../view_base_fempostvisualization.py | 8 +- .../femviewprovider/view_post_histogram.py | 68 ++++++----- .../Fem/femviewprovider/view_post_lineplot.py | 93 ++++++++------ .../Fem/femviewprovider/view_post_table.py | 19 +-- 35 files changed, 447 insertions(+), 348 deletions(-) diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index 2e09c9e3f0..69a5d8f23e 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -199,7 +199,7 @@ PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args) auto algorithm = getFemPostFilterPtr()->getFilterOutput(); PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); - return Py::new_reference_to(py_algorithm); + return Py::new_reference_to(py_algorithm); #else PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); Py_Return; diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index a35e0b569a..8b242abcf7 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -30,9 +30,9 @@ #include "FemPostObjectPy.cpp" #ifdef FC_USE_VTK_PYTHON - #include - #include -#endif //BUILD_FEM_VTK +#include +#include +#endif // BUILD_FEM_VTK using namespace Fem; @@ -71,7 +71,7 @@ PyObject* FemPostObjectPy::getDataSet(PyObject* args) auto dataset = getFemPostObjectPtr()->getDataSet(); if (dataset) { PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset); - return Py::new_reference_to(py_algorithm); + return Py::new_reference_to(py_algorithm); } return Py_None; #else diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index 388aed35de..2c493074e6 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -35,8 +35,8 @@ // clang-format on #ifdef FC_USE_VTK_PYTHON - #include -#endif //BUILD_FEM_VTK +#include +#endif // BUILD_FEM_VTK using namespace Fem; @@ -329,7 +329,7 @@ PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args) auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm(); PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); - return Py::new_reference_to(py_algorithm); + return Py::new_reference_to(py_algorithm); #else PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); Py_Return; diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 88cf26f8f9..e50bb0bf98 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -213,7 +213,11 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, setWindowIcon(icon); m_icon = icon; - m_connection = m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, this, boost::placeholders::_1, boost::placeholders::_2)); + m_connection = + m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, + this, + boost::placeholders::_1, + boost::placeholders::_2)); } TaskPostWidget::~TaskPostWidget() @@ -404,7 +408,8 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) } } -void TaskDlgPost::processCollapsedWidgets() { +void TaskDlgPost::processCollapsedWidgets() +{ for (auto& widget : Content) { auto* task_box = dynamic_cast(widget); @@ -417,7 +422,7 @@ void TaskDlgPost::processCollapsedWidgets() { if (!post_widget || !post_widget->initiallyCollapsed()) { continue; } - post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); + post_widget->setGeometry(QRect(QPoint(0, 0), post_widget->sizeHint())); task_box->hideGroupBox(); } } @@ -584,7 +589,8 @@ void TaskPostFrames::applyPythonCode() // we apply the views widgets python code } -bool TaskPostFrames::initiallyCollapsed() { +bool TaskPostFrames::initiallyCollapsed() +{ return (ui->FrameTable->rowCount() == 0); } diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index 3c60fe8ddf..816dafb080 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -157,7 +157,8 @@ public: virtual void apply() {}; // returns if the widget shall be collapsed when opening the task dialog - virtual bool initiallyCollapsed() { + virtual bool initiallyCollapsed() + { return false; }; diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 4de2401c7c..e61033957c 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -48,9 +48,7 @@ using namespace Gui; // box to handle data extractions TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent) - : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), - parent) + : TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), parent) { // we load the python implementation, and try to get the widget from it, to add // directly our widget @@ -73,7 +71,7 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* m_panel = Py::Object(method.apply(args)); } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } @@ -97,7 +95,8 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* Base::Console().error("Unable to import data extraction widget\n"); }; -TaskPostExtraction::~TaskPostExtraction() { +TaskPostExtraction::~TaskPostExtraction() +{ Base::PyGILStateLocker lock; try { @@ -123,7 +122,7 @@ void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } }; @@ -139,7 +138,7 @@ bool TaskPostExtraction::isGuiTaskOnly() } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } @@ -156,7 +155,7 @@ void TaskPostExtraction::apply() } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } } @@ -172,7 +171,7 @@ bool TaskPostExtraction::initiallyCollapsed() } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 44c1186c6f..31f396fbe9 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -218,7 +218,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const #ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif - ; + ; #endif Gui::ToolBarItem* utils = new Gui::ToolBarItem(root); @@ -374,7 +374,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const #ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif - ; + ; #endif Gui::MenuItem* utils = new Gui::MenuItem; diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 35c835f81e..e7d0a2ada7 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -83,6 +83,7 @@ class FemWorkbench(Workbench): # check vtk version to potentially find missmatchs if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: 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 1ca11e2566..2bd6e74056 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -696,6 +696,7 @@ def makePostLineplot(doc, name="Lineplot"): post_lineplot.PostLineplot(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplot(obj.ViewObject) return obj @@ -710,6 +711,7 @@ def makePostLineplotFieldData(doc, name="FieldData2D"): post_lineplot.PostLineplotFieldData(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) return obj @@ -724,6 +726,7 @@ def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): post_lineplot.PostLineplotIndexOverFrames(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotIndexOverFrames(obj.ViewObject) return obj @@ -738,6 +741,7 @@ def makePostHistogram(doc, name="Histogram"): post_histogram.PostHistogram(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogram(obj.ViewObject) return obj @@ -752,6 +756,7 @@ def makePostHistogramFieldData(doc, name="FieldData1D"): post_histogram.PostHistogramFieldData(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) return obj @@ -766,6 +771,7 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): post_histogram.PostHistogramIndexOverFrames(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramIndexOverFrames(obj.ViewObject) return obj @@ -780,6 +786,7 @@ def makePostTable(doc, name="Table"): post_table.PostTable(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_table + view_post_table.VPPostTable(obj.ViewObject) return obj @@ -794,6 +801,7 @@ def makePostTableFieldData(doc, name="FieldData1D"): post_table.PostTableFieldData(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_table + view_post_table.VPPostTableFieldData(obj.ViewObject) return obj @@ -808,6 +816,7 @@ def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): post_table.PostTableIndexOverFrames(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_table + view_post_table.VPPostTableIndexOverFrames(obj.ViewObject) return obj diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 8c85fc7270..5d662074be 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1231,6 +1231,7 @@ class _PostFilterGlyph(CommandManager): self.is_active = "with_vtk_selresult" self.do_activated = "add_filter_set_edit" + # the string in add command will be the page name on FreeCAD wiki FreeCADGui.addCommand("FEM_Analysis", _Analysis()) FreeCADGui.addCommand("FEM_ClippingPlaneAdd", _ClippingPlaneAdd()) @@ -1295,4 +1296,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: import femobjects.post_table from femguiutils import post_visualization + post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index f653d053a0..bb2edc3e05 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -381,6 +381,7 @@ class CommandManager: # check if we should use python filter from femguiutils.vtk_module_handling import vtk_compatibility_abort + if vtk_compatibility_abort(True): return diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index cb55295703..dfe0cea7f8 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -37,8 +37,7 @@ from vtkmodules.vtkCommonCore import vtkVersion from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents -if vtkVersion.GetVTKMajorVersion() > 9 and \ - vtkVersion.GetVTKMinorVersion() > 3: +if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3: from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter else: from vtkmodules.vtkInfovisCore import vtkDataObjectToTable @@ -51,6 +50,7 @@ import femobjects.base_fempostextractors as extr from femtaskpanels.base_fempostpanel import _BasePostTaskPanel from . import extract_link_view + ExtractLinkView = extract_link_view.ExtractLinkView @@ -83,11 +83,10 @@ class DataExtraction(_BasePostTaskPanel): # setup the extraction widget self._extraction_view = ExtractLinkView(self.Object, True, self) - self.widget.layout().addSpacing(self.widget.Data.size().height()/3) + self.widget.layout().addSpacing(self.widget.Data.size().height() / 3) self.widget.layout().addWidget(self._extraction_view) self._extraction_view.repopulate() - @QtCore.Slot() def showData(self): @@ -96,7 +95,7 @@ class DataExtraction(_BasePostTaskPanel): widget = vtk_table_view.VtkTableView(self.data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(1500, 900) @@ -110,7 +109,7 @@ class DataExtraction(_BasePostTaskPanel): widget = vtk_table_view.VtkTableView(self.summary_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(600, 900) @@ -126,8 +125,7 @@ class DataExtraction(_BasePostTaskPanel): if not algo: self.data_model.setTable(vtkTable()) - if vtkVersion.GetVTKMajorVersion() > 9 and \ - vtkVersion.GetVTKMinorVersion() > 3: + if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3: filter = vtkAttributeDataToTableFilter() else: filter = vtkDataObjectToTable() diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index a6f6067080..eec8ba6927 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -44,6 +44,7 @@ translate = FreeCAD.Qt.translate # a model showing available visualizations and possible extractions # ################################################################# + def build_new_visualization_tree_model(): # model that shows all options to create new visualizations @@ -67,6 +68,7 @@ def build_new_visualization_tree_model(): return model + def build_add_to_visualization_tree_model(): # model that shows all possible visualization objects to add data to @@ -92,7 +94,9 @@ def build_add_to_visualization_tree_model(): for ext in visualizations[vis_type].extractions: icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) - ext_item = QtGui.QStandardItem(icon, translate("FEM", "Add {}").format(name)) + ext_item = QtGui.QStandardItem( + icon, translate("FEM", "Add {}").format(name) + ) ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) vis_item.appendRow(ext_item) @@ -102,10 +106,13 @@ def build_add_to_visualization_tree_model(): return model + def build_post_object_item(post_object, extractions, vis_type): # definitely build a item and add the extractions - post_item = QtGui.QStandardItem(post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label)) + post_item = QtGui.QStandardItem( + post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label) + ) post_item.setFlags(QtGui.Qt.ItemIsEnabled) post_item.setData(post_object) @@ -150,9 +157,11 @@ def build_add_from_data_tree_model(vis_type): return model + # implementation of GUI and its functionality # ########################################### + class _ElideToolButton(QtGui.QToolButton): # tool button that elides its text, and left align icon and text @@ -174,7 +183,7 @@ class _ElideToolButton(QtGui.QToolButton): button_size = super().sizeHint() icn_size = self.iconSize() min_margin = max((button_size - icn_size).height(), 6) - return QtCore.QSize(self.iconSize().width()+10, icn_size.height() + min_margin) + return QtCore.QSize(self.iconSize().width() + 10, icn_size.height() + min_margin) def paintEvent(self, event): @@ -190,11 +199,10 @@ class _ElideToolButton(QtGui.QToolButton): margin = (self.height() - self.iconSize().height()) / 2 icn_width = self.iconSize().width() if self._icon.isNull(): - icn_width = 0; - + icn_width = 0 fm = self.fontMetrics() - txt_size = self.width() - icn_width - 2*margin + txt_size = self.width() - icn_width - 2 * margin if not self._icon.isNull(): # we add the margin between icon and text txt_size -= margin @@ -205,7 +213,7 @@ class _ElideToolButton(QtGui.QToolButton): xpos = margin if not self._icon.isNull() and txt_size < txt_min: # center icon - xpos = self.width()/2 - self.iconSize().width()/2 + xpos = self.width() / 2 - self.iconSize().width() / 2 if not self._icon.isNull(): match type(self._icon): @@ -213,10 +221,12 @@ class _ElideToolButton(QtGui.QToolButton): painter.drawPixmap(xpos, margin, self._icon.scaled(self.iconSize())) xpos += self.iconSize().width() case QtGui.QIcon: - self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + self._icon.paint( + painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize()) + ) xpos += self.iconSize().width() - xpos += margin # the margin to the text + xpos += margin # the margin to the text if txt_size >= txt_min: text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, txt_size) @@ -227,7 +237,7 @@ class _ElideToolButton(QtGui.QToolButton): class _TreeChoiceButton(QtGui.QToolButton): - selection = QtCore.Signal(object,object) + selection = QtCore.Signal(object, object) def __init__(self, model): super().__init__() @@ -254,9 +264,10 @@ class _TreeChoiceButton(QtGui.QToolButton): self.popup = QtGui.QWidgetAction(self) self.popup.setDefaultWidget(self.tree_view) self.setPopupMode(QtGui.QToolButton.InstantPopup) - self.addAction(self.popup); + self.addAction(self.popup) QtCore.Slot(QtCore.QModelIndex) + def selectIndex(self, index): item = self.model.itemFromIndex(index) @@ -274,6 +285,7 @@ class _TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) + class _SettingsPopup(QtGui.QMenu): close = QtCore.Signal() @@ -297,7 +309,7 @@ class _SettingsPopup(QtGui.QMenu): widget.setLayout(vbox) vbox2 = QtGui.QVBoxLayout() - vbox2.setContentsMargins(0,0,0,0) + vbox2.setContentsMargins(0, 0, 0, 0) vbox2.addWidget(widget) self.setLayout(vbox2) @@ -321,7 +333,7 @@ class _SettingsPopup(QtGui.QMenu): class _SummaryWidget(QtGui.QWidget): - delete = QtCore.Signal(object, object) # to delete: document object, summary widget + delete = QtCore.Signal(object, object) # to delete: document object, summary widget def __init__(self, st_object, extractor, post_dialog): super().__init__() @@ -335,17 +347,16 @@ class _SummaryWidget(QtGui.QWidget): # build the UI hbox = QtGui.QHBoxLayout() - hbox.setContentsMargins(6,0,6,0) + hbox.setContentsMargins(6, 0, 6, 0) hbox.setSpacing(2) self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) size = self.viewButton.iconSize() - size.setWidth(size.width()*2) + size.setWidth(size.width() * 2) self.viewButton.setIconSize(size) - if st_object: self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) hbox.addWidget(self.stButton) @@ -357,7 +368,9 @@ class _SummaryWidget(QtGui.QWidget): self.viewButton.hide() self.warning = QtGui.QLabel(self) - self.warning.full_text = translate("FEM", "{}: Data source not available").format(extractor.Label) + self.warning.full_text = translate("FEM", "{}: Data source not available").format( + extractor.Label + ) hbox.addWidget(self.warning) self.rmButton = QtGui.QToolButton(self) @@ -371,16 +384,16 @@ class _SummaryWidget(QtGui.QWidget): # add the separation line vbox = QtGui.QVBoxLayout() - vbox.setContentsMargins(0,0,0,0) + vbox.setContentsMargins(0, 0, 0, 0) vbox.setSpacing(5) vbox.addItem(hbox) self.frame = QtGui.QFrame(self) - self.frame.setFrameShape(QtGui.QFrame.HLine); + self.frame.setFrameShape(QtGui.QFrame.HLine) vbox.addWidget(self.frame) policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.setSizePolicy(policy) - #self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + # self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) self.setLayout(vbox) # connect actions. We add functions to widget, as well as the data we need, @@ -393,8 +406,7 @@ class _SummaryWidget(QtGui.QWidget): self.rmButton.clicked.connect(self.deleteTriggered) # make sure initial drawing happened - #self._redraw() - + # self._redraw() def _button(self, icon, text, stretch=2): @@ -408,7 +420,6 @@ class _SummaryWidget(QtGui.QWidget): btn.setSizePolicy(policy) return btn - @QtCore.Slot() def showVisualization(self): if vis.is_visualization_object(self._st_object): @@ -426,14 +437,18 @@ class _SummaryWidget(QtGui.QWidget): # very weird values. Hence we build the coords of the widget # ourself - summary = dialog.parent() # == self + summary = dialog.parent() # == self base_widget = summary.parent() viewport = summary.parent() scroll = viewport.parent() - top_left = summary.geometry().topLeft() + base_widget.geometry().topLeft() + viewport.geometry().topLeft() - delta = (summary.width() - dialog.size().width())/2 - local_point = QtCore.QPoint(top_left.x()+delta, top_left.y()+summary.height()) + top_left = ( + summary.geometry().topLeft() + + base_widget.geometry().topLeft() + + viewport.geometry().topLeft() + ) + delta = (summary.width() - dialog.size().width()) / 2 + local_point = QtCore.QPoint(top_left.x() + delta, top_left.y() + summary.height()) global_point = scroll.mapToGlobal(local_point) dialog.setGeometry(QtCore.QRect(global_point, dialog.sizeHint())) @@ -485,7 +500,6 @@ class _SummaryWidget(QtGui.QWidget): self.extrButton.setToolTip(extr_label) - class ExtractLinkView(QtGui.QWidget): def __init__(self, obj, is_source, post_dialog): @@ -506,7 +520,7 @@ class ExtractLinkView(QtGui.QWidget): self._scroll_view.setWidgetResizable(True) self._scroll_widget = QtGui.QWidget(self._scroll_view) vbox = QtGui.QVBoxLayout() - vbox.setContentsMargins(0,6,0,0) + vbox.setContentsMargins(0, 6, 0, 0) vbox.addStretch() self._scroll_widget.setLayout(vbox) self._scroll_view.setWidget(self._scroll_widget) @@ -541,7 +555,7 @@ class ExtractLinkView(QtGui.QWidget): hbox.addWidget(self._add) vbox = QtGui.QVBoxLayout() - vbox.setContentsMargins(0,0,0,0) + vbox.setContentsMargins(0, 0, 0, 0) vbox.addItem(hbox) vbox.addWidget(self._scroll_view) @@ -616,7 +630,8 @@ class ExtractLinkView(QtGui.QWidget): return None - QtCore.Slot(object, object) # visualization data, extraction data + QtCore.Slot(object, object) # visualization data, extraction data + def newVisualization(self, vis_data, ext_data): FreeCADGui.addModule(vis_data.module) @@ -630,17 +645,13 @@ class ExtractLinkView(QtGui.QWidget): analysis = self._find_parent_analysis(self._object) if analysis: - FreeCADGui.doCommand( - f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)" - ) + FreeCADGui.doCommand(f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)") # create extraction and add it FreeCADGui.doCommand( f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" ) - FreeCADGui.doCommand( - f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" - ) + FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}") # default values: color color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() if color_prop: @@ -648,15 +659,13 @@ class ExtractLinkView(QtGui.QWidget): f"extraction.ViewObject.{color_prop} = visualization.ViewObject.Proxy.get_next_default_color()" ) - FreeCADGui.doCommand( - f"visualization.addObject(extraction)" - ) - + FreeCADGui.doCommand(f"visualization.addObject(extraction)") self._post_dialog._recompute() self.repopulate() - QtCore.Slot(object, object) # visualization object, extraction data + QtCore.Slot(object, object) # visualization object, extraction data + def addExtractionToVisualization(self, vis_obj, ext_data): FreeCADGui.addModule(ext_data.module) @@ -666,9 +675,7 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" ) - FreeCADGui.doCommand( - f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" - ) + FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}") # default values: color color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() @@ -677,14 +684,13 @@ class ExtractLinkView(QtGui.QWidget): f"extraction.ViewObject.{color_prop} = (Gui.ActiveDocument.{vis_obj.Name}.Proxy.get_next_default_color())" ) - FreeCADGui.doCommand( - f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)" - ) + FreeCADGui.doCommand(f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)") self._post_dialog._recompute() self.repopulate() - QtCore.Slot(object, object) # post object, extraction data + QtCore.Slot(object, object) # post object, extraction data + def addExtractionToPostObject(self, post_obj, ext_data): FreeCADGui.addModule(ext_data.module) @@ -694,9 +700,7 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" ) - FreeCADGui.doCommand( - f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}" - ) + FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}") # default values for color color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() @@ -705,10 +709,7 @@ class ExtractLinkView(QtGui.QWidget): f"extraction.ViewObject.{color_prop} = Gui.ActiveDocument.{self._object.Name}.Proxy.get_next_default_color()" ) - FreeCADGui.doCommand( - f"App.ActiveDocument.{self._object.Name}.addObject(extraction)" - ) + FreeCADGui.doCommand(f"App.ActiveDocument.{self._object.Name}.addObject(extraction)") self._post_dialog._recompute() self.repopulate() - diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index 6724fda779..557c177cd3 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -45,6 +45,7 @@ import FreeCAD _registry = {} + @dataclass class _Extraction: @@ -55,6 +56,7 @@ class _Extraction: module: str factory: str + @dataclass class _Visualization: @@ -64,6 +66,7 @@ class _Visualization: factory: str extractions: list[_Extraction] + # Register a visualization by type, icon and factory function def register_visualization(visualization_type, icon, module, factory): if visualization_type in _registry: @@ -71,7 +74,10 @@ def register_visualization(visualization_type, icon, module, factory): _registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, []) -def register_extractor(visualization_type, extraction_type, icon, dimension, etype, module, factory): + +def register_extractor( + visualization_type, extraction_type, icon, dimension, etype, module, factory +): if not visualization_type in _registry: raise ValueError("visualization not registered yet") @@ -79,6 +85,7 @@ def register_extractor(visualization_type, extraction_type, icon, dimension, ety extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory) _registry[visualization_type].extractions.append(extraction) + def get_registered_visualizations(): return copy.deepcopy(_registry) @@ -86,6 +93,7 @@ def get_registered_visualizations(): def _to_command_name(name): return "FEM_PostVisualization" + name + class _VisualizationGroupCommand: def GetCommands(self): @@ -97,14 +105,19 @@ class _VisualizationGroupCommand: return 0 def GetResources(self): - return { 'MenuText': QtCore.QT_TRANSLATE_NOOP("FEM", 'Data Visualizations'), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("FEM", 'Different visualizations to show post processing data in')} + return { + "MenuText": QtCore.QT_TRANSLATE_NOOP("FEM", "Data Visualizations"), + "ToolTip": QtCore.QT_TRANSLATE_NOOP( + "FEM", "Different visualizations to show post processing data in" + ), + } def IsActive(self): if not FreeCAD.ActiveDocument: return False import FemGui + return bool(FemGui.getActiveAnalysis()) @@ -120,12 +133,12 @@ class _VisualizationCommand: tooltip = f"Create a {self._visualization_type} post processing data visualization" return { - "Pixmap": vis.icon, - "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), - "Accel": "", - "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), - "CmdType": "AlterDoc" - } + "Pixmap": vis.icon, + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), + "Accel": "", + "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), + "CmdType": "AlterDoc", + } def IsActive(self): # active analysis available @@ -133,6 +146,7 @@ class _VisualizationCommand: return False import FemGui + return bool(FemGui.getActiveAnalysis()) def Activated(self): @@ -144,17 +158,12 @@ class _VisualizationCommand: FreeCADGui.addModule(vis.module) FreeCADGui.addModule("FemGui") - FreeCADGui.doCommand( - f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)" - ) - FreeCADGui.doCommand( - f"FemGui.getActiveAnalysis().addObject(obj)" - ) + FreeCADGui.doCommand(f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)") + FreeCADGui.doCommand(f"FemGui.getActiveAnalysis().addObject(obj)") FreeCADGui.Selection.clearSelection() - FreeCADGui.doCommand( - "FreeCADGui.ActiveDocument.setEdit(obj)" - ) + FreeCADGui.doCommand("FreeCADGui.ActiveDocument.setEdit(obj)") + def setup_commands(toplevel_name): # creates all visualization commands and registers them. The diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index f126640d24..b6e8c939b3 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -39,13 +39,14 @@ from vtkmodules.vtkIOCore import vtkDelimitedTextWriter translate = FreeCAD.Qt.translate + class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns # One can supply a header_names dict to replace the table column names # in the header. It is a dict "column_idx (int)" to "new name"" or # "orig_name (str)" to "new name" - def __init__(self, header_names = None): + def __init__(self, header_names=None): super().__init__() self._table = None if header_names: @@ -53,7 +54,7 @@ class VtkTableModel(QtCore.QAbstractTableModel): else: self._header = {} - def setTable(self, table, header_names = None): + def setTable(self, table, header_names=None): self.beginResetModel() self._table = table if header_names: @@ -105,6 +106,7 @@ class VtkTableModel(QtCore.QAbstractTableModel): def getTable(self): return self._table + class VtkTableSummaryModel(QtCore.QAbstractTableModel): # Simple model showing a summary of the table. # Only supports single component columns @@ -126,7 +128,7 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): return self._table.GetNumberOfColumns() def columnCount(self, index): - return 2 # min, max + return 2 # min, max def data(self, index, role): @@ -143,7 +145,7 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: - return ["Min","Max"][section] + return ["Min", "Max"][section] if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return self._table.GetColumnName(section) @@ -162,7 +164,7 @@ class VtkTableView(QtGui.QWidget): self.model = model layout = QtGui.QVBoxLayout() - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # start with the toolbar @@ -177,7 +179,9 @@ class VtkTableView(QtGui.QWidget): copy_action.triggered.connect(self.copyToClipboard) copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) - copy_action.setToolTip(translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString()))) + copy_action.setToolTip( + translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString())) + ) copy_action.setShortcut(shortcut) self.toolbar.addAction(copy_action) @@ -205,15 +209,19 @@ class VtkTableView(QtGui.QWidget): @QtCore.Slot(bool) def exportCsv(self, state): - file_path, filter = QtGui.QFileDialog.getSaveFileName(None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)") + file_path, filter = QtGui.QFileDialog.getSaveFileName( + None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)" + ) if not file_path: - FreeCAD.Console.PrintMessage(translate("FEM", "CSV file export aborted: no filename selected")) + FreeCAD.Console.PrintMessage( + translate("FEM", "CSV file export aborted: no filename selected") + ) return writer = vtkDelimitedTextWriter() writer.SetFileName(file_path) - writer.SetInputData(self.model.getTable()); - writer.Write(); + writer.SetInputData(self.model.getTable()) + writer.Write() @QtCore.Slot() def copyToClipboard(self): @@ -228,19 +236,18 @@ class VtkTableView(QtGui.QWidget): previous = selection.pop(0) for current in selection: - data = self.model.data(previous, QtCore.Qt.DisplayRole); + data = self.model.data(previous, QtCore.Qt.DisplayRole) copy_table += str(data) if current.row() != previous.row(): - copy_table += '\n' + copy_table += "\n" else: - copy_table += '\t' + copy_table += "\t" previous = current copy_table += str(self.model.data(selection[-1], QtCore.Qt.DisplayRole)) - copy_table += '\n' + copy_table += "\n" clipboard = QtGui.QApplication.instance().clipboard() clipboard.setText(copy_table) - diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 48bd9c4951..9e2ad7104d 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -36,22 +36,26 @@ from vtkmodules.vtkCommonDataModel import vtkTable from PySide.QtCore import QT_TRANSLATE_NOOP from . import base_fempythonobject + _PropHelper = base_fempythonobject._PropHelper # helper functions # ################ + def is_extractor_object(obj): if not hasattr(obj, "Proxy"): return False return hasattr(obj.Proxy, "ExtractionType") + def get_extraction_type(obj): # returns the extractor type string, or throws exception if # not a extractor return obj.Proxy.ExtractionType + def get_extraction_dimension(obj): # returns the extractor dimension string, or throws exception if # not a extractor @@ -104,7 +108,6 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") obj.Source = None - def get_vtk_table(self, obj): if not obj.DataTable: obj.DataTable = vtkTable() @@ -135,7 +138,6 @@ class Extractor1D(Extractor): def __init__(self, obj): super().__init__(obj) - def _get_properties(self): prop = [ _PropHelper( @@ -149,14 +151,15 @@ class Extractor1D(Extractor): type="App::PropertyEnumeration", name="XComponent", group="X Data", - doc=QT_TRANSLATE_NOOP("FEM", "Which part of the X field vector to use for the X axis"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Which part of the X field vector to use for the X axis" + ), value=[], ), ] return super()._get_properties() + prop - def onChanged(self, obj, prop): super().onChanged(obj, prop) @@ -213,7 +216,7 @@ class Extractor1D(Extractor): if array.GetNumberOfComponents() == 1: table.AddColumn(array) else: - component_array = vtkDoubleArray(); + component_array = vtkDoubleArray() component_array.SetNumberOfComponents(1) component_array.SetNumberOfTuples(array.GetNumberOfTuples()) c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) @@ -233,7 +236,7 @@ class Extractor1D(Extractor): array.SetNumberOfTuples(num) array.SetNumberOfComponents(1) for i in range(num): - array.SetValue(i,i) + array.SetValue(i, i) case "Position": @@ -266,6 +269,7 @@ class Extractor1D(Extractor): return label + class Extractor2D(Extractor1D): ExtractionDimension = "2D" @@ -273,7 +277,6 @@ class Extractor2D(Extractor1D): def __init__(self, obj): super().__init__(obj) - def _get_properties(self): prop = [ _PropHelper( @@ -287,14 +290,15 @@ class Extractor2D(Extractor1D): type="App::PropertyEnumeration", name="YComponent", group="Y Data", - doc=QT_TRANSLATE_NOOP("FEM", "Which part of the Y field vector to use for the Y axis"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Which part of the Y field vector to use for the Y axis" + ), value=[], ), ] return super()._get_properties() + prop - def onChanged(self, obj, prop): super().onChanged(obj, prop) @@ -348,7 +352,7 @@ class Extractor2D(Extractor1D): if array.GetNumberOfComponents() == 1: table.AddColumn(array) else: - component_array = vtkDoubleArray(); + component_array = vtkDoubleArray() component_array.SetNumberOfComponents(1) component_array.SetNumberOfTuples(array.GetNumberOfTuples()) c_idx = obj.getEnumerationsOfProperty("YComponent").index(obj.YComponent) diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index ffaa94ee8f..5c7465d5bc 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -38,6 +38,7 @@ from . import base_fempostextractors # helper functions # ################ + def is_visualization_object(obj): if not obj: return False @@ -71,27 +72,23 @@ def is_visualization_extractor_type(obj, vistype): return True - # Base class for all visualizations # It collects all data from its extraction objects into a table. # Note: Never use directly, always subclass! This class does not create a # Visualization variable, hence will not work correctly. class PostVisualization(base_fempythonobject.BaseFemPythonObject): - def __init__(self, obj): super().__init__(obj) obj.addExtension("App::GroupExtensionPython") self._setup_properties(obj) - def _setup_properties(self, obj): pl = obj.PropertiesList for prop in self._get_properties(): if not prop.name in pl: prop.add_to_object(obj) - def _get_properties(self): # override if subclass wants to add additional properties @@ -106,14 +103,12 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): ] return prop - def onDocumentRestored(self, obj): # if a new property was added we handle it by setup # Override if subclass needs to handle changed property type self._setup_properties(obj) - def onChanged(self, obj, prop): # Ensure only correct child object types are in the group @@ -123,13 +118,14 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): children = obj.Group for child in obj.Group: if not is_visualization_extractor_type(child, self.VisualizationType): - FreeCAD.Console.PrintWarning(f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added") + FreeCAD.Console.PrintWarning( + f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added" + ) children.remove(child) if len(obj.Group) != len(children): obj.Group = children - def execute(self, obj): # Collect all extractor child data into our table # Note: Each childs table can have different number of rows. We need @@ -144,7 +140,9 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): # to none without recompute, and the visualization was manually # recomputed afterwards if not child.Source and (child.Table.GetNumberOfColumns() > 0): - FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored") + FreeCAD.Console.PrintWarning( + f"{child.Label} has data, but no Source object. Will be ignored" + ) continue c_table = child.Table @@ -159,18 +157,16 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): else: array.SetNumberOfComponents(c_array.GetNumberOfComponents()) array.SetNumberOfTuples(rows) - array.Fill(0) # so that all non-used entries are set to 0 + array.Fill(0) # so that all non-used entries are set to 0 for j in range(c_array.GetNumberOfTuples()): array.SetTuple(j, c_array.GetTuple(j)) array.SetName(f"{child.Source.Name}: {c_array.GetName()}") table.AddColumn(array) - obj.Table = table return False - def getLongestColumnLength(self, obj): # iterate all extractor children and get the column lengths diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 8fcec6e2da..f70c6c65c9 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -33,6 +33,7 @@ import FreeCAD from . import base_fempostextractors from . import base_fempythonobject + _PropHelper = base_fempythonobject._PropHelper from vtkmodules.vtkCommonCore import vtkDoubleArray @@ -53,11 +54,14 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): super().__init__(obj) def _get_properties(self): - prop =[ _PropHelper( + prop = [ + _PropHelper( type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify if the field shall be extracted for every available frame" + ), value=False, ), ] @@ -77,14 +81,16 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): obj.Table = table return - timesteps=[] + timesteps = [] if obj.ExtractFrames: # check if we have timesteps info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) else: - FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + FreeCAD.Console.PrintWarning( + 'No frames available in data, ignoring "ExtractFrames" property' + ) if not timesteps: # get the dataset and extract the correct array @@ -124,11 +130,14 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): super().__init__(obj) def _get_properties(self): - prop =[_PropHelper( + prop = [ + _PropHelper( type="App::PropertyInteger", name="Index", group="X Data", - doc=QT_TRANSLATE_NOOP("FEM", "Specify for which index the data should be extracted"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify for which index the data should be extracted" + ), value=0, ), ] @@ -154,7 +163,6 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - algo = obj.Source.getOutputAlgorithm() frame_array = vtkDoubleArray() idx = obj.Index @@ -168,8 +176,10 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): array = self._x_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) if not setup: frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) @@ -183,14 +193,15 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): array = self._x_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_array.SetNumberOfTuples(1) frame_array.SetTuple(0, idx, array) - if frame_array.GetNumberOfComponents() > 1: frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") else: diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 86827d7a7e..64cba2d5c7 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -33,6 +33,7 @@ import FreeCAD from . import base_fempostextractors from . import base_fempythonobject + _PropHelper = base_fempythonobject._PropHelper from vtkmodules.vtkCommonCore import vtkDoubleArray @@ -53,17 +54,19 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): super().__init__(obj) def _get_properties(self): - prop =[ _PropHelper( + prop = [ + _PropHelper( type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify if the field shall be extracted for every available frame" + ), value=False, ), ] return super()._get_properties() + prop - def execute(self, obj): # on execution we populate the vtk table @@ -85,7 +88,9 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) else: - FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + FreeCAD.Console.PrintWarning( + 'No frames available in data, ignoring "ExtractFrames" property' + ) if not timesteps: # get the dataset and extract the correct array @@ -140,11 +145,14 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): super().__init__(obj) def _get_properties(self): - prop =[_PropHelper( + prop = [ + _PropHelper( type="App::PropertyInteger", name="Index", group="Data", - doc=QT_TRANSLATE_NOOP("FEM", "Specify for which point index the data should be extracted"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify for which point index the data should be extracted" + ), value=0, ), ] @@ -178,7 +186,6 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - algo = obj.Source.getOutputAlgorithm() frame_x_array = vtkDoubleArray() @@ -198,8 +205,10 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): array = self._y_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) if not setup: frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) @@ -211,21 +220,22 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): else: frame_x_array.SetNumberOfTuples(1) frame_x_array.SetNumberOfComponents(1) - frame_x_array.SetTuple1(0,0) + frame_x_array.SetTuple1(0, 0) algo.Update() dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_y_array.SetNumberOfTuples(1) frame_y_array.SetTuple(0, idx, array) - frame_x_array.SetName("Frames") if frame_y_array.GetNumberOfComponents() > 1: frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}") diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py index 51c7d480c6..a783835656 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 diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index fcbb1ce2e7..fb0b1343cc 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecad.org" # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() from . import base_fempostextractors @@ -40,31 +41,35 @@ from . import post_extract1D from femguiutils import post_visualization # register visualization and extractors -post_visualization.register_visualization("Histogram", - ":/icons/FEM_PostHistogram.svg", - "ObjectsFem", - "makePostHistogram") +post_visualization.register_visualization( + "Histogram", ":/icons/FEM_PostHistogram.svg", "ObjectsFem", "makePostHistogram" +) -post_visualization.register_extractor("Histogram", - "HistogramFieldData", - ":/icons/FEM_PostField.svg", - "1D", - "Field", - "ObjectsFem", - "makePostHistogramFieldData") +post_visualization.register_extractor( + "Histogram", + "HistogramFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostHistogramFieldData", +) -post_visualization.register_extractor("Histogram", - "HistogramIndexOverFrames", - ":/icons/FEM_PostIndex.svg", - "1D", - "Index", - "ObjectsFem", - "makePostHistogramIndexOverFrames") +post_visualization.register_extractor( + "Histogram", + "HistogramIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostHistogramIndexOverFrames", +) # Implementation # ############## + def is_histogram_extractor(obj): if not base_fempostextractors.is_extractor_object(obj): @@ -80,6 +85,7 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D): """ A 1D Field extraction for histograms. """ + VisualizationType = "Histogram" @@ -87,6 +93,7 @@ class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): """ A 1D index extraction for histogram. """ + VisualizationType = "Histogram" @@ -94,13 +101,5 @@ class PostHistogram(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as histograms """ + VisualizationType = "Histogram" - - - - - - - - - diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 8d4b725128..3216400415 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecad.org" # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() from . import base_fempostextractors @@ -40,31 +41,35 @@ from . import post_extract2D from femguiutils import post_visualization # register visualization and extractors -post_visualization.register_visualization("Lineplot", - ":/icons/FEM_PostLineplot.svg", - "ObjectsFem", - "makePostLineplot") +post_visualization.register_visualization( + "Lineplot", ":/icons/FEM_PostLineplot.svg", "ObjectsFem", "makePostLineplot" +) -post_visualization.register_extractor("Lineplot", - "LineplotFieldData", - ":/icons/FEM_PostField.svg", - "2D", - "Field", - "ObjectsFem", - "makePostLineplotFieldData") +post_visualization.register_extractor( + "Lineplot", + "LineplotFieldData", + ":/icons/FEM_PostField.svg", + "2D", + "Field", + "ObjectsFem", + "makePostLineplotFieldData", +) -post_visualization.register_extractor("Lineplot", - "LineplotIndexOverFrames", - ":/icons/FEM_PostIndex.svg", - "2D", - "Index", - "ObjectsFem", - "makePostLineplotIndexOverFrames") +post_visualization.register_extractor( + "Lineplot", + "LineplotIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "2D", + "Index", + "ObjectsFem", + "makePostLineplotIndexOverFrames", +) # Implementation # ############## + def is_lineplot_extractor(obj): if not base_fempostextractors.is_extractor_object(obj): @@ -80,6 +85,7 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ A 2D Field extraction for lineplot. """ + VisualizationType = "Lineplot" @@ -87,15 +93,13 @@ class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): """ A 2D index extraction for lineplot. """ - VisualizationType = "Lineplot" + VisualizationType = "Lineplot" class PostLineplot(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as line plots """ + VisualizationType = "Lineplot" - - - diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index 0a64dc733d..a12398ab9e 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecad.org" # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() from . import base_fempostextractors @@ -40,31 +41,35 @@ from . import post_extract1D from femguiutils import post_visualization # register visualization and extractors -post_visualization.register_visualization("Table", - ":/icons/FEM_PostSpreadsheet.svg", - "ObjectsFem", - "makePostTable") +post_visualization.register_visualization( + "Table", ":/icons/FEM_PostSpreadsheet.svg", "ObjectsFem", "makePostTable" +) -post_visualization.register_extractor("Table", - "TableFieldData", - ":/icons/FEM_PostField.svg", - "1D", - "Field", - "ObjectsFem", - "makePostTableFieldData") +post_visualization.register_extractor( + "Table", + "TableFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostTableFieldData", +) -post_visualization.register_extractor("Table", - "TableIndexOverFrames", - ":/icons/FEM_PostIndex.svg", - "1D", - "Index", - "ObjectsFem", - "makePostTableIndexOverFrames") +post_visualization.register_extractor( + "Table", + "TableIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostTableIndexOverFrames", +) # Implementation # ############## + def is_table_extractor(obj): if not base_fempostextractors.is_extractor_object(obj): @@ -80,6 +85,7 @@ class PostTableFieldData(post_extract1D.PostFieldData1D): """ A 1D Field extraction for tables. """ + VisualizationType = "Table" @@ -87,6 +93,7 @@ class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D): """ A 1D index extraction for table. """ + VisualizationType = "Table" @@ -94,6 +101,5 @@ class PostTable(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as tables """ + VisualizationType = "Table" - - diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 5efa1aa12a..81fa9107eb 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -53,7 +53,9 @@ class _BasePostTaskPanel(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? @@ -63,8 +65,9 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): def open(self): # open a new transaction if non is open if not FreeCAD.getActiveTransaction(): - FreeCAD.ActiveDocument.openTransaction(translate("FEM", "Edit {}").format(self.obj.Label)) - + FreeCAD.ActiveDocument.openTransaction( + translate("FEM", "Edit {}").format(self.obj.Label) + ) # Helper functions # ################ @@ -83,6 +86,3 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): cbox.setCurrentText(getattr(obj, prop)) cbox.blockSignals(False) - - - diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py index 56fdd998b5..9c54352f10 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -52,6 +52,3 @@ class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): view.setWindowIcon(obj.ViewObject.Icon) self.form = [app, view] - - - diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index a9a13139e9..570ca63b66 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -56,7 +56,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.widget, vobj.createDisplayTaskWidget()] - # Setup functions # ############### diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py index 496fc1792a..df70e2f18d 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_histogram.py +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -40,6 +40,7 @@ from femguiutils import vtk_table_view translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -68,7 +69,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setWindowTitle(translate("FEM", "Histogram data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) - # histogram parameter widget self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" @@ -81,7 +81,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.data_widget, self.view_widget] - # Setup functions # ############### @@ -121,12 +120,13 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget.BarWidth.valueChanged.connect(self.barWidthChanged) self.view_widget.HatchWidth.valueChanged.connect(self.hatchWidthChanged) - QtCore.Slot() + def showPlot(self): self.obj.ViewObject.Proxy.show_visualization() QtCore.Slot() + def showTable(self): # TODO: make data model update when object is recomputed @@ -137,49 +137,58 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): widget = vtk_table_view.VtkTableView(data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(1500, 900) dialog.show() - QtCore.Slot(int) + def binsChanged(self, bins): self.obj.ViewObject.Bins = bins QtCore.Slot(int) + def typeChanged(self, idx): self.obj.ViewObject.Type = idx QtCore.Slot(bool) + def comulativeChanged(self, state): self.obj.ViewObject.Cumulative = state QtCore.Slot() + def titleChanged(self): self.obj.ViewObject.Title = self.view_widget.Title.text() QtCore.Slot() + def xLabelChanged(self): self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() QtCore.Slot() + def yLabelChanged(self): self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() QtCore.Slot(int) + def legendPosChanged(self, idx): self.obj.ViewObject.LegendLocation = idx QtCore.Slot(bool) + def legendShowChanged(self, state): self.obj.ViewObject.Legend = state QtCore.Slot(float) + def barWidthChanged(self, value): self.obj.ViewObject.BarWidth = value QtCore.Slot(float) + def hatchWidthChanged(self, value): self.obj.ViewObject.HatchLineWidth = value diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py index 474d84b80b..f5598e1874 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -40,6 +40,7 @@ from femguiutils import vtk_table_view translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -68,7 +69,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setWindowTitle(translate("FEM", "Lineplot data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) - # lineplot parameter widget self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" @@ -81,7 +81,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.data_widget, self.view_widget] - # Setup functions # ############### @@ -104,7 +103,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget.LegendShow.setChecked(viewObj.Legend) self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) - # connect callbacks self.view_widget.Scale.activated.connect(self.scaleChanged) self.view_widget.Grid.toggled.connect(self.gridChanged) @@ -116,12 +114,13 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) self.view_widget.LegendPos.activated.connect(self.legendPosChanged) - QtCore.Slot() + def showPlot(self): self.obj.ViewObject.Proxy.show_visualization() QtCore.Slot() + def showTable(self): # TODO: make data model update when object is recomputed @@ -132,37 +131,43 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): widget = vtk_table_view.VtkTableView(data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(1500, 900) dialog.show() - QtCore.Slot(int) + def scaleChanged(self, idx): self.obj.ViewObject.Scale = idx QtCore.Slot(bool) + def gridChanged(self, state): self.obj.ViewObject.Grid = state QtCore.Slot() + def titleChanged(self): self.obj.ViewObject.Title = self.view_widget.Title.text() QtCore.Slot() + def xLabelChanged(self): self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() QtCore.Slot() + def yLabelChanged(self): self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() QtCore.Slot(int) + def legendPosChanged(self, idx): self.obj.ViewObject.LegendLocation = idx QtCore.Slot(bool) + def legendShowChanged(self, state): self.obj.ViewObject.Legend = state diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py index 1eda150016..98fd1686d6 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_table.py +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -39,6 +39,7 @@ from femguiutils import extract_link_view as elv translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -68,7 +69,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.data_widget] - # Setup functions # ############### @@ -77,8 +77,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # connect data widget self.data_widget.show_table.clicked.connect(self.showTable) - @QtCore.Slot() def showTable(self): self.obj.ViewObject.Proxy.show_visualization() - diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index a86c1288a2..10ba8e2fd0 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -40,6 +40,7 @@ from femobjects.base_fempythonobject import _PropHelper False if FemGui.__name__ else True # flake8, dummy FemGui usage + class _GuiPropHelper(_PropHelper): """ Helper class to manage property data inside proxy objects. diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index bb4aca0934..b2df81ef0d 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -1,4 +1,3 @@ - # *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * @@ -37,6 +36,7 @@ from PySide import QtGui from femtaskpanels import task_post_extractor + class VPPostExtractor: """ A View Provider for extraction of data @@ -74,18 +74,18 @@ class VPPostExtractor: if not group: return - if (hasattr(group.ViewObject, "Proxy") and - hasattr(group.ViewObject.Proxy, "childViewPropertyChanged")): + if hasattr(group.ViewObject, "Proxy") and hasattr( + group.ViewObject.Proxy, "childViewPropertyChanged" + ): group.ViewObject.Proxy.childViewPropertyChanged(vobj, prop) - def setEdit(self, vobj, mode): # build up the task panel taskd = task_post_extractor._ExtractorTaskPanel(vobj.Object) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True @@ -112,7 +112,6 @@ class VPPostExtractor: def loads(self, state): return None - # To be implemented by subclasses: # ################################ diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 9b357a4d0c..3abf56b29a 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -32,6 +32,7 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui + class VPPostVisualization: """ A View Provider for visualization objects @@ -42,23 +43,19 @@ class VPPostVisualization: self._setup_properties(vobj) vobj.addExtension("Gui::ViewProviderGroupExtensionPython") - def _setup_properties(self, vobj): pl = vobj.PropertiesList for prop in self._get_properties(): if not prop.name in pl: prop.add_to_object(vobj) - def _get_properties(self): return [] - def attach(self, vobj): self.Object = vobj.Object self.ViewObject = vobj - def isShow(self): # Mark ourself as visible in the tree return True @@ -66,7 +63,7 @@ class VPPostVisualization: def getDisplayModes(self, obj): return ["Dialog"] - def doubleClicked(self,vobj): + def doubleClicked(self, vobj): guidoc = FreeCADGui.getDocument(vobj.Object.Document) @@ -107,7 +104,6 @@ class VPPostVisualization: def loads(self, state): return None - # To be implemented by subclasses: # ################################ diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 337ad62f86..f6323cafc6 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -48,6 +48,7 @@ from . import view_base_fempostvisualization from femtaskpanels import task_post_histogram from . import view_base_femobject + _GuiPropHelper = view_base_femobject._GuiPropHelper @@ -92,12 +93,11 @@ class EditViewWidget(QtGui.QWidget): self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height()) self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) - def _setup_color_button(self, button, fcColor, callback): - barColor = QtGui.QColor(*[v*255 for v in fcColor]) + barColor = QtGui.QColor(*[v * 255 for v in fcColor]) icon_size = button.iconSize() - icon_size.setWidth(icon_size.width()*2) + icon_size.setWidth(icon_size.width() * 2) button.setIconSize(icon_size) pixmap = QtGui.QPixmap(icon_size) pixmap.fill(barColor) @@ -114,7 +114,6 @@ class EditViewWidget(QtGui.QWidget): button.addAction(action) button.setPopupMode(QtGui.QToolButton.InstantPopup) - @QtCore.Slot(QtGui.QColor) def lineColorChanged(self, color): @@ -199,6 +198,7 @@ class EditFieldAppWidget(QtGui.QWidget): self._object.ExtractFrames = extract self._post_dialog._recompute() + class EditIndexAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -279,7 +279,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="Hatch", group="HistogramBar", doc=QT_TRANSLATE_NOOP("FEM", "The hatch pattern drawn in the bar"), - value=['None', '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'], + value=["None", "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"], ), _GuiPropHelper( type="App::PropertyIntegerConstraint", @@ -293,13 +293,15 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="LineColor", group="HistogramLine", doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), - value=(0, 0, 0, 1), # black + value=(0, 0, 0, 1), # black ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="LineWidth", group="HistogramLine", - doc=QT_TRANSLATE_NOOP("FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"), + doc=QT_TRANSLATE_NOOP( + "FEM", "The width of the bar, between 0 and 1 (1 being without gaps)" + ), value=(1, 0, 99, 0.1), ), _GuiPropHelper( @@ -307,7 +309,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="LineStyle", group="HistogramLine", doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), - value=['None', '-', '--', '-.', ':'], + value=["None", "-", "--", "-.", ":"], ), ] return super()._get_properties() + prop @@ -327,13 +329,13 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): def get_preview(self): - fig = mpl.pyplot.figure(figsize=(0.4,0.2), dpi=500) - ax = mpl.pyplot.Axes(fig, [0., 0., 2, 1]) + fig = mpl.pyplot.figure(figsize=(0.4, 0.2), dpi=500) + ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 2, 1]) ax.set_axis_off() fig.add_axes(ax) kwargs = self.get_kw_args() - patch = mpl.patches.Rectangle(xy=(0,0), width=2, height=1, **kwargs) + patch = mpl.patches.Rectangle(xy=(0, 0), width=2, height=1, **kwargs) ax.add_patch(patch) data = io.BytesIO() @@ -355,7 +357,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): kwargs["linestyle"] = self.ViewObject.LineStyle kwargs["linewidth"] = self.ViewObject.LineWidth if self.ViewObject.Hatch != "None": - kwargs["hatch"] = self.ViewObject.Hatch*self.ViewObject.HatchDensity + kwargs["hatch"] = self.ViewObject.Hatch * self.ViewObject.HatchDensity return kwargs @@ -386,7 +388,6 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - def _get_properties(self): prop = [ @@ -394,7 +395,9 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Cumulative", group="Histogram", - doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), + doc=QT_TRANSLATE_NOOP( + "FEM", "If be the bars shoud show the cumulative sum left to rigth" + ), value=False, ), _GuiPropHelper( @@ -402,13 +405,15 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): name="Type", group="Histogram", doc=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"), - value=["bar","barstacked", "step", "stepfilled"], + value=["bar", "barstacked", "step", "stepfilled"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="BarWidth", group="Histogram", - doc=QT_TRANSLATE_NOOP("FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"), + doc=QT_TRANSLATE_NOOP( + "FEM", "The width of the bar, between 0 and 1 (1 being without gaps)" + ), value=(0.9, 0, 1, 0.05), ), _GuiPropHelper( @@ -458,29 +463,36 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): name="LegendLocation", group="Plot", doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), - value=['best','upper right','upper left','lower left','lower right','right', - 'center left','center right','lower center','upper center','center'], + value=[ + "best", + "upper right", + "upper left", + "lower left", + "lower right", + "right", + "center left", + "center right", + "lower center", + "upper center", + "center", + ], ), - ] return prop - def getIcon(self): return ":/icons/FEM_PostHistogram.svg" - def setEdit(self, vobj, mode): # build up the task panel taskd = task_post_histogram._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True - def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -489,12 +501,11 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) - self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square + self._plot.resize(main.size().height() / 2, main.size().height() / 3) # keep it square self.update_visualization() self._plot.show() - def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): @@ -503,7 +514,6 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -523,7 +533,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): kwargs = self.get_kw_args(child) # iterate over the table and plot all - color_factor = np.linspace(1,0.5,table.GetNumberOfColumns()) + color_factor = np.linspace(1, 0.5, table.GetNumberOfColumns()) legend_multiframe = table.GetNumberOfColumns() > 1 for i in range(table.GetNumberOfColumns()): @@ -532,7 +542,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): for key in kwargs: if "color" in key: - value = np.array(kwargs[key])*color_factor[i] + value = np.array(kwargs[key]) * color_factor[i] args[key] = mpl.colors.to_hex(value) full_args.append(args) @@ -581,7 +591,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.axes.set_ylabel(self.ViewObject.YLabel) if self.ViewObject.Legend and labels: - self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + self._plot.axes.legend(loc=self.ViewObject.LegendLocation) self._plot.update() diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 3066e009b7..f73d0aad7b 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -48,8 +48,10 @@ from . import view_base_fempostvisualization from femtaskpanels import task_post_lineplot from . import view_base_femobject + _GuiPropHelper = view_base_femobject._GuiPropHelper + class EditViewWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -91,9 +93,9 @@ class EditViewWidget(QtGui.QWidget): def _setup_color_button(self, button, fcColor, callback): - barColor = QtGui.QColor(*[v*255 for v in fcColor]) + barColor = QtGui.QColor(*[v * 255 for v in fcColor]) icon_size = button.iconSize() - icon_size.setWidth(icon_size.width()*2) + icon_size.setWidth(icon_size.width() * 2) button.setIconSize(icon_size) pixmap = QtGui.QPixmap(icon_size) pixmap.fill(barColor) @@ -110,7 +112,6 @@ class EditViewWidget(QtGui.QWidget): button.addAction(action) button.setPopupMode(QtGui.QToolButton.InstantPopup) - @QtCore.Slot(QtGui.QColor) def colorChanged(self, color): @@ -163,9 +164,13 @@ class EditFieldAppWidget(QtGui.QWidget): # set the other properties self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.XField) - self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "XComponent", self.widget.XComponent + ) self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self.widget.Extract.setChecked(self._object.ExtractFrames) self.widget.XField.activated.connect(self.xFieldChanged) @@ -177,7 +182,9 @@ class EditFieldAppWidget(QtGui.QWidget): @QtCore.Slot(int) def xFieldChanged(self, index): self._object.XField = index - self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "XComponent", self.widget.XComponent + ) self._post_dialog._recompute() @QtCore.Slot(int) @@ -188,7 +195,9 @@ class EditFieldAppWidget(QtGui.QWidget): @QtCore.Slot(int) def yFieldChanged(self, index): self._object.YField = index - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self._post_dialog._recompute() @QtCore.Slot(int) @@ -225,7 +234,9 @@ class EditIndexAppWidget(QtGui.QWidget): self.widget.Index.setValue(self._object.Index) self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self.widget.Index.valueChanged.connect(self.indexChanged) self.widget.YField.activated.connect(self.yFieldChanged) @@ -242,7 +253,9 @@ class EditIndexAppWidget(QtGui.QWidget): @QtCore.Slot(int) def yFieldChanged(self, index): self._object.YField = index - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self._post_dialog._recompute() @QtCore.Slot(int) @@ -282,7 +295,7 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): name="LineStyle", group="Lineplot", doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), - value=['-', '--', '-.', ':', 'None'], + value=["-", "--", "-.", ":", "None"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", @@ -296,7 +309,7 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): name="MarkerStyle", group="Lineplot", doc=QT_TRANSLATE_NOOP("FEM", "The style the data markers are drawn with"), - value=['None', '*', '+', 's', '.', 'o', 'x'], + value=["None", "*", "+", "s", ".", "o", "x"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", @@ -325,13 +338,13 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): # Returns the preview tuple of icon and label: (QPixmap, str) # Note: QPixmap in ratio 2:1 - fig = mpl.pyplot.figure(figsize=(0.2,0.1), dpi=1000) - ax = mpl.pyplot.Axes(fig, [0., 0., 1., 1.]) + fig = mpl.pyplot.figure(figsize=(0.2, 0.1), dpi=1000) + ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 1.0, 1.0]) ax.set_axis_off() fig.add_axes(ax) kwargs = self.get_kw_args() kwargs["markevery"] = [1] - ax.plot([0,0.5,1],[0.5,0.5,0.5], **kwargs) + ax.plot([0, 0.5, 1], [0.5, 0.5, 0.5], **kwargs) data = io.BytesIO() mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) mpl.pyplot.close() @@ -341,7 +354,6 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): return (pixmap, self.ViewObject.Legend) - def get_kw_args(self): # builds kw args from the properties kwargs = {} @@ -383,7 +395,6 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - def _get_properties(self): prop = [ @@ -391,7 +402,9 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Grid", group="Lineplot", - doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), + doc=QT_TRANSLATE_NOOP( + "FEM", "If be the bars shoud show the cumulative sum left to rigth" + ), value=True, ), _GuiPropHelper( @@ -399,9 +412,9 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="Scale", group="Lineplot", doc=QT_TRANSLATE_NOOP("FEM", "The scale the axis are drawn in"), - value=["linear","semi-log x", "semi-log y", "log"], + value=["linear", "semi-log x", "semi-log y", "log"], ), - _GuiPropHelper( + _GuiPropHelper( type="App::PropertyString", name="Title", group="Plot", @@ -434,29 +447,36 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="LegendLocation", group="Plot", doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), - value=['best','upper right','upper left','lower left','lower right','right', - 'center left','center right','lower center','upper center','center'], + value=[ + "best", + "upper right", + "upper left", + "lower left", + "lower right", + "right", + "center left", + "center right", + "lower center", + "upper center", + "center", + ], ), - ] return prop - def getIcon(self): return ":/icons/FEM_PostLineplot.svg" - def setEdit(self, vobj, mode): # build up the task panel taskd = task_post_lineplot._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True - def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -465,12 +485,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) - self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self._plot.resize( + main.size().height() / 2, main.size().height() / 3 + ) # keep the aspect ratio self.update_visualization() self._plot.show() - def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): @@ -479,7 +500,6 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -496,10 +516,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): kwargs = self.get_kw_args(child) # iterate over the table and plot all (note: column 0 is always X!) - color_factor = np.linspace(1,0.5,int(table.GetNumberOfColumns()/2)) + color_factor = np.linspace(1, 0.5, int(table.GetNumberOfColumns() / 2)) legend_multiframe = table.GetNumberOfColumns() > 2 - for i in range(0,table.GetNumberOfColumns(),2): + for i in range(0, table.GetNumberOfColumns(), 2): plotted = True @@ -507,13 +527,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): tmp_args = {} for key in kwargs: if "color" in key: - value = np.array(kwargs[key])*color_factor[int(i/2)] + value = np.array(kwargs[key]) * color_factor[int(i / 2)] tmp_args[key] = mpl.colors.to_hex(value) else: tmp_args[key] = kwargs[key] xdata = VTKArray(table.GetColumn(i)) - ydata = VTKArray(table.GetColumn(i+1)) + ydata = VTKArray(table.GetColumn(i + 1)) # ensure points are visible if it is a single datapoint if len(xdata) == 1 and tmp_args["marker"] == "None": @@ -524,13 +544,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if not legend_multiframe: label = child.ViewObject.Legend else: - postfix = table.GetColumnName(i+1).split("-")[-1] + postfix = table.GetColumnName(i + 1).split("-")[-1] label = child.ViewObject.Legend + " - " + postfix else: legend_prefix = "" if len(self.Object.Group) > 1: legend_prefix = child.Source.Label + ": " - label = legend_prefix + table.GetColumnName(i+1) + label = legend_prefix + table.GetColumnName(i + 1) match self.ViewObject.Scale: case "log": @@ -550,7 +570,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): self._plot.axes.set_ylabel(self.ViewObject.YLabel) if self.ViewObject.Legend and plotted: - self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + self._plot.axes.legend(loc=self.ViewObject.LegendLocation) self._plot.axes.grid(self.ViewObject.Grid) self._plot.update() @@ -561,4 +581,3 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): i = len(self.Object.Group) cmap = mpl.pyplot.get_cmap("tab10") return cmap(i) - diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 536a3665e2..3c22d8b999 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -41,8 +41,10 @@ from femtaskpanels import task_post_table from femguiutils import vtk_table_view as vtv from . import view_base_femobject + _GuiPropHelper = view_base_femobject._GuiPropHelper + class EditViewWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -116,6 +118,7 @@ class EditFieldAppWidget(QtGui.QWidget): self._object.ExtractFrames = extract self._post_dialog._recompute() + class EditIndexAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -180,7 +183,9 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Name", group="Table", - doc=QT_TRANSLATE_NOOP("FEM", "The name used in the table header. Default name is used if empty"), + doc=QT_TRANSLATE_NOOP( + "FEM", "The name used in the table header. Default name is used if empty" + ), value="", ), ] @@ -232,22 +237,19 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - def getIcon(self): return ":/icons/FEM_PostSpreadsheet.svg" - def setEdit(self, vobj, mode): # build up the task panel taskd = task_post_table._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True - def show_visualization(self): if not hasattr(self, "_tableview") or not self._tableview: @@ -257,13 +259,14 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): self._tableview.setWindowTitle(self.Object.Label) self._tableview.setParent(main) self._tableview.setWindowFlags(QtGui.Qt.Dialog) - self._tableview.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self._tableview.resize( + main.size().height() / 2, main.size().height() / 3 + ) # keep the aspect ratio self.update_visualization() self._tableview.show() - def update_visualization(self): if not hasattr(self, "_tableModel") or not self._tableModel: @@ -286,5 +289,3 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): header[table.GetColumnName(i)] = new_name self._tableModel.setTable(self.Object.Table, header) - -