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; }; 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..69a5d8f23e 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 FC_USE_VTK_PYTHON + // 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..8b242abcf7 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 FC_USE_VTK_PYTHON +#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 FC_USE_VTK_PYTHON + // 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..2c493074e6 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 FC_USE_VTK_PYTHON +#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 FC_USE_VTK_PYTHON + // 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..aca1b443f0 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -214,9 +214,15 @@ 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 + femobjects/post_histogram.py + femobjects/post_lineplot.py + femobjects/post_table.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -597,6 +603,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 @@ -625,9 +632,12 @@ 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 + femtaskpanels/task_post_table.py + femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -641,9 +651,18 @@ SET(FemGuiUtils_SRCS femguiutils/disambiguate_solid_selection.py femguiutils/migrate_gui.py femguiutils/selection_widgets.py - femguiutils/vtk_module_handling.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 @@ -683,9 +702,13 @@ 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_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 7abc3b4df0..7729f8af55 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,16 @@ 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/TaskPostLineplot.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 + Resources/ui/PostTableFieldViewEdit.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..351dad3e48 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -86,6 +86,11 @@ icons/FEM_PostFrames.svg icons/FEM_PostBranchFilter.svg icons/FEM_PostPipelineFromResult.svg + icons/FEM_PostLineplot.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..a93343fd25 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + 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..333e138d83 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg @@ -0,0 +1,69 @@ + + + + + + + + + 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..9198dcdba0 --- /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..6e90515778 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg @@ -0,0 +1,46 @@ + + + + + + + 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..b8453c0756 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + 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..8e611e7790 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -0,0 +1,78 @@ + + + Form + + + + 0 + 0 + 317 + 118 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One field for each 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..5fe4a7d3dc --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -0,0 +1,167 @@ + + + PostHistogramEdit + + + + 0 + 0 + 278 + 110 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 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: + + + + + + + + 0 + 0 + + + + Density of hatch pattern + + + 1 + + + + + + + Bars: + + + + + + + + + + Legend: + + + + + + + + 0 + 0 + + + + Color of all lines (bar outline and hatches) + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + + Legend + BarColor + Hatch + HatchDensity + LineColor + LineStyle + LineWidth + + + + 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..496f42229b --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui @@ -0,0 +1,84 @@ + + + Form + + + + 0 + 0 + 261 + 110 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + 999999999 + + + + + + + + + + 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..b0d1830852 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui @@ -0,0 +1,101 @@ + + + Form + + + + 0 + 0 + 271 + 174 + + + + 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..f197016d12 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui @@ -0,0 +1,151 @@ + + + PostHistogramEdit + + + + 0 + 0 + 274 + 114 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Marker: + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + + + + Legend: + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + Line: + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + 0 + 0 + + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + + Legend + Color + LineStyle + MarkerStyle + + + + 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/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui new file mode 100644 index 0000000000..6b3000248a --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -0,0 +1,50 @@ + + + PostHistogramEdit + + + + 0 + 0 + 279 + 38 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + Name: + + + + + + + + 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..a753071f9a --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui @@ -0,0 +1,241 @@ + + + TaskPostGlyph + + + + 0 + 0 + 343 + 498 + + + + Glyph settings + + + Qt::LeftToRight + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + The form of the glyph + + + Bins + + + + + + + + 0 + 0 + + + + Qt::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::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 + + + + + + + Bar width + + + + + + + + + + + + + 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/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 805977f6d2..e50bb0bf98 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,7 +71,6 @@ #include "ViewProviderFemPostObject.h" #include "ViewProviderFemPostBranchFilter.h" - using namespace FemGui; using namespace Gui; @@ -214,9 +212,18 @@ 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 @@ -393,6 +408,24 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) } } +void TaskDlgPost::processCollapsedWidgets() +{ + + for (auto& widget : Content) { + 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(); + } +} // *************************************************************************** // box to set the coloring @@ -475,7 +508,6 @@ void TaskPostDisplay::onTransparencyValueChanged(int i) void TaskPostDisplay::applyPythonCode() {} - // *************************************************************************** // functions TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent) @@ -557,6 +589,11 @@ 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 d37742dd27..816dafb080 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; @@ -155,6 +156,12 @@ 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 { @@ -187,10 +194,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; }; @@ -229,6 +241,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(); @@ -267,7 +282,6 @@ private: std::unique_ptr ui; }; - // *************************************************************************** // functions class ViewProviderFemPostFunction; @@ -295,6 +309,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 new file mode 100644 index 0000000000..e61033957c --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -0,0 +1,181 @@ +/*************************************************************************** + * 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 "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; + + + 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())); + 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()) { + 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; + } + } + } + + // if we are here something went wrong! + Base::Console().error("Unable to import data extraction widget\n"); +}; + +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(); + } +} + +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 new file mode 100644 index 0000000000..5fe2518760 --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.h @@ -0,0 +1,68 @@ +/*************************************************************************** + * 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; + bool initiallyCollapsed() 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/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 4cbacb5cad..f6c60491b8 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -32,6 +32,9 @@ #include "ViewProviderFemPostFilter.h" #include "ViewProviderFemPostFilterPy.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif using namespace FemGui; @@ -89,6 +92,12 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); 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 } @@ -138,6 +147,12 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); 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/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..5683ce2467 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -27,6 +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" @@ -60,6 +63,29 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) return nullptr; } +PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args) +{ +#ifdef FC_USE_VTK_PYTHON + // 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; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + 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..9205e2d708 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -67,6 +67,9 @@ #include #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif #include "ViewProviderAnalysis.h" #include "ViewProviderFemPostObject.h" @@ -1006,6 +1009,7 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) postDlg = new TaskDlgPost(this); setupTaskDialog(postDlg); postDlg->connectSlots(); + postDlg->processCollapsedWidgets(); Gui::Control().showDialog(postDlg); } @@ -1019,8 +1023,13 @@ 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 dispPanel = new TaskPostDisplay(this); + dlg->addTaskBox(dispPanel->windowIcon().pixmap(32), dispPanel); + +#ifdef FC_USE_VTK_PYTHON + auto extrPanel = new TaskPostExtraction(this); + dlg->addTaskBox(extrPanel->windowIcon().pixmap(32), extrPanel); +#endif } void ViewProviderFemPostObject::unsetEdit(int ModNum) diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 3ae3219705..31f396fbe9 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 FC_USE_VTK_PYTHON + << "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 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 8ac271d379..e7d0a2ada7 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -81,9 +81,10 @@ 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 + if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + from femguiutils.vtk_module_handling import vtk_module_handling - 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/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c05ecc6108..2bd6e74056 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -686,6 +686,141 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): return obj +def makePostLineplot(doc, name="Lineplot"): + """makePostLineplot(document, [name]): + creates a FEM post processing line plot + """ + obj = doc.addObject("Fem::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 obj + + +def makePostLineplotFieldData(doc, name="FieldData2D"): + """makePostLineplotFieldData(document, [name]): + creates a FEM post processing data extractor for 2D Field data + """ + obj = doc.addObject("Fem::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLineplotFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + + 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("Fem::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 + """ + obj = doc.addObject("Fem::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 + + +def makePostHistogramFieldData(doc, name="FieldData1D"): + """makePostHistogramFieldData(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("Fem::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 makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostHistogramIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("Fem::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 + + +def makePostTable(doc, name="Table"): + """makePostTable(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("Fem::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("Fem::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("Fem::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 0c61dcff2b..5d662074be 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1289,3 +1289,12 @@ 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_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..bb2edc3e05 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 @@ -381,6 +380,8 @@ class CommandManager: # and the selobj is expanded in the tree to see the added obj # 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 new file mode 100644 index 0000000000..dfe0cea7f8 --- /dev/null +++ b/src/Mod/Fem/femguiutils/data_extraction.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 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.vtkCommonCore import vtkVersion +from vtkmodules.vtkCommonDataModel import vtkTable +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 + +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. + # 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) + dialog.setWindowTitle(f"Data of {self.Object.Label}") + 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) + dialog.setWindowTitle(f"Data summary of {self.Object.Label}") + 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()) + + 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) + + # add the points + points = algo.GetOutputDataObject(0).GetPoints().GetData() + table.AddColumn(points) + + # 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 + + 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 new file mode 100644 index 0000000000..eec8ba6927 --- /dev/null +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -0,0 +1,715 @@ +# *************************************************************************** +# * 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 + +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 + + 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, 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, translate("FEM", "with {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) + 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, translate("FEM", "Add {}").format(name) + ) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) + 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, translate("FEM", "From {}").format(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, translate("FEM", "add {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) + 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 + + +# 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() + 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): + + # 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 + icn_width = self.iconSize().width() + if self._icon.isNull(): + icn_width = 0 + + fm = self.fontMetrics() + 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() + + +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.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows) + self.tree_view.expandAll() + 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) + 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())) + + +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) + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(setting) + + buttonBox = QtGui.QDialogButtonBox() + buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + buttonBox.accepted.connect(self.hide) + vbox.addWidget(buttonBox) + + widget = QtGui.QFrame() + widget.setLayout(vbox) + + vbox2 = QtGui.QVBoxLayout() + vbox2.setContentsMargins(0, 0, 0, 0) + vbox2.addWidget(widget) + self.setLayout(vbox2) + + def size(self): + return self._setting.sizeHint() + + def showEvent(self, event): + # required to get keyboard events + self.setFocus() + + 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): + + delete = QtCore.Signal(object, object) # to delete: document object, summary widget + + def __init__(self, st_object, extractor, post_dialog): + super().__init__() + + self._st_object = st_object + self._extractor = extractor + self._post_dialog = post_dialog + + extr_label = extractor.Proxy.get_representive_fieldname(extractor) + extr_repr = extractor.ViewObject.Proxy.get_preview() + + # build the UI + hbox = QtGui.QHBoxLayout() + 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) + self.viewButton.setIconSize(size) + + if st_object: + 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 + # that property is set to None + self.extrButton.hide() + self.viewButton.hide() + + 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(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.Preferred) + self.setSizePolicy(policy) + # 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 + 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 + # self._redraw() + + def _button(self, icon, text, stretch=2): + + btn = _ElideToolButton(icon, text, self) + btn.setMinimumWidth(0) + btn.setAutoRaise(True) + btn.setToolTip(text) + + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + policy.setHorizontalStretch(stretch) + btn.setSizePolicy(policy) + return btn + + @QtCore.Slot() + def showVisualization(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) + + def _position_dialog(self, dialog): + + # 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.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())) + + @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) + self.appDialog.close.connect(self.appAccept) + + if not self.appDialog.isVisible(): + # position correctly and show + self._position_dialog(self.appDialog) + self.appDialog.show() + + @QtCore.Slot() + def editView(self): + + if not hasattr(self, "viewDialog"): + widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog) + self.viewDialog = _SettingsPopup(widget, self) + self.viewDialog.close.connect(self.viewAccept) + + if not self.viewDialog.isVisible(): + # position correctly and show + self._position_dialog(self.viewDialog) + self.viewDialog.show() + + @QtCore.Slot() + def deleteTriggered(self): + self.delete.emit(self._extractor, self) + + @QtCore.Slot() + def viewAccept(self): + + # update the preview + extr_repr = self._extractor.ViewObject.Proxy.get_preview() + self.viewButton.setCustomIcon(extr_repr[0]) + self.viewButton.setCustomText(extr_repr[1]) + self.viewButton.setToolTip(extr_repr[1]) + + @QtCore.Slot() + def appAccept(self): + + # update the preview + extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) + self.extrButton.setCustomText(extr_label) + self.extrButton.setToolTip(extr_label) + + +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) + 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:")) + + 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(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(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(translate("FEM", "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): + + if self._is_source: + st_object = extractor.getParentGroup() + else: + st_object = extractor.Source + + 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 + + # 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 + 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 = self._scroll_widget.layout() + for widget in reversed(self._widgets): + vbox.insertWidget(0, 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): + + 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}") + # 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() + + 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}") + + # 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)") + + 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}") + + # 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)") + + 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..557c177cd3 --- /dev/null +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -0,0 +1,179 @@ +# *************************************************************************** +# * 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 + +# 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 QtCore + +import FreeCAD + + +# Registry to handle visualization 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": 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()) + + +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, "Create {}".format(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 + + import FemGui + + return bool(FemGui.getActiveAnalysis()) + + def Activated(self): + import FreeCADGui + + 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. + + import FreeCADGui + + # 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_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 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..b6e8c939b3 --- /dev/null +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -0,0 +1,253 @@ +# *************************************************************************** +# * 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 + +import FreeCAD +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 + # 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): + super().__init__() + self._table = None + if header_names: + self._header = header_names + else: + self._header = {} + + 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): + + 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] + + return None + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + 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 + + return None + + def getTable(self): + return self._table + + +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()] + + return None + + 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) + + return None + + def getTable(self): + return self._table + + +class VtkTableView(QtGui.QWidget): + + def __init__(self, model): + 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(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( + translate("FEM", "Copy selection to clipboard ({})".format(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.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, translate("FEM", "Save as csv file"), "", "CSV (*.csv)" + ) + if not file_path: + 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() + + @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) diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py new file mode 100644 index 0000000000..9e2ad7104d --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -0,0 +1,398 @@ +# *************************************************************************** +# * 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.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 + +# 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=QT_TRANSLATE_NOOP("FEM", "The data table that stores the extracted data"), + value=vtkTable(), + ), + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Base", + doc=QT_TRANSLATE_NOOP("FEM", "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 6: + return ["XX", "YY", "ZZ", "XY", "XZ", "YZ"] + case _: + return ["Not a vector"] + + def get_representive_fieldname(self, obj): + # should return the representive field name, e.g. Position (X) + return "" + + +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=QT_TRANSLATE_NOOP("FEM", "The field to use as X data"), + value=[], + ), + _PropHelper( + 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" + ), + 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) + + 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, 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) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i, i) + + case "Position": + + orig_array = dataset.GetPoints().GetData() + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.XField) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = 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=QT_TRANSLATE_NOOP("FEM", "The field to use as Y data"), + value=[], + ), + _PropHelper( + 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" + ), + 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, 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 "Position": + + orig_array = dataset.GetPoints().GetData() + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.YField) + + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = 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/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py new file mode 100644 index 0000000000..5c7465d5bc --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -0,0 +1,183 @@ +# *************************************************************************** +# * 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 visualization base object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \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(): + if not prop.name in pl: + 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 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 (child.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 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 + + 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/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..f70c6c65c9 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -0,0 +1,213 @@ +# *************************************************************************** +# * 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 + +import FreeCAD + +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 + +from PySide.QtCore import QT_TRANSLATE_NOOP + + +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=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 + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + 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' + ) + + if not timesteps: + # get the dataset and extract the correct array + array = self._x_array_from_dataset(obj, dataset) + if array.GetNumberOfComponents() > 1: + array.SetName(obj.XField + " (" + obj.XComponent + ")") + else: + array.SetName(obj.XField) + + 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._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._x_array_component_to_table(obj, array, table) + + # set the final table + obj.Table = table + + +class PostIndexOverFrames1D(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::PropertyInteger", + name="Index", + group="X Data", + doc=QT_TRANSLATE_NOOP( + "FEM", "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 + timesteps = [] + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + + algo = obj.Source.getOutputAlgorithm() + frame_array = vtkDoubleArray() + idx = obj.Index + + 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) + + # 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)) + 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) + + # 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) + + if frame_array.GetNumberOfComponents() > 1: + frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") + else: + frame_array.SetName(f"{obj.XField} @Idx {obj.Index}") + + 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 new file mode 100644 index 0000000000..64cba2d5c7 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -0,0 +1,249 @@ +# *************************************************************************** +# * 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 + +import FreeCAD + +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 + +from PySide.QtCore import QT_TRANSLATE_NOOP + + +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=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 + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + 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' + ) + + if not timesteps: + # 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 PostIndexOverFrames2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction for two dimensional data with X always being the frames + """ + + ExtractionType = "Index" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyInteger", + name="Index", + group="Data", + doc=QT_TRANSLATE_NOOP( + "FEM", "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!) + timesteps = [] + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + + algo = obj.Source.getOutputAlgorithm() + + frame_x_array = vtkDoubleArray() + frame_y_array = vtkDoubleArray() + idx = obj.Index + + if timesteps: + setup = False + frame_x_array.SetNumberOfTuples(len(timesteps)) + frame_x_array.SetNumberOfComponents(1) + 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) + + # 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)) + 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) + + # 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) + + frame_x_array.SetName("Frames") + if frame_y_array.GetNumberOfComponents() > 1: + frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}") + else: + 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) + + # 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 new file mode 100644 index 0000000000..fb0b1343cc --- /dev/null +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -0,0 +1,105 @@ +# *************************************************************************** +# * 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 histogram" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \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 +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_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", +) + +# 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 PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for histogram. + """ + + VisualizationType = "Histogram" + + +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 new file mode 100644 index 0000000000..3216400415 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -0,0 +1,105 @@ +# *************************************************************************** +# * 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 + +# 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 + +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", +) + +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): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Lineplot" + + +class PostLineplotFieldData(post_extract2D.PostFieldData2D): + """ + A 2D Field extraction for lineplot. + """ + + VisualizationType = "Lineplot" + + +class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): + """ + A 2D index extraction for 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 new file mode 100644 index 0000000000..a12398ab9e --- /dev/null +++ b/src/Mod/Fem/femobjects/post_table.py @@ -0,0 +1,105 @@ +# *************************************************************************** +# * 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 table" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_table +# \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 +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 1D Field extraction for tables. + """ + + VisualizationType = "Table" + + +class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for table. + """ + + VisualizationType = "Table" + + +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 new file mode 100644 index 0000000000..81fa9107eb --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -0,0 +1,88 @@ +# *************************************************************************** +# * 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 + +from . import base_femtaskpanel + +translate = FreeCAD.Qt.translate + + +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() + + 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 + # ################ + + 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..9c54352f10 --- /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 + +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 + 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_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index a0658812e6..570ca63b66 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 """ @@ -56,50 +56,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): # 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_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py new file mode 100644 index 0000000000..df70e2f18d --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -0,0 +1,194 @@ +# *************************************************************************** +# * 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 + +translate = FreeCAD.Qt.translate + + +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(translate("FEM", "Show plot")) + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText(translate("FEM", "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(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" + ) + self.view_widget.setWindowTitle(translate("FEM", "Histogram view settings")) + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) + + 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/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py new file mode 100644 index 0000000000..f5598e1874 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -0,0 +1,173 @@ +# *************************************************************************** +# * 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 + +translate = FreeCAD.Qt.translate + + +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(translate("FEM", "Show plot")) + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText(translate("FEM", "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(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" + ) + self.view_widget.setWindowTitle(translate("FEM", "Lineplot view settings")) + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) + + 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/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py new file mode 100644 index 0000000000..98fd1686d6 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -0,0 +1,82 @@ +# *************************************************************************** +# * 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 + +translate = FreeCAD.Qt.translate + + +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(translate("FEM", "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(translate("FEM", "Table data")) + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.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) + + @QtCore.Slot() + def showTable(self): + self.obj.ViewObject.Proxy.show_visualization() 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] diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index dc7e6ba8ba..10ba8e2fd0 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -36,9 +36,27 @@ 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_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py new file mode 100644 index 0000000000..b2df81ef0d --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -0,0 +1,148 @@ +# *************************************************************************** +# * 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 + +from PySide import QtGui + +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 visualization + # 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 unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + 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 dumps(self): + return None + + def loads(self, state): + return None + + # 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. + raise FreeCAD.Base.FreeCADError("Not implemented") + + 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_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") 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..3abf56b29a --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -0,0 +1,124 @@ +# *************************************************************************** +# * 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 + +import FreeCAD +import FreeCADGui + + +class VPPostVisualization: + """ + A View Provider for visualization objects + """ + + def __init__(self, vobj): + vobj.Proxy = self + 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 + + def getDisplayModes(self, obj): + return ["Dialog"] + + 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() + + # open task dialog + guidoc.setEdit(vobj.Object.Name) + + # show visualization + self.show_visualization() + + return True + + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True + + 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") + + 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 new file mode 100644 index 0000000000..f6323cafc6 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -0,0 +1,603 @@ +# *************************************************************************** +# * 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 +from PySide import QtGui, QtCore +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 + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +from femtaskpanels import task_post_histogram + +from . import view_base_femobject + +_GuiPropHelper = view_base_femobject._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) + + # 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) + + # 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.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + 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(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) + 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 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 + 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 + 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_base_fempostextractors.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=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="BarColor", + group="HistogramBar", + 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=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=QT_TRANSLATE_NOOP("FEM", "The line width of the hatch)"), + value=(1, 1, 99, 1), + ), + _GuiPropHelper( + type="App::PropertyColor", + name="LineColor", + group="HistogramLine", + 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=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=QT_TRANSLATE_NOOP("FEM", "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_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): + + 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) + 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 + 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 + + def get_default_color_property(self): + return "BarColor" + + +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 + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Cumulative", + group="Histogram", + 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=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"), + 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)" + ), + value=(0.9, 0, 1, 0.05), + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="HatchLineWidth", + group="Histogram", + 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=QT_TRANSLATE_NOOP("FEM", "The number of bins the data is split into"), + value=10, + ), + _GuiPropHelper( + type="App::PropertyString", + name="Title", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + 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", + ], + ), + ] + 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 + FreeCADGui.Control.showDialog(taskd) + + return True + + def show_visualization(self): + + 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 + self.update_visualization() + + self._plot.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 update_visualization(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 + args = kwargs.copy() + for key in kwargs: + + if "color" in key: + value = np.array(kwargs[key]) * color_factor[i] + args[key] = mpl.colors.to_hex(value) + + full_args.append(args) + + 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)) + + 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 + + n, b, patches = self._plot.axes.hist(full_data, bins, **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: + 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 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 new file mode 100644 index 0000000000..f73d0aad7b --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -0,0 +1,583 @@ +# *************************************************************************** +# * 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 +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_lineplot + +from . import view_base_femobject + +_GuiPropHelper = view_base_femobject._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._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) + + # 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.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + 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) + 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 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 + 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 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_base_fempostextractors.VPPostExtractor): + """ + A View Provider for extraction of 2D 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=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="Color", + group="Lineplot", + 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=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), + value=["-", "--", "-.", ":", "None"], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="Lineplot", + 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=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=QT_TRANSLATE_NOOP("FEM", "The size the data markers are drawn in"), + value=(5, 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 EditFieldAppWidget(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 = 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) + 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 + 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 + + def get_default_color_property(self): + return "Color" + + +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 + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Grid", + group="Lineplot", + 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=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=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + 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", + ], + ), + ] + 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 + FreeCADGui.Control.showDialog(taskd) + + return True + + def show_visualization(self): + + 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 + self.update_visualization() + + self._plot.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 update_visualization(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 + plotted = False + 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): + + plotted = True + + # 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)) + + # 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: + 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 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 new file mode 100644 index 0000000000..3c22d8b999 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -0,0 +1,291 @@ +# *************************************************************************** +# * 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 + +from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +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): + 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=QT_TRANSLATE_NOOP( + "FEM", "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 = QT_TRANSLATE_NOOP("FEM", "default") + if self.ViewObject.Name: + name = self.ViewObject.Name + return (QtGui.QPixmap(), name) + + def get_default_color_property(self): + return None + + +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: + 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 + + 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) 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: