diff --git a/src/Mod/Fem/App/FemPostFilterPy.xml b/src/Mod/Fem/App/FemPostFilterPy.xml
index 28d1823f69..3fe0e4fd88 100644
--- a/src/Mod/Fem/App/FemPostFilterPy.xml
+++ b/src/Mod/Fem/App/FemPostFilterPy.xml
@@ -49,6 +49,13 @@ Note: Can lead to a full recompute of the whole pipeline, hence best to call thi
Returns the names of all scalar fields available on this filter's input.
Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles.
+
+
+
+
+
+
+Returns the filters vtk algorithm currently used as output (the one generating the Data field). Note that the output algorithm may change depending on filter settings.
"
diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp
index dddf9048e1..097915f78e 100644
--- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp
+++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp
@@ -38,6 +38,7 @@
#ifdef FC_USE_VTK_PYTHON
#include
#include
+#include
#endif // BUILD_FEM_VTK
using namespace Fem;
@@ -129,6 +130,9 @@ PyObject* FemPostFilterPy::getInputData(PyObject* args)
case VTK_UNSTRUCTURED_GRID:
copy = vtkUnstructuredGrid::New();
break;
+ case VTK_POLY_DATA:
+ copy = vtkPolyData::New();
+ break;
default:
PyErr_SetString(PyExc_TypeError,
"cannot return datatype object; not unstructured grid");
@@ -183,6 +187,25 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args)
return Py::new_reference_to(list);
}
+PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args)
+{
+#ifdef BUILD_FEM_VTK_WRAPPER
+ // we take no arguments
+ if (!PyArg_ParseTuple(args, "")) {
+ return nullptr;
+ }
+
+ // return python object for the algorithm
+ auto algorithm = getFemPostFilterPtr()->getFilterOutput();
+ PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm);
+
+ return Py::new_reference_to(py_algorithm);
+#else
+ PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
+ Py_Return;
+#endif
+}
+
PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
diff --git a/src/Mod/Fem/App/FemPostObjectPy.xml b/src/Mod/Fem/App/FemPostObjectPy.xml
index cc4d4dacef..8f5603234b 100644
--- a/src/Mod/Fem/App/FemPostObjectPy.xml
+++ b/src/Mod/Fem/App/FemPostObjectPy.xml
@@ -23,6 +23,13 @@ filename: str
File extension is automatically detected from data type.
+
+
+ getDataset() -> vtkDataSet
+
+Returns the current output dataset. For normal filters this is equal to the objects Data property output. However, a pipelines Data property could store multiple frames, and hence Data can be of type vtkCompositeData, which is not a vtkDataset. To simplify implementations this function always returns a vtkDataSet, and for a pipeline it will be the dataset of the currently selected frame. Note that the returned value could be None, if no data is set at all.
+
+
diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp
index 81ee5119ac..27a1204bc0 100644
--- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp
+++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp
@@ -29,6 +29,10 @@
#include "FemPostObjectPy.h"
#include "FemPostObjectPy.cpp"
+#ifdef BUILD_FEM_VTK_WRAPPER
+ #include
+ #include
+#endif //BUILD_FEM_VTK
using namespace Fem;
@@ -55,6 +59,27 @@ PyObject* FemPostObjectPy::writeVTK(PyObject* args)
Py_Return;
}
+PyObject* FemPostObjectPy::getDataSet(PyObject* args)
+{
+#ifdef BUILD_FEM_VTK_WRAPPER
+ // we take no arguments
+ if (!PyArg_ParseTuple(args, "")) {
+ return nullptr;
+ }
+
+ // return python object for the dataset
+ auto dataset = getFemPostObjectPtr()->getDataSet();
+ if (dataset) {
+ PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset);
+ return Py::new_reference_to(py_algorithm);
+ }
+ return Py_None;
+#else
+ PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
+ Py_Return;
+#endif
+}
+
PyObject* FemPostObjectPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
diff --git a/src/Mod/Fem/App/FemPostPipeline.h b/src/Mod/Fem/App/FemPostPipeline.h
index 15d9149705..d590915cb4 100644
--- a/src/Mod/Fem/App/FemPostPipeline.h
+++ b/src/Mod/Fem/App/FemPostPipeline.h
@@ -118,6 +118,12 @@ public:
unsigned int getFrameNumber();
std::vector getFrameValues();
+ // output algorithm handling
+ vtkSmartPointer getOutputAlgorithm()
+ {
+ return m_source_algorithm;
+ }
+
protected:
void onChanged(const App::Property* prop) override;
bool allowObject(App::DocumentObject* obj) override;
diff --git a/src/Mod/Fem/App/FemPostPipelinePy.xml b/src/Mod/Fem/App/FemPostPipelinePy.xml
index c71981393b..ab15496be9 100644
--- a/src/Mod/Fem/App/FemPostPipelinePy.xml
+++ b/src/Mod/Fem/App/FemPostPipelinePy.xml
@@ -71,5 +71,12 @@ Load a single result object or create a multiframe result by loading multiple re
Change name of data arrays
+
+
+
+Returns the pipeline vtk algorithm, which generates the data passed to the pipelines filters. Note that the output algorithm may change depending on pipeline settings.
+
+
+ "
diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp
index be59cdefb2..3154800802 100644
--- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp
+++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp
@@ -34,6 +34,10 @@
#include "FemPostPipelinePy.cpp"
// clang-format on
+#ifdef BUILD_FEM_VTK_WRAPPER
+ #include
+#endif //BUILD_FEM_VTK
+
using namespace Fem;
@@ -313,6 +317,25 @@ PyObject* FemPostPipelinePy::renameArrays(PyObject* args)
Py_Return;
}
+PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args)
+{
+#ifdef BUILD_FEM_VTK_WRAPPER
+ // we take no arguments
+ if (!PyArg_ParseTuple(args, "")) {
+ return nullptr;
+ }
+
+ // return python object for the algorithm
+ auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm();
+ PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm);
+
+ return Py::new_reference_to(py_algorithm);
+#else
+ PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
+ Py_Return;
+#endif
+}
+
PyObject* FemPostPipelinePy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp
index 3ab5e1cbbf..83648350ad 100644
--- a/src/Mod/Fem/App/PropertyPostDataObject.cpp
+++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp
@@ -32,8 +32,11 @@
#include
#include
#include
+#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -243,6 +246,9 @@ void PropertyPostDataObject::createDataObjectByExternalType(vtkSmartPointer::New();
break;
+ case VTK_TABLE:
+ m_dataObject = vtkSmartPointer::New();
+ break;
default:
throw Base::TypeError("Unsupported VTK data type");
};
@@ -313,6 +319,9 @@ void PropertyPostDataObject::Save(Base::Writer& writer) const
case VTK_MULTIBLOCK_DATA_SET:
extension = "zip";
break;
+ case VTK_TABLE:
+ extension = ".vtt";
+ break;
default:
break;
};
@@ -382,13 +391,16 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const
xmlWriter = vtkSmartPointer::New();
xmlWriter->SetInputDataObject(m_dataObject);
xmlWriter->SetFileName(datafile.filePath().c_str());
- xmlWriter->SetDataModeToBinary();
+ }
+ else if (m_dataObject->IsA("vtkTable")) {
+ xmlWriter = vtkSmartPointer::New();
+ xmlWriter->SetInputDataObject(m_dataObject);
+ xmlWriter->SetFileName(fi.filePath().c_str());
}
else {
xmlWriter = vtkSmartPointer::New();
xmlWriter->SetInputDataObject(m_dataObject);
xmlWriter->SetFileName(fi.filePath().c_str());
- xmlWriter->SetDataModeToBinary();
#ifdef VTK_CELL_ARRAY_V2
// Looks like an invalid data object that causes a crash with vtk9
@@ -399,6 +411,7 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const
}
#endif
}
+ xmlWriter->SetDataModeToBinary();
if (xmlWriter->Write() != 1) {
// Note: Do NOT throw an exception here because if the tmp. file could
@@ -481,6 +494,9 @@ void PropertyPostDataObject::RestoreDocFile(Base::Reader& reader)
else if (extension == "vti") {
xmlReader = vtkSmartPointer::New();
}
+ else if (extension == "vtt") {
+ xmlReader = vtkSmartPointer::New();
+ }
else if (extension == "zip") {
// first unzip the file into a datafolder
diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt
index a0390b1035..e5df8521ea 100755
--- a/src/Mod/Fem/CMakeLists.txt
+++ b/src/Mod/Fem/CMakeLists.txt
@@ -36,7 +36,7 @@ SET(FemBaseModules_SRCS
coding_conventions.md
Init.py
InitGui.py
- ObjectsFem.py
+ # ObjectsFem.py
TestFemApp.py
CreateLabels.py
)
@@ -182,6 +182,8 @@ SET(FemObjects_SRCS
femobjects/base_femelement.py
femobjects/base_femmeshelement.py
femobjects/base_fempythonobject.py
+ femobjects/base_fempostextractors.py
+ femobjects/base_fempostvisualizations.py
femobjects/constant_vacuumpermittivity.py
femobjects/constraint_bodyheatsource.py
femobjects/constraint_centrif.py
@@ -217,6 +219,8 @@ if(BUILD_FEM_VTK_PYTHON)
SET(FemObjects_SRCS
${FemObjects_SRCS}
femobjects/post_glyphfilter.py
+ femobjects/post_extract1D.py
+ femobjects/post_histogram.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -597,6 +601,7 @@ SET(FemGuiTaskPanels_SRCS
femtaskpanels/__init__.py
femtaskpanels/base_femtaskpanel.py
femtaskpanels/base_femlogtaskpanel.py
+ femtaskpanels/base_fempostpanel.py
femtaskpanels/task_constraint_bodyheatsource.py
femtaskpanels/task_constraint_centrif.py
femtaskpanels/task_constraint_currentdensity.py
@@ -628,6 +633,8 @@ if(BUILD_FEM_VTK_PYTHON)
SET(FemGuiTaskPanels_SRCS
${FemGuiTaskPanels_SRCS}
femtaskpanels/task_post_glyphfilter.py
+ femtaskpanels/task_post_histogram.py
+ femtaskpanels/task_post_extractor.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -642,6 +649,10 @@ SET(FemGuiUtils_SRCS
femguiutils/migrate_gui.py
femguiutils/selection_widgets.py
femguiutils/vtk_module_handling.py
+ femguiutils/vtk_table_view.py
+ femguiutils/data_extraction.py
+ femguiutils/extract_link_view.py
+ femguiutils/post_visualization.py
)
SET(FemGuiViewProvider_SRCS
@@ -651,6 +662,7 @@ SET(FemGuiViewProvider_SRCS
femviewprovider/view_base_femmaterial.py
femviewprovider/view_base_femmeshelement.py
femviewprovider/view_base_femobject.py
+ femviewprovider/view_base_fempostvisualization.py
femviewprovider/view_constant_vacuumpermittivity.py
femviewprovider/view_constraint_bodyheatsource.py
femviewprovider/view_constraint_centrif.py
@@ -686,6 +698,8 @@ if(BUILD_FEM_VTK_PYTHON)
SET(FemGuiViewProvider_SRCS
${FemGuiViewProvider_SRCS}
femviewprovider/view_post_glyphfilter.py
+ femviewprovider/view_post_extract.py
+ femviewprovider/view_post_histogram.py
)
endif(BUILD_FEM_VTK_PYTHON)
diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt
index 7abc3b4df0..f624778753 100755
--- a/src/Mod/Fem/Gui/CMakeLists.txt
+++ b/src/Mod/Fem/Gui/CMakeLists.txt
@@ -291,6 +291,8 @@ if(BUILD_FEM_VTK)
SphereWidget.ui
TaskPostBoxes.h
TaskPostBoxes.cpp
+ TaskPostExtraction.h
+ TaskPostExtraction.cpp
TaskPostCalculator.ui
TaskPostClip.ui
TaskPostContours.ui
@@ -440,6 +442,10 @@ SET(FemGuiPythonUI_SRCS
Resources/ui/SolverCalculiX.ui
Resources/ui/SolverCcxTools.ui
Resources/ui/TaskPostGlyph.ui
+ Resources/ui/TaskPostExtraction.ui
+ Resources/ui/TaskPostHistogram.ui
+ Resources/ui/PostHistogramFieldViewEdit.ui
+ Resources/ui/PostHistogramFieldAppEdit.ui
)
ADD_CUSTOM_TARGET(FemPythonUi ALL
diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc
index 7e15fdf17e..8777f6b4dc 100755
--- a/src/Mod/Fem/Gui/Resources/Fem.qrc
+++ b/src/Mod/Fem/Gui/Resources/Fem.qrc
@@ -86,6 +86,12 @@
icons/FEM_PostFrames.svgicons/FEM_PostBranchFilter.svgicons/FEM_PostPipelineFromResult.svg
+ icons/FEM_PostLineplot.svg
+ icons/FEM_PostPlotline.svg
+ icons/FEM_PostHistogram.svg
+ icons/FEM_PostSpreadsheet.svg
+ icons/FEM_PostField.svg
+ icons/FEM_PostIndex.svgicons/FEM_ResultShow.svgicons/FEM_ResultsPurge.svg
diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg
new file mode 100644
index 0000000000..5a42219430
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg
@@ -0,0 +1,60 @@
+
+
diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg
new file mode 100644
index 0000000000..4e6d52d4a1
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg
@@ -0,0 +1,40 @@
+
+
diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg
new file mode 100644
index 0000000000..36c93c04ba
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg
@@ -0,0 +1,42 @@
+
+
diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg
new file mode 100644
index 0000000000..637dac60be
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg
@@ -0,0 +1,41 @@
+
+
diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg
new file mode 100644
index 0000000000..a788318bac
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg
@@ -0,0 +1,41 @@
+
+
diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg
new file mode 100644
index 0000000000..6220e8e87f
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg
@@ -0,0 +1,40 @@
+
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui
new file mode 100644
index 0000000000..a89c7ef39b
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui
@@ -0,0 +1,66 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 317
+ 118
+
+
+
+ Form
+
+
+
+
+
+
+
+ Field:
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ Frames:
+
+
+
+
+
+
+ One field for all frames
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui
new file mode 100644
index 0000000000..bc26238b94
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui
@@ -0,0 +1,162 @@
+
+
+ PostHistogramEdit
+
+
+
+ 0
+ 0
+ 293
+ 126
+
+
+
+ Form
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Outline draw style (None does not draw outlines)
+
+
+
+ None
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Width of all lines (outline and hatch)
+
+
+ 99.000000000000000
+
+
+ 0.100000000000000
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Hatch pattern
+
+
+
+ None
+
+
+
+
+
+
+
+ Lines:
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Density of hatch pattern
+
+
+ 1
+
+
+
+
+
+
+ Bars:
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Color of all lines (bar outline and hatches)
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Color of the bars in histogram
+
+
+
+
+
+
+
+
+
+ Legend:
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+
+
+
+
+
+
+ Gui::ColorButton
+ QPushButton
+ Gui/Widgets.h
+
+
+
+
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui
new file mode 100644
index 0000000000..8f082da23f
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui
@@ -0,0 +1,54 @@
+
+
+ TaskPostExtraction
+
+
+
+ 0
+ 0
+ 515
+ 36
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+ Data Summary
+
+
+
+
+
+
+ Show Data
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui
new file mode 100644
index 0000000000..70e2f3ecba
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui
@@ -0,0 +1,223 @@
+
+
+ TaskPostGlyph
+
+
+
+ 0
+ 0
+ 343
+ 498
+
+
+
+ Glyph settings
+
+
+ Qt::LayoutDirection::LeftToRight
+
+
+
+
+
+
+
+ The form of the glyph
+
+
+ Bins
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LayoutDirection::LeftToRight
+
+
+ 2
+
+
+ 1000
+
+
+
+
+
+
+ Which vector field is used to orient the glyphs
+
+
+ Type
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Which vector field is used to orient the glyphs
+
+
+
+ None
+
+
+
+
+
+
+
+ Cumulative
+
+
+
+
+
+
+
+
+ Legend
+
+
+
+
+
+ Qt::LayoutDirection::LeftToRight
+
+
+ Show
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 0
+
+
+
+ Labels
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+
+ Y Axis
+
+
+
+
+
+
+
+
+
+ If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components
+
+
+ X Axis
+
+
+
+
+
+
+ A constant multiplier the glyphs are scaled with
+
+
+ Title
+
+
+
+
+
+
+
+
+
+
+
+
+ Visuals
+
+
+
+
+
+ 1.000000000000000
+
+
+ 0.050000000000000
+
+
+
+
+
+
+ Hatch Line Width
+
+
+ Qt::AlignmentFlag::AlignCenter
+
+
+
+
+
+
+ Bar width
+
+
+ Qt::AlignmentFlag::AlignCenter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp
index 805977f6d2..d880f73d33 100644
--- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp
+++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp
@@ -64,7 +64,6 @@
#include "ui_TaskPostFrames.h"
#include "ui_TaskPostBranch.h"
-
#include "FemSettings.h"
#include "TaskPostBoxes.h"
#include "ViewProviderFemPostFilter.h"
@@ -72,6 +71,9 @@
#include "ViewProviderFemPostObject.h"
#include "ViewProviderFemPostBranchFilter.h"
+#include
+#include
+#include
using namespace FemGui;
using namespace Gui;
@@ -214,9 +216,14 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view,
setWindowTitle(title);
setWindowIcon(icon);
m_icon = icon;
+
+ m_connection = m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, this, boost::placeholders::_1, boost::placeholders::_2));
}
-TaskPostWidget::~TaskPostWidget() = default;
+TaskPostWidget::~TaskPostWidget()
+{
+ m_connection.disconnect();
+};
bool TaskPostWidget::autoApply()
{
@@ -256,6 +263,14 @@ void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComb
box->setCurrentIndex(index);
}
+void TaskPostWidget::handlePropertyChange(const App::DocumentObject& obj, const App::Property& prop)
+{
+ if (auto postobj = m_object.get()) {
+ if (&prop == &postobj->Data) {
+ this->onPostDataChanged(postobj);
+ }
+ }
+}
// ***************************************************************************
// simulation dialog for the TaskView
@@ -475,7 +490,6 @@ void TaskPostDisplay::onTransparencyValueChanged(int i)
void TaskPostDisplay::applyPythonCode()
{}
-
// ***************************************************************************
// functions
TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent)
diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h
index d37742dd27..9b24eb314f 100644
--- a/src/Mod/Fem/Gui/TaskPostBoxes.h
+++ b/src/Mod/Fem/Gui/TaskPostBoxes.h
@@ -42,6 +42,7 @@ class Ui_TaskPostWarpVector;
class Ui_TaskPostCut;
class Ui_TaskPostFrames;
class Ui_TaskPostBranch;
+class Ui_TaskPostExtraction;
class SoFontStyle;
class SoText2;
@@ -187,10 +188,15 @@ protected:
static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box);
+ // object update handling
+ void handlePropertyChange(const App::DocumentObject&, const App::Property&);
+ virtual void onPostDataChanged(Fem::FemPostObject*) {};
+
private:
QPixmap m_icon;
App::DocumentObjectWeakPtrT m_object;
Gui::ViewProviderWeakPtrT m_view;
+ boost::signals2::connection m_connection;
};
@@ -267,7 +273,6 @@ private:
std::unique_ptr ui;
};
-
// ***************************************************************************
// functions
class ViewProviderFemPostFunction;
diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp
new file mode 100644
index 0000000000..ef70109462
--- /dev/null
+++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp
@@ -0,0 +1,169 @@
+/***************************************************************************
+ * Copyright (c) 2015 Stefan Tröger *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Library General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2 of the License, or (at your option) any later version. *
+ * *
+ * This library is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Library General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Library General Public *
+ * License along with this library; see the file COPYING.LIB. If not, *
+ * write to the Free Software Foundation, Inc., 59 Temple Place, *
+ * Suite 330, Boston, MA 02111-1307, USA *
+ * *
+ ***************************************************************************/
+
+#include "PreCompiled.h"
+
+#ifndef _PreComp_
+
+#endif
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "ViewProviderFemPostObject.h"
+#include "TaskPostExtraction.h"
+
+using namespace FemGui;
+using namespace Gui;
+
+
+// ***************************************************************************
+// box to handle data extractions
+
+TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent)
+ : TaskPostWidget(view,
+ Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(),
+ parent)
+{
+ // we load the python implementation, and try to get the widget from it, to add
+ // directly our widget
+
+ setWindowTitle(tr("Data and extractions"));
+
+ Base::PyGILStateLocker lock;
+
+ Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true);
+ if (mod.isNull())
+ throw Base::ImportError("Unable to import data extraction widget");
+
+ try {
+ Py::Callable method(mod.getAttr(std::string("DataExtraction")));
+ Py::Tuple args(1);
+ args.setItem(0, Py::Object(view->getPyObject()));
+ m_panel = Py::Object(method.apply(args));
+ }
+ catch (Py::Exception&) {
+ Base::PyException e; // extract the Python error text
+ e.ReportException();
+ }
+
+ if (m_panel.hasAttr(std::string("widget"))) {
+ Py::Object pywidget(m_panel.getAttr(std::string("widget")));
+
+ Gui::PythonWrapper wrap;
+ if (wrap.loadCoreModule()) {
+ QObject* object = wrap.toQObject(pywidget);
+ if (object) {
+ QWidget* widget = qobject_cast(object);
+ if (widget) {
+ // finally we have the usable QWidget. Add to us!
+
+ auto layout = new QVBoxLayout();
+ layout->addWidget(widget);
+ setLayout(layout);
+ return;
+ }
+ }
+ }
+ }
+ // if we are here somethign went wrong!
+ throw Base::ImportError("Unable to import data extraction widget");
+};
+
+TaskPostExtraction::~TaskPostExtraction() {
+
+ Base::PyGILStateLocker lock;
+ try {
+ if (m_panel.hasAttr(std::string("widget"))) {
+ m_panel.setAttr(std::string("widget"), Py::None());
+ }
+ m_panel = Py::None();
+ }
+ catch (Py::AttributeError& e) {
+ e.clear();
+ }
+}
+
+void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj)
+{
+ Base::PyGILStateLocker lock;
+ try {
+ if (m_panel.hasAttr(std::string("onPostDataChanged"))) {
+ Py::Callable method(m_panel.getAttr(std::string("onPostDataChanged")));
+ Py::Tuple args(1);
+ args.setItem(0, Py::Object(obj->getPyObject()));
+ method.apply(args);
+ }
+ }
+ catch (Py::Exception&) {
+ Base::PyException e; // extract the Python error text
+ e.ReportException();
+ }
+};
+
+bool TaskPostExtraction::isGuiTaskOnly()
+{
+ Base::PyGILStateLocker lock;
+ try {
+ if (m_panel.hasAttr(std::string("isGuiTaskOnly"))) {
+ Py::Callable method(m_panel.getAttr(std::string("isGuiTaskOnly")));
+ auto result = Py::Boolean(method.apply());
+ return result.as_bool();
+ }
+ }
+ catch (Py::Exception&) {
+ Base::PyException e; // extract the Python error text
+ e.ReportException();
+ }
+
+ return false;
+};
+
+void TaskPostExtraction::apply()
+{
+ Base::PyGILStateLocker lock;
+ try {
+ if (m_panel.hasAttr(std::string("apply"))) {
+ Py::Callable method(m_panel.getAttr(std::string("apply")));
+ method.apply();
+ }
+ }
+ catch (Py::Exception&) {
+ Base::PyException e; // extract the Python error text
+ e.ReportException();
+ }
+}
+
+#include "moc_TaskPostExtraction.cpp"
diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.h b/src/Mod/Fem/Gui/TaskPostExtraction.h
new file mode 100644
index 0000000000..5423a83d00
--- /dev/null
+++ b/src/Mod/Fem/Gui/TaskPostExtraction.h
@@ -0,0 +1,67 @@
+/***************************************************************************
+ * Copyright (c) 2025 Stefan Tröger *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This library is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU Library General Public *
+ * License as published by the Free Software Foundation; either *
+ * version 2 of the License, or (at your option) any later version. *
+ * *
+ * This library is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Library General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Library General Public *
+ * License along with this library; see the file COPYING.LIB. If not, *
+ * write to the Free Software Foundation, Inc., 59 Temple Place, *
+ * Suite 330, Boston, MA 02111-1307, USA *
+ * *
+ ***************************************************************************/
+
+#ifndef GUI_TASKVIEW_TaskPostExtraction_H
+#define GUI_TASKVIEW_TaskPostExtraction_H
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "TaskPostBoxes.h"
+
+#include
+#include
+
+class Ui_TaskPostExtraction;
+
+
+namespace FemGui
+{
+
+// ***************************************************************************
+// box to handle data extractions: It is implemented in python, the c++
+// code is used to access it and manage it for the c++ task panels
+class TaskPostExtraction: public TaskPostWidget
+{
+ Q_OBJECT
+
+public:
+ explicit TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent = nullptr);
+ ~TaskPostExtraction();
+
+protected:
+ bool isGuiTaskOnly() override;
+ void apply() override;
+ void onPostDataChanged(Fem::FemPostObject* obj) override;
+
+private:
+ Py::Object m_panel;
+};
+
+
+} // namespace FemGui
+
+#endif // GUI_TASKVIEW_TaskPostExtraction_H
diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/TaskPostExtraction.ui
new file mode 100644
index 0000000000..7387ffb7de
--- /dev/null
+++ b/src/Mod/Fem/Gui/TaskPostExtraction.ui
@@ -0,0 +1,135 @@
+
+
+ TaskPostExtraction
+
+
+
+ 0
+ 0
+ 375
+ 302
+
+
+
+ Form
+
+
+
+
+
+
+
+ Data Summary
+
+
+
+
+
+
+ Show Data
+
+
+
+
+
+
+
+
+ Qt::Orientation::Vertical
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 20
+ 10
+
+
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Data used in:
+
+
+ Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft
+
+
+
+
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ Add data to
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ Create and add
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 359
+ 188
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml
index c41959e24d..9a41e8e972 100644
--- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml
+++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml
@@ -20,5 +20,10 @@
Returns the display option task panel for a post processing edit task dialog.
+
+
+ Returns the data extraction task panel for a post processing edit task dialog.
+
+
diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp
index c922d76840..7caff695eb 100644
--- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp
+++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp
@@ -27,6 +27,7 @@
#include
#include "ViewProviderFemPostFilter.h"
#include "TaskPostBoxes.h"
+#include "TaskPostExtraction.h"
// inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml)
#include "ViewProviderFemPostFilterPy.h"
#include "ViewProviderFemPostFilterPy.cpp"
@@ -60,6 +61,24 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args)
return nullptr;
}
+PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args)
+{
+ // we take no arguments
+ if (!PyArg_ParseTuple(args, "")) {
+ return nullptr;
+ }
+
+ auto panel = new TaskPostExtraction(getViewProviderFemPostObjectPtr());
+
+ Gui::PythonWrapper wrap;
+ if (wrap.loadCoreModule()) {
+ return Py::new_reference_to(wrap.fromQWidget(panel));
+ }
+
+ PyErr_SetString(PyExc_TypeError, "creating the panel failed");
+ return nullptr;
+}
+
PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp
index 0d44e6486e..bc4dd1d953 100644
--- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp
+++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp
@@ -67,6 +67,7 @@
#include
#include "TaskPostBoxes.h"
+#include "TaskPostExtraction.h"
#include "ViewProviderAnalysis.h"
#include "ViewProviderFemPostObject.h"
@@ -1019,8 +1020,11 @@ bool ViewProviderFemPostObject::setEdit(int ModNum)
void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg)
{
assert(dlg->getView() == this);
- auto panel = new TaskPostDisplay(this);
- dlg->addTaskBox(panel->windowIcon().pixmap(32), panel);
+ auto disp_panel = new TaskPostDisplay(this);
+ dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel);
+
+ auto extr_panel = new TaskPostExtraction(this);
+ dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel);
}
void ViewProviderFemPostObject::unsetEdit(int ModNum)
diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp
index 3ae3219705..acd7202acc 100644
--- a/src/Mod/Fem/Gui/Workbench.cpp
+++ b/src/Mod/Fem/Gui/Workbench.cpp
@@ -214,7 +214,11 @@ Gui::ToolBarItem* Workbench::setupToolBars() const
<< "FEM_PostFilterDataAtPoint"
<< "FEM_PostFilterCalculator"
<< "Separator"
- << "FEM_PostCreateFunctions";
+ << "FEM_PostCreateFunctions"
+#ifdef BUILD_FEM_VTK_WRAPPER
+ << "FEM_PostVisualization"
+#endif
+ ;
#endif
Gui::ToolBarItem* utils = new Gui::ToolBarItem(root);
@@ -366,7 +370,11 @@ Gui::MenuItem* Workbench::setupMenuBar() const
<< "FEM_PostFilterDataAtPoint"
<< "FEM_PostFilterCalculator"
<< "Separator"
- << "FEM_PostCreateFunctions";
+ << "FEM_PostCreateFunctions"
+#ifdef BUILD_FEM_VTK_WRAPPER
+ << "FEM_PostVisualization"
+#endif
+ ;
#endif
Gui::MenuItem* utils = new Gui::MenuItem;
diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py
index c05ecc6108..1dce3db5f7 100644
--- a/src/Mod/Fem/ObjectsFem.py
+++ b/src/Mod/Fem/ObjectsFem.py
@@ -686,6 +686,48 @@ def makePostVtkResult(doc, result_data, name="VtkResult"):
return obj
+def makePostVtkLinePlot(doc, name="Lineplot"):
+ """makePostVtkLineplot(document, [name]):
+ creates a FEM post processing line plot
+ """
+ obj = doc.addObject("App::FeaturePython", name)
+ from femobjects import post_lineplot
+
+ post_lineplot.PostLinePlot(obj)
+ if FreeCAD.GuiUp:
+ from femviewprovider import view_post_lineplot
+ view_post_lineplot.VPPostLinePlot(obj.ViewObject)
+ return
+
+
+def makePostVtkHistogramFieldData(doc, name="FieldData1D"):
+ """makePostVtkFieldData1D(document, [name]):
+ creates a FEM post processing data extractor for 1D Field data
+ """
+ obj = doc.addObject("App::FeaturePython", name)
+ from femobjects import post_histogram
+
+ post_histogram.PostHistogramFieldData(obj)
+ if FreeCAD.GuiUp:
+ from femviewprovider import view_post_histogram
+ view_post_histogram.VPPostHistogramFieldData(obj.ViewObject)
+ return obj
+
+
+def makePostVtkHistogram(doc, name="Histogram"):
+ """makePostVtkHistogram(document, [name]):
+ creates a FEM post processing histogram plot
+ """
+ obj = doc.addObject("App::FeaturePython", name)
+ from femobjects import post_histogram
+
+ post_histogram.PostHistogram(obj)
+ if FreeCAD.GuiUp:
+ from femviewprovider import view_post_histogram
+ view_post_histogram.VPPostHistogram(obj.ViewObject)
+ return obj
+
+
# ********* solver objects ***********************************************************************
def makeEquationDeformation(doc, base_solver=None, name="Deformation"):
"""makeEquationDeformation(document, [base_solver], [name]):
diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py
index 0c61dcff2b..98c620a96c 100644
--- a/src/Mod/Fem/femcommands/commands.py
+++ b/src/Mod/Fem/femcommands/commands.py
@@ -40,6 +40,7 @@ from .manager import CommandManager
from femtools.femutils import expandParentObject
from femtools.femutils import is_of_type
from femsolver.settings import get_default_solver
+from femguiutils import post_visualization
# Python command definitions:
# for C++ command definitions see src/Mod/Fem/Command.cpp
@@ -1231,7 +1232,6 @@ class _PostFilterGlyph(CommandManager):
self.is_active = "with_vtk_selresult"
self.do_activated = "add_filter_set_edit"
-
# the string in add command will be the page name on FreeCAD wiki
FreeCADGui.addCommand("FEM_Analysis", _Analysis())
FreeCADGui.addCommand("FEM_ClippingPlaneAdd", _ClippingPlaneAdd())
@@ -1289,3 +1289,8 @@ FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88())
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph())
+
+ # setup all visualization commands (register by importing)
+ import femobjects.post_histogram
+ post_visualization.setup_commands("FEM_PostVisualization")
+
diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py
new file mode 100644
index 0000000000..4eeffbcef4
--- /dev/null
+++ b/src/Mod/Fem/femguiutils/data_extraction.py
@@ -0,0 +1,139 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing ldata view and extraction widget"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package data_extraction
+# \ingroup FEM
+# \brief A widget for data extraction. Used in the PostObject task panel.
+
+from . import vtk_table_view
+
+from PySide import QtCore, QtGui
+
+from vtkmodules.vtkCommonDataModel import vtkTable
+from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter
+from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents
+
+import FreeCAD
+import FreeCADGui
+
+from femtaskpanels.base_fempostpanel import _BasePostTaskPanel
+
+from . import extract_link_view
+ExtractLinkView = extract_link_view.ExtractLinkView
+
+class DataExtraction(_BasePostTaskPanel):
+ # The class is not a widget itself, but provides a widget. It implements
+ # all required callbacks for the widget and the task dialog.
+ # Note: This object is created and used from c++! See PostTaskExtraction
+
+ def __init__(self, vobj):
+
+ super().__init__(vobj.Object)
+
+ self.ViewObject = vobj
+ self.Object = vobj.Object
+
+ self.widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostExtraction.ui"
+ )
+
+ # connect all signals as required
+ self.widget.Data.clicked.connect(self.showData)
+ self.widget.Summary.clicked.connect(self.showSummary)
+
+ # setup the data models
+ self.data_model = vtk_table_view.VtkTableModel()
+ self.summary_model = vtk_table_view.VtkTableSummaryModel()
+
+ # generate the data
+ self.onPostDataChanged(self.Object)
+
+ # setup the extraction widget
+ self._extraction_view = ExtractLinkView(self.Object, True, self)
+ self.widget.layout().addSpacing(self.widget.Data.size().height()/3)
+ self.widget.layout().addWidget(self._extraction_view)
+ self._extraction_view.repopulate()
+
+
+ @QtCore.Slot()
+ def showData(self):
+
+ dialog = QtGui.QDialog(self.widget)
+ widget = vtk_table_view.VtkTableView(self.data_model)
+ layout = QtGui.QVBoxLayout()
+ layout.addWidget(widget)
+ layout.setContentsMargins(0,0,0,0)
+
+ dialog.setLayout(layout)
+ dialog.resize(1500, 900)
+ dialog.show()
+
+ @QtCore.Slot()
+ def showSummary(self):
+
+ dialog = QtGui.QDialog(self.widget)
+ widget = vtk_table_view.VtkTableView(self.summary_model)
+ layout = QtGui.QVBoxLayout()
+ layout.addWidget(widget)
+ layout.setContentsMargins(0,0,0,0)
+
+ dialog.setLayout(layout)
+ dialog.resize(600, 900)
+ dialog.show()
+
+ def isGuiTaskOnly(self):
+ # If all panels return true it omits the Apply button in the dialog
+ return True
+
+ def onPostDataChanged(self, obj):
+
+ algo = obj.getOutputAlgorithm()
+ if not algo:
+ self.data_model.setTable(vtkTable())
+
+ filter = vtkAttributeDataToTableFilter()
+ filter.SetInputConnection(0, algo.GetOutputPort(0))
+ filter.Update()
+ table = filter.GetOutputDataObject(0)
+
+ # add the points
+ points = algo.GetOutputDataObject(0).GetPoints().GetData()
+ table.InsertColumn(points, 0)
+
+ # split the components
+ splitter = vtkSplitColumnComponents()
+ splitter.SetNamingModeToNamesWithParens()
+ splitter.SetInputData(0, table)
+
+ splitter.Update()
+ table = splitter.GetOutputDataObject(0)
+
+ self.data_model.setTable(table)
+ self.summary_model.setTable(table)
+
+ def apply(self):
+ pass
diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py
new file mode 100644
index 0000000000..60baecd9a4
--- /dev/null
+++ b/src/Mod/Fem/femguiutils/extract_link_view.py
@@ -0,0 +1,490 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing view for summarizing extractor links"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package data_extraction
+# \ingroup FEM
+# \brief A widget that shows summaries of all available links to extractors
+
+from PySide import QtGui, QtCore
+
+import femobjects.base_fempostextractors as extr
+import femobjects.base_fempostvisualizations as vis
+
+import FreeCAD
+import FreeCADGui
+
+from . import post_visualization as pv
+
+# a model showing available visualizations and possible extractions
+# #################################################################
+
+def build_new_visualization_tree_model():
+ # model that shows all options to create new visualizations
+
+ model = QtGui.QStandardItemModel()
+
+ visualizations = pv.get_registered_visualizations()
+ for vis_name in visualizations:
+ vis_icon = FreeCADGui.getIcon(visualizations[vis_name].icon)
+ vis_item = QtGui.QStandardItem(vis_icon, f"New {vis_name}")
+ vis_item.setFlags(QtGui.Qt.ItemIsEnabled)
+ vis_item.setData(visualizations[vis_name])
+
+ for ext in visualizations[vis_name].extractions:
+ icon = FreeCADGui.getIcon(ext.icon)
+ name = ext.name.removeprefix(vis_name)
+ ext_item = QtGui.QStandardItem(icon, f"with {name}")
+ ext_item.setData(ext)
+ vis_item.appendRow(ext_item)
+ model.appendRow(vis_item)
+
+ return model
+
+def build_add_to_visualization_tree_model():
+ # model that shows all possible visualization objects to add data to
+
+ visualizations = pv.get_registered_visualizations()
+ model = QtGui.QStandardItemModel()
+
+ for obj in FreeCAD.ActiveDocument.Objects:
+ if obj.isDerivedFrom("Fem::FemAnalysis"):
+ ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label)
+ ana_item.setFlags(QtGui.Qt.ItemIsEnabled)
+
+ # check all children it it is a visualization
+ for child in obj.Group:
+ if vis.is_visualization_object(child):
+
+ vis_item = QtGui.QStandardItem(child.ViewObject.Icon, child.Label)
+ vis_type = vis.get_visualization_type(child)
+ vis_item.setFlags(QtGui.Qt.ItemIsEnabled)
+ vis_item.setData(child)
+ ana_item.appendRow(vis_item)
+
+ # add extractor items
+ for ext in visualizations[vis_type].extractions:
+ icon = FreeCADGui.getIcon(ext.icon)
+ name = ext.name.removeprefix(vis_type)
+ ext_item = QtGui.QStandardItem(icon, f"Add {name}")
+ ext_item.setData(ext)
+ vis_item.appendRow(ext_item)
+
+ if ana_item.rowCount():
+ model.appendRow(ana_item)
+
+ return model
+
+def build_post_object_item(post_object, extractions, vis_type):
+
+ # definitely build a item and add the extractions
+ post_item = QtGui.QStandardItem(post_object.ViewObject.Icon, f"From {post_object.Label}")
+ post_item.setFlags(QtGui.Qt.ItemIsEnabled)
+ post_item.setData(post_object)
+
+ # add extractor items
+ for ext in extractions:
+ icon = FreeCADGui.getIcon(ext.icon)
+ name = ext.name.removeprefix(vis_type)
+ ext_item = QtGui.QStandardItem(icon, f"add {name}")
+ ext_item.setData(ext)
+ post_item.appendRow(ext_item)
+
+ # if we are a post group, we need to add the children
+ if post_object.hasExtension("Fem::FemPostGroupExtension"):
+
+ for child in post_object.Group:
+ if child.isDerivedFrom("Fem::FemPostObject"):
+ item = build_post_object_item(child, extractions, vis_type)
+ post_item.appendRow(item)
+
+ return post_item
+
+
+def build_add_from_data_tree_model(vis_type):
+ # model that shows all Post data objects from which data can be extracted
+ extractions = pv.get_registered_visualizations()[vis_type].extractions
+
+ model = QtGui.QStandardItemModel()
+ for obj in FreeCAD.ActiveDocument.Objects:
+ if obj.isDerivedFrom("Fem::FemAnalysis"):
+ ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label)
+ ana_item.setFlags(QtGui.Qt.ItemIsEnabled)
+
+ # check all children if it is a post object
+ for child in obj.Group:
+ if child.isDerivedFrom("Fem::FemPostObject"):
+ item = build_post_object_item(child, extractions, vis_type)
+ ana_item.appendRow(item)
+
+ if ana_item.rowCount():
+ model.appendRow(ana_item)
+
+ return model
+
+class TreeChoiceButton(QtGui.QToolButton):
+
+ selection = QtCore.Signal(object,object)
+
+ def __init__(self, model):
+ super().__init__()
+
+ self.model = model
+ self.setEnabled(bool(model.rowCount()))
+
+ self.__skip_next_hide = False
+
+ self.tree_view = QtGui.QTreeView(self)
+ self.tree_view.setModel(model)
+
+ self.tree_view.setFrameShape(QtGui.QFrame.NoFrame)
+ self.tree_view.setHeaderHidden(True)
+ self.tree_view.setEditTriggers(QtGui.QTreeView.EditTriggers.NoEditTriggers)
+ self.tree_view.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows)
+ self.tree_view.expandAll()
+ self.tree_view.activated.connect(self.selectIndex)
+
+ # set a complex menu
+ self.popup = QtGui.QWidgetAction(self)
+ self.popup.setDefaultWidget(self.tree_view)
+ self.setPopupMode(QtGui.QToolButton.InstantPopup)
+ self.addAction(self.popup);
+
+ QtCore.Slot(QtCore.QModelIndex)
+ def selectIndex(self, index):
+ item = self.model.itemFromIndex(index)
+
+ if item and not item.hasChildren():
+ extraction = item.data()
+ parent = item.parent().data()
+ self.selection.emit(parent, extraction)
+ self.popup.trigger()
+
+ def setModel(self, model):
+ self.model = model
+ self.tree_view.setModel(model)
+ self.tree_view.expandAll()
+
+ # check if we should be disabled
+ self.setEnabled(bool(model.rowCount()))
+
+
+# implementationof GUI and its functionality
+# ##########################################
+
+class _ShowVisualization:
+ def __init__(self, st_object):
+ self._st_object = st_object
+
+ def __call__(self):
+ if vis.is_visualization_object(self._st_object):
+ # show the visualization
+ self._st_object.ViewObject.Proxy.show_visualization()
+ else:
+ # for now just select the thing
+ FreeCADGui.Selection.clearSelection()
+ FreeCADGui.Selection.addSelection(self._st_object)
+
+class _ShowEditDialog:
+ def __init__(self, extractor, post_dialog, widget):
+ self._extractor = extractor
+ self._post_dialog = post_dialog
+ self._widget = widget
+
+ widgets = self._extractor.ViewObject.Proxy.get_edit_widgets(self._post_dialog)
+ vbox = QtGui.QVBoxLayout()
+
+ buttonBox = QtGui.QDialogButtonBox()
+ buttonBox.setCenterButtons(True)
+ buttonBox.setStandardButtons(self._post_dialog.getStandardButtons())
+ vbox.addWidget(buttonBox)
+
+ started = False
+ for widget in widgets:
+
+ if started:
+ # add a seperator line
+ frame = QtGui.QFrame()
+ frame.setFrameShape(QtGui.QFrame.HLine);
+ vbox.addWidget(frame);
+ else:
+ started = True
+
+ vbox.addWidget(widget)
+
+ vbox.addStretch()
+
+ self.dialog = QtGui.QDialog(self._widget)
+ buttonBox.accepted.connect(self.accept)
+ buttonBox.rejected.connect(self.dialog.close)
+ buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.apply)
+ self.dialog.setLayout(vbox)
+
+
+ def accept(self):
+ # recompute and close
+ self._extractor.Document.recompute()
+ self.dialog.close()
+
+ def apply(self):
+ self._extractor.Document.recompute()
+
+ def __call__(self):
+ # create the widgets, add it to dialog
+ self.dialog.show()
+
+class _DeleteExtractor:
+ def __init__(self, extractor, widget):
+ self._extractor = extractor
+ self._widget = widget
+
+ def __call__(self):
+ # remove the document object
+ doc = self._extractor.Document
+ doc.removeObject(self._extractor.Name)
+ doc.recompute()
+
+ # remove the widget
+ self._widget.deleteLater()
+
+class ExtractLinkView(QtGui.QWidget):
+
+ def __init__(self, obj, is_source, post_dialog):
+ # initializes the view.
+ # obj: The object for which the links should be shown / summarized
+ # is_source: Bool, if the object is the data source (e.g. postobject), or the target (e.g. plots)
+
+ super().__init__()
+
+ self._object = obj
+ self._is_source = is_source
+ self._post_dialog = post_dialog
+ self._widgets = []
+
+ # build the layout:
+ self._scroll_view = QtGui.QScrollArea(self)
+ self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff)
+ self._scroll_view.setWidgetResizable(True)
+
+ hbox = QtGui.QHBoxLayout()
+ label = QtGui.QLabel("Data used in:")
+ if not self._is_source:
+ label.setText("Data used from:")
+
+ label.setAlignment(QtGui.Qt.AlignBottom)
+ hbox.addWidget(label)
+ hbox.addStretch()
+
+ if self._is_source:
+
+ self._add = TreeChoiceButton(build_add_to_visualization_tree_model())
+ self._add.setText("Add data to")
+ self._add.selection.connect(self.addExtractionToVisualization)
+ hbox.addWidget(self._add)
+
+ self._create = TreeChoiceButton(build_new_visualization_tree_model())
+ self._create.setText("New")
+ self._create.selection.connect(self.newVisualization)
+ hbox.addWidget(self._create)
+
+ else:
+ vis_type = vis.get_visualization_type(self._object)
+ self._add = TreeChoiceButton(build_add_from_data_tree_model(vis_type))
+ self._add.setText("Add data from")
+ self._add.selection.connect(self.addExtractionToPostObject)
+ hbox.addWidget(self._add)
+
+ vbox = QtGui.QVBoxLayout()
+ vbox.setContentsMargins(0,0,0,0)
+ vbox.addItem(hbox)
+ vbox.addWidget(self._scroll_view)
+
+ self.setLayout(vbox)
+
+
+
+ # add the content
+ self.repopulate()
+
+ def _build_summary_widget(self, extractor):
+
+ widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostExtractionSummaryWidget.ui"
+ )
+
+ # add the separation line
+ frame = QtGui.QFrame()
+ frame.setFrameShape(QtGui.QFrame.HLine);
+ widget.layout().addWidget(frame);
+
+ if self._is_source:
+ st_object = extractor.getParentGroup()
+ else:
+ st_object = extractor.Source
+
+ widget.RemoveButton.setIcon(QtGui.QIcon.fromTheme("delete"))
+
+ widget.STButton.setIcon(st_object.ViewObject.Icon)
+ widget.STButton.setText(st_object.Label)
+
+ widget.ExtractButton.setIcon(extractor.ViewObject.Icon)
+
+ extr_label = extr.get_extraction_dimension(extractor)
+ extr_label += " " + extr.get_extraction_type(extractor)
+ widget.ExtractButton.setText(extr_label)
+
+ # connect actions. We add functions to widget, as well as the data we need,
+ # and use those as callback. This way every widget knows which objects to use
+ widget.STButton.clicked.connect(_ShowVisualization(st_object))
+ widget.ExtractButton.clicked.connect(_ShowEditDialog(extractor, self._post_dialog, widget))
+ widget.RemoveButton.clicked.connect(_DeleteExtractor(extractor, widget))
+
+ return widget
+
+ def repopulate(self):
+ # collect all links that are available and shows them
+
+ # clear the view
+ for widget in self._widgets:
+ widget.hide()
+ widget.deleteLater()
+
+ self._widgets = []
+
+ # rebuild the widgets
+
+ if self._is_source:
+ candidates = self._object.InList
+ else:
+ candidates = self._object.OutList
+
+ # get all widgets from the candidates
+ extractors = []
+ for candidate in candidates:
+ if extr.is_extractor_object(candidate):
+ summary = self._build_summary_widget(candidate)
+ self._widgets.append(summary)
+
+ # fill the scroll area
+ vbox = QtGui.QVBoxLayout()
+ for widget in self._widgets:
+ vbox.addWidget(widget)
+
+ vbox.addStretch()
+ widget = QtGui.QWidget()
+ widget.setLayout(vbox)
+
+ self._scroll_view.setWidget(widget)
+
+ # also reset the add button model
+ if self._is_source:
+ self._add.setModel(build_add_to_visualization_tree_model())
+
+ def _find_parent_analysis(self, obj):
+ # iterate upwards, till we find a analysis
+ for parent in obj.InList:
+ if parent.isDerivedFrom("Fem::FemAnalysis"):
+ return parent
+
+ analysis = self._find_parent_analysis(parent)
+ if analysis:
+ return analysis
+
+ return None
+
+ QtCore.Slot(object, object) # visualization data, extraction data
+ def newVisualization(self, vis_data, ext_data):
+
+ doc = self._object.Document
+
+ FreeCADGui.addModule(vis_data.module)
+ FreeCADGui.addModule(ext_data.module)
+ FreeCADGui.addModule("FemGui")
+
+ # create visualization
+ FreeCADGui.doCommand(
+ f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)"
+ )
+ analysis = self._find_parent_analysis(self._object)
+ if analysis:
+ FreeCADGui.doCommand(
+ f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)"
+ )
+
+ # create extraction and add it
+ FreeCADGui.doCommand(
+ f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)"
+ )
+ FreeCADGui.doCommand(
+ f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}"
+ )
+ FreeCADGui.doCommand(
+ f"visualization.addObject(extraction)"
+ )
+
+ self._post_dialog._recompute()
+ self.repopulate()
+
+ QtCore.Slot(object, object) # visualization object, extraction data
+ def addExtractionToVisualization(self, vis_obj, ext_data):
+
+ FreeCADGui.addModule(ext_data.module)
+ FreeCADGui.addModule("FemGui")
+
+ # create extraction and add it
+ FreeCADGui.doCommand(
+ f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)"
+ )
+ FreeCADGui.doCommand(
+ f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}"
+ )
+ FreeCADGui.doCommand(
+ f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)"
+ )
+
+ self._post_dialog._recompute()
+ self.repopulate()
+
+ QtCore.Slot(object, object) # post object, extraction data
+ def addExtractionToPostObject(self, post_obj, ext_data):
+
+ FreeCADGui.addModule(ext_data.module)
+ FreeCADGui.addModule("FemGui")
+
+ # create extraction and add it
+ FreeCADGui.doCommand(
+ f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)"
+ )
+ FreeCADGui.doCommand(
+ f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}"
+ )
+ FreeCADGui.doCommand(
+ f"App.ActiveDocument.{self._object.Name}.addObject(extraction)"
+ )
+
+ self._post_dialog._recompute()
+ self.repopulate()
+
diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py
new file mode 100644
index 0000000000..d1bfc93898
--- /dev/null
+++ b/src/Mod/Fem/femguiutils/post_visualization.py
@@ -0,0 +1,162 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD visualization registry"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package post_visualization
+# \ingroup FEM
+# \brief A registry to collect visualizations for use in menus
+
+import copy
+from dataclasses import dataclass
+
+from PySide import QtGui, QtCore
+
+import FreeCAD
+import FreeCADGui
+import FemGui
+
+# Registry to handle visulization commands
+# ########################################
+
+_registry = {}
+
+@dataclass
+class _Extraction:
+
+ name: str
+ icon: str
+ dimension: str
+ extracttype: str
+ module: str
+ factory: str
+
+@dataclass
+class _Visualization:
+
+ name: str
+ icon: str
+ module: str
+ factory: str
+ extractions: list[_Extraction]
+
+# Register a visualization by type, icon and factory function
+def register_visualization(visualization_type, icon, module, factory):
+ if visualization_type in _registry:
+ raise ValueError("Visualization type already registered")
+
+ _registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, [])
+
+def register_extractor(visualization_type, extraction_type, icon, dimension, etype, module, factory):
+
+ if not visualization_type in _registry:
+ raise ValueError("visualization not registered yet")
+
+ extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory)
+ _registry[visualization_type].extractions.append(extraction)
+
+def get_registered_visualizations():
+ return copy.deepcopy(_registry)
+
+
+def _to_command_name(name):
+ return "FEM_PostVisualization" + name
+
+class _VisualizationGroupCommand:
+
+ def GetCommands(self):
+ visus = _registry.keys()
+ cmds = [_to_command_name(v) for v in visus]
+ return cmds
+
+ def GetDefaultCommand(self):
+ return 0
+
+ def GetResources(self):
+ return { 'MenuText': 'Data Visualizations', 'ToolTip': 'Different visualizations to show post processing data in'}
+
+ def IsActive(self):
+ if not FreeCAD.ActiveDocument:
+ return False
+
+ return bool(FemGui.getActiveAnalysis())
+
+
+class _VisualizationCommand:
+
+ def __init__(self, visualization_type):
+ self._visualization_type = visualization_type
+
+ def GetResources(self):
+
+ cmd = _to_command_name(self._visualization_type)
+ vis = _registry[self._visualization_type]
+ tooltip = f"Create a {self._visualization_type} post processing data visualization"
+
+ return {
+ "Pixmap": vis.icon,
+ "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, f"{self._visualization_type}"),
+ "Accel": "",
+ "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip),
+ "CmdType": "AlterDoc"
+ }
+
+ def IsActive(self):
+ # active analysis available
+ if not FreeCAD.ActiveDocument:
+ return False
+
+ return bool(FemGui.getActiveAnalysis())
+
+ def Activated(self):
+
+ vis = _registry[self._visualization_type]
+ FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}")
+
+ FreeCADGui.addModule(vis.module)
+ FreeCADGui.addModule("FemGui")
+
+ FreeCADGui.doCommand(
+ f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)"
+ )
+ FreeCADGui.doCommand(
+ f"FemGui.getActiveAnalysis().addObject(obj)"
+ )
+
+ FreeCADGui.Selection.clearSelection()
+ FreeCADGui.doCommand(
+ "FreeCADGui.ActiveDocument.setEdit(obj)"
+ )
+
+def setup_commands(toplevel_name):
+ # creates all visualization commands and registers them. The
+ # toplevel group command will have the name provided to this function.
+
+ # first all visualization and extraction commands
+ for vis in _registry:
+ FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis))
+
+ # build the group command!
+ FreeCADGui.addCommand("FEM_PostVisualization", _VisualizationGroupCommand())
diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py
new file mode 100644
index 0000000000..df06c51ee0
--- /dev/null
+++ b/src/Mod/Fem/femguiutils/vtk_table_view.py
@@ -0,0 +1,138 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD table view widget to visualize vtkTable"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package vtk_table_view
+# \ingroup FEM
+# \brief A Qt widget to show a vtkTable
+
+from PySide import QtGui
+from PySide import QtCore
+
+class VtkTableModel(QtCore.QAbstractTableModel):
+ # Simple table model. Only supports single component columns
+
+ def __init__(self):
+ super().__init__()
+ self._table = None
+
+ def setTable(self, table):
+ self.beginResetModel()
+ self._table = table
+ self.endResetModel()
+
+ def rowCount(self, index):
+
+ if not self._table:
+ return 0
+
+ return self._table.GetNumberOfRows()
+
+ def columnCount(self, index):
+
+ if not self._table:
+ return 0
+
+ return self._table.GetNumberOfColumns()
+
+ def data(self, index, role):
+
+ if not self._table:
+ return None
+
+ if role == QtCore.Qt.DisplayRole:
+ col = self._table.GetColumn(index.column())
+ return col.GetTuple(index.row())[0]
+
+ def headerData(self, section, orientation, role):
+
+ if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
+ return self._table.GetColumnName(section)
+
+ if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
+ return section
+
+class VtkTableSummaryModel(QtCore.QAbstractTableModel):
+ # Simple model showing a summary of the table.
+ # Only supports single component columns
+
+ def __init__(self):
+ super().__init__()
+ self._table = None
+
+ def setTable(self, table):
+ self.beginResetModel()
+ self._table = table
+ self.endResetModel()
+
+ def rowCount(self, index):
+
+ if not self._table:
+ return 0
+
+ return self._table.GetNumberOfColumns()
+
+ def columnCount(self, index):
+ return 2 # min, max
+
+ def data(self, index, role):
+
+ if not self._table:
+ return None
+
+ if role == QtCore.Qt.DisplayRole:
+ col = self._table.GetColumn(index.row())
+ range = col.GetRange()
+ return range[index.column()]
+
+ def headerData(self, section, orientation, role):
+
+ if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
+ return ["Min","Max"][section]
+
+ if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
+ return self._table.GetColumnName(section)
+
+
+class VtkTableView(QtGui.QWidget):
+
+ def __init__(self, model):
+ super().__init__()
+
+ self.model = model
+ self.table_view = QtGui.QTableView()
+ self.table_view.setModel(model)
+
+ # fast initial resize and manual resizing still allowed!
+ header = self.table_view.horizontalHeader()
+ header.setResizeContentsPrecision(10)
+ self.table_view.resizeColumnsToContents()
+
+ layout = QtGui.QVBoxLayout()
+ layout.setContentsMargins(0,0,0,0)
+ layout.addWidget(self.table_view)
+ self.setLayout(layout)
+
diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py
new file mode 100644
index 0000000000..4ccef7018a
--- /dev/null
+++ b/src/Mod/Fem/femobjects/base_fempostextractors.py
@@ -0,0 +1,199 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package base_fempostextractors
+# \ingroup FEM
+# \brief base objects for data extractors
+
+from vtkmodules.vtkCommonDataModel import vtkTable
+
+from . import base_fempythonobject
+_PropHelper = base_fempythonobject._PropHelper
+
+# helper functions
+# ################
+
+def is_extractor_object(obj):
+ if not hasattr(obj, "Proxy"):
+ return False
+
+ return hasattr(obj.Proxy, "ExtractionType")
+
+def get_extraction_type(obj):
+ # returns the extractor type string, or throws exception if
+ # not a extractor
+ return obj.Proxy.ExtractionType
+
+def get_extraction_dimension(obj):
+ # returns the extractor dimension string, or throws exception if
+ # not a extractor
+ return obj.Proxy.ExtractionDimension
+
+
+# Base class for all extractors with common source and table handling functionality
+# Note: Never use directly, always subclass! This class does not create a
+# ExtractionType/Dimension variable, hence will not work correctly.
+class Extractor(base_fempythonobject.BaseFemPythonObject):
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ self._setup_properties(obj)
+
+ def _setup_properties(self, obj):
+ pl = obj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.name in pl:
+ prop.add_to_object(obj)
+
+ def _get_properties(self):
+
+ prop = [
+ _PropHelper(
+ type="Fem::PropertyPostDataObject",
+ name="Table",
+ group="Base",
+ doc="The data table that stores the extracted data",
+ value=vtkTable(),
+ ),
+ _PropHelper(
+ type="App::PropertyLink",
+ name="Source",
+ group="Base",
+ doc="The data source from which the data is extracted",
+ value=None,
+ ),
+ ]
+ return prop
+
+ def onDocumentRestored(self, obj):
+ self._setup_properties(obj)
+
+ def onChanged(self, obj, prop):
+
+ if prop == "Source":
+ # check if the source is a Post object
+ if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"):
+ FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject")
+ obj.Source = None
+
+
+ def get_vtk_table(self, obj):
+ if not obj.DataTable:
+ obj.DataTable = vtkTable()
+
+ return obj.DataTable
+
+ def component_options(self, num):
+
+ match num:
+ case 2:
+ return ["X", "Y"]
+ case 3:
+ return ["X", "Y", "Z"]
+ case _:
+ return ["Not a vector"]
+
+
+class Extractor1D(Extractor):
+
+ ExtractionDimension = "1D"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+
+ def _get_properties(self):
+ prop = [
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="XField",
+ group="X Data",
+ doc="The field to use as X data",
+ value=[],
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="XComponent",
+ group="X Data",
+ doc="Which part of the X field vector to use for the X axis",
+ value=[],
+ ),
+ ]
+
+ return super()._get_properties() + prop
+
+
+ def onChanged(self, obj, prop):
+
+ super().onChanged(obj, prop)
+
+ if prop == "XField" and obj.Source and obj.Source.getDataSet():
+ point_data = obj.Source.getDataSet().GetPointData()
+ self._setup_x_component_property(obj, point_data)
+
+ if prop == "Source":
+ if obj.Source:
+ dset = obj.Source.getDataSet()
+ if dset:
+ self._setup_x_properties(obj, dset)
+ else:
+ self._clear_x_properties(obj)
+ else:
+ self._clear_x_properties(obj)
+
+ def _setup_x_component_property(self, obj, point_data):
+
+ if obj.XField == "Index":
+ obj.XComponent = self.component_options(1)
+ elif obj.XField == "Position":
+ obj.XComponent = self.component_options(3)
+ else:
+ array = point_data.GetAbstractArray(obj.XField)
+ obj.XComponent = self.component_options(array.GetNumberOfComponents())
+
+ def _clear_x_properties(self, obj):
+ if hasattr(obj, "XComponent"):
+ obj.XComponent = []
+ if hasattr(obj, "XField"):
+ obj.XField = []
+
+ def _setup_x_properties(self, obj, dataset):
+ # Set all X Data properties correctly for the given dataset
+ fields = ["Index", "Position"]
+ point_data = dataset.GetPointData()
+
+ for i in range(point_data.GetNumberOfArrays()):
+ fields.append(point_data.GetArrayName(i))
+
+ current_field = obj.XField
+ obj.XField = fields
+ if current_field in fields:
+ obj.XField = current_field
+
+ self._setup_x_component_property(obj, point_data)
+
+
diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py
new file mode 100644
index 0000000000..fae9c58b6c
--- /dev/null
+++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py
@@ -0,0 +1,71 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package base_fempostextractors
+# \ingroup FEM
+# \brief base objects for data extractors
+
+from vtkmodules.vtkCommonDataModel import vtkTable
+
+from . import base_fempythonobject
+
+# helper functions
+# ################
+
+def is_visualization_object(obj):
+ if not hasattr(obj, "Proxy"):
+ return False
+
+ return hasattr(obj.Proxy, "VisualizationType")
+
+def get_visualization_type(obj):
+ # returns the extractor type string, or throws exception if
+ # not a extractor
+ return obj.Proxy.VisualizationType
+
+
+# Base class for all visualizations
+# Note: Never use directly, always subclass! This class does not create a
+# Visualization variable, hence will not work correctly.
+class PostVisualization(base_fempythonobject.BaseFemPythonObject):
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ self._setup_properties(obj)
+
+ def _setup_properties(self, obj):
+ pl = obj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.name in pl:
+ prop.add_to_object(obj)
+
+
+ def onDocumentRestored(self, obj):
+ self._setup_properties(obj)
+
+ def _get_properties(self):
+ return []
diff --git a/src/Mod/Fem/femobjects/base_fempythonobject.py b/src/Mod/Fem/femobjects/base_fempythonobject.py
index 45b8de4e7d..48003443c2 100644
--- a/src/Mod/Fem/femobjects/base_fempythonobject.py
+++ b/src/Mod/Fem/femobjects/base_fempythonobject.py
@@ -54,6 +54,7 @@ class _PropHelper:
Helper class to manage property data inside proxy objects.
Initialization keywords are the same used with PropertyContainer
to add dynamics properties plus "value" for the initial value.
+ Note: Is used as base for a GUI version, be aware when refactoring
"""
def __init__(self, **kwds):
diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py
new file mode 100644
index 0000000000..5a9404e149
--- /dev/null
+++ b/src/Mod/Fem/femobjects/post_extract1D.py
@@ -0,0 +1,178 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD post line plot"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package post_histogram
+# \ingroup FEM
+# \brief Post processing plot displaying lines
+
+from . import base_fempostextractors
+from . import base_fempythonobject
+_PropHelper = base_fempythonobject._PropHelper
+
+from vtkmodules.vtkCommonCore import vtkDoubleArray
+from vtkmodules.vtkCommonCore import vtkIntArray
+from vtkmodules.vtkCommonDataModel import vtkTable
+from vtkmodules.vtkCommonDataModel import vtkDataObject
+from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline
+
+class PostFieldData1D(base_fempostextractors.Extractor1D):
+ """
+ A post processing extraction of one dimensional field data
+ """
+
+ ExtractionType = "Field"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ def _get_properties(self):
+ prop =[ _PropHelper(
+ type="App::PropertyBool",
+ name="ExtractFrames",
+ group="Multiframe",
+ doc="Specify if the field shall be extracted for every available frame",
+ value=False,
+ ),
+ ]
+ return super()._get_properties() + prop
+
+ def __array_to_table(self, obj, array, table):
+ if array.GetNumberOfComponents() == 1:
+ table.AddColumn(array)
+ else:
+ component_array = vtkDoubleArray();
+ component_array.SetNumberOfComponents(1)
+ component_array.SetNumberOfTuples(array.GetNumberOfTuples())
+ c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent)
+ component_array.CopyComponent(0, array, c_idx)
+ component_array.SetName(array.GetName())
+ table.AddColumn(component_array)
+
+ def __array_from_dataset(self, obj, dataset):
+ # extracts the relevant array from the dataset and returns a copy
+
+ match obj.XField:
+ case "Index":
+ num = dataset.GetPoints().GetNumberOfPoints()
+ array = vtkIntArray()
+ array.SetNumberOfTuples(num)
+ array.SetNumberOfComponents(1)
+ for i in range(num):
+ array.SetValue(i,i)
+
+ case "Position":
+ orig_array = dataset.GetPoints().GetData()
+ array = vtkDoubleArray()
+ array.DeepCopy(orig_array)
+
+ case _:
+ point_data = dataset.GetPointData()
+ orig_array = point_data.GetAbstractArray(obj.XField)
+ array = vtkDoubleArray()
+ array.DeepCopy(orig_array)
+
+ return array
+
+
+ def execute(self, obj):
+
+ # on execution we populate the vtk table
+ table = vtkTable()
+
+ if not obj.Source:
+ obj.Table = table
+ return
+
+ dataset = obj.Source.getDataSet()
+ if not dataset:
+ obj.Table = table
+ return
+
+ frames = False
+ if obj.ExtractFrames:
+ # check if we have timesteps
+ info = obj.Source.getOutputAlgorithm().GetOutputInformation(0)
+ if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()):
+ timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS())
+ frames = True
+ else:
+ FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property")
+
+ if not frames:
+ # get the dataset and extract the correct array
+ array = self.__array_from_dataset(obj, dataset)
+ if array.GetNumberOfComponents() > 1:
+ array.SetName(obj.XField + " (" + obj.XComponent + ")")
+ else:
+ array.SetName(obj.XField)
+
+ self.__array_to_table(obj, array, table)
+
+ else:
+ algo = obj.Source.getOutputAlgorithm()
+ for timestep in timesteps:
+ algo.UpdateTimeStep(timestep)
+ dataset = algo.GetOutputDataObject(0)
+ array = self.__array_from_dataset(obj, dataset)
+
+ if array.GetNumberOfComponents() > 1:
+ array.SetName(f"{obj.XField} ({obj.XComponent}) - {timestep}")
+ else:
+ array.SetName(f"{obj.XField} - {timestep}")
+ self.__array_to_table(obj, array, table)
+
+ # set the final table
+ obj.Table = table
+
+
+class PostIndexData1D(base_fempostextractors.Extractor1D):
+ """
+ A post processing extraction of one dimensional index data
+ """
+
+ ExtractionType = "Index"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ def _get_properties(self):
+ prop =[ _PropHelper(
+ type="App::PropertyBool",
+ name="ExtractFrames",
+ group="Multiframe",
+ doc="Specify if the data at the index should be extracted for each frame",
+ value=False,
+ ),
+ _PropHelper(
+ type="App::PropertyInteger",
+ name="XIndex",
+ group="X Data",
+ doc="Specify for which point index the data should be extracted",
+ value=0,
+ ),
+ ]
+ return super()._get_properties() + prop
diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py
new file mode 100644
index 0000000000..df238c6e08
--- /dev/null
+++ b/src/Mod/Fem/femobjects/post_histogram.py
@@ -0,0 +1,142 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD post line plot"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package post_histogram
+# \ingroup FEM
+# \brief Post processing plot displaying lines
+
+from . import base_fempythonobject
+_PropHelper = base_fempythonobject._PropHelper
+
+from . import base_fempostextractors
+from . import base_fempostvisualizations
+from . import post_extract1D
+
+from vtkmodules.vtkCommonCore import vtkDoubleArray
+from vtkmodules.vtkCommonDataModel import vtkTable
+
+
+from femguiutils import post_visualization
+
+# register visualization and extractors
+post_visualization.register_visualization("Histogram",
+ ":/icons/FEM_PostHistogram.svg",
+ "ObjectsFem",
+ "makePostVtkHistogram")
+
+post_visualization.register_extractor("Histogram",
+ "HistogramFieldData",
+ ":/icons/FEM_PostField.svg",
+ "1D",
+ "Field",
+ "ObjectsFem",
+ "makePostVtkHistogramFieldData")
+
+
+# Implementation
+# ##############
+
+def is_histogram_extractor(obj):
+
+ if not base_fempostextractors.is_extractor_object(obj):
+ return False
+
+ if not hasattr(obj.Proxy, "VisualizationType"):
+ return False
+
+ return obj.Proxy.VisualizationType == "Histogram"
+
+
+class PostHistogramFieldData(post_extract1D.PostFieldData1D):
+ """
+ A 1D Field extraction for histograms.
+ """
+ VisualizationType = "Histogram"
+
+
+
+
+class PostHistogram(base_fempostvisualizations.PostVisualization):
+ """
+ A post processing plot for showing extracted data as histograms
+ """
+
+ VisualizationType = "Histogram"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ obj.addExtension("App::GroupExtensionPython")
+
+ def _get_properties(self):
+ prop = [
+ _PropHelper(
+ type="Fem::PropertyPostDataObject",
+ name="Table",
+ group="Base",
+ doc="The data table that stores the plotted data, one column per histogram",
+ value=vtkTable(),
+ ),
+ ]
+ return super()._get_properties() + prop
+
+
+ def onChanged(self, obj, prop):
+
+ if prop == "Group":
+ # check if all objects are allowed
+
+ children = obj.Group
+ for child in obj.Group:
+ if not is_histogram_extractor(child):
+ FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added")
+ children.remove(child)
+
+ if len(obj.Group) != len(children):
+ obj.Group = children
+
+ def execute(self, obj):
+
+ # during execution we collect all child data into our table
+ table = vtkTable()
+ for child in obj.Group:
+
+ c_table = child.Table
+ for i in range(c_table.GetNumberOfColumns()):
+ c_array = c_table.GetColumn(i)
+ # TODO: check which array type it is and use that one
+ array = vtkDoubleArray()
+ array.DeepCopy(c_array)
+ array.SetName(f"{child.Source.Label}: {c_array.GetName()}")
+ table.AddColumn(array)
+
+ obj.Table = table
+ return False
+
+
+
+
+
diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py
new file mode 100644
index 0000000000..f8798fbc23
--- /dev/null
+++ b/src/Mod/Fem/femobjects/post_lineplot.py
@@ -0,0 +1,211 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD post line plot"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package post_lineplot
+# \ingroup FEM
+# \brief Post processing plot displaying lines
+
+from . import base_fempythonobject
+_PropHelper = base_fempythonobject._PropHelper
+
+# helper function to extract plot object type
+def _get_extraction_subtype(obj):
+ if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"):
+ return obj.Proxy.Type
+
+ return "unknown"
+
+
+class PostLinePlot(base_fempythonobject.BaseFemPythonObject):
+ """
+ A post processing extraction for plotting lines
+ """
+
+ Type = "App::FeaturePython"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ obj.addExtension("App::GroupExtension")
+ self._setup_properties(obj)
+
+ def _setup_properties(self, obj):
+
+ self.ExtractionType = "LinePlot"
+
+ pl = obj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.Name in pl:
+ prop.add_to_object(obj)
+
+ def _get_properties(self):
+ prop = []
+ return prop
+
+ def onDocumentRestored(self, obj):
+ self._setup_properties(self, obj):
+
+ def onChanged(self, obj, prop):
+
+ if prop == "Group":
+ # check if all objects are allowed
+
+ children = obj.Group
+ for child in obj.Group:
+ if _get_extraction_subtype(child) not in ["Line"]:
+ children.remove(child)
+
+ if len(obj.Group) != len(children):
+ obj.Group = children
+
+
+class PostPlotLine(base_fempythonobject.BaseFemPythonObject):
+
+ Type = "App::FeaturePython"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ self._setup_properties(obj)
+
+ def _setup_properties(self, obj):
+
+ self.ExtractionType = "Line"
+
+ pl = obj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.Name in pl:
+ prop.add_to_object(obj)
+
+ def _get_properties(self):
+
+ prop = [
+ _PropHelper(
+ type="App::PropertyLink",
+ name="Source",
+ group="Line",
+ doc="The data source, the line uses",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="XField",
+ group="X Data",
+ doc="The field to use as X data",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="XComponent",
+ group="X Data",
+ doc="Which part of the X field vector to use for the X axis",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="YField",
+ group="Y Data",
+ doc="The field to use as Y data for the line plot",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="YComponent",
+ group="Y Data",
+ doc="Which part of the Y field vector to use for the X axis",
+ value=None,
+ ),
+ ]
+ return prop
+
+ def onDocumentRestored(self, obj):
+ self._setup_properties(self, obj):
+
+ def onChanged(self, obj, prop):
+
+ if prop == "Source":
+ # check if the source is a Post object
+ if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"):
+ FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject")
+ obj.XField = []
+ obj.YField = []
+ obj.Source = None
+
+ if prop == "XField":
+ if not obj.Source:
+ obj.XComponent = []
+ return
+
+ point_data = obj.Source.Data.GetPointData()
+ if not point_data.HasArray(obj.XField):
+ obj.XComponent = []
+ return
+
+ match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents:
+ case 1:
+ obj.XComponent = ["Not a vector"]
+ case 2:
+ obj.XComponent = ["Magnitude", "X", "Y"]
+ case 3:
+ obj.XComponent = ["Magnitude", "X", "Y", "Z"]
+
+ if prop == "YField":
+ if not obj.Source:
+ obj.YComponent = []
+ return
+
+ point_data = obj.Source.Data.GetPointData()
+ if not point_data.HasArray(obj.YField):
+ obj.YComponent = []
+ return
+
+ match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents:
+ case 1:
+ obj.YComponent = ["Not a vector"]
+ case 2:
+ obj.YComponent = ["Magnitude", "X", "Y"]
+ case 3:
+ obj.YComponent = ["Magnitude", "X", "Y", "Z"]
+
+ def onExecute(self, obj):
+ # we need to make sure that we show the correct fields to the user as option for data extraction
+
+ fields = []
+ if obj.Source:
+ point_data = obj.Source.Data.GetPointData()
+ fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())]
+
+ current_X = obj.XField
+ obj.XField = fields
+ if current_X in fields:
+ obj.XField = current_X
+
+ current_Y = obj.YField
+ obj.YField = fields
+ if current_Y in fields:
+ obj.YField = current_Y
+
+ return True
+
diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py
new file mode 100644
index 0000000000..f90af0e260
--- /dev/null
+++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py
@@ -0,0 +1,83 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD task panel base for post object task panels"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package base_fempostpanel
+# \ingroup FEM
+# \brief task panel base for post objects
+
+from PySide import QtCore, QtGui
+
+import FreeCAD
+import FreeCADGui
+
+from femguiutils import selection_widgets
+from . import base_femtaskpanel
+
+
+class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel):
+ """
+ The TaskPanel for post objects, mimicing the c++ functionality
+ """
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ # get the settings group
+ self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem")
+
+ # Implement parent functions
+ # ##########################
+
+ def getStandardButtons(self):
+ return QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
+
+ def clicked(self, button):
+ # apply button hit?
+ if button == QtGui.QDialogButtonBox.Apply:
+ self.obj.Document.recompute()
+
+
+ # Helper functions
+ # ################
+
+ def _recompute(self):
+ # only recompute if the user wants automatic recompute
+ if self.__settings_grp.GetBool("PostAutoRecompute", True):
+ self.obj.Document.recompute()
+
+ def _enumPropertyToCombobox(self, obj, prop, cbox):
+ cbox.blockSignals(True)
+ cbox.clear()
+ entries = obj.getEnumerationsOfProperty(prop)
+ for entry in entries:
+ cbox.addItem(entry)
+
+ cbox.setCurrentText(getattr(obj, prop))
+ cbox.blockSignals(False)
+
+
+
diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py
new file mode 100644
index 0000000000..5a56077c3e
--- /dev/null
+++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py
@@ -0,0 +1,54 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM post extractor object task panel"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package task_post_extractor
+# \ingroup FEM
+# \brief universal task dialog for extractor objects.
+
+from PySide import QtCore, QtGui
+
+import FreeCAD
+import FreeCADGui
+
+from femguiutils import selection_widgets
+from . import base_fempostpanel
+
+
+class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel):
+ """
+ The TaskPanel for editing properties extractor objects. The actual UI is
+ provided by the viewproviders. This allows using a universal task panel
+ """
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ # form is used to display individual task panels
+ self.form = obj.ViewObject.Proxy.get_edit_widgets(self)
+
+
+
diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py
index a0658812e6..8804951067 100644
--- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py
+++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py
@@ -35,10 +35,10 @@ import FreeCAD
import FreeCADGui
from femguiutils import selection_widgets
-from . import base_femtaskpanel
+from . import base_fempostpanel
-class _TaskPanel(base_femtaskpanel._BaseTaskPanel):
+class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
"""
The TaskPanel for editing properties of glyph filter
"""
diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py
new file mode 100644
index 0000000000..593f177a94
--- /dev/null
+++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py
@@ -0,0 +1,180 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM histogram plot task panel"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package task_post_histogram
+# \ingroup FEM
+# \brief task panel for post histogram plot
+
+from PySide import QtCore, QtGui
+
+import FreeCAD
+import FreeCADGui
+
+from . import base_fempostpanel
+from femguiutils import extract_link_view as elv
+from femguiutils import vtk_table_view
+
+class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
+ """
+ The TaskPanel for editing properties of glyph filter
+ """
+
+ def __init__(self, vobj):
+ super().__init__(vobj.Object)
+
+ # data widget
+ self.data_widget = QtGui.QWidget()
+ hbox = QtGui.QHBoxLayout()
+ self.data_widget.show_plot = QtGui.QPushButton()
+ self.data_widget.show_plot.setText("Show plot")
+ hbox.addWidget(self.data_widget.show_plot)
+ self.data_widget.show_table = QtGui.QPushButton()
+ self.data_widget.show_table.setText("Show data")
+ hbox.addWidget(self.data_widget.show_table)
+ vbox = QtGui.QVBoxLayout()
+ vbox.addItem(hbox)
+ vbox.addSpacing(10)
+
+ extracts = elv.ExtractLinkView(self.obj, False, self)
+ vbox.addWidget(extracts)
+
+ self.data_widget.setLayout(vbox)
+ self.data_widget.setWindowTitle("Histogram data")
+
+
+ # histogram parameter widget
+ self.view_widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui"
+ )
+ self.view_widget.setWindowTitle("Histogram view settings")
+ self.__init_widgets()
+
+ # form made from param and selection widget
+ self.form = [self.data_widget, self.view_widget]
+
+
+ # Setup functions
+ # ###############
+
+ def __init_widgets(self):
+
+ # connect data widget
+ self.data_widget.show_plot.clicked.connect(self.showPlot)
+ self.data_widget.show_table.clicked.connect(self.showTable)
+
+ # set current values to view widget
+ viewObj = self.obj.ViewObject
+
+ self.view_widget.Bins.setValue(viewObj.Bins)
+ self._enumPropertyToCombobox(viewObj, "Type", self.view_widget.Type)
+ self.view_widget.Cumulative.setChecked(viewObj.Cumulative)
+
+ self.view_widget.Title.setText(viewObj.Title)
+ self.view_widget.XLabel.setText(viewObj.XLabel)
+ self.view_widget.YLabel.setText(viewObj.YLabel)
+
+ self.view_widget.LegendShow.setChecked(viewObj.Legend)
+ self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos)
+ self.view_widget.BarWidth.setValue(viewObj.BarWidth)
+ self.view_widget.HatchWidth.setValue(viewObj.HatchLineWidth)
+
+ # connect callbacks
+ self.view_widget.Bins.valueChanged.connect(self.binsChanged)
+ self.view_widget.Type.activated.connect(self.typeChanged)
+ self.view_widget.Cumulative.toggled.connect(self.comulativeChanged)
+
+ self.view_widget.Title.editingFinished.connect(self.titleChanged)
+ self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged)
+ self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged)
+
+ self.view_widget.LegendShow.toggled.connect(self.legendShowChanged)
+ self.view_widget.LegendPos.activated.connect(self.legendPosChanged)
+ self.view_widget.BarWidth.valueChanged.connect(self.barWidthChanged)
+ self.view_widget.HatchWidth.valueChanged.connect(self.hatchWidthChanged)
+
+
+ QtCore.Slot()
+ def showPlot(self):
+ self.obj.ViewObject.Proxy.show_visualization()
+
+ QtCore.Slot()
+ def showTable(self):
+
+ # TODO: make data model update when object is recomputed
+ data_model = vtk_table_view.VtkTableModel()
+ data_model.setTable(self.obj.Table)
+
+ dialog = QtGui.QDialog(self.data_widget)
+ widget = vtk_table_view.VtkTableView(data_model)
+ layout = QtGui.QVBoxLayout()
+ layout.addWidget(widget)
+ layout.setContentsMargins(0,0,0,0)
+
+ dialog.setLayout(layout)
+ dialog.resize(1500, 900)
+ dialog.show()
+
+
+ QtCore.Slot(int)
+ def binsChanged(self, bins):
+ self.obj.ViewObject.Bins = bins
+
+ QtCore.Slot(int)
+ def typeChanged(self, idx):
+ self.obj.ViewObject.Type = idx
+
+ QtCore.Slot(bool)
+ def comulativeChanged(self, state):
+ self.obj.ViewObject.Cumulative = state
+
+ QtCore.Slot()
+ def titleChanged(self):
+ self.obj.ViewObject.Title = self.view_widget.Title.text()
+
+ QtCore.Slot()
+ def xLabelChanged(self):
+ self.obj.ViewObject.XLabel = self.view_widget.XLabel.text()
+
+ QtCore.Slot()
+ def yLabelChanged(self):
+ self.obj.ViewObject.YLabel = self.view_widget.YLabel.text()
+
+ QtCore.Slot(int)
+ def legendPosChanged(self, idx):
+ self.obj.ViewObject.LegendLocation = idx
+
+ QtCore.Slot(bool)
+ def legendShowChanged(self, state):
+ self.obj.ViewObject.Legend = state
+
+ QtCore.Slot(float)
+ def barWidthChanged(self, value):
+ self.obj.ViewObject.BarWidth = value
+
+ QtCore.Slot(float)
+ def hatchWidthChanged(self, value):
+ self.obj.ViewObject.HatchLineWidth = value
diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py
index dc7e6ba8ba..a86c1288a2 100644
--- a/src/Mod/Fem/femviewprovider/view_base_femobject.py
+++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py
@@ -36,8 +36,25 @@ import FreeCADGui
import FemGui # needed to display the icons in TreeView
+from femobjects.base_fempythonobject import _PropHelper
+
False if FemGui.__name__ else True # flake8, dummy FemGui usage
+class _GuiPropHelper(_PropHelper):
+ """
+ Helper class to manage property data inside proxy objects.
+ Based on the App verison, but viewprovider addProperty does
+ not take keyword args, hence we use positional arguments here
+ """
+
+ def __init__(self, **kwds):
+ super().__init__(**kwds)
+
+ def add_to_object(self, obj):
+ obj.addProperty(self.info["type"], self.info["name"], self.info["group"], self.info["doc"])
+ obj.setPropertyStatus(self.name, "LockDynamic")
+ setattr(obj, self.name, self.value)
+
class VPBaseFemObject:
"""Proxy View Provider for FEM FeaturePythons base constraint."""
diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py
new file mode 100644
index 0000000000..153537d669
--- /dev/null
+++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py
@@ -0,0 +1,91 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing visualization base ViewProvider"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package view_base_fempostvisualizations
+# \ingroup FEM
+# \brief view provider for post visualization object
+
+from PySide import QtGui, QtCore
+
+import Plot
+import FreeCADGui
+
+from . import view_base_femobject
+_GuiPropHelper = view_base_femobject._GuiPropHelper
+
+class VPPostVisualization:
+ """
+ A View Provider for visualization objects
+ """
+
+ def __init__(self, vobj):
+ vobj.Proxy = self
+ self._setup_properties(vobj)
+
+ def _setup_properties(self, vobj):
+ pl = vobj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.name in pl:
+ prop.add_to_object(vobj)
+
+ def _get_properties(self):
+ return []
+
+ def attach(self, vobj):
+ self.Object = vobj.Object
+ self.ViewObject = vobj
+
+ def isShow(self):
+ return True
+
+ def doubleClicked(self,vobj):
+
+ guidoc = FreeCADGui.getDocument(vobj.Object.Document)
+
+ # check if another VP is in edit mode and close it then
+ if guidoc.getInEdit():
+ FreeCADGui.Control.closeDialog()
+ guidoc.resetEdit()
+
+ guidoc.setEdit(vobj.Object.Name)
+ return True
+
+ def show_visualization(self):
+ # shows the visualization without going into edit mode
+ # to be implemented by subclasses
+ pass
+
+ def get_kw_args(self, obj):
+ # returns a dictionary with all visualization options needed for plotting
+ # based on the view provider properties
+ return {}
+
+ def dumps(self):
+ return None
+
+ def loads(self, state):
+ return None
diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_post_extract.py
new file mode 100644
index 0000000000..c75dd4bc8b
--- /dev/null
+++ b/src/Mod/Fem/femviewprovider/view_post_extract.py
@@ -0,0 +1,129 @@
+
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package view_post_lineplot
+# \ingroup FEM
+# \brief view provider for post line plot object
+
+import FreeCAD
+import FreeCADGui
+
+import FemGui
+from PySide import QtGui
+
+import femobjects.base_fempostextractors as fpe
+from femtaskpanels import task_post_extractor
+
+class VPPostExtractor:
+ """
+ A View Provider for extraction of data
+ """
+
+ def __init__(self, vobj):
+ vobj.Proxy = self
+ self._setup_properties(vobj)
+
+ def _setup_properties(self, vobj):
+ pl = vobj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.name in pl:
+ prop.add_to_object(vobj)
+
+ def _get_properties(self):
+ return []
+
+ def attach(self, vobj):
+ self.Object = vobj.Object # used on various places, claim childreens, get icon, etc.
+ self.ViewObject = vobj
+
+ def isShow(self):
+ return True
+
+ def onChanged(self, vobj, prop):
+
+ # one of our view properties was changed. Lets inform our parent plot
+ # that this happend, as this is the one that needs to redraw
+
+ if prop == "Proxy":
+ return
+
+ group = vobj.Object.getParentGroup()
+ if not group:
+ return
+
+ if (hasattr(group.ViewObject, "Proxy") and
+ hasattr(group.ViewObject.Proxy, "childViewPropertyChanged")):
+
+ group.ViewObject.Proxy.childViewPropertyChanged(vobj, prop)
+
+
+ def setEdit(self, vobj, mode):
+
+ # build up the task panel
+ taskd = task_post_extractor._ExtractorTaskPanel(vobj.Object)
+
+ #show it
+ FreeCADGui.Control.showDialog(taskd)
+
+ return True
+
+ def doubleClicked(self, vobj):
+ guidoc = FreeCADGui.getDocument(vobj.Object.Document)
+
+ # check if another VP is in edit mode and close it then
+ if guidoc.getInEdit():
+ FreeCADGui.Control.closeDialog()
+ guidoc.resetEdit()
+
+ guidoc.setEdit(vobj.Object.Name)
+
+ return True
+
+ def get_kw_args(self):
+ # should return the plot keyword arguments that represent the properties
+ # of the object
+ return {}
+
+ def get_edit_widgets(self, post_dialog):
+ # Returns a list of widgets for editing the object/viewprovider.
+ # The widget will be part of the provided post_dialog, and
+ # should use its functionality to inform of changes.
+ raise FreeCAD.Base.FreeCADError("Not implemented")
+
+ def get_preview_widget(self, post_dialog):
+ # Returns a widget for editing the object/viewprovider.
+ # The widget will be part of the provided post_dialog, and
+ # should use its functionality to inform of changes.
+ raise FreeCAD.Base.FreeCADError("Not implemented")
+
+
+ def dumps(self):
+ return None
+
+ def loads(self, state):
+ return None
diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py
new file mode 100644
index 0000000000..5a433f17bc
--- /dev/null
+++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py
@@ -0,0 +1,476 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package view_post_lineplot
+# \ingroup FEM
+# \brief view provider for post line plot object
+
+import FreeCAD
+import FreeCADGui
+
+import Plot
+import FemGui
+from PySide import QtGui, QtCore
+
+import numpy as np
+import matplotlib as mpl
+
+from vtkmodules.numpy_interface.dataset_adapter import VTKArray
+
+from . import view_post_extract
+from . import view_base_fempostvisualization
+from femtaskpanels import task_post_histogram
+
+_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper
+
+class EditViewWidget(QtGui.QWidget):
+
+ def __init__(self, obj, post_dialog):
+ super().__init__()
+
+ self._object = obj
+ self._post_dialog = post_dialog
+
+ # load the ui and set it up
+ self.widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldViewEdit.ui"
+ )
+ layout = QtGui.QVBoxLayout()
+ layout.addWidget(self.widget)
+ self.setLayout(layout)
+
+ self.__init_widget()
+
+ def __init_widget(self):
+ vobj = self._object.ViewObject
+
+ self.widget.Legend.setText(vobj.Legend)
+ self._post_dialog._enumPropertyToCombobox(vobj, "Hatch", self.widget.Hatch)
+ self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle)
+ self.widget.LineWidth.setValue(vobj.LineWidth)
+ self.widget.HatchDensity.setValue(vobj.HatchDensity)
+ self.widget.BarColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.BarColor]))
+ self.widget.LineColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.LineColor]))
+
+ self.widget.Legend.editingFinished.connect(self.legendChanged)
+ self.widget.Hatch.activated.connect(self.hatchPatternChanged)
+ self.widget.LineStyle.activated.connect(self.lineStyleChanged)
+ self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged)
+ self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged)
+ self.widget.LineColor.changed.connect(self.lineColorChanged)
+ self.widget.BarColor.changed.connect(self.barColorChanged)
+
+ @QtCore.Slot()
+ def lineColorChanged(self):
+ color = self.widget.LineColor.property("color")
+ self._object.ViewObject.LineColor = color.getRgb()
+
+ @QtCore.Slot()
+ def barColorChanged(self):
+ color = self.widget.BarColor.property("color")
+ self._object.ViewObject.BarColor = color.getRgb()
+
+ @QtCore.Slot(float)
+ def lineWidthChanged(self, value):
+ self._object.ViewObject.LineWidth = value
+
+ @QtCore.Slot(float)
+ def hatchDensityChanged(self, value):
+ self._object.ViewObject.HatchDensity = value
+
+ @QtCore.Slot(int)
+ def hatchPatternChanged(self, index):
+ self._object.ViewObject.Hatch = index
+
+ @QtCore.Slot(int)
+ def lineStyleChanged(self, index):
+ self._object.ViewObject.LineStyle = index
+
+ @QtCore.Slot()
+ def legendChanged(self):
+ self._object.ViewObject.Legend = self.widget.Legend.text()
+
+
+class EditAppWidget(QtGui.QWidget):
+
+ def __init__(self, obj, post_dialog):
+ super().__init__()
+
+ self._object = obj
+ self._post_dialog = post_dialog
+
+ # load the ui and set it up
+ self.widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui"
+ )
+ layout = QtGui.QVBoxLayout()
+ layout.addWidget(self.widget)
+ self.setLayout(layout)
+
+ self.__init_widget()
+
+ def __init_widget(self):
+ # set the other properties
+
+ self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
+ self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
+ self.widget.Extract.setChecked(self._object.ExtractFrames)
+
+ self.widget.Field.activated.connect(self.fieldChanged)
+ self.widget.Component.activated.connect(self.componentChanged)
+ self.widget.Extract.toggled.connect(self.extractionChanged)
+
+ @QtCore.Slot(int)
+ def fieldChanged(self, index):
+ self._object.XField = index
+ self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
+ self._post_dialog._recompute()
+
+ @QtCore.Slot(int)
+ def componentChanged(self, index):
+ self._object.XComponent = index
+ self._post_dialog._recompute()
+
+ @QtCore.Slot(bool)
+ def extractionChanged(self, extract):
+ self._object.ExtractFrames = extract
+ self._post_dialog._recompute()
+
+
+class VPPostHistogramFieldData(view_post_extract.VPPostExtractor):
+ """
+ A View Provider for extraction of 1D field data specialy for histograms
+ """
+
+ def __init__(self, vobj):
+ super().__init__(vobj)
+ vobj.Proxy = self
+
+ def _get_properties(self):
+
+ prop = [
+ _GuiPropHelper(
+ type="App::PropertyString",
+ name="Legend",
+ group="HistogramPlot",
+ doc="The name used in the plots legend",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyColor",
+ name="BarColor",
+ group="HistogramBar",
+ doc="The color the data bin area is drawn with",
+ value=(0, 85, 255, 255),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="Hatch",
+ group="HistogramBar",
+ doc="The hatch pattern drawn in the bar",
+ value=['None', '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'],
+ ),
+ _GuiPropHelper(
+ type="App::PropertyIntegerConstraint",
+ name="HatchDensity",
+ group="HistogramBar",
+ doc="The line width of the hatch",
+ value=(1, 1, 99, 1),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyColor",
+ name="LineColor",
+ group="HistogramLine",
+ doc="The color the data bin area is drawn with",
+ value=(0, 85, 255, 255),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyFloatConstraint",
+ name="LineWidth",
+ group="HistogramLine",
+ doc="The width of the bar, between 0 and 1 (1 being without gaps)",
+ value=(1, 0, 99, 0.1),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="LineStyle",
+ group="HistogramLine",
+ doc="The style the line is drawn in",
+ value=['None', '-', '--', '-.', ':'],
+ ),
+ ]
+ return super()._get_properties() + prop
+
+ def attach(self, vobj):
+ self.Object = vobj.Object
+ self.ViewObject = vobj
+
+ def getIcon(self):
+ return ":/icons/FEM_PostField.svg"
+
+ def get_edit_widgets(self, post_dialog):
+ return [ EditAppWidget(self.Object, post_dialog),
+ EditViewWidget(self.Object, post_dialog)]
+
+ def get_preview_widget(self, post_dialog):
+ return QtGui.QComboBox()
+
+ def get_kw_args(self):
+ # builds kw args from the properties
+ kwargs = {}
+
+ # colors need a workaround, some error occurs with rgba tuple
+ kwargs["edgecolor"] = self.ViewObject.LineColor
+ kwargs["facecolor"] = self.ViewObject.BarColor
+ kwargs["linestyle"] = self.ViewObject.LineStyle
+ kwargs["linewidth"] = self.ViewObject.LineWidth
+ if self.ViewObject.Hatch != "None":
+ kwargs["hatch"] = self.ViewObject.Hatch*self.ViewObject.HatchDensity
+
+ return kwargs
+
+
+class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
+ """
+ A View Provider for Histogram plots
+ """
+
+ def __init__(self, vobj):
+ super().__init__(vobj)
+ vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
+
+ def _get_properties(self):
+
+ prop = [
+ _GuiPropHelper(
+ type="App::PropertyBool",
+ name="Cumulative",
+ group="Histogram",
+ doc="If be the bars shoud show the cumulative sum left to rigth",
+ value=False,
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="Type",
+ group="Histogram",
+ doc="The type of histogram plotted",
+ value=["bar","barstacked", "step", "stepfilled"],
+ ),
+ _GuiPropHelper(
+ type="App::PropertyFloatConstraint",
+ name="BarWidth",
+ group="Histogram",
+ doc="The width of the bar, between 0 and 1 (1 being without gaps)",
+ value=(0.9, 0, 1, 0.05),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyFloatConstraint",
+ name="HatchLineWidth",
+ group="Histogram",
+ doc="The line width of all drawn hatch patterns",
+ value=(1, 0, 99, 0.1),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyInteger",
+ name="Bins",
+ group="Histogram",
+ doc="The number of bins the data is split into",
+ value=10,
+ ),
+ _GuiPropHelper(
+ type="App::PropertyString",
+ name="Title",
+ group="Plot",
+ doc="The histogram plot title",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyString",
+ name="XLabel",
+ group="Plot",
+ doc="The label shown for the histogram X axis",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyString",
+ name="YLabel",
+ group="Plot",
+ doc="The label shown for the histogram Y axis",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyBool",
+ name="Legend",
+ group="Plot",
+ doc="Determines if the legend is plotted",
+ value=True,
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="LegendLocation",
+ group="Plot",
+ doc="Determines if the legend is plotted",
+ value=['best','upper right','upper left','lower left','lower right','right',
+ 'center left','center right','lower center','upper center','center'],
+ ),
+
+ ]
+ return prop
+
+ def getIcon(self):
+ return ":/icons/FEM_PostHistogram.svg"
+
+ def doubleClicked(self,vobj):
+
+ self.show_visualization()
+ super().doubleClicked(vobj)
+
+ def setEdit(self, vobj, mode):
+
+ # build up the task panel
+ taskd = task_post_histogram._TaskPanel(vobj)
+
+ #show it
+ FreeCADGui.Control.showDialog(taskd)
+
+ return True
+
+
+ def show_visualization(self):
+
+ if not hasattr(self, "_plot") or not self._plot:
+ self._plot = Plot.Plot()
+ self._dialog = QtGui.QDialog(Plot.getMainWindow())
+ box = QtGui.QVBoxLayout()
+ box.addWidget(self._plot)
+ self._dialog.setLayout(box)
+
+ self.drawPlot()
+ self._dialog.show()
+
+ def get_kw_args(self, obj):
+ view = obj.ViewObject
+ if not view or not hasattr(view, "Proxy"):
+ return {}
+ if not hasattr(view.Proxy, "get_kw_args"):
+ return {}
+ return view.Proxy.get_kw_args()
+
+ def drawPlot(self):
+
+ if not hasattr(self, "_plot") or not self._plot:
+ return
+
+ self._plot.axes.clear()
+ bins = self.ViewObject.Bins
+
+ # we do not iterate the table, but iterate the children. This makes it possible
+ # to attribute the correct styles
+ full_args = {}
+ full_data = []
+ labels = []
+ for child in self.Object.Group:
+
+ table = child.Table
+ kwargs = self.get_kw_args(child)
+
+ # iterate over the table and plot all
+ color_factor = np.linspace(1,0.5,table.GetNumberOfColumns())
+ legend_multiframe = table.GetNumberOfColumns() > 1
+ for i in range(table.GetNumberOfColumns()):
+
+ # add the kw args, with some slide change over color for multiple frames
+ for key in kwargs:
+ if not (key in full_args):
+ full_args[key] = []
+
+ if "color" in key:
+ value = np.array(kwargs[key])*color_factor[i]
+ full_args[key].append(mpl.colors.to_hex(value))
+ else:
+ full_args[key].append(kwargs[key])
+
+ data = VTKArray(table.GetColumn(i))
+ full_data.append(data)
+
+ # legend labels
+ if child.ViewObject.Legend:
+ if not legend_multiframe:
+ labels.append(child.ViewObject.Legend)
+ else:
+ postfix = table.GetColumnName(i).split("-")[-1]
+ labels.append(child.ViewObject.Legend + " - " + postfix)
+ else:
+ legend_prefix = ""
+ if len(self.Object.Group) > 1:
+ legend_prefix = child.Source.Label + ": "
+ labels.append(legend_prefix + table.GetColumnName(i))
+
+
+ full_args["hatch_linewidth"] = self.ViewObject.HatchLineWidth
+ full_args["rwidth"] = self.ViewObject.BarWidth
+ full_args["cumulative"] = self.ViewObject.Cumulative
+ full_args["histtype"] = self.ViewObject.Type
+ full_args["label"] = labels
+
+ self._plot.axes.hist(full_data, bins, **full_args)
+
+ if self.ViewObject.Title:
+ self._plot.axes.set_title(self.ViewObject.Title)
+ if self.ViewObject.XLabel:
+ self._plot.axes.set_xlabel(self.ViewObject.XLabel)
+ if self.ViewObject.YLabel:
+ self._plot.axes.set_ylabel(self.ViewObject.YLabel)
+
+ if self.ViewObject.Legend and labels:
+ self._plot.axes.legend(loc = self.ViewObject.LegendLocation)
+
+ self._plot.update()
+
+
+ def updateData(self, obj, prop):
+ # we only react if the table changed, as then know that new data is available
+ if prop == "Table":
+ self.drawPlot()
+
+
+ def onChanged(self, vobj, prop):
+
+ # for all property changes we need to redraw the plot
+ self.drawPlot()
+
+ def childViewPropertyChanged(self, vobj, prop):
+
+ # on of our extractors has a changed view property.
+ self.drawPlot()
+
+ def dumps(self):
+ return None
+
+
+ def loads(self, state):
+ return None
diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py
new file mode 100644
index 0000000000..0ce5ec8954
--- /dev/null
+++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py
@@ -0,0 +1,71 @@
+
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package view_post_lineplot
+# \ingroup FEM
+# \brief view provider for post line plot object
+
+import FreeCAD
+import FreeCADGui
+
+import FemGui
+from PySide import QtGui
+
+
+class VPPostLinePlot:
+ """
+ A View Provider for the Post LinePlot object
+ """
+
+ def __init__(self, vobj):
+ vobj.Proxy = self
+
+ def getIcon(self):
+ return ":/icons/FEM_PostLineplot.svg"
+
+ def setEdit(self, vobj, mode):
+ # make sure we see what we edit
+ vobj.show()
+
+ # build up the task panel
+ #taskd = task_post_glyphfilter._TaskPanel(vobj)
+
+ #show it
+ #FreeCADGui.Control.showDialog(taskd)
+
+ return True
+
+ def unsetEdit(self, vobj, mode):
+ FreeCADGui.Control.closeDialog()
+ return True
+
+ def dumps(self):
+ return None
+
+ def loads(self, state):
+ return None