FEM: Draft architecture of post data extraction with histogram example

This commit is contained in:
Stefan Tröger
2025-03-31 20:11:09 +02:00
parent ce54149637
commit ac02a222ff
51 changed files with 4228 additions and 14 deletions

View File

@@ -49,6 +49,13 @@ Note: Can lead to a full recompute of the whole pipeline, hence best to call thi
<UserDocu>
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.
</UserDocu>
</Documentation>
</Methode>
<Methode Name="getOutputAlgorithm">
<Documentation>
<UserDocu>
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.
</UserDocu>
</Documentation>
</Methode>"

View File

@@ -38,6 +38,7 @@
#ifdef FC_USE_VTK_PYTHON
#include <vtkUnstructuredGrid.h>
#include <vtkPythonUtil.h>
#include <vtkPolyData.h>
#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;

View File

@@ -23,6 +23,13 @@ filename: str
File extension is automatically detected from data type.</UserDocu>
</Documentation>
</Methode>
<Methode Name="getDataSet">
<Documentation>
<UserDocu>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.</UserDocu>
</Documentation>
</Methode>
</PythonExport>
</GenerateModel>

View File

@@ -29,6 +29,10 @@
#include "FemPostObjectPy.h"
#include "FemPostObjectPy.cpp"
#ifdef BUILD_FEM_VTK_WRAPPER
#include <vtkDataSet.h>
#include <vtkPythonUtil.h>
#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;

View File

@@ -118,6 +118,12 @@ public:
unsigned int getFrameNumber();
std::vector<double> getFrameValues();
// output algorithm handling
vtkSmartPointer<vtkAlgorithm> getOutputAlgorithm()
{
return m_source_algorithm;
}
protected:
void onChanged(const App::Property* prop) override;
bool allowObject(App::DocumentObject* obj) override;

View File

@@ -71,5 +71,12 @@ Load a single result object or create a multiframe result by loading multiple re
<UserDocu>Change name of data arrays</UserDocu>
</Documentation>
</Methode>
<Methode Name="getOutputAlgorithm">
<Documentation>
<UserDocu>
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.
</UserDocu>
</Documentation>
</Methode>"
</PythonExport>
</GenerateModel>

View File

@@ -34,6 +34,10 @@
#include "FemPostPipelinePy.cpp"
// clang-format on
#ifdef BUILD_FEM_VTK_WRAPPER
#include <vtkPythonUtil.h>
#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;

View File

@@ -32,8 +32,11 @@
#include <vtkStructuredGrid.h>
#include <vtkUniformGrid.h>
#include <vtkUnstructuredGrid.h>
#include <vtkTable.h>
#include <vtkXMLTableWriter.h>
#include <vtkXMLDataSetWriter.h>
#include <vtkXMLMultiBlockDataWriter.h>
#include <vtkXMLTableReader.h>
#include <vtkXMLMultiBlockDataReader.h>
#include <vtkXMLImageDataReader.h>
#include <vtkXMLPolyDataReader.h>
@@ -243,6 +246,9 @@ void PropertyPostDataObject::createDataObjectByExternalType(vtkSmartPointer<vtkD
case VTK_MULTIPIECE_DATA_SET:
m_dataObject = vtkSmartPointer<vtkMultiPieceDataSet>::New();
break;
case VTK_TABLE:
m_dataObject = vtkSmartPointer<vtkTable>::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<vtkXMLMultiBlockDataWriter>::New();
xmlWriter->SetInputDataObject(m_dataObject);
xmlWriter->SetFileName(datafile.filePath().c_str());
xmlWriter->SetDataModeToBinary();
}
else if (m_dataObject->IsA("vtkTable")) {
xmlWriter = vtkSmartPointer<vtkXMLTableWriter>::New();
xmlWriter->SetInputDataObject(m_dataObject);
xmlWriter->SetFileName(fi.filePath().c_str());
}
else {
xmlWriter = vtkSmartPointer<vtkXMLDataSetWriter>::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<vtkXMLImageDataReader>::New();
}
else if (extension == "vtt") {
xmlReader = vtkSmartPointer<vtkXMLTableReader>::New();
}
else if (extension == "zip") {
// first unzip the file into a datafolder

View File

@@ -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)

View File

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

View File

@@ -86,6 +86,12 @@
<file>icons/FEM_PostFrames.svg</file>
<file>icons/FEM_PostBranchFilter.svg</file>
<file>icons/FEM_PostPipelineFromResult.svg</file>
<file>icons/FEM_PostLineplot.svg</file>
<file>icons/FEM_PostPlotline.svg</file>
<file>icons/FEM_PostHistogram.svg</file>
<file>icons/FEM_PostSpreadsheet.svg</file>
<file>icons/FEM_PostField.svg</file>
<file>icons/FEM_PostIndex.svg</file>
<file>icons/FEM_ResultShow.svg</file>
<file>icons/FEM_ResultsPurge.svg</file>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
fill="currentColor"
class="bi bi-table"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="FEM_PostField.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="34.537747"
inkscape:cx="8.8743484"
inkscape:cy="9.6850556"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<circle
style="fill:#800080;stroke:none;stroke-width:14.2999;stroke-linejoin:round"
id="path2"
cx="4.0849209"
cy="4.0125365"
r="3.9000001" />
<circle
style="fill:#800080;stroke:none;stroke-width:14.2999;stroke-linejoin:round"
id="circle2"
cx="11.960362"
cy="4.0414896"
r="3.9000001" />
<circle
style="fill:#800080;stroke:none;stroke-width:14.2999;stroke-linejoin:round"
id="circle4"
cx="4.0270133"
cy="12.003795"
r="3.9000001" />
<circle
style="fill:#800080;stroke:none;stroke-width:14.2999;stroke-linejoin:round"
id="circle5"
cx="11.902454"
cy="12.061702"
r="3.9000001" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
fill="currentColor"
class="bi bi-bar-chart"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="FEM_PostHistogram.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="97.6875"
inkscape:cx="8"
inkscape:cy="8"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M4 11H2v3h2zm5-4H7v7h2zm5-5v12h-2V2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"
id="path1"
style="fill:#800080;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
fill="currentColor"
class="bi bi-table"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="FEM_PostIndex.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="34.537747"
inkscape:cx="8.8453946"
inkscape:cy="9.6561018"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<circle
style="fill:#800080;stroke:none;stroke-width:14.2999;stroke-linejoin:round"
id="path2"
cx="8.0515957"
cy="7.9792109"
r="3.9000001" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
fill="currentColor"
class="bi bi-graph-up"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="FEM_PostLineplot.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="97.6875"
inkscape:cx="8"
inkscape:cy="8"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill-rule="evenodd"
d="M0 0h1v15h15v1H0zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07"
id="path1"
style="fill:#800080;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
fill="currentColor"
class="bi bi-graph-up"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="Fem_PostPlotline.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="69.075494"
inkscape:cx="11.458478"
inkscape:cy="9.6343864"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill-rule="evenodd"
d="m 14.817,3.113 c 0.21387,0.1750014 0.245222,0.4903105 0.07,0.704 l -4.5,5.5 C 10.199729,9.5455781 9.8561383,9.5627576 9.647,9.354 L 7.06,6.767 3.404,11.794 C 3.006988,12.298831 2.2377371,11.73903 2.596,11.206 l 4,-5.5 C 6.7765369,5.4573046 7.1365852,5.4288047 7.354,5.646 l 2.609,2.61 4.15,-5.073 c 0.175001,-0.2138702 0.49031,-0.245222 0.704,-0.07"
id="path1"
sodipodi:nodetypes="cccccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
fill="currentColor"
class="bi bi-table"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="FEM_PostSpreadsheet.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="97.6875"
inkscape:cx="8"
inkscape:cy="8"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 2h-4v3h4zm0 4h-4v3h4zm0 4h-4v3h3a1 1 0 0 0 1-1zm-5 3v-3H6v3zm-5 0v-3H1v2a1 1 0 0 0 1 1zm-4-4h4V8H1zm0-4h4V4H1zm5-3v3h4V4zm4 4H6v3h4z"
id="path1"
style="fill:#800080;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>317</width>
<height>118</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Field:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="Field">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="Component">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Frames:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="Extract">
<property name="text">
<string>One field for all frames</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PostHistogramEdit</class>
<widget class="QWidget" name="PostHistogramEdit">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>293</width>
<height>126</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
<widget class="QComboBox" name="LineStyle">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Outline draw style (None does not draw outlines)</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
<item row="2" column="3">
<widget class="QDoubleSpinBox" name="LineWidth">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Width of all lines (outline and hatch)</string>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QComboBox" name="Hatch">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Hatch pattern</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Lines:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QSpinBox" name="HatchDensity">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Density of hatch pattern</string>
</property>
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Bars:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="Gui::ColorButton" name="LineColor">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Color of all lines (bar outline and hatches)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::ColorButton" name="BarColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Color of the bars in histogram</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QLineEdit" name="Legend"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Legend:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::ColorButton</class>
<extends>QPushButton</extends>
<header>Gui/Widgets.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskPostExtraction</class>
<widget class="QWidget" name="TaskPostExtraction">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>515</width>
<height>36</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="Summary">
<property name="text">
<string>Data Summary</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="Data">
<property name="text">
<string>Show Data</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskPostGlyph</class>
<widget class="QWidget" name="TaskPostGlyph">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>343</width>
<height>498</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Glyph settings</string>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>The form of the glyph</string>
</property>
<property name="text">
<string>Bins</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="Bins">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_10">
<property name="toolTip">
<string>Which vector field is used to orient the glyphs</string>
</property>
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="Type">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Which vector field is used to orient the glyphs</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="Cumulative">
<property name="text">
<string>Cumulative</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Legend</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="LegendShow">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="LegendPos">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="Scale">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Labels</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="Title"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Y Axis</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="YLabel"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components</string>
</property>
<property name="text">
<string>X Axis</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string>A constant multiplier the glyphs are scaled with</string>
</property>
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="XLabel"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Visuals</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QDoubleSpinBox" name="BarWidth">
<property name="maximum">
<double>1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.050000000000000</double>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Hatch Line Width</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Bar width</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="HatchWidth"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -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 <vtkQtTableView.h>
#include <vtkQtTableModelAdapter.h>
#include <vtkAttributeDataToTableFilter.h>
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<Fem::FemPostObject>()) {
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)

View File

@@ -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_TaskPostDisplay> ui;
};
// ***************************************************************************
// functions
class ViewProviderFemPostFunction;

View File

@@ -0,0 +1,169 @@
/***************************************************************************
* Copyright (c) 2015 Stefan Tröger <stefantroeger@gmx.net> *
* *
* 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 <Gui/PythonWrapper.h>
#include <Gui/BitmapFactory.h>
#include <Mod/Fem/App/FemPostFilter.h>
#include <Mod/Fem/App/FemPostPipeline.h>
#include <QString>
#include <QTableView>
#include <QHeaderView>
#include <QDialog>
#include <QVBoxLayout>
#include <vtkTable.h>
#include <vtkAttributeDataToTableFilter.h>
#include <vtkSplitColumnComponents.h>
#include <vtkAbstractArray.h>
#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<QWidget*>(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"

View File

@@ -0,0 +1,67 @@
/***************************************************************************
* Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
* *
* 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 <Gui/DocumentObserver.h>
#include <Gui/TaskView/TaskDialog.h>
#include <Gui/TaskView/TaskView.h>
#include <Gui/ViewProviderDocumentObject.h>
#include <QAbstractTableModel>
#include "TaskPostBoxes.h"
#include <vtkSmartPointer.h>
#include <vtkTableAlgorithm.h>
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

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskPostExtraction</class>
<widget class="QWidget" name="TaskPostExtraction">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>375</width>
<height>302</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="Summary">
<property name="text">
<string>Data Summary</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="Data">
<property name="text">
<string>Show Data</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Data used in:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="AddBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Add data to</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="CreateBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Create and add</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="ExtractionArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="ExtractionContent">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>359</width>
<height>188</height>
</rect>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -20,5 +20,10 @@
<UserDocu>Returns the display option task panel for a post processing edit task dialog.</UserDocu>
</Documentation>
</Methode>
<Methode Name="createExtractionTaskWidget">
<Documentation>
<UserDocu>Returns the data extraction task panel for a post processing edit task dialog.</UserDocu>
</Documentation>
</Methode>
</PythonExport>
</GenerateModel>

View File

@@ -27,6 +27,7 @@
#include <Gui/PythonWrapper.h>
#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;

View File

@@ -67,6 +67,7 @@
#include <Mod/Fem/App/FemPostFilter.h>
#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)

View File

@@ -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;

View File

@@ -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]):

View File

@@ -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")

View File

@@ -0,0 +1,139 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,490 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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()

View File

@@ -0,0 +1,162 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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())

View File

@@ -0,0 +1,138 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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)

View File

@@ -0,0 +1,199 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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)

View File

@@ -0,0 +1,71 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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 []

View File

@@ -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):

View File

@@ -0,0 +1,178 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,142 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,211 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,83 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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)

View File

@@ -0,0 +1,54 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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)

View File

@@ -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
"""

View File

@@ -0,0 +1,180 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -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."""

View File

@@ -0,0 +1,91 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,129 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,476 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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

View File

@@ -0,0 +1,71 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * 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