Merge pull request #21221 from ickby/FEM_extract_data

FEM: Data extraction and visualization
This commit is contained in:
Benjamin Nauck
2025-06-23 17:33:07 +02:00
committed by GitHub
69 changed files with 7405 additions and 84 deletions

View File

@@ -382,18 +382,21 @@ FemPostDataAlongLineFilter::FemPostDataAlongLineFilter()
m_line->SetPoint2(vec2.x, vec2.y, vec2.z);
m_line->SetResolution(Resolution.getValue());
m_arclength = vtkSmartPointer<vtkAppendArcLength>::New();
m_arclength->SetInputConnection(m_line->GetOutputPort(0));
auto passthrough = vtkSmartPointer<vtkPassThrough>::New();
m_probe = vtkSmartPointer<vtkProbeFilter>::New();
m_probe->SetSourceConnection(passthrough->GetOutputPort(0));
m_probe->SetInputConnection(m_line->GetOutputPort());
m_probe->SetValidPointMaskArrayName("ValidPointArray");
m_probe->SetInputConnection(m_arclength->GetOutputPort());
m_probe->SetPassPointArrays(1);
m_probe->SetPassCellArrays(1);
m_probe->ComputeToleranceOff();
m_probe->SetTolerance(0.01);
clip.source = passthrough;
clip.algorithmStorage.push_back(m_arclength);
clip.target = m_probe;
addFilterPipeline(clip, "DataAlongLine");
@@ -488,12 +491,7 @@ void FemPostDataAlongLineFilter::GetAxisData()
return;
}
vtkDataArray* tcoords = dset->GetPointData()->GetTCoords("Texture Coordinates");
const Base::Vector3d& vec1 = Point1.getValue();
const Base::Vector3d& vec2 = Point2.getValue();
const Base::Vector3d diff = vec1 - vec2;
double Len = diff.Length();
vtkDataArray* alength = dset->GetPointData()->GetArray("arc_length");
for (vtkIdType i = 0; i < dset->GetNumberOfPoints(); ++i) {
double value = 0;
@@ -517,8 +515,7 @@ void FemPostDataAlongLineFilter::GetAxisData()
}
values.push_back(value);
double tcoord = tcoords->GetComponent(i, 0);
coords.push_back(tcoord * Len);
coords.push_back(alength->GetTuple1(i));
}
YAxisData.setValues(values);

View File

@@ -32,6 +32,7 @@
#include <vtkLineSource.h>
#include <vtkPointSource.h>
#include <vtkProbeFilter.h>
#include <vtkAppendArcLength.h>
#include <vtkSmartPointer.h>
#include <vtkTableBasedClipDataSet.h>
#include <vtkVectorNorm.h>
@@ -181,6 +182,7 @@ protected:
private:
vtkSmartPointer<vtkLineSource> m_line;
vtkSmartPointer<vtkAppendArcLength> m_arclength;
vtkSmartPointer<vtkProbeFilter> m_probe;
};

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 FC_USE_VTK_PYTHON
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
// return python object for the algorithm
auto algorithm = getFemPostFilterPtr()->getFilterOutput();
PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm);
return Py::new_reference_to(py_algorithm);
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;

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 FC_USE_VTK_PYTHON
#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 FC_USE_VTK_PYTHON
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
// return python object for the dataset
auto dataset = getFemPostObjectPtr()->getDataSet();
if (dataset) {
PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset);
return Py::new_reference_to(py_algorithm);
}
return Py_None;
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
PyObject* FemPostObjectPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;

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 FC_USE_VTK_PYTHON
#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 FC_USE_VTK_PYTHON
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
// return python object for the algorithm
auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm();
PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm);
return Py::new_reference_to(py_algorithm);
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
PyObject* FemPostPipelinePy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;

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

@@ -214,9 +214,15 @@ SET(FemObjects_SRCS
)
if(BUILD_FEM_VTK_PYTHON)
SET(FemObjects_SRCS
${FemObjects_SRCS}
list(APPEND FemObjects_SRCS
femobjects/base_fempostextractors.py
femobjects/base_fempostvisualizations.py
femobjects/post_glyphfilter.py
femobjects/post_extract1D.py
femobjects/post_extract2D.py
femobjects/post_histogram.py
femobjects/post_lineplot.py
femobjects/post_table.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -597,6 +603,7 @@ SET(FemGuiTaskPanels_SRCS
femtaskpanels/__init__.py
femtaskpanels/base_femtaskpanel.py
femtaskpanels/base_femlogtaskpanel.py
femtaskpanels/base_fempostpanel.py
femtaskpanels/task_constraint_bodyheatsource.py
femtaskpanels/task_constraint_centrif.py
femtaskpanels/task_constraint_currentdensity.py
@@ -625,9 +632,12 @@ SET(FemGuiTaskPanels_SRCS
)
if(BUILD_FEM_VTK_PYTHON)
SET(FemGuiTaskPanels_SRCS
${FemGuiTaskPanels_SRCS}
list(APPEND FemGuiTaskPanels_SRCS
femtaskpanels/task_post_glyphfilter.py
femtaskpanels/task_post_histogram.py
femtaskpanels/task_post_lineplot.py
femtaskpanels/task_post_table.py
femtaskpanels/task_post_extractor.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -641,9 +651,18 @@ SET(FemGuiUtils_SRCS
femguiutils/disambiguate_solid_selection.py
femguiutils/migrate_gui.py
femguiutils/selection_widgets.py
femguiutils/vtk_module_handling.py
)
if(BUILD_FEM_VTK_PYTHON)
list(APPEND FemGuiUtils_SRCS
femguiutils/vtk_module_handling.py
femguiutils/vtk_table_view.py
femguiutils/data_extraction.py
femguiutils/extract_link_view.py
femguiutils/post_visualization.py
)
endif(BUILD_FEM_VTK_PYTHON)
SET(FemGuiViewProvider_SRCS
femviewprovider/__init__.py
femviewprovider/view_base_femconstraint.py
@@ -683,9 +702,13 @@ SET(FemGuiViewProvider_SRCS
)
if(BUILD_FEM_VTK_PYTHON)
SET(FemGuiViewProvider_SRCS
${FemGuiViewProvider_SRCS}
list(APPEND FemGuiViewProvider_SRCS
femviewprovider/view_base_fempostextractors.py
femviewprovider/view_base_fempostvisualization.py
femviewprovider/view_post_glyphfilter.py
femviewprovider/view_post_histogram.py
femviewprovider/view_post_lineplot.py
femviewprovider/view_post_table.py
)
endif(BUILD_FEM_VTK_PYTHON)

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,16 @@ SET(FemGuiPythonUI_SRCS
Resources/ui/SolverCalculiX.ui
Resources/ui/SolverCcxTools.ui
Resources/ui/TaskPostGlyph.ui
Resources/ui/TaskPostExtraction.ui
Resources/ui/TaskPostHistogram.ui
Resources/ui/TaskPostLineplot.ui
Resources/ui/PostHistogramFieldViewEdit.ui
Resources/ui/PostHistogramFieldAppEdit.ui
Resources/ui/PostHistogramIndexAppEdit.ui
Resources/ui/PostLineplotFieldViewEdit.ui
Resources/ui/PostLineplotFieldAppEdit.ui
Resources/ui/PostLineplotIndexAppEdit.ui
Resources/ui/PostTableFieldViewEdit.ui
)
ADD_CUSTOM_TARGET(FemPythonUi ALL

View File

@@ -86,6 +86,11 @@
<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_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,102 @@
<?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.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
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.8598715"
inkscape:cy="9.6561018"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.463544;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect5"
width="10.440784"
height="2.0364563"
x="2.1338818"
y="-5.0367694"
rx="1.9162874"
ry="2.0364563"
transform="rotate(90)" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.463544;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect6"
width="10.440784"
height="2.0364563"
x="2.2207432"
y="-13.056973"
rx="1.9162874"
ry="2.0364563"
transform="rotate(90)" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.463544;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect4"
width="10.440784"
height="2.0364563"
x="2.7107689"
y="11.254348"
rx="1.9162874"
ry="2.0364563" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.463537;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect3"
width="10.440463"
height="2.0364628"
x="2.5080891"
y="2.7419074"
rx="1.9162284"
ry="2.0364628" />
<ellipse
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="path1"
cx="4.0924597"
cy="3.7761521"
rx="2.7138755"
ry="2.7138758" />
<ellipse
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="circle1"
cx="12.092448"
cy="3.7761521"
rx="2.7138755"
ry="2.7138758" />
<ellipse
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="circle2"
cx="4.0927677"
cy="12.27616"
rx="2.7138755"
ry="2.7138758" />
<ellipse
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="circle3"
cx="12.092757"
cy="12.27616"
rx="2.7138755"
ry="2.7138758" />
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,69 @@
<?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.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
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="48.84375"
inkscape:cx="9.4996801"
inkscape:cy="6.1932182"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.461535;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect2"
width="3.5384648"
height="4.3404469"
x="0.67558533"
y="9.1421566"
ry="0.68956506" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.459332;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect3"
width="3.540668"
height="8.5446272"
x="6.2196369"
y="4.9390783"
ry="0.62339282" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.459208;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect4"
width="3.5407922"
height="12.789965"
x="11.764729"
y="0.69380343"
ry="0.64040416" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;fill-opacity:1"
id="rect5"
width="14.966091"
height="1.412668"
x="0.53230965"
y="14.167626"
rx="0.12284084"
ry="0" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 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.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
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="7.7885798"
inkscape:cy="9.6561018"
inkscape:window-width="1960"
inkscape:window-height="1308"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<circle
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="path1"
cx="7.9564848"
cy="7.9564848"
r="4.2860532" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,46 @@
<?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.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
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="45.254834"
inkscape:cx="1.6793786"
inkscape:cy="9.534893"
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
id="rect1"
style="fill:#e5007e;fill-opacity:1;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 0.5234375,0.5390625 V 14.324219 15.548828 H 2.0332031 15.248047 V 14.324219 H 2.0332031 V 0.5390625 Z"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#e5007e;fill-opacity:1;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 2.6295533,11.313708 6.8942911,5.4137863 9.8994949,7.6234949 13.788583,1.8119611 15.490059,2.9831067 10.496116,10.54031 7.2920386,8.1980192 4.0879611,12.484854 Z"
id="path8"
sodipodi:nodetypes="ccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,64 @@
<?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.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
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="48.84375"
inkscape:cx="2.9277031"
inkscape:cy="8.4248241"
inkscape:window-width="3132"
inkscape:window-height="1772"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<rect
style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
id="rect2"
width="14.571755"
height="14.865558"
x="0.72790933"
y="0.64063662"
ry="2.3616853" />
<path
style="fill:#e5007e;stroke:#260013;stroke-width:0.694259;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
d="M 1.013422,5.2704734 H 15.11965"
id="path2" />
<path
style="fill:#e5007e;stroke:#260013;stroke-width:0.694244;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
d="M 0.96706864,8.6996694 H 15.033695"
id="path2-8" />
<path
style="fill:#e5007e;stroke:#260013;stroke-width:0.688579;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
d="M 5.572664,15.121805 V 5.3933605"
id="path2-8-3" />
<path
style="fill:#e5007e;stroke:#260013;stroke-width:0.690432;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
d="M 1.0023108,12.128865 H 14.992054"
id="path2-8-6" />
<path
style="fill:#e5007e;stroke:#260013;stroke-width:0.688011;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none"
d="M 10.451292,15.119885 V 5.313384"
id="path3" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,78 @@
<?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">
<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="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 each frames</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,167 @@
<?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>278</width>
<height>110</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<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="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="Expanding" 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>
</widget>
</item>
<item row="1" column="3">
<widget class="QSpinBox" name="HatchDensity">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<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>
</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>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="LineColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" 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="QToolButton" 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>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>Legend</tabstop>
<tabstop>BarColor</tabstop>
<tabstop>Hatch</tabstop>
<tabstop>HatchDensity</tabstop>
<tabstop>LineColor</tabstop>
<tabstop>LineStyle</tabstop>
<tabstop>LineWidth</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,84 @@
<?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>261</width>
<height>110</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<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="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Field:</string>
</property>
</widget>
</item>
<item row="1" 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="2" 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="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Index:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="Index">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<number>999999999</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,101 @@
<?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>271</width>
<height>174</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QFormLayout" name="formLayout">
<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 row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>X Field:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="XField">
<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="XComponent">
<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_4">
<property name="text">
<string>Y Field:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="YField">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="YComponent">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Frames:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="Extract">
<property name="text">
<string>One Y field for each frames</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,151 @@
<?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>274</width>
<height>114</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<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="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Marker:</string>
</property>
</widget>
</item>
<item row="1" 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>Hatch pattern</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1" colspan="4">
<widget class="QLineEdit" name="Legend"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Legend:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="MarkerStyle">
<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="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Line:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QToolButton" name="Color">
<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="1" column="3">
<widget class="QDoubleSpinBox" name="LineWidth">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QDoubleSpinBox" name="MarkerSize">
<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>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>Legend</tabstop>
<tabstop>Color</tabstop>
<tabstop>LineStyle</tabstop>
<tabstop>MarkerStyle</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,85 @@
<?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>310</width>
<height>108</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QFormLayout" name="formLayout">
<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 row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Y Field:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="YField">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="YComponent">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Index:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="Index">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<number>99999999</number>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>Index</tabstop>
<tabstop>YField</tabstop>
<tabstop>YComponent</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,50 @@
<?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>279</width>
<height>38</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QFormLayout" name="formLayout">
<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 row="1" column="1">
<widget class="QLineEdit" name="Name">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
</layout>
</widget>
<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,241 @@
<?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::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<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="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::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::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>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Bar width</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="HatchWidth"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>Bins</tabstop>
<tabstop>Type</tabstop>
<tabstop>Cumulative</tabstop>
<tabstop>LegendShow</tabstop>
<tabstop>LegendPos</tabstop>
<tabstop>Title</tabstop>
<tabstop>XLabel</tabstop>
<tabstop>YLabel</tabstop>
<tabstop>BarWidth</tabstop>
<tabstop>HatchWidth</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,181 @@
<?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>302</width>
<height>302</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Glyph settings</string>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<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="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>Grid</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="Grid">
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="LegendShow">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Legend</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="LegendPos">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="3" 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>Scale</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="Scale">
<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>
</layout>
</item>
<item>
<widget class="QGroupBox" name="Form">
<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>
</layout>
</widget>
<tabstops>
<tabstop>Grid</tabstop>
<tabstop>LegendShow</tabstop>
<tabstop>LegendPos</tabstop>
<tabstop>Scale</tabstop>
<tabstop>Title</tabstop>
<tabstop>XLabel</tabstop>
<tabstop>YLabel</tabstop>
</tabstops>
<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,7 +71,6 @@
#include "ViewProviderFemPostObject.h"
#include "ViewProviderFemPostBranchFilter.h"
using namespace FemGui;
using namespace Gui;
@@ -214,9 +212,18 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view,
setWindowTitle(title);
setWindowIcon(icon);
m_icon = icon;
m_connection =
m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange,
this,
boost::placeholders::_1,
boost::placeholders::_2));
}
TaskPostWidget::~TaskPostWidget() = default;
TaskPostWidget::~TaskPostWidget()
{
m_connection.disconnect();
};
bool TaskPostWidget::autoApply()
{
@@ -256,6 +263,14 @@ void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComb
box->setCurrentIndex(index);
}
void TaskPostWidget::handlePropertyChange(const App::DocumentObject& obj, const App::Property& prop)
{
if (auto postobj = m_object.get<Fem::FemPostObject>()) {
if (&prop == &postobj->Data) {
this->onPostDataChanged(postobj);
}
}
}
// ***************************************************************************
// simulation dialog for the TaskView
@@ -393,6 +408,24 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box)
}
}
void TaskDlgPost::processCollapsedWidgets()
{
for (auto& widget : Content) {
auto* task_box = dynamic_cast<Gui::TaskView::TaskBox*>(widget);
if (!task_box) {
continue;
}
// get the task widget and check if it is a post widget
auto* taskwidget = task_box->groupLayout()->itemAt(0)->widget();
auto* post_widget = dynamic_cast<TaskPostWidget*>(taskwidget);
if (!post_widget || !post_widget->initiallyCollapsed()) {
continue;
}
post_widget->setGeometry(QRect(QPoint(0, 0), post_widget->sizeHint()));
task_box->hideGroupBox();
}
}
// ***************************************************************************
// box to set the coloring
@@ -475,7 +508,6 @@ void TaskPostDisplay::onTransparencyValueChanged(int i)
void TaskPostDisplay::applyPythonCode()
{}
// ***************************************************************************
// functions
TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent)
@@ -557,6 +589,11 @@ void TaskPostFrames::applyPythonCode()
// we apply the views widgets python code
}
bool TaskPostFrames::initiallyCollapsed()
{
return (ui->FrameTable->rowCount() == 0);
}
// ***************************************************************************
// in the following, the different filters sorted alphabetically

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;
@@ -155,6 +156,12 @@ public:
// executed when the apply button is pressed in the task dialog
virtual void apply() {};
// returns if the widget shall be collapsed when opening the task dialog
virtual bool initiallyCollapsed()
{
return false;
};
protected:
App::DocumentObject* getObject() const
{
@@ -187,10 +194,15 @@ protected:
static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box);
// object update handling
void handlePropertyChange(const App::DocumentObject&, const App::Property&);
virtual void onPostDataChanged(Fem::FemPostObject*) {};
private:
QPixmap m_icon;
App::DocumentObjectWeakPtrT m_object;
Gui::ViewProviderWeakPtrT m_view;
boost::signals2::connection m_connection;
};
@@ -229,6 +241,9 @@ public:
/// returns for Close and Help button
QDialogButtonBox::StandardButtons getStandardButtons() const override;
/// makes sure all widgets are collapsed, if they want to be
void processCollapsedWidgets();
protected:
void recompute();
@@ -267,7 +282,6 @@ private:
std::unique_ptr<Ui_TaskPostDisplay> ui;
};
// ***************************************************************************
// functions
class ViewProviderFemPostFunction;
@@ -295,6 +309,8 @@ public:
void applyPythonCode() override;
bool initiallyCollapsed() override;
private:
void setupConnections();
void onSelectionChanged();

View File

@@ -0,0 +1,181 @@
/***************************************************************************
* 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 "ViewProviderFemPostObject.h"
#include "TaskPostExtraction.h"
using namespace FemGui;
using namespace Gui;
// ***************************************************************************
// box to handle data extractions
TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent)
: TaskPostWidget(view, Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), parent)
{
// we load the python implementation, and try to get the widget from it, to add
// directly our widget
setWindowTitle(tr("Data and extractions"));
Base::PyGILStateLocker lock;
try {
Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true);
if (mod.isNull()) {
Base::Console().error("Unable to import data extraction widget\n");
return;
}
Py::Callable method(mod.getAttr(std::string("DataExtraction")));
Py::Tuple args(1);
args.setItem(0, Py::Object(view->getPyObject()));
m_panel = Py::Object(method.apply(args));
}
catch (Py::Exception&) {
Base::PyException e; // extract the Python error text
e.reportException();
}
if (m_panel.hasAttr(std::string("widget"))) {
Py::Object pywidget(m_panel.getAttr(std::string("widget")));
Gui::PythonWrapper wrap;
if (wrap.loadCoreModule()) {
if (auto* widget = qobject_cast<QWidget*>(wrap.toQObject(pywidget))) {
// finally we have the usable QWidget. Add to us!
auto layout = new QVBoxLayout();
layout->addWidget(widget);
setLayout(layout);
return;
}
}
}
// if we are here something went wrong!
Base::Console().error("Unable to import data extraction widget\n");
};
TaskPostExtraction::~TaskPostExtraction()
{
Base::PyGILStateLocker lock;
try {
if (m_panel.hasAttr(std::string("widget"))) {
m_panel.setAttr(std::string("widget"), Py::None());
}
m_panel = Py::None();
}
catch (Py::AttributeError& e) {
e.clear();
}
}
void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj)
{
Base::PyGILStateLocker lock;
try {
if (m_panel.hasAttr(std::string("onPostDataChanged"))) {
Py::Callable method(m_panel.getAttr(std::string("onPostDataChanged")));
Py::Tuple args(1);
args.setItem(0, Py::Object(obj->getPyObject()));
method.apply(args);
}
}
catch (Py::Exception&) {
Base::PyException e; // extract the Python error text
e.reportException();
}
};
bool TaskPostExtraction::isGuiTaskOnly()
{
Base::PyGILStateLocker lock;
try {
if (m_panel.hasAttr(std::string("isGuiTaskOnly"))) {
Py::Callable method(m_panel.getAttr(std::string("isGuiTaskOnly")));
auto result = Py::Boolean(method.apply());
return result.as_bool();
}
}
catch (Py::Exception&) {
Base::PyException e; // extract the Python error text
e.reportException();
}
return false;
};
void TaskPostExtraction::apply()
{
Base::PyGILStateLocker lock;
try {
if (m_panel.hasAttr(std::string("apply"))) {
Py::Callable method(m_panel.getAttr(std::string("apply")));
method.apply();
}
}
catch (Py::Exception&) {
Base::PyException e; // extract the Python error text
e.reportException();
}
}
bool TaskPostExtraction::initiallyCollapsed()
{
Base::PyGILStateLocker lock;
try {
if (m_panel.hasAttr(std::string("initiallyCollapsed"))) {
Py::Callable method(m_panel.getAttr(std::string("initiallyCollapsed")));
auto result = Py::Boolean(method.apply());
return result.as_bool();
}
}
catch (Py::Exception&) {
Base::PyException e; // extract the Python error text
e.reportException();
}
return false;
}
#include "moc_TaskPostExtraction.cpp"

View File

@@ -0,0 +1,68 @@
/***************************************************************************
* 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;
bool initiallyCollapsed() 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

@@ -32,6 +32,9 @@
#include "ViewProviderFemPostFilter.h"
#include "ViewProviderFemPostFilterPy.h"
#ifdef FC_USE_VTK_PYTHON
#include "TaskPostExtraction.h"
#endif
using namespace FemGui;
@@ -89,6 +92,12 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg)
assert(dlg->getView() == this);
auto panel = new TaskPostDataAlongLine(this);
dlg->addTaskBox(panel->getIcon(), panel);
#ifdef FC_USE_VTK_PYTHON
// and the extraction
auto extr_panel = new TaskPostExtraction(this);
dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel);
#endif
}
@@ -138,6 +147,12 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg)
assert(dlg->getView() == this);
auto panel = new TaskPostDataAtPoint(this);
dlg->addTaskBox(panel->getIcon(), panel);
#ifdef FC_USE_VTK_PYTHON
// and the extraction
auto extr_panel = new TaskPostExtraction(this);
dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel);
#endif
}

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,9 @@
#include <Gui/PythonWrapper.h>
#include "ViewProviderFemPostFilter.h"
#include "TaskPostBoxes.h"
#ifdef FC_USE_VTK_PYTHON
#include "TaskPostExtraction.h"
#endif
// inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml)
#include "ViewProviderFemPostFilterPy.h"
#include "ViewProviderFemPostFilterPy.cpp"
@@ -60,6 +63,29 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args)
return nullptr;
}
PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args)
{
#ifdef FC_USE_VTK_PYTHON
// we take no arguments
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
auto panel = new TaskPostExtraction(getViewProviderFemPostObjectPtr());
Gui::PythonWrapper wrap;
if (wrap.loadCoreModule()) {
return Py::new_reference_to(wrap.fromQWidget(panel));
}
PyErr_SetString(PyExc_TypeError, "creating the panel failed");
return nullptr;
#else
PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available");
Py_Return;
#endif
}
PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;

View File

@@ -67,6 +67,9 @@
#include <Mod/Fem/App/FemPostFilter.h>
#include "TaskPostBoxes.h"
#ifdef FC_USE_VTK_PYTHON
#include "TaskPostExtraction.h"
#endif
#include "ViewProviderAnalysis.h"
#include "ViewProviderFemPostObject.h"
@@ -1006,6 +1009,7 @@ bool ViewProviderFemPostObject::setEdit(int ModNum)
postDlg = new TaskDlgPost(this);
setupTaskDialog(postDlg);
postDlg->connectSlots();
postDlg->processCollapsedWidgets();
Gui::Control().showDialog(postDlg);
}
@@ -1019,8 +1023,13 @@ bool ViewProviderFemPostObject::setEdit(int ModNum)
void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg)
{
assert(dlg->getView() == this);
auto panel = new TaskPostDisplay(this);
dlg->addTaskBox(panel->windowIcon().pixmap(32), panel);
auto dispPanel = new TaskPostDisplay(this);
dlg->addTaskBox(dispPanel->windowIcon().pixmap(32), dispPanel);
#ifdef FC_USE_VTK_PYTHON
auto extrPanel = new TaskPostExtraction(this);
dlg->addTaskBox(extrPanel->windowIcon().pixmap(32), extrPanel);
#endif
}
void ViewProviderFemPostObject::unsetEdit(int ModNum)

View File

@@ -214,7 +214,11 @@ Gui::ToolBarItem* Workbench::setupToolBars() const
<< "FEM_PostFilterDataAtPoint"
<< "FEM_PostFilterCalculator"
<< "Separator"
<< "FEM_PostCreateFunctions";
<< "FEM_PostCreateFunctions"
#ifdef FC_USE_VTK_PYTHON
<< "FEM_PostVisualization"
#endif
;
#endif
Gui::ToolBarItem* utils = new Gui::ToolBarItem(root);
@@ -366,7 +370,11 @@ Gui::MenuItem* Workbench::setupMenuBar() const
<< "FEM_PostFilterDataAtPoint"
<< "FEM_PostFilterCalculator"
<< "Separator"
<< "FEM_PostCreateFunctions";
<< "FEM_PostCreateFunctions"
#ifdef FC_USE_VTK_PYTHON
<< "FEM_PostVisualization"
#endif
;
#endif
Gui::MenuItem* utils = new Gui::MenuItem;

View File

@@ -81,9 +81,10 @@ class FemWorkbench(Workbench):
False if femcommands.commands.__name__ else True
# check vtk version to potentially find missmatchs
from femguiutils.vtk_module_handling import vtk_module_handling
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
from femguiutils.vtk_module_handling import vtk_module_handling
vtk_module_handling()
vtk_module_handling()
def GetClassName(self):
# see https://forum.freecad.org/viewtopic.php?f=10&t=43300

View File

@@ -686,6 +686,141 @@ def makePostVtkResult(doc, result_data, name="VtkResult"):
return obj
def makePostLineplot(doc, name="Lineplot"):
"""makePostLineplot(document, [name]):
creates a FEM post processing line plot
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_lineplot
post_lineplot.PostLineplot(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_lineplot
view_post_lineplot.VPPostLineplot(obj.ViewObject)
return obj
def makePostLineplotFieldData(doc, name="FieldData2D"):
"""makePostLineplotFieldData(document, [name]):
creates a FEM post processing data extractor for 2D Field data
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_lineplot
post_lineplot.PostLineplotFieldData(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_lineplot
view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject)
return obj
def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"):
"""makePostLineplotIndexOverFrames(document, [name]):
creates a FEM post processing data extractor for 2D index data
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_lineplot
post_lineplot.PostLineplotIndexOverFrames(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_lineplot
view_post_lineplot.VPPostLineplotIndexOverFrames(obj.ViewObject)
return obj
def makePostHistogram(doc, name="Histogram"):
"""makePostHistogram(document, [name]):
creates a FEM post processing histogram plot
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_histogram
post_histogram.PostHistogram(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_histogram
view_post_histogram.VPPostHistogram(obj.ViewObject)
return obj
def makePostHistogramFieldData(doc, name="FieldData1D"):
"""makePostHistogramFieldData(document, [name]):
creates a FEM post processing data extractor for 1D Field data
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_histogram
post_histogram.PostHistogramFieldData(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_histogram
view_post_histogram.VPPostHistogramFieldData(obj.ViewObject)
return obj
def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"):
"""makePostHistogramIndexOverFrames(document, [name]):
creates a FEM post processing data extractor for 1D Field data
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_histogram
post_histogram.PostHistogramIndexOverFrames(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_histogram
view_post_histogram.VPPostHistogramIndexOverFrames(obj.ViewObject)
return obj
def makePostTable(doc, name="Table"):
"""makePostTable(document, [name]):
creates a FEM post processing histogram plot
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_table
post_table.PostTable(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_table
view_post_table.VPPostTable(obj.ViewObject)
return obj
def makePostTableFieldData(doc, name="FieldData1D"):
"""makePostTableFieldData(document, [name]):
creates a FEM post processing data extractor for 1D Field data
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_table
post_table.PostTableFieldData(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_table
view_post_table.VPPostTableFieldData(obj.ViewObject)
return obj
def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"):
"""makePostTableIndexOverFrames(document, [name]):
creates a FEM post processing data extractor for 1D Field data
"""
obj = doc.addObject("Fem::FeaturePython", name)
from femobjects import post_table
post_table.PostTableIndexOverFrames(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_table
view_post_table.VPPostTableIndexOverFrames(obj.ViewObject)
return obj
# ********* solver objects ***********************************************************************
def makeEquationDeformation(doc, base_solver=None, name="Deformation"):
"""makeEquationDeformation(document, [base_solver], [name]):

View File

@@ -1289,3 +1289,12 @@ FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88())
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph())
# setup all visualization commands (register by importing)
import femobjects.post_lineplot
import femobjects.post_histogram
import femobjects.post_table
from femguiutils import post_visualization
post_visualization.setup_commands("FEM_PostVisualization")

View File

@@ -34,7 +34,6 @@ import FreeCAD
from femtools.femutils import expandParentObject
from femtools.femutils import is_of_type
from femguiutils.vtk_module_handling import vtk_compatibility_abort
if FreeCAD.GuiUp:
from PySide import QtCore
@@ -381,6 +380,8 @@ class CommandManager:
# and the selobj is expanded in the tree to see the added obj
# check if we should use python filter
from femguiutils.vtk_module_handling import vtk_compatibility_abort
if vtk_compatibility_abort(True):
return

View File

@@ -0,0 +1,163 @@
# ***************************************************************************
# * 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.vtkCommonCore import vtkVersion
from vtkmodules.vtkCommonDataModel import vtkTable
from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents
if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3:
from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter
else:
from vtkmodules.vtkInfovisCore import vtkDataObjectToTable
import FreeCAD
import FreeCADGui
import femobjects.base_fempostextractors as extr
from femtaskpanels.base_fempostpanel import _BasePostTaskPanel
from . import extract_link_view
ExtractLinkView = extract_link_view.ExtractLinkView
class DataExtraction(_BasePostTaskPanel):
# The class is not a widget itself, but provides a widget. It implements
# all required callbacks for the widget and the task dialog.
# Note: This object is created and used from c++! See PostTaskExtraction
def __init__(self, vobj):
super().__init__(vobj.Object)
self.ViewObject = vobj
self.Object = vobj.Object
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostExtraction.ui"
)
# connect all signals as required
self.widget.Data.clicked.connect(self.showData)
self.widget.Summary.clicked.connect(self.showSummary)
# setup the data models
self.data_model = vtk_table_view.VtkTableModel()
self.summary_model = vtk_table_view.VtkTableSummaryModel()
# generate the data
self.onPostDataChanged(self.Object)
# setup the extraction widget
self._extraction_view = ExtractLinkView(self.Object, True, self)
self.widget.layout().addSpacing(self.widget.Data.size().height() / 3)
self.widget.layout().addWidget(self._extraction_view)
self._extraction_view.repopulate()
@QtCore.Slot()
def showData(self):
dialog = QtGui.QDialog(self.widget)
dialog.setWindowTitle(f"Data of {self.Object.Label}")
widget = vtk_table_view.VtkTableView(self.data_model)
layout = QtGui.QVBoxLayout()
layout.addWidget(widget)
layout.setContentsMargins(0, 0, 0, 0)
dialog.setLayout(layout)
dialog.resize(1500, 900)
dialog.show()
@QtCore.Slot()
def showSummary(self):
dialog = QtGui.QDialog(self.widget)
dialog.setWindowTitle(f"Data summary of {self.Object.Label}")
widget = vtk_table_view.VtkTableView(self.summary_model)
layout = QtGui.QVBoxLayout()
layout.addWidget(widget)
layout.setContentsMargins(0, 0, 0, 0)
dialog.setLayout(layout)
dialog.resize(600, 900)
dialog.show()
def isGuiTaskOnly(self):
# If all panels return true it omits the Apply button in the dialog
return True
def onPostDataChanged(self, obj):
algo = obj.getOutputAlgorithm()
if not algo:
self.data_model.setTable(vtkTable())
if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3:
filter = vtkAttributeDataToTableFilter()
else:
filter = vtkDataObjectToTable()
filter.SetFieldType(vtkDataObjectToTable.POINT_DATA)
filter.SetInputConnection(0, algo.GetOutputPort(0))
filter.Update()
table = filter.GetOutputDataObject(0)
# add the points
points = algo.GetOutputDataObject(0).GetPoints().GetData()
table.AddColumn(points)
# split the components
splitter = vtkSplitColumnComponents()
splitter.SetNamingModeToNamesWithParens()
splitter.SetInputData(0, table)
splitter.Update()
table = splitter.GetOutputDataObject(0)
self.data_model.setTable(table)
self.summary_model.setTable(table)
def apply(self):
pass
def initiallyCollapsed(self):
# if we do not have any extractions to show we hide initially to remove clutter
for obj in self.Object.InList:
if extr.is_extractor_object(obj):
return False
return True

View File

@@ -0,0 +1,715 @@
# ***************************************************************************
# * 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
translate = FreeCAD.Qt.translate
# a model showing available visualizations and possible extractions
# #################################################################
def build_new_visualization_tree_model():
# model that shows all options to create new visualizations
model = QtGui.QStandardItemModel()
visualizations = pv.get_registered_visualizations()
for vis_name in visualizations:
vis_icon = FreeCADGui.getIcon(visualizations[vis_name].icon)
vis_item = QtGui.QStandardItem(vis_icon, translate("FEM", "New {}").format(vis_name))
vis_item.setFlags(QtGui.Qt.ItemIsEnabled)
vis_item.setData(visualizations[vis_name])
for ext in visualizations[vis_name].extractions:
icon = FreeCADGui.getIcon(ext.icon)
name = ext.name.removeprefix(vis_name)
ext_item = QtGui.QStandardItem(icon, translate("FEM", "with {}").format(name))
ext_item.setFlags(QtGui.Qt.ItemIsEnabled)
ext_item.setData(ext)
vis_item.appendRow(ext_item)
model.appendRow(vis_item)
return model
def build_add_to_visualization_tree_model():
# model that shows all possible visualization objects to add data to
visualizations = pv.get_registered_visualizations()
model = QtGui.QStandardItemModel()
for obj in FreeCAD.ActiveDocument.Objects:
if obj.isDerivedFrom("Fem::FemAnalysis"):
ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label)
ana_item.setFlags(QtGui.Qt.ItemIsEnabled)
# check all children it it is a visualization
for child in obj.Group:
if vis.is_visualization_object(child):
vis_item = QtGui.QStandardItem(child.ViewObject.Icon, child.Label)
vis_type = vis.get_visualization_type(child)
vis_item.setFlags(QtGui.Qt.ItemIsEnabled)
vis_item.setData(child)
ana_item.appendRow(vis_item)
# add extractor items
for ext in visualizations[vis_type].extractions:
icon = FreeCADGui.getIcon(ext.icon)
name = ext.name.removeprefix(vis_type)
ext_item = QtGui.QStandardItem(
icon, translate("FEM", "Add {}").format(name)
)
ext_item.setFlags(QtGui.Qt.ItemIsEnabled)
ext_item.setData(ext)
vis_item.appendRow(ext_item)
if ana_item.rowCount():
model.appendRow(ana_item)
return model
def build_post_object_item(post_object, extractions, vis_type):
# definitely build a item and add the extractions
post_item = QtGui.QStandardItem(
post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label)
)
post_item.setFlags(QtGui.Qt.ItemIsEnabled)
post_item.setData(post_object)
# add extractor items
for ext in extractions:
icon = FreeCADGui.getIcon(ext.icon)
name = ext.name.removeprefix(vis_type)
ext_item = QtGui.QStandardItem(icon, translate("FEM", "add {}").format(name))
ext_item.setFlags(QtGui.Qt.ItemIsEnabled)
ext_item.setData(ext)
post_item.appendRow(ext_item)
# if we are a post group, we need to add the children
if post_object.hasExtension("Fem::FemPostGroupExtension"):
for child in post_object.Group:
if child.isDerivedFrom("Fem::FemPostObject"):
item = build_post_object_item(child, extractions, vis_type)
post_item.appendRow(item)
return post_item
def build_add_from_data_tree_model(vis_type):
# model that shows all Post data objects from which data can be extracted
extractions = pv.get_registered_visualizations()[vis_type].extractions
model = QtGui.QStandardItemModel()
for obj in FreeCAD.ActiveDocument.Objects:
if obj.isDerivedFrom("Fem::FemAnalysis"):
ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label)
ana_item.setFlags(QtGui.Qt.ItemIsEnabled)
# check all children if it is a post object
for child in obj.Group:
if child.isDerivedFrom("Fem::FemPostObject"):
item = build_post_object_item(child, extractions, vis_type)
ana_item.appendRow(item)
if ana_item.rowCount():
model.appendRow(ana_item)
return model
# implementation of GUI and its functionality
# ###########################################
class _ElideToolButton(QtGui.QToolButton):
# tool button that elides its text, and left align icon and text
def __init__(self, icon, text, parent):
super().__init__(parent)
self._text = text
self._icon = icon
def setCustomText(self, text):
self._text = text
self.repaint()
def setCustomIcon(self, icon):
self._icon = icon
self.repaint()
def sizeHint(self):
button_size = super().sizeHint()
icn_size = self.iconSize()
min_margin = max((button_size - icn_size).height(), 6)
return QtCore.QSize(self.iconSize().width() + 10, icn_size.height() + min_margin)
def paintEvent(self, event):
# draw notmal button, without text and icon
super().paintEvent(event)
# add icon and elided text
painter = QtGui.QPainter()
painter.begin(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True)
margin = (self.height() - self.iconSize().height()) / 2
icn_width = self.iconSize().width()
if self._icon.isNull():
icn_width = 0
fm = self.fontMetrics()
txt_size = self.width() - icn_width - 2 * margin
if not self._icon.isNull():
# we add the margin between icon and text
txt_size -= margin
txt_min = fm.boundingRect("...").width()
# should we center the icon?
xpos = margin
if not self._icon.isNull() and txt_size < txt_min:
# center icon
xpos = self.width() / 2 - self.iconSize().width() / 2
if not self._icon.isNull():
match type(self._icon):
case QtGui.QPixmap:
painter.drawPixmap(xpos, margin, self._icon.scaled(self.iconSize()))
xpos += self.iconSize().width()
case QtGui.QIcon:
self._icon.paint(
painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())
)
xpos += self.iconSize().width()
xpos += margin # the margin to the text
if txt_size >= txt_min:
text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, txt_size)
painter.drawText(xpos, margin + fm.ascent(), text)
painter.end()
class _TreeChoiceButton(QtGui.QToolButton):
selection = QtCore.Signal(object, object)
def __init__(self, model):
super().__init__()
self.model = model
self.setEnabled(bool(model.rowCount()))
self.__skip_next_hide = False
self.tree_view = QtGui.QTreeView(self)
self.tree_view.setModel(model)
self.tree_view.setFrameShape(QtGui.QFrame.NoFrame)
self.tree_view.setHeaderHidden(True)
self.tree_view.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows)
self.tree_view.expandAll()
self.tree_view.clicked.connect(self.selectIndex)
style = self.style()
if not style.styleHint(QtGui.QStyle.SH_ItemView_ActivateItemOnSingleClick):
self.tree_view.activated.connect(self.selectIndex)
# set a complex menu
self.popup = QtGui.QWidgetAction(self)
self.popup.setDefaultWidget(self.tree_view)
self.setPopupMode(QtGui.QToolButton.InstantPopup)
self.addAction(self.popup)
QtCore.Slot(QtCore.QModelIndex)
def selectIndex(self, index):
item = self.model.itemFromIndex(index)
if item and not item.hasChildren():
extraction = item.data()
parent = item.parent().data()
self.selection.emit(parent, extraction)
self.popup.trigger()
def setModel(self, model):
self.model = model
self.tree_view.setModel(model)
self.tree_view.expandAll()
# check if we should be disabled
self.setEnabled(bool(model.rowCount()))
class _SettingsPopup(QtGui.QMenu):
close = QtCore.Signal()
def __init__(self, setting, parent):
super().__init__(parent)
self._setting = setting
self.setWindowFlags(QtGui.Qt.Popup)
self.setFocusPolicy(QtGui.Qt.ClickFocus)
vbox = QtGui.QVBoxLayout()
vbox.addWidget(setting)
buttonBox = QtGui.QDialogButtonBox()
buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok)
buttonBox.accepted.connect(self.hide)
vbox.addWidget(buttonBox)
widget = QtGui.QFrame()
widget.setLayout(vbox)
vbox2 = QtGui.QVBoxLayout()
vbox2.setContentsMargins(0, 0, 0, 0)
vbox2.addWidget(widget)
self.setLayout(vbox2)
def size(self):
return self._setting.sizeHint()
def showEvent(self, event):
# required to get keyboard events
self.setFocus()
def hideEvent(self, event):
# emit on hide: this happens for OK button as well as
# "click away" closing of the popup
self.close.emit()
def keyPressEvent(self, event):
# close on hitting enter
if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return:
self.hide()
class _SummaryWidget(QtGui.QWidget):
delete = QtCore.Signal(object, object) # to delete: document object, summary widget
def __init__(self, st_object, extractor, post_dialog):
super().__init__()
self._st_object = st_object
self._extractor = extractor
self._post_dialog = post_dialog
extr_label = extractor.Proxy.get_representive_fieldname(extractor)
extr_repr = extractor.ViewObject.Proxy.get_preview()
# build the UI
hbox = QtGui.QHBoxLayout()
hbox.setContentsMargins(6, 0, 6, 0)
hbox.setSpacing(2)
self.extrButton = self._button(extractor.ViewObject.Icon, extr_label)
self.viewButton = self._button(extr_repr[0], extr_repr[1], 1)
size = self.viewButton.iconSize()
size.setWidth(size.width() * 2)
self.viewButton.setIconSize(size)
if st_object:
self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label)
hbox.addWidget(self.stButton)
else:
# that happens if the source of the extractor was deleted and now
# that property is set to None
self.extrButton.hide()
self.viewButton.hide()
self.warning = QtGui.QLabel(self)
self.warning.full_text = translate("FEM", "{}: Data source not available").format(
extractor.Label
)
hbox.addWidget(self.warning)
self.rmButton = QtGui.QToolButton(self)
self.rmButton.setIcon(FreeCADGui.getIcon("delete.svg"))
self.rmButton.setAutoRaise(True)
hbox.addWidget(self.extrButton)
hbox.addWidget(self.viewButton)
hbox.addSpacing(15)
hbox.addWidget(self.rmButton)
# add the separation line
vbox = QtGui.QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
vbox.setSpacing(5)
vbox.addItem(hbox)
self.frame = QtGui.QFrame(self)
self.frame.setFrameShape(QtGui.QFrame.HLine)
vbox.addWidget(self.frame)
policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
self.setSizePolicy(policy)
# self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3)
self.setLayout(vbox)
# connect actions. We add functions to widget, as well as the data we need,
# and use those as callback. This way every widget knows which objects to use
if st_object:
self.stButton.clicked.connect(self.showVisualization)
self.extrButton.clicked.connect(self.editApp)
self.viewButton.clicked.connect(self.editView)
self.rmButton.clicked.connect(self.deleteTriggered)
# make sure initial drawing happened
# self._redraw()
def _button(self, icon, text, stretch=2):
btn = _ElideToolButton(icon, text, self)
btn.setMinimumWidth(0)
btn.setAutoRaise(True)
btn.setToolTip(text)
policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
policy.setHorizontalStretch(stretch)
btn.setSizePolicy(policy)
return btn
@QtCore.Slot()
def showVisualization(self):
if vis.is_visualization_object(self._st_object):
# show the visualization
self._st_object.ViewObject.Proxy.show_visualization()
else:
# for now just select the thing
FreeCADGui.Selection.clearSelection()
FreeCADGui.Selection.addSelection(self._st_object)
def _position_dialog(self, dialog):
# the scroll area does mess the mapping to global up, somehow
# the transformation from the widget ot the scroll area gives
# very weird values. Hence we build the coords of the widget
# ourself
summary = dialog.parent() # == self
base_widget = summary.parent()
viewport = summary.parent()
scroll = viewport.parent()
top_left = (
summary.geometry().topLeft()
+ base_widget.geometry().topLeft()
+ viewport.geometry().topLeft()
)
delta = (summary.width() - dialog.size().width()) / 2
local_point = QtCore.QPoint(top_left.x() + delta, top_left.y() + summary.height())
global_point = scroll.mapToGlobal(local_point)
dialog.setGeometry(QtCore.QRect(global_point, dialog.sizeHint()))
@QtCore.Slot()
def editApp(self):
if not hasattr(self, "appDialog"):
widget = self._extractor.ViewObject.Proxy.get_app_edit_widget(self._post_dialog)
self.appDialog = _SettingsPopup(widget, self)
self.appDialog.close.connect(self.appAccept)
if not self.appDialog.isVisible():
# position correctly and show
self._position_dialog(self.appDialog)
self.appDialog.show()
@QtCore.Slot()
def editView(self):
if not hasattr(self, "viewDialog"):
widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog)
self.viewDialog = _SettingsPopup(widget, self)
self.viewDialog.close.connect(self.viewAccept)
if not self.viewDialog.isVisible():
# position correctly and show
self._position_dialog(self.viewDialog)
self.viewDialog.show()
@QtCore.Slot()
def deleteTriggered(self):
self.delete.emit(self._extractor, self)
@QtCore.Slot()
def viewAccept(self):
# update the preview
extr_repr = self._extractor.ViewObject.Proxy.get_preview()
self.viewButton.setCustomIcon(extr_repr[0])
self.viewButton.setCustomText(extr_repr[1])
self.viewButton.setToolTip(extr_repr[1])
@QtCore.Slot()
def appAccept(self):
# update the preview
extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor)
self.extrButton.setCustomText(extr_label)
self.extrButton.setToolTip(extr_label)
class ExtractLinkView(QtGui.QWidget):
def __init__(self, obj, is_source, post_dialog):
# initializes the view.
# obj: The object for which the links should be shown / summarized
# is_source: Bool, if the object is the data source (e.g. postobject), or the target (e.g. plots)
super().__init__()
self._object = obj
self._is_source = is_source
self._post_dialog = post_dialog
self._widgets = []
# build the layout:
self._scroll_view = QtGui.QScrollArea(self)
self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff)
self._scroll_view.setWidgetResizable(True)
self._scroll_widget = QtGui.QWidget(self._scroll_view)
vbox = QtGui.QVBoxLayout()
vbox.setContentsMargins(0, 6, 0, 0)
vbox.addStretch()
self._scroll_widget.setLayout(vbox)
self._scroll_view.setWidget(self._scroll_widget)
hbox = QtGui.QHBoxLayout()
hbox.setSpacing(6)
label = QtGui.QLabel(translate("FEM", "Data used in:"))
if not self._is_source:
label.setText(translate("FEM", "Data used from:"))
label.setAlignment(QtGui.Qt.AlignBottom)
hbox.addWidget(label)
hbox.addStretch()
if self._is_source:
self._add = _TreeChoiceButton(build_add_to_visualization_tree_model())
self._add.setText(translate("FEM", "Add data to"))
self._add.selection.connect(self.addExtractionToVisualization)
hbox.addWidget(self._add)
self._create = _TreeChoiceButton(build_new_visualization_tree_model())
self._create.setText(translate("FEM", "New"))
self._create.selection.connect(self.newVisualization)
hbox.addWidget(self._create)
else:
vis_type = vis.get_visualization_type(self._object)
self._add = _TreeChoiceButton(build_add_from_data_tree_model(vis_type))
self._add.setText(translate("FEM", "Add data from"))
self._add.selection.connect(self.addExtractionToPostObject)
hbox.addWidget(self._add)
vbox = QtGui.QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
vbox.addItem(hbox)
vbox.addWidget(self._scroll_view)
self.setLayout(vbox)
# add the content
self.repopulate()
def _build_summary_widget(self, extractor):
if self._is_source:
st_object = extractor.getParentGroup()
else:
st_object = extractor.Source
widget = _SummaryWidget(st_object, extractor, self._post_dialog)
widget.delete.connect(self._delete_extraction)
return widget
def _delete_extraction(self, extractor, widget):
# remove the document object
doc = extractor.Document
doc.removeObject(extractor.Name)
doc.recompute()
# remove the widget
self._widgets.remove(widget)
widget.deleteLater()
def repopulate(self):
# collect all links that are available and shows them
# clear the view
for widget in self._widgets:
widget.hide()
widget.deleteLater()
self._widgets = []
# rebuild the widgets
if self._is_source:
candidates = self._object.InList
else:
candidates = self._object.OutList
# get all widgets from the candidates
for candidate in candidates:
if extr.is_extractor_object(candidate):
summary = self._build_summary_widget(candidate)
self._widgets.append(summary)
# fill the scroll area
vbox = self._scroll_widget.layout()
for widget in reversed(self._widgets):
vbox.insertWidget(0, widget)
# also reset the add button model
if self._is_source:
self._add.setModel(build_add_to_visualization_tree_model())
def _find_parent_analysis(self, obj):
# iterate upwards, till we find a analysis
for parent in obj.InList:
if parent.isDerivedFrom("Fem::FemAnalysis"):
return parent
analysis = self._find_parent_analysis(parent)
if analysis:
return analysis
return None
QtCore.Slot(object, object) # visualization data, extraction data
def newVisualization(self, vis_data, ext_data):
FreeCADGui.addModule(vis_data.module)
FreeCADGui.addModule(ext_data.module)
FreeCADGui.addModule("FemGui")
# create visualization
FreeCADGui.doCommand(
f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)"
)
analysis = self._find_parent_analysis(self._object)
if analysis:
FreeCADGui.doCommand(f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)")
# create extraction and add it
FreeCADGui.doCommand(
f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)"
)
FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}")
# default values: color
color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property()
if color_prop:
FreeCADGui.doCommand(
f"extraction.ViewObject.{color_prop} = visualization.ViewObject.Proxy.get_next_default_color()"
)
FreeCADGui.doCommand(f"visualization.addObject(extraction)")
self._post_dialog._recompute()
self.repopulate()
QtCore.Slot(object, object) # visualization object, extraction data
def addExtractionToVisualization(self, vis_obj, ext_data):
FreeCADGui.addModule(ext_data.module)
FreeCADGui.addModule("FemGui")
# create extraction and add it
FreeCADGui.doCommand(
f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)"
)
FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}")
# default values: color
color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property()
if color_prop:
FreeCADGui.doCommand(
f"extraction.ViewObject.{color_prop} = (Gui.ActiveDocument.{vis_obj.Name}.Proxy.get_next_default_color())"
)
FreeCADGui.doCommand(f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)")
self._post_dialog._recompute()
self.repopulate()
QtCore.Slot(object, object) # post object, extraction data
def addExtractionToPostObject(self, post_obj, ext_data):
FreeCADGui.addModule(ext_data.module)
FreeCADGui.addModule("FemGui")
# create extraction and add it
FreeCADGui.doCommand(
f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)"
)
FreeCADGui.doCommand(f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}")
# default values for color
color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property()
if color_prop:
FreeCADGui.doCommand(
f"extraction.ViewObject.{color_prop} = Gui.ActiveDocument.{self._object.Name}.Proxy.get_next_default_color()"
)
FreeCADGui.doCommand(f"App.ActiveDocument.{self._object.Name}.addObject(extraction)")
self._post_dialog._recompute()
self.repopulate()

View File

@@ -0,0 +1,179 @@
# ***************************************************************************
# * 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
# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui
# directly to support cmd line use.
import copy
from dataclasses import dataclass
from PySide import QtCore
import FreeCAD
# Registry to handle visualization commands
# #########################################
_registry = {}
@dataclass
class _Extraction:
name: str
icon: str
dimension: str
extracttype: str
module: str
factory: str
@dataclass
class _Visualization:
name: str
icon: str
module: str
factory: str
extractions: list[_Extraction]
# Register a visualization by type, icon and factory function
def register_visualization(visualization_type, icon, module, factory):
if visualization_type in _registry:
raise ValueError("Visualization type already registered")
_registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, [])
def register_extractor(
visualization_type, extraction_type, icon, dimension, etype, module, factory
):
if not visualization_type in _registry:
raise ValueError("visualization not registered yet")
extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory)
_registry[visualization_type].extractions.append(extraction)
def get_registered_visualizations():
return copy.deepcopy(_registry)
def _to_command_name(name):
return "FEM_PostVisualization" + name
class _VisualizationGroupCommand:
def GetCommands(self):
visus = _registry.keys()
cmds = [_to_command_name(v) for v in visus]
return cmds
def GetDefaultCommand(self):
return 0
def GetResources(self):
return {
"MenuText": QtCore.QT_TRANSLATE_NOOP("FEM", "Data Visualizations"),
"ToolTip": QtCore.QT_TRANSLATE_NOOP(
"FEM", "Different visualizations to show post processing data in"
),
}
def IsActive(self):
if not FreeCAD.ActiveDocument:
return False
import FemGui
return bool(FemGui.getActiveAnalysis())
class _VisualizationCommand:
def __init__(self, visualization_type):
self._visualization_type = visualization_type
def GetResources(self):
cmd = _to_command_name(self._visualization_type)
vis = _registry[self._visualization_type]
tooltip = f"Create a {self._visualization_type} post processing data visualization"
return {
"Pixmap": vis.icon,
"MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)),
"Accel": "",
"ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip),
"CmdType": "AlterDoc",
}
def IsActive(self):
# active analysis available
if not FreeCAD.ActiveDocument:
return False
import FemGui
return bool(FemGui.getActiveAnalysis())
def Activated(self):
import FreeCADGui
vis = _registry[self._visualization_type]
FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}")
FreeCADGui.addModule(vis.module)
FreeCADGui.addModule("FemGui")
FreeCADGui.doCommand(f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)")
FreeCADGui.doCommand(f"FemGui.getActiveAnalysis().addObject(obj)")
FreeCADGui.Selection.clearSelection()
FreeCADGui.doCommand("FreeCADGui.ActiveDocument.setEdit(obj)")
def setup_commands(toplevel_name):
# creates all visualization commands and registers them. The
# toplevel group command will have the name provided to this function.
import FreeCADGui
# first all visualization and extraction commands
for vis in _registry:
FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis))
# build the group command!
FreeCADGui.addCommand("FEM_PostVisualization", _VisualizationGroupCommand())

View File

@@ -47,6 +47,10 @@ __title__ = "FEM GUI vtk python module check"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui
# directly to support cmd line use.
__user_input_received = False

View File

@@ -0,0 +1,253 @@
# ***************************************************************************
# * 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
import FreeCAD
import FreeCADGui
from vtkmodules.vtkIOCore import vtkDelimitedTextWriter
translate = FreeCAD.Qt.translate
class VtkTableModel(QtCore.QAbstractTableModel):
# Simple table model. Only supports single component columns
# One can supply a header_names dict to replace the table column names
# in the header. It is a dict "column_idx (int)" to "new name"" or
# "orig_name (str)" to "new name"
def __init__(self, header_names=None):
super().__init__()
self._table = None
if header_names:
self._header = header_names
else:
self._header = {}
def setTable(self, table, header_names=None):
self.beginResetModel()
self._table = table
if header_names:
self._header = header_names
self.endResetModel()
def rowCount(self, index):
if not self._table:
return 0
return self._table.GetNumberOfRows()
def columnCount(self, index):
if not self._table:
return 0
return self._table.GetNumberOfColumns()
def data(self, index, role):
if not self._table:
return None
if role == QtCore.Qt.DisplayRole:
col = self._table.GetColumn(index.column())
return col.GetTuple(index.row())[0]
return None
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
if section in self._header:
return self._header[section]
name = self._table.GetColumnName(section)
if name in self._header:
return self._header[name]
return name
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return section
return None
def getTable(self):
return self._table
class VtkTableSummaryModel(QtCore.QAbstractTableModel):
# Simple model showing a summary of the table.
# Only supports single component columns
def __init__(self):
super().__init__()
self._table = None
def setTable(self, table):
self.beginResetModel()
self._table = table
self.endResetModel()
def rowCount(self, index):
if not self._table:
return 0
return self._table.GetNumberOfColumns()
def columnCount(self, index):
return 2 # min, max
def data(self, index, role):
if not self._table:
return None
if role == QtCore.Qt.DisplayRole:
col = self._table.GetColumn(index.row())
range = col.GetRange()
return range[index.column()]
return None
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return ["Min", "Max"][section]
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return self._table.GetColumnName(section)
return None
def getTable(self):
return self._table
class VtkTableView(QtGui.QWidget):
def __init__(self, model):
super().__init__()
self.model = model
layout = QtGui.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# start with the toolbar
self.toolbar = QtGui.QToolBar()
csv_action = QtGui.QAction(self)
csv_action.triggered.connect(self.exportCsv)
csv_action.setIcon(FreeCADGui.getIcon("Std_Export"))
csv_action.setToolTip(translate("FEM", "Export to CSV"))
self.toolbar.addAction(csv_action)
copy_action = QtGui.QAction(self)
copy_action.triggered.connect(self.copyToClipboard)
copy_action.setIcon(FreeCADGui.getIcon("edit-copy"))
shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy)
copy_action.setToolTip(
translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString()))
)
copy_action.setShortcut(shortcut)
self.toolbar.addAction(copy_action)
layout.addWidget(self.toolbar)
# now the table view
self.table_view = QtGui.QTableView()
self.table_view.setModel(model)
self.model.modelReset.connect(self.modelReset)
# fast initial resize and manual resizing still allowed!
header = self.table_view.horizontalHeader()
header.setResizeContentsPrecision(10)
self.table_view.resizeColumnsToContents()
layout.addWidget(self.table_view)
self.setLayout(layout)
@QtCore.Slot()
def modelReset(self):
# The model is reset, make sure the header visibility is working
# This is needed in case new data was added
self.table_view.resizeColumnsToContents()
@QtCore.Slot(bool)
def exportCsv(self, state):
file_path, filter = QtGui.QFileDialog.getSaveFileName(
None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)"
)
if not file_path:
FreeCAD.Console.PrintMessage(
translate("FEM", "CSV file export aborted: no filename selected")
)
return
writer = vtkDelimitedTextWriter()
writer.SetFileName(file_path)
writer.SetInputData(self.model.getTable())
writer.Write()
@QtCore.Slot()
def copyToClipboard(self):
sel_model = self.table_view.selectionModel()
selection = sel_model.selectedIndexes()
if len(selection) < 1:
return
copy_table = ""
previous = selection.pop(0)
for current in selection:
data = self.model.data(previous, QtCore.Qt.DisplayRole)
copy_table += str(data)
if current.row() != previous.row():
copy_table += "\n"
else:
copy_table += "\t"
previous = current
copy_table += str(self.model.data(selection[-1], QtCore.Qt.DisplayRole))
copy_table += "\n"
clipboard = QtGui.QApplication.instance().clipboard()
clipboard.setText(copy_table)

View File

@@ -0,0 +1,398 @@
# ***************************************************************************
# * 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.vtkCommonCore import vtkIntArray
from vtkmodules.vtkCommonCore import vtkDoubleArray
from vtkmodules.vtkCommonDataModel import vtkTable
from PySide.QtCore import QT_TRANSLATE_NOOP
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
# helper functions
# ################
def is_extractor_object(obj):
if not hasattr(obj, "Proxy"):
return False
return hasattr(obj.Proxy, "ExtractionType")
def get_extraction_type(obj):
# returns the extractor type string, or throws exception if
# not a extractor
return obj.Proxy.ExtractionType
def get_extraction_dimension(obj):
# returns the extractor dimension string, or throws exception if
# not a extractor
return obj.Proxy.ExtractionDimension
# Base class for all extractors with common source and table handling functionality
# Note: Never use directly, always subclass! This class does not create a
# ExtractionType/Dimension variable, hence will not work correctly.
class Extractor(base_fempythonobject.BaseFemPythonObject):
def __init__(self, obj):
super().__init__(obj)
self._setup_properties(obj)
def _setup_properties(self, obj):
pl = obj.PropertiesList
for prop in self._get_properties():
if not prop.name in pl:
prop.add_to_object(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="Fem::PropertyPostDataObject",
name="Table",
group="Base",
doc=QT_TRANSLATE_NOOP("FEM", "The data table that stores the extracted data"),
value=vtkTable(),
),
_PropHelper(
type="App::PropertyLink",
name="Source",
group="Base",
doc=QT_TRANSLATE_NOOP("FEM", "The data source from which the data is extracted"),
value=None,
),
]
return prop
def onDocumentRestored(self, obj):
self._setup_properties(obj)
def onChanged(self, obj, prop):
if prop == "Source":
# check if the source is a Post object
if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"):
FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject")
obj.Source = None
def get_vtk_table(self, obj):
if not obj.DataTable:
obj.DataTable = vtkTable()
return obj.DataTable
def component_options(self, num):
match num:
case 2:
return ["X", "Y"]
case 3:
return ["X", "Y", "Z"]
case 6:
return ["XX", "YY", "ZZ", "XY", "XZ", "YZ"]
case _:
return ["Not a vector"]
def get_representive_fieldname(self, obj):
# should return the representive field name, e.g. Position (X)
return ""
class Extractor1D(Extractor):
ExtractionDimension = "1D"
def __init__(self, obj):
super().__init__(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyEnumeration",
name="XField",
group="X Data",
doc=QT_TRANSLATE_NOOP("FEM", "The field to use as X data"),
value=[],
),
_PropHelper(
type="App::PropertyEnumeration",
name="XComponent",
group="X Data",
doc=QT_TRANSLATE_NOOP(
"FEM", "Which part of the X field vector to use for the X axis"
),
value=[],
),
]
return super()._get_properties() + prop
def onChanged(self, obj, prop):
super().onChanged(obj, prop)
if prop == "XField" and obj.Source and obj.Source.getDataSet():
point_data = obj.Source.getDataSet().GetPointData()
self._setup_x_component_property(obj, point_data)
if prop == "Source":
if obj.Source:
dset = obj.Source.getDataSet()
if dset:
self._setup_x_properties(obj, dset)
else:
self._clear_x_properties(obj)
else:
self._clear_x_properties(obj)
def _setup_x_component_property(self, obj, point_data):
if obj.XField == "Index":
obj.XComponent = self.component_options(1)
elif obj.XField == "Position":
obj.XComponent = self.component_options(3)
else:
array = point_data.GetAbstractArray(obj.XField)
obj.XComponent = self.component_options(array.GetNumberOfComponents())
def _clear_x_properties(self, obj):
if hasattr(obj, "XComponent"):
obj.XComponent = []
if hasattr(obj, "XField"):
obj.XField = []
def _setup_x_properties(self, obj, dataset):
# Set all X Data properties correctly for the given dataset
fields = ["Index", "Position"]
point_data = dataset.GetPointData()
for i in range(point_data.GetNumberOfArrays()):
fields.append(point_data.GetArrayName(i))
current_field = obj.XField
obj.XField = fields
if current_field in fields:
obj.XField = current_field
self._setup_x_component_property(obj, point_data)
def _x_array_component_to_table(self, obj, array, table):
# extracts the component out of the array according to XComponent setting
# Note: Uses the array name unchanged
if array.GetNumberOfComponents() == 1:
table.AddColumn(array)
else:
component_array = vtkDoubleArray()
component_array.SetNumberOfComponents(1)
component_array.SetNumberOfTuples(array.GetNumberOfTuples())
c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent)
component_array.CopyComponent(0, array, c_idx)
component_array.SetName(array.GetName())
table.AddColumn(component_array)
def _x_array_from_dataset(self, obj, dataset, copy=True):
# extracts the relevant array from the dataset and returns a copy
# indices = None uses all indices, otherwise the values in this list
match obj.XField:
case "Index":
# index needs always to be build, ignore copy argument
num = dataset.GetPoints().GetNumberOfPoints()
array = vtkIntArray()
array.SetNumberOfTuples(num)
array.SetNumberOfComponents(1)
for i in range(num):
array.SetValue(i, i)
case "Position":
orig_array = dataset.GetPoints().GetData()
if copy:
array = vtkDoubleArray()
array.DeepCopy(orig_array)
else:
array = orig_array
case _:
point_data = dataset.GetPointData()
orig_array = point_data.GetAbstractArray(obj.XField)
if copy:
array = vtkDoubleArray()
array.DeepCopy(orig_array)
else:
array = orig_array
return array
def get_representive_fieldname(self, obj):
# representive field is the x field
label = obj.XField
if not label:
return ""
if len(obj.getEnumerationsOfProperty("XComponent")) > 1:
label += f" ({obj.XComponent})"
return label
class Extractor2D(Extractor1D):
ExtractionDimension = "2D"
def __init__(self, obj):
super().__init__(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyEnumeration",
name="YField",
group="Y Data",
doc=QT_TRANSLATE_NOOP("FEM", "The field to use as Y data"),
value=[],
),
_PropHelper(
type="App::PropertyEnumeration",
name="YComponent",
group="Y Data",
doc=QT_TRANSLATE_NOOP(
"FEM", "Which part of the Y field vector to use for the Y axis"
),
value=[],
),
]
return super()._get_properties() + prop
def onChanged(self, obj, prop):
super().onChanged(obj, prop)
if prop == "YField" and obj.Source and obj.Source.getDataSet():
point_data = obj.Source.getDataSet().GetPointData()
self._setup_y_component_property(obj, point_data)
if prop == "Source":
if obj.Source:
dset = obj.Source.getDataSet()
if dset:
self._setup_y_properties(obj, dset)
else:
self._clear_y_properties(obj)
else:
self._clear_y_properties(obj)
def _setup_y_component_property(self, obj, point_data):
if obj.YField == "Position":
obj.YComponent = self.component_options(3)
else:
array = point_data.GetAbstractArray(obj.YField)
obj.YComponent = self.component_options(array.GetNumberOfComponents())
def _clear_y_properties(self, obj):
if hasattr(obj, "YComponent"):
obj.YComponent = []
if hasattr(obj, "YField"):
obj.YField = []
def _setup_y_properties(self, obj, dataset):
# Set all X Data properties correctly for the given dataset
fields = ["Position"]
point_data = dataset.GetPointData()
for i in range(point_data.GetNumberOfArrays()):
fields.append(point_data.GetArrayName(i))
current_field = obj.YField
obj.YField = fields
if current_field in fields:
obj.YField = current_field
self._setup_y_component_property(obj, point_data)
def _y_array_component_to_table(self, obj, array, table):
# extracts the component out of the array according to XComponent setting
if array.GetNumberOfComponents() == 1:
table.AddColumn(array)
else:
component_array = vtkDoubleArray()
component_array.SetNumberOfComponents(1)
component_array.SetNumberOfTuples(array.GetNumberOfTuples())
c_idx = obj.getEnumerationsOfProperty("YComponent").index(obj.YComponent)
component_array.CopyComponent(0, array, c_idx)
component_array.SetName(array.GetName())
table.AddColumn(component_array)
def _y_array_from_dataset(self, obj, dataset, copy=True):
# extracts the relevant array from the dataset and returns a copy
# indices = None uses all indices, otherwise the values in this list
match obj.YField:
case "Position":
orig_array = dataset.GetPoints().GetData()
if copy:
array = vtkDoubleArray()
array.DeepCopy(orig_array)
else:
array = orig_array
case _:
point_data = dataset.GetPointData()
orig_array = point_data.GetAbstractArray(obj.YField)
if copy:
array = vtkDoubleArray()
array.DeepCopy(orig_array)
else:
array = orig_array
return array
def get_representive_fieldname(self, obj):
# representive field is the y field
label = obj.YField
if not label:
return ""
if len(obj.getEnumerationsOfProperty("YComponent")) > 1:
label += f" ({obj.YComponent})"
return label

View File

@@ -0,0 +1,183 @@
# ***************************************************************************
# * 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 visualization base object"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package base_fempostextractors
# \ingroup FEM
# \brief base objects for data visualizations
from vtkmodules.vtkCommonDataModel import vtkTable
from vtkmodules.vtkCommonCore import vtkDoubleArray
from . import base_fempythonobject
from . import base_fempostextractors
# helper functions
# ################
def is_visualization_object(obj):
if not obj:
return False
if not hasattr(obj, "Proxy"):
return False
return hasattr(obj.Proxy, "VisualizationType")
def get_visualization_type(obj):
# returns the extractor type string, or throws exception if
# not a extractor
return obj.Proxy.VisualizationType
def is_visualization_extractor_type(obj, vistype):
# must be extractor
if not base_fempostextractors.is_extractor_object(obj):
return False
# must be visualization object
if not is_visualization_object(obj):
return False
# must be correct type
if get_visualization_type(obj) != vistype:
return False
return True
# Base class for all visualizations
# It collects all data from its extraction objects into a table.
# Note: Never use directly, always subclass! This class does not create a
# Visualization variable, hence will not work correctly.
class PostVisualization(base_fempythonobject.BaseFemPythonObject):
def __init__(self, obj):
super().__init__(obj)
obj.addExtension("App::GroupExtensionPython")
self._setup_properties(obj)
def _setup_properties(self, obj):
pl = obj.PropertiesList
for prop in self._get_properties():
if not prop.name in pl:
prop.add_to_object(obj)
def _get_properties(self):
# override if subclass wants to add additional properties
prop = [
base_fempostextractors._PropHelper(
type="Fem::PropertyPostDataObject",
name="Table",
group="Base",
doc="The data table that stores the data for visualization",
value=vtkTable(),
),
]
return prop
def onDocumentRestored(self, obj):
# if a new property was added we handle it by setup
# Override if subclass needs to handle changed property type
self._setup_properties(obj)
def onChanged(self, obj, prop):
# Ensure only correct child object types are in the group
if prop == "Group":
# check if all objects are allowed
children = obj.Group
for child in obj.Group:
if not is_visualization_extractor_type(child, self.VisualizationType):
FreeCAD.Console.PrintWarning(
f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added"
)
children.remove(child)
if len(obj.Group) != len(children):
obj.Group = children
def execute(self, obj):
# Collect all extractor child data into our table
# Note: Each childs table can have different number of rows. We need
# to pad the date for our table in this case
rows = self.getLongestColumnLength(obj)
table = vtkTable()
for child in obj.Group:
# If child has no Source, its table should be empty. However,
# it would theoretical be possible that child source was set
# to none without recompute, and the visualization was manually
# recomputed afterwards
if not child.Source and (child.Table.GetNumberOfColumns() > 0):
FreeCAD.Console.PrintWarning(
f"{child.Label} has data, but no Source object. Will be ignored"
)
continue
c_table = child.Table
for i in range(c_table.GetNumberOfColumns()):
c_array = c_table.GetColumn(i)
array = vtkDoubleArray()
if c_array.GetNumberOfTuples() == rows:
# simple deep copy is enough
array.DeepCopy(c_array)
else:
array.SetNumberOfComponents(c_array.GetNumberOfComponents())
array.SetNumberOfTuples(rows)
array.Fill(0) # so that all non-used entries are set to 0
for j in range(c_array.GetNumberOfTuples()):
array.SetTuple(j, c_array.GetTuple(j))
array.SetName(f"{child.Source.Name}: {c_array.GetName()}")
table.AddColumn(array)
obj.Table = table
return False
def getLongestColumnLength(self, obj):
# iterate all extractor children and get the column lengths
length = 0
for child in obj.Group:
if base_fempostextractors.is_extractor_object(child):
table = child.Table
if table.GetNumberOfColumns() > 0:
# we assume all columns of an extractor have same length
num = table.GetColumn(0).GetNumberOfTuples()
if num > length:
length = num
return length

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,213 @@
# ***************************************************************************
# * 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
import FreeCAD
from . import base_fempostextractors
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
from vtkmodules.vtkCommonCore import vtkDoubleArray
from vtkmodules.vtkCommonDataModel import vtkTable
from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline
from PySide.QtCore import QT_TRANSLATE_NOOP
class PostFieldData1D(base_fempostextractors.Extractor1D):
"""
A post processing extraction of one dimensional field data
"""
ExtractionType = "Field"
def __init__(self, obj):
super().__init__(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyBool",
name="ExtractFrames",
group="Multiframe",
doc=QT_TRANSLATE_NOOP(
"FEM", "Specify if the field shall be extracted for every available frame"
),
value=False,
),
]
return super()._get_properties() + prop
def execute(self, obj):
# on execution we populate the vtk table
table = vtkTable()
if not obj.Source:
obj.Table = table
return
dataset = obj.Source.getDataSet()
if not dataset:
obj.Table = table
return
timesteps = []
if obj.ExtractFrames:
# check if we have timesteps
info = obj.Source.getOutputAlgorithm().GetOutputInformation(0)
if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()):
timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS())
else:
FreeCAD.Console.PrintWarning(
'No frames available in data, ignoring "ExtractFrames" property'
)
if not timesteps:
# get the dataset and extract the correct array
array = self._x_array_from_dataset(obj, dataset)
if array.GetNumberOfComponents() > 1:
array.SetName(obj.XField + " (" + obj.XComponent + ")")
else:
array.SetName(obj.XField)
self._x_array_component_to_table(obj, array, table)
else:
algo = obj.Source.getOutputAlgorithm()
for timestep in timesteps:
algo.UpdateTimeStep(timestep)
dataset = algo.GetOutputDataObject(0)
array = self._x_array_from_dataset(obj, dataset)
if array.GetNumberOfComponents() > 1:
array.SetName(f"{obj.XField} ({obj.XComponent}) - {timestep}")
else:
array.SetName(f"{obj.XField} - {timestep}")
self._x_array_component_to_table(obj, array, table)
# set the final table
obj.Table = table
class PostIndexOverFrames1D(base_fempostextractors.Extractor1D):
"""
A post processing extraction of one dimensional index data
"""
ExtractionType = "Index"
def __init__(self, obj):
super().__init__(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyInteger",
name="Index",
group="X Data",
doc=QT_TRANSLATE_NOOP(
"FEM", "Specify for which index the data should be extracted"
),
value=0,
),
]
return super()._get_properties() + prop
def execute(self, obj):
# on execution we populate the vtk table
table = vtkTable()
if not obj.Source:
obj.Table = table
return
dataset = obj.Source.getDataSet()
if not dataset:
obj.Table = table
return
# check if we have timesteps
timesteps = []
info = obj.Source.getOutputAlgorithm().GetOutputInformation(0)
if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()):
timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS())
algo = obj.Source.getOutputAlgorithm()
frame_array = vtkDoubleArray()
idx = obj.Index
if timesteps:
setup = False
for i, timestep in enumerate(timesteps):
algo.UpdateTimeStep(timestep)
dataset = algo.GetOutputDataObject(0)
array = self._x_array_from_dataset(obj, dataset, copy=False)
# safeguard for invalid access
if idx < 0 or array.GetNumberOfTuples() - 1 < idx:
raise Exception(
f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}"
)
if not setup:
frame_array.SetNumberOfComponents(array.GetNumberOfComponents())
frame_array.SetNumberOfTuples(len(timesteps))
setup = True
frame_array.SetTuple(i, idx, array)
else:
algo.Update()
dataset = algo.GetOutputDataObject(0)
array = self._x_array_from_dataset(obj, dataset, copy=False)
# safeguard for invalid access
if idx < 0 or array.GetNumberOfTuples() - 1 < idx:
raise Exception(
f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}"
)
frame_array.SetNumberOfComponents(array.GetNumberOfComponents())
frame_array.SetNumberOfTuples(1)
frame_array.SetTuple(0, idx, array)
if frame_array.GetNumberOfComponents() > 1:
frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}")
else:
frame_array.SetName(f"{obj.XField} @Idx {obj.Index}")
self._x_array_component_to_table(obj, frame_array, table)
# set the final table
obj.Table = table

View File

@@ -0,0 +1,249 @@
# ***************************************************************************
# * 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 extractors 2D"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_histogram
# \ingroup FEM
# \brief Post processing plot displaying lines
import FreeCAD
from . import base_fempostextractors
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
from vtkmodules.vtkCommonCore import vtkDoubleArray
from vtkmodules.vtkCommonDataModel import vtkTable
from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline
from PySide.QtCore import QT_TRANSLATE_NOOP
class PostFieldData2D(base_fempostextractors.Extractor2D):
"""
A post processing extraction of two dimensional field data
"""
ExtractionType = "Field"
def __init__(self, obj):
super().__init__(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyBool",
name="ExtractFrames",
group="Multiframe",
doc=QT_TRANSLATE_NOOP(
"FEM", "Specify if the field shall be extracted for every available frame"
),
value=False,
),
]
return super()._get_properties() + prop
def execute(self, obj):
# on execution we populate the vtk table
table = vtkTable()
if not obj.Source:
obj.Table = table
return
dataset = obj.Source.getDataSet()
if not dataset:
obj.Table = table
return
timesteps = []
if obj.ExtractFrames:
# check if we have timesteps
info = obj.Source.getOutputAlgorithm().GetOutputInformation(0)
if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()):
timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS())
else:
FreeCAD.Console.PrintWarning(
'No frames available in data, ignoring "ExtractFrames" property'
)
if not timesteps:
# get the dataset and extract the correct array
xarray = self._x_array_from_dataset(obj, dataset)
if xarray.GetNumberOfComponents() > 1:
xarray.SetName(obj.XField + " (" + obj.XComponent + ")")
else:
xarray.SetName(obj.XField)
self._x_array_component_to_table(obj, xarray, table)
yarray = self._y_array_from_dataset(obj, dataset)
if yarray.GetNumberOfComponents() > 1:
yarray.SetName(obj.YField + " (" + obj.YComponent + ")")
else:
yarray.SetName(obj.YField)
self._y_array_component_to_table(obj, yarray, table)
else:
algo = obj.Source.getOutputAlgorithm()
for timestep in timesteps:
algo.UpdateTimeStep(timestep)
dataset = algo.GetOutputDataObject(0)
xarray = self._x_array_from_dataset(obj, dataset)
if xarray.GetNumberOfComponents() > 1:
xarray.SetName(f"X - {obj.XField} ({obj.XComponent}) - {timestep}")
else:
xarray.SetName(f"X - {obj.XField} - {timestep}")
self._x_array_component_to_table(obj, xarray, table)
yarray = self._y_array_from_dataset(obj, dataset)
if yarray.GetNumberOfComponents() > 1:
yarray.SetName(f"{obj.YField} ({obj.YComponent}) - {timestep}")
else:
yarray.SetName(f"{obj.YField} - {timestep}")
self._y_array_component_to_table(obj, yarray, table)
# set the final table
obj.Table = table
class PostIndexOverFrames2D(base_fempostextractors.Extractor2D):
"""
A post processing extraction for two dimensional data with X always being the frames
"""
ExtractionType = "Index"
def __init__(self, obj):
super().__init__(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyInteger",
name="Index",
group="Data",
doc=QT_TRANSLATE_NOOP(
"FEM", "Specify for which point index the data should be extracted"
),
value=0,
),
]
return super()._get_properties() + prop
def _setup_x_component_property(self, obj, point_data):
# override to only allow "Frames" as X data
obj.XComponent = ["Not a vector"]
def _setup_x_properties(self, obj, dataset):
# override to only allow "Frames" as X data
obj.XField = ["Frames"]
def execute(self, obj):
# on execution we populate the vtk table
table = vtkTable()
if not obj.Source:
obj.Table = table
return
dataset = obj.Source.getDataSet()
if not dataset:
obj.Table = table
return
# check if we have timesteps (required!)
timesteps = []
info = obj.Source.getOutputAlgorithm().GetOutputInformation(0)
if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()):
timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS())
algo = obj.Source.getOutputAlgorithm()
frame_x_array = vtkDoubleArray()
frame_y_array = vtkDoubleArray()
idx = obj.Index
if timesteps:
setup = False
frame_x_array.SetNumberOfTuples(len(timesteps))
frame_x_array.SetNumberOfComponents(1)
for i, timestep in enumerate(timesteps):
frame_x_array.SetTuple1(i, timestep)
algo.UpdateTimeStep(timestep)
dataset = algo.GetOutputDataObject(0)
array = self._y_array_from_dataset(obj, dataset, copy=False)
# safeguard for invalid access
if idx < 0 or array.GetNumberOfTuples() - 1 < idx:
raise Exception(
f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}"
)
if not setup:
frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents())
frame_y_array.SetNumberOfTuples(len(timesteps))
setup = True
frame_y_array.SetTuple(i, idx, array)
else:
frame_x_array.SetNumberOfTuples(1)
frame_x_array.SetNumberOfComponents(1)
frame_x_array.SetTuple1(0, 0)
algo.Update()
dataset = algo.GetOutputDataObject(0)
array = self._y_array_from_dataset(obj, dataset, copy=False)
# safeguard for invalid access
if idx < 0 or array.GetNumberOfTuples() - 1 < idx:
raise Exception(
f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}"
)
frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents())
frame_y_array.SetNumberOfTuples(1)
frame_y_array.SetTuple(0, idx, array)
frame_x_array.SetName("Frames")
if frame_y_array.GetNumberOfComponents() > 1:
frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}")
else:
frame_y_array.SetName(f"{obj.YField} @Idx {obj.Index}")
table.AddColumn(frame_x_array)
self._y_array_component_to_table(obj, frame_y_array, table)
# set the final table
obj.Table = table

View File

@@ -0,0 +1,105 @@
# ***************************************************************************
# * 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 histogram"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_histogram
# \ingroup FEM
# \brief Post processing plot displaying histograms
# check vtk version to potentially find missmatchs
from femguiutils.vtk_module_handling import vtk_module_handling
vtk_module_handling()
from . import base_fempostextractors
from . import base_fempostvisualizations
from . import post_extract1D
from femguiutils import post_visualization
# register visualization and extractors
post_visualization.register_visualization(
"Histogram", ":/icons/FEM_PostHistogram.svg", "ObjectsFem", "makePostHistogram"
)
post_visualization.register_extractor(
"Histogram",
"HistogramFieldData",
":/icons/FEM_PostField.svg",
"1D",
"Field",
"ObjectsFem",
"makePostHistogramFieldData",
)
post_visualization.register_extractor(
"Histogram",
"HistogramIndexOverFrames",
":/icons/FEM_PostIndex.svg",
"1D",
"Index",
"ObjectsFem",
"makePostHistogramIndexOverFrames",
)
# Implementation
# ##############
def is_histogram_extractor(obj):
if not base_fempostextractors.is_extractor_object(obj):
return False
if not hasattr(obj.Proxy, "VisualizationType"):
return False
return obj.Proxy.VisualizationType == "Histogram"
class PostHistogramFieldData(post_extract1D.PostFieldData1D):
"""
A 1D Field extraction for histograms.
"""
VisualizationType = "Histogram"
class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D):
"""
A 1D index extraction for histogram.
"""
VisualizationType = "Histogram"
class PostHistogram(base_fempostvisualizations.PostVisualization):
"""
A post processing plot for showing extracted data as histograms
"""
VisualizationType = "Histogram"

View File

@@ -0,0 +1,105 @@
# ***************************************************************************
# * 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
# check vtk version to potentially find missmatchs
from femguiutils.vtk_module_handling import vtk_module_handling
vtk_module_handling()
from . import base_fempostextractors
from . import base_fempostvisualizations
from . import post_extract2D
from femguiutils import post_visualization
# register visualization and extractors
post_visualization.register_visualization(
"Lineplot", ":/icons/FEM_PostLineplot.svg", "ObjectsFem", "makePostLineplot"
)
post_visualization.register_extractor(
"Lineplot",
"LineplotFieldData",
":/icons/FEM_PostField.svg",
"2D",
"Field",
"ObjectsFem",
"makePostLineplotFieldData",
)
post_visualization.register_extractor(
"Lineplot",
"LineplotIndexOverFrames",
":/icons/FEM_PostIndex.svg",
"2D",
"Index",
"ObjectsFem",
"makePostLineplotIndexOverFrames",
)
# Implementation
# ##############
def is_lineplot_extractor(obj):
if not base_fempostextractors.is_extractor_object(obj):
return False
if not hasattr(obj.Proxy, "VisualizationType"):
return False
return obj.Proxy.VisualizationType == "Lineplot"
class PostLineplotFieldData(post_extract2D.PostFieldData2D):
"""
A 2D Field extraction for lineplot.
"""
VisualizationType = "Lineplot"
class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D):
"""
A 2D index extraction for lineplot.
"""
VisualizationType = "Lineplot"
class PostLineplot(base_fempostvisualizations.PostVisualization):
"""
A post processing plot for showing extracted data as line plots
"""
VisualizationType = "Lineplot"

View File

@@ -0,0 +1,105 @@
# ***************************************************************************
# * 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 table"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_table
# \ingroup FEM
# \brief Post processing plot displaying tables
# check vtk version to potentially find missmatchs
from femguiutils.vtk_module_handling import vtk_module_handling
vtk_module_handling()
from . import base_fempostextractors
from . import base_fempostvisualizations
from . import post_extract1D
from femguiutils import post_visualization
# register visualization and extractors
post_visualization.register_visualization(
"Table", ":/icons/FEM_PostSpreadsheet.svg", "ObjectsFem", "makePostTable"
)
post_visualization.register_extractor(
"Table",
"TableFieldData",
":/icons/FEM_PostField.svg",
"1D",
"Field",
"ObjectsFem",
"makePostTableFieldData",
)
post_visualization.register_extractor(
"Table",
"TableIndexOverFrames",
":/icons/FEM_PostIndex.svg",
"1D",
"Index",
"ObjectsFem",
"makePostTableIndexOverFrames",
)
# Implementation
# ##############
def is_table_extractor(obj):
if not base_fempostextractors.is_extractor_object(obj):
return False
if not hasattr(obj.Proxy, "VisualizationType"):
return False
return obj.Proxy.VisualizationType == "Table"
class PostTableFieldData(post_extract1D.PostFieldData1D):
"""
A 1D Field extraction for tables.
"""
VisualizationType = "Table"
class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D):
"""
A 1D index extraction for table.
"""
VisualizationType = "Table"
class PostTable(base_fempostvisualizations.PostVisualization):
"""
A post processing plot for showing extracted data as tables
"""
VisualizationType = "Table"

View File

@@ -0,0 +1,88 @@
# ***************************************************************************
# * 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
from . import base_femtaskpanel
translate = FreeCAD.Qt.translate
class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel):
"""
The TaskPanel for post objects, mimicing the c++ functionality
"""
def __init__(self, obj):
super().__init__(obj)
# get the settings group
self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem")
# Implement parent functions
# ##########################
def getStandardButtons(self):
return (
QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
)
def clicked(self, button):
# apply button hit?
if button == QtGui.QDialogButtonBox.Apply:
self.obj.Document.recompute()
def open(self):
# open a new transaction if non is open
if not FreeCAD.getActiveTransaction():
FreeCAD.ActiveDocument.openTransaction(
translate("FEM", "Edit {}").format(self.obj.Label)
)
# Helper functions
# ################
def _recompute(self):
# only recompute if the user wants automatic recompute
if self.__settings_grp.GetBool("PostAutoRecompute", True):
self.obj.Document.recompute()
def _enumPropertyToCombobox(self, obj, prop, cbox):
cbox.blockSignals(True)
cbox.clear()
entries = obj.getEnumerationsOfProperty(prop)
for entry in entries:
cbox.addItem(entry)
cbox.setCurrentText(getattr(obj, prop))
cbox.blockSignals(False)

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
from . import base_fempostpanel
class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel):
"""
The TaskPanel for editing properties extractor objects. The actual UI is
provided by the viewproviders. This allows using a universal task panel
"""
def __init__(self, obj):
super().__init__(obj)
# form is used to display individual task panels
app = obj.ViewObject.Proxy.get_app_edit_widget(self)
app.setWindowTitle("Data extraction")
app.setWindowIcon(obj.ViewObject.Icon)
view = obj.ViewObject.Proxy.get_view_edit_widget(self)
view.setWindowTitle("Visualization settings")
view.setWindowIcon(obj.ViewObject.Icon)
self.form = [app, view]

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
"""
@@ -56,50 +56,6 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel):
# form made from param and selection widget
self.form = [self.widget, vobj.createDisplayTaskWidget()]
# get the settings group
self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem")
# Implement parent functions
# ##########################
def getStandardButtons(self):
return (
QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
)
def clicked(self, button):
# apply button hit?
if button == QtGui.QDialogButtonBox.Apply:
self.obj.Document.recompute()
def accept(self):
# self.obj.CharacteristicLength = self.elelen
# self.obj.References = self.selection_widget.references
# self.selection_widget.finish_selection()
return super().accept()
def reject(self):
# self.selection_widget.finish_selection()
return super().reject()
# Helper functions
# ##################
def _recompute(self):
# only recompute if the user wants automatic recompute
if self.__settings_grp.GetBool("PostAutoRecompute", True):
self.obj.Document.recompute()
def _enumPropertyToCombobox(self, obj, prop, cbox):
cbox.blockSignals(True)
cbox.clear()
entries = obj.getEnumerationsOfProperty(prop)
for entry in entries:
cbox.addItem(entry)
cbox.setCurrentText(getattr(obj, prop))
cbox.blockSignals(False)
# Setup functions
# ###############

View File

@@ -0,0 +1,194 @@
# ***************************************************************************
# * 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
translate = FreeCAD.Qt.translate
class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
"""
The TaskPanel for editing properties of glyph filter
"""
def __init__(self, vobj):
super().__init__(vobj.Object)
# data widget
self.data_widget = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
self.data_widget.show_plot = QtGui.QPushButton()
self.data_widget.show_plot.setText(translate("FEM", "Show plot"))
hbox.addWidget(self.data_widget.show_plot)
self.data_widget.show_table = QtGui.QPushButton()
self.data_widget.show_table.setText(translate("FEM", "Show data"))
hbox.addWidget(self.data_widget.show_table)
vbox = QtGui.QVBoxLayout()
vbox.addItem(hbox)
vbox.addSpacing(10)
extracts = elv.ExtractLinkView(self.obj, False, self)
vbox.addWidget(extracts)
self.data_widget.setLayout(vbox)
self.data_widget.setWindowTitle(translate("FEM", "Histogram data"))
self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg"))
# histogram parameter widget
self.view_widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui"
)
self.view_widget.setWindowTitle(translate("FEM", "Histogram view settings"))
self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg"))
self.__init_widgets()
# form made from param and selection widget
self.form = [self.data_widget, self.view_widget]
# Setup functions
# ###############
def __init_widgets(self):
# connect data widget
self.data_widget.show_plot.clicked.connect(self.showPlot)
self.data_widget.show_table.clicked.connect(self.showTable)
# set current values to view widget
viewObj = self.obj.ViewObject
self.view_widget.Bins.setValue(viewObj.Bins)
self._enumPropertyToCombobox(viewObj, "Type", self.view_widget.Type)
self.view_widget.Cumulative.setChecked(viewObj.Cumulative)
self.view_widget.Title.setText(viewObj.Title)
self.view_widget.XLabel.setText(viewObj.XLabel)
self.view_widget.YLabel.setText(viewObj.YLabel)
self.view_widget.LegendShow.setChecked(viewObj.Legend)
self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos)
self.view_widget.BarWidth.setValue(viewObj.BarWidth)
self.view_widget.HatchWidth.setValue(viewObj.HatchLineWidth)
# connect callbacks
self.view_widget.Bins.valueChanged.connect(self.binsChanged)
self.view_widget.Type.activated.connect(self.typeChanged)
self.view_widget.Cumulative.toggled.connect(self.comulativeChanged)
self.view_widget.Title.editingFinished.connect(self.titleChanged)
self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged)
self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged)
self.view_widget.LegendShow.toggled.connect(self.legendShowChanged)
self.view_widget.LegendPos.activated.connect(self.legendPosChanged)
self.view_widget.BarWidth.valueChanged.connect(self.barWidthChanged)
self.view_widget.HatchWidth.valueChanged.connect(self.hatchWidthChanged)
QtCore.Slot()
def showPlot(self):
self.obj.ViewObject.Proxy.show_visualization()
QtCore.Slot()
def showTable(self):
# TODO: make data model update when object is recomputed
data_model = vtk_table_view.VtkTableModel()
data_model.setTable(self.obj.Table)
dialog = QtGui.QDialog(self.data_widget)
widget = vtk_table_view.VtkTableView(data_model)
layout = QtGui.QVBoxLayout()
layout.addWidget(widget)
layout.setContentsMargins(0, 0, 0, 0)
dialog.setLayout(layout)
dialog.resize(1500, 900)
dialog.show()
QtCore.Slot(int)
def binsChanged(self, bins):
self.obj.ViewObject.Bins = bins
QtCore.Slot(int)
def typeChanged(self, idx):
self.obj.ViewObject.Type = idx
QtCore.Slot(bool)
def comulativeChanged(self, state):
self.obj.ViewObject.Cumulative = state
QtCore.Slot()
def titleChanged(self):
self.obj.ViewObject.Title = self.view_widget.Title.text()
QtCore.Slot()
def xLabelChanged(self):
self.obj.ViewObject.XLabel = self.view_widget.XLabel.text()
QtCore.Slot()
def yLabelChanged(self):
self.obj.ViewObject.YLabel = self.view_widget.YLabel.text()
QtCore.Slot(int)
def legendPosChanged(self, idx):
self.obj.ViewObject.LegendLocation = idx
QtCore.Slot(bool)
def legendShowChanged(self, state):
self.obj.ViewObject.Legend = state
QtCore.Slot(float)
def barWidthChanged(self, value):
self.obj.ViewObject.BarWidth = value
QtCore.Slot(float)
def hatchWidthChanged(self, value):
self.obj.ViewObject.HatchLineWidth = value

View File

@@ -0,0 +1,173 @@
# ***************************************************************************
# * 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 lineplot plot task panel"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package task_post_lineplot
# \ingroup FEM
# \brief task panel for post lineplot plot
from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
from . import base_fempostpanel
from femguiutils import extract_link_view as elv
from femguiutils import vtk_table_view
translate = FreeCAD.Qt.translate
class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
"""
The TaskPanel for editing properties of glyph filter
"""
def __init__(self, vobj):
super().__init__(vobj.Object)
# data widget
self.data_widget = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
self.data_widget.show_plot = QtGui.QPushButton()
self.data_widget.show_plot.setText(translate("FEM", "Show plot"))
hbox.addWidget(self.data_widget.show_plot)
self.data_widget.show_table = QtGui.QPushButton()
self.data_widget.show_table.setText(translate("FEM", "Show data"))
hbox.addWidget(self.data_widget.show_table)
vbox = QtGui.QVBoxLayout()
vbox.addItem(hbox)
vbox.addSpacing(10)
extracts = elv.ExtractLinkView(self.obj, False, self)
vbox.addWidget(extracts)
self.data_widget.setLayout(vbox)
self.data_widget.setWindowTitle(translate("FEM", "Lineplot data"))
self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg"))
# lineplot parameter widget
self.view_widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui"
)
self.view_widget.setWindowTitle(translate("FEM", "Lineplot view settings"))
self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg"))
self.__init_widgets()
# form made from param and selection widget
self.form = [self.data_widget, self.view_widget]
# Setup functions
# ###############
def __init_widgets(self):
# connect data widget
self.data_widget.show_plot.clicked.connect(self.showPlot)
self.data_widget.show_table.clicked.connect(self.showTable)
# set current values to view widget
viewObj = self.obj.ViewObject
self._enumPropertyToCombobox(viewObj, "Scale", self.view_widget.Scale)
self.view_widget.Grid.setChecked(viewObj.Grid)
self.view_widget.Title.setText(viewObj.Title)
self.view_widget.XLabel.setText(viewObj.XLabel)
self.view_widget.YLabel.setText(viewObj.YLabel)
self.view_widget.LegendShow.setChecked(viewObj.Legend)
self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos)
# connect callbacks
self.view_widget.Scale.activated.connect(self.scaleChanged)
self.view_widget.Grid.toggled.connect(self.gridChanged)
self.view_widget.Title.editingFinished.connect(self.titleChanged)
self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged)
self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged)
self.view_widget.LegendShow.toggled.connect(self.legendShowChanged)
self.view_widget.LegendPos.activated.connect(self.legendPosChanged)
QtCore.Slot()
def showPlot(self):
self.obj.ViewObject.Proxy.show_visualization()
QtCore.Slot()
def showTable(self):
# TODO: make data model update when object is recomputed
data_model = vtk_table_view.VtkTableModel()
data_model.setTable(self.obj.Table)
dialog = QtGui.QDialog(self.data_widget)
widget = vtk_table_view.VtkTableView(data_model)
layout = QtGui.QVBoxLayout()
layout.addWidget(widget)
layout.setContentsMargins(0, 0, 0, 0)
dialog.setLayout(layout)
dialog.resize(1500, 900)
dialog.show()
QtCore.Slot(int)
def scaleChanged(self, idx):
self.obj.ViewObject.Scale = idx
QtCore.Slot(bool)
def gridChanged(self, state):
self.obj.ViewObject.Grid = state
QtCore.Slot()
def titleChanged(self):
self.obj.ViewObject.Title = self.view_widget.Title.text()
QtCore.Slot()
def xLabelChanged(self):
self.obj.ViewObject.XLabel = self.view_widget.XLabel.text()
QtCore.Slot()
def yLabelChanged(self):
self.obj.ViewObject.YLabel = self.view_widget.YLabel.text()
QtCore.Slot(int)
def legendPosChanged(self, idx):
self.obj.ViewObject.LegendLocation = idx
QtCore.Slot(bool)
def legendShowChanged(self, state):
self.obj.ViewObject.Legend = state

View File

@@ -0,0 +1,82 @@
# ***************************************************************************
# * 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
translate = FreeCAD.Qt.translate
class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
"""
The TaskPanel for editing properties of glyph filter
"""
def __init__(self, vobj):
super().__init__(vobj.Object)
# data widget
self.data_widget = QtGui.QWidget()
self.data_widget.show_table = QtGui.QPushButton()
self.data_widget.show_table.setText(translate("FEM", "Show table"))
vbox = QtGui.QVBoxLayout()
vbox.addWidget(self.data_widget.show_table)
vbox.addSpacing(10)
extracts = elv.ExtractLinkView(self.obj, False, self)
vbox.addWidget(extracts)
self.data_widget.setLayout(vbox)
self.data_widget.setWindowTitle(translate("FEM", "Table data"))
self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg"))
self.__init_widgets()
# form made from param and selection widget
self.form = [self.data_widget]
# Setup functions
# ###############
def __init_widgets(self):
# connect data widget
self.data_widget.show_table.clicked.connect(self.showTable)
@QtCore.Slot()
def showTable(self):
self.obj.ViewObject.Proxy.show_visualization()

View File

@@ -79,12 +79,16 @@ class TestObjectCreate(unittest.TestCase):
# gmsh mesh children: group, region, boundary layer --> 3
# result children: mesh result --> 1
# analysis itself is not in analysis group --> 1
# vtk post pipeline children: region, scalar, cut, wrap, glyph --> 5
# vtk python post objects: glyph --> 1
# vtk post pipeline children: region, scalar, cut, wrap, contour --> 5
# vtk python post objects: glyph, 6x data extraction --> 7
subtraction = 15
if vtk_objects_used:
subtraction += 6
subtraction += 12
if not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__):
# remove the 3 data visualization objects that would be in the Analysis
# if they would be available (Lineplot, histogram, table)
subtraction += 3
self.assertEqual(len(doc.Analysis.Group), count_defmake - subtraction)
@@ -92,7 +96,9 @@ class TestObjectCreate(unittest.TestCase):
# have been counted, but will not be executed to create objects
failed = 0
if vtk_objects_used and not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__):
failed += 1
# the 7 objects also counted in subtraction, +3 additional objects that are
# added directly to the analysis
failed += 10
self.assertEqual(len(doc.Objects), count_defmake - failed)
@@ -1167,6 +1173,19 @@ def create_all_fem_objects_doc(doc):
if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
ObjectsFem.makePostFilterGlyph(doc, vres)
# data extraction objects
lp = analysis.addObject(ObjectsFem.makePostLineplot(doc))[0]
lp.addObject(ObjectsFem.makePostLineplotFieldData(doc))
lp.addObject(ObjectsFem.makePostLineplotIndexOverFrames(doc))
hp = analysis.addObject(ObjectsFem.makePostHistogram(doc))[0]
hp.addObject(ObjectsFem.makePostHistogramFieldData(doc))
hp.addObject(ObjectsFem.makePostHistogramIndexOverFrames(doc))
tb = analysis.addObject(ObjectsFem.makePostTable(doc))[0]
tb.addObject(ObjectsFem.makePostTableFieldData(doc))
tb.addObject(ObjectsFem.makePostTableIndexOverFrames(doc))
analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc))
analysis.addObject(ObjectsFem.makeSolverCalculiX(doc))
sol = analysis.addObject(ObjectsFem.makeSolverElmer(doc))[0]

View File

@@ -36,9 +36,27 @@ import FreeCADGui
import FemGui # needed to display the icons in TreeView
from femobjects.base_fempythonobject import _PropHelper
False if FemGui.__name__ else True # flake8, dummy FemGui usage
class _GuiPropHelper(_PropHelper):
"""
Helper class to manage property data inside proxy objects.
Based on the App verison, but viewprovider addProperty does
not take keyword args, hence we use positional arguments here
"""
def __init__(self, **kwds):
super().__init__(**kwds)
def add_to_object(self, obj):
obj.addProperty(self.info["type"], self.info["name"], self.info["group"], self.info["doc"])
obj.setPropertyStatus(self.name, "LockDynamic")
setattr(obj, self.name, self.value)
class VPBaseFemObject:
"""Proxy View Provider for FEM FeaturePythons base constraint."""

View File

@@ -0,0 +1,148 @@
# ***************************************************************************
# * 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
from PySide import QtGui
from femtaskpanels import task_post_extractor
class VPPostExtractor:
"""
A View Provider for extraction of data
"""
def __init__(self, vobj):
vobj.Proxy = self
self._setup_properties(vobj)
def _setup_properties(self, vobj):
pl = vobj.PropertiesList
for prop in self._get_properties():
if not prop.name in pl:
prop.add_to_object(vobj)
def _get_properties(self):
return []
def attach(self, vobj):
self.Object = vobj.Object # used on various places, claim childreens, get icon, etc.
self.ViewObject = vobj
def isShow(self):
return True
def onChanged(self, vobj, prop):
# one of our view properties was changed. Lets inform our parent visualization
# that this happend, as this is the one that needs to redraw
if prop == "Proxy":
return
group = vobj.Object.getParentGroup()
if not group:
return
if hasattr(group.ViewObject, "Proxy") and hasattr(
group.ViewObject.Proxy, "childViewPropertyChanged"
):
group.ViewObject.Proxy.childViewPropertyChanged(vobj, prop)
def setEdit(self, vobj, mode):
# build up the task panel
taskd = task_post_extractor._ExtractorTaskPanel(vobj.Object)
# show it
FreeCADGui.Control.showDialog(taskd)
return True
def unsetEdit(self, vobj, mode=0):
FreeCADGui.Control.closeDialog()
return True
def doubleClicked(self, vobj):
guidoc = FreeCADGui.getDocument(vobj.Object.Document)
# check if another VP is in edit mode and close it then
if guidoc.getInEdit():
FreeCADGui.Control.closeDialog()
guidoc.resetEdit()
guidoc.setEdit(vobj.Object.Name)
return True
def dumps(self):
return None
def loads(self, state):
return None
# To be implemented by subclasses:
# ################################
def get_default_color_property(self):
# Returns the property name to set the default color to.
# Return None if no such property
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_default_field_properties(self):
# Returns the property name to which the default field name should be set
# ret: [FieldProperty, ComponentProperty]
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_kw_args(self):
# Returns the matplotlib plot keyword arguments that represent the
# properties of the object.
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_app_edit_widget(self, post_dialog):
# Returns a widgets for editing the object (not viewprovider!)
# The widget will be part of the provided post_dialog, and
# should use its functionality to inform of changes.
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_view_edit_widget(self, post_dialog):
# Returns a widgets for editing the viewprovider (not object!)
# The widget will be part of the provided post_dialog, and
# should use its functionality to inform of changes.
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_preview(self):
# Returns the preview tuple of icon and label: (QPixmap, str)
# Note: QPixmap in ratio 2:1
raise FreeCAD.Base.FreeCADError("Not implemented")

View File

@@ -0,0 +1,124 @@
# ***************************************************************************
# * 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
import FreeCAD
import FreeCADGui
class VPPostVisualization:
"""
A View Provider for visualization objects
"""
def __init__(self, vobj):
vobj.Proxy = self
self._setup_properties(vobj)
vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
def _setup_properties(self, vobj):
pl = vobj.PropertiesList
for prop in self._get_properties():
if not prop.name in pl:
prop.add_to_object(vobj)
def _get_properties(self):
return []
def attach(self, vobj):
self.Object = vobj.Object
self.ViewObject = vobj
def isShow(self):
# Mark ourself as visible in the tree
return True
def getDisplayModes(self, obj):
return ["Dialog"]
def doubleClicked(self, vobj):
guidoc = FreeCADGui.getDocument(vobj.Object.Document)
# check if another VP is in edit mode and close it then
if guidoc.getInEdit():
FreeCADGui.Control.closeDialog()
guidoc.resetEdit()
# open task dialog
guidoc.setEdit(vobj.Object.Name)
# show visualization
self.show_visualization()
return True
def unsetEdit(self, vobj, mode=0):
FreeCADGui.Control.closeDialog()
return True
def updateData(self, obj, prop):
# If the data changed we need to update the visualization
if prop == "Table":
self.update_visualization()
def onChanged(self, vobj, prop):
# for all property changes we need to update the visualization
self.update_visualization()
def childViewPropertyChanged(self, vobj, prop):
# One of the extractors view properties has changed, we need to
# update the visualization
self.update_visualization()
def dumps(self):
return None
def loads(self, state):
return None
# To be implemented by subclasses:
# ################################
def update_visualization(self):
# The visualization data or any relevant view property has changed,
# and the visualization itself needs to update to reflect that
raise FreeCAD.Base.FreeCADError("Not implemented")
def show_visualization(self):
# Shows the visualization without going into edit mode
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_next_default_color(self):
# Returns the next default color a new object should use
# Returns color in FreeCAD proeprty notation (r,g,b,a)
# If the relevant extractors do not have color properties, this
# can stay unimplemented
raise FreeCAD.Base.FreeCADError("Not implemented")

View File

@@ -0,0 +1,603 @@
# ***************************************************************************
# * 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
from PySide import QtGui, QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
import io
import numpy as np
import matplotlib as mpl
from packaging.version import Version
from vtkmodules.numpy_interface.dataset_adapter import VTKArray
from . import view_base_fempostextractors
from . import view_base_fempostvisualization
from femtaskpanels import task_post_histogram
from . import view_base_femobject
_GuiPropHelper = view_base_femobject._GuiPropHelper
class EditViewWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldViewEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
vobj = self._object.ViewObject
self.widget.Legend.setText(vobj.Legend)
self._post_dialog._enumPropertyToCombobox(vobj, "Hatch", self.widget.Hatch)
self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle)
self.widget.LineWidth.setValue(vobj.LineWidth)
self.widget.HatchDensity.setValue(vobj.HatchDensity)
# setup the color buttons (don't use FreeCADs color button, as this does not work in popups!)
self._setup_color_button(self.widget.BarColor, vobj.BarColor, self.barColorChanged)
self._setup_color_button(self.widget.LineColor, vobj.LineColor, self.lineColorChanged)
self.widget.Legend.editingFinished.connect(self.legendChanged)
self.widget.Hatch.activated.connect(self.hatchPatternChanged)
self.widget.LineStyle.activated.connect(self.lineStyleChanged)
self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged)
self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged)
# sometimes wierd sizes occur with spinboxes
self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height())
self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height())
def _setup_color_button(self, button, fcColor, callback):
barColor = QtGui.QColor(*[v * 255 for v in fcColor])
icon_size = button.iconSize()
icon_size.setWidth(icon_size.width() * 2)
button.setIconSize(icon_size)
pixmap = QtGui.QPixmap(icon_size)
pixmap.fill(barColor)
button.setIcon(pixmap)
action = QtGui.QWidgetAction(button)
diag = QtGui.QColorDialog(barColor, parent=button)
diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True)
diag.accepted.connect(action.trigger)
diag.rejected.connect(action.trigger)
diag.colorSelected.connect(callback)
action.setDefaultWidget(diag)
button.addAction(action)
button.setPopupMode(QtGui.QToolButton.InstantPopup)
@QtCore.Slot(QtGui.QColor)
def lineColorChanged(self, color):
pixmap = QtGui.QPixmap(self.widget.LineColor.iconSize())
pixmap.fill(color)
self.widget.LineColor.setIcon(pixmap)
self._object.ViewObject.LineColor = color.getRgb()
@QtCore.Slot(QtGui.QColor)
def barColorChanged(self, color):
pixmap = QtGui.QPixmap(self.widget.BarColor.iconSize())
pixmap.fill(color)
self.widget.BarColor.setIcon(pixmap)
self._object.ViewObject.BarColor = color.getRgb()
@QtCore.Slot(float)
def lineWidthChanged(self, value):
self._object.ViewObject.LineWidth = value
@QtCore.Slot(float)
def hatchDensityChanged(self, value):
self._object.ViewObject.HatchDensity = value
@QtCore.Slot(int)
def hatchPatternChanged(self, index):
self._object.ViewObject.Hatch = index
@QtCore.Slot(int)
def lineStyleChanged(self, index):
self._object.ViewObject.LineStyle = index
@QtCore.Slot()
def legendChanged(self):
self._object.ViewObject.Legend = self.widget.Legend.text()
class EditFieldAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self.widget.Extract.setChecked(self._object.ExtractFrames)
self.widget.Field.activated.connect(self.fieldChanged)
self.widget.Component.activated.connect(self.componentChanged)
self.widget.Extract.toggled.connect(self.extractionChanged)
@QtCore.Slot(int)
def fieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self._post_dialog._recompute()
@QtCore.Slot(int)
def componentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(bool)
def extractionChanged(self, extract):
self._object.ExtractFrames = extract
self._post_dialog._recompute()
class EditIndexAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self.widget.Index.setValue(self._object.Index)
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self.widget.Index.valueChanged.connect(self.indexChanged)
self.widget.Field.activated.connect(self.fieldChanged)
self.widget.Component.activated.connect(self.componentChanged)
# sometimes wierd sizes occur with spinboxes
self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height())
@QtCore.Slot(int)
def fieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self._post_dialog._recompute()
@QtCore.Slot(int)
def componentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(int)
def indexChanged(self, value):
self._object.Index = value
self._post_dialog._recompute()
class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor):
"""
A View Provider for extraction of 1D field data specialy for histograms
"""
def __init__(self, vobj):
super().__init__(vobj)
vobj.Proxy = self
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyString",
name="Legend",
group="HistogramPlot",
doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"),
value="",
),
_GuiPropHelper(
type="App::PropertyColor",
name="BarColor",
group="HistogramBar",
doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"),
value=(0, 85, 255, 255),
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="Hatch",
group="HistogramBar",
doc=QT_TRANSLATE_NOOP("FEM", "The hatch pattern drawn in the bar"),
value=["None", "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"],
),
_GuiPropHelper(
type="App::PropertyIntegerConstraint",
name="HatchDensity",
group="HistogramBar",
doc=QT_TRANSLATE_NOOP("FEM", "The line width of the hatch)"),
value=(1, 1, 99, 1),
),
_GuiPropHelper(
type="App::PropertyColor",
name="LineColor",
group="HistogramLine",
doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"),
value=(0, 0, 0, 1), # black
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="LineWidth",
group="HistogramLine",
doc=QT_TRANSLATE_NOOP(
"FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"
),
value=(1, 0, 99, 0.1),
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="LineStyle",
group="HistogramLine",
doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"),
value=["None", "-", "--", "-.", ":"],
),
]
return super()._get_properties() + prop
def attach(self, vobj):
self.Object = vobj.Object
self.ViewObject = vobj
def getIcon(self):
return ":/icons/FEM_PostField.svg"
def get_app_edit_widget(self, post_dialog):
return EditFieldAppWidget(self.Object, post_dialog)
def get_view_edit_widget(self, post_dialog):
return EditViewWidget(self.Object, post_dialog)
def get_preview(self):
fig = mpl.pyplot.figure(figsize=(0.4, 0.2), dpi=500)
ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 2, 1])
ax.set_axis_off()
fig.add_axes(ax)
kwargs = self.get_kw_args()
patch = mpl.patches.Rectangle(xy=(0, 0), width=2, height=1, **kwargs)
ax.add_patch(patch)
data = io.BytesIO()
mpl.pyplot.savefig(data, bbox_inches=0, transparent=True)
mpl.pyplot.close()
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data.getvalue())
return (pixmap, self.ViewObject.Legend)
def get_kw_args(self):
# builds kw args from the properties
kwargs = {}
# colors need a workaround, some error occurs with rgba tuple
kwargs["edgecolor"] = self.ViewObject.LineColor
kwargs["facecolor"] = self.ViewObject.BarColor
kwargs["linestyle"] = self.ViewObject.LineStyle
kwargs["linewidth"] = self.ViewObject.LineWidth
if self.ViewObject.Hatch != "None":
kwargs["hatch"] = self.ViewObject.Hatch * self.ViewObject.HatchDensity
return kwargs
def get_default_color_property(self):
return "BarColor"
class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData):
"""
A View Provider for extraction of 1D index over frames data
"""
def __init__(self, vobj):
super().__init__(vobj)
def getIcon(self):
return ":/icons/FEM_PostIndex.svg"
def get_app_edit_widget(self, post_dialog):
return EditIndexAppWidget(self.Object, post_dialog)
class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
"""
A View Provider for Histogram plots
"""
def __init__(self, vobj):
super().__init__(vobj)
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyBool",
name="Cumulative",
group="Histogram",
doc=QT_TRANSLATE_NOOP(
"FEM", "If be the bars shoud show the cumulative sum left to rigth"
),
value=False,
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="Type",
group="Histogram",
doc=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"),
value=["bar", "barstacked", "step", "stepfilled"],
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="BarWidth",
group="Histogram",
doc=QT_TRANSLATE_NOOP(
"FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"
),
value=(0.9, 0, 1, 0.05),
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="HatchLineWidth",
group="Histogram",
doc=QT_TRANSLATE_NOOP("FEM", "The line width of all drawn hatch patterns"),
value=(1, 0, 99, 0.1),
),
_GuiPropHelper(
type="App::PropertyInteger",
name="Bins",
group="Histogram",
doc=QT_TRANSLATE_NOOP("FEM", "The number of bins the data is split into"),
value=10,
),
_GuiPropHelper(
type="App::PropertyString",
name="Title",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"),
value="",
),
_GuiPropHelper(
type="App::PropertyString",
name="XLabel",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"),
value="",
),
_GuiPropHelper(
type="App::PropertyString",
name="YLabel",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"),
value="",
),
_GuiPropHelper(
type="App::PropertyBool",
name="Legend",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"),
value=True,
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="LegendLocation",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"),
value=[
"best",
"upper right",
"upper left",
"lower left",
"lower right",
"right",
"center left",
"center right",
"lower center",
"upper center",
"center",
],
),
]
return prop
def getIcon(self):
return ":/icons/FEM_PostHistogram.svg"
def setEdit(self, vobj, mode):
# build up the task panel
taskd = task_post_histogram._TaskPanel(vobj)
# show it
FreeCADGui.Control.showDialog(taskd)
return True
def show_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
main = FreeCADGui.getMainWindow()
self._plot = Plot.Plot()
self._plot.setWindowTitle(self.Object.Label)
self._plot.setParent(main)
self._plot.setWindowFlags(QtGui.Qt.Dialog)
self._plot.resize(main.size().height() / 2, main.size().height() / 3) # keep it square
self.update_visualization()
self._plot.show()
def get_kw_args(self, obj):
view = obj.ViewObject
if not view or not hasattr(view, "Proxy"):
return {}
if not hasattr(view.Proxy, "get_kw_args"):
return {}
return view.Proxy.get_kw_args()
def update_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
return
self._plot.axes.clear()
bins = self.ViewObject.Bins
# we do not iterate the table, but iterate the children. This makes it possible
# to attribute the correct styles
full_args = []
full_data = []
labels = []
for child in self.Object.Group:
table = child.Table
kwargs = self.get_kw_args(child)
# iterate over the table and plot all
color_factor = np.linspace(1, 0.5, table.GetNumberOfColumns())
legend_multiframe = table.GetNumberOfColumns() > 1
for i in range(table.GetNumberOfColumns()):
# add the kw args, with some slide change over color for multiple frames
args = kwargs.copy()
for key in kwargs:
if "color" in key:
value = np.array(kwargs[key]) * color_factor[i]
args[key] = mpl.colors.to_hex(value)
full_args.append(args)
data = VTKArray(table.GetColumn(i))
full_data.append(data)
# legend labels
if child.ViewObject.Legend:
if not legend_multiframe:
labels.append(child.ViewObject.Legend)
else:
postfix = table.GetColumnName(i).split("-")[-1]
labels.append(child.ViewObject.Legend + " - " + postfix)
else:
legend_prefix = ""
if len(self.Object.Group) > 1:
legend_prefix = child.Source.Label + ": "
labels.append(legend_prefix + table.GetColumnName(i))
args = {}
args["rwidth"] = self.ViewObject.BarWidth
args["cumulative"] = self.ViewObject.Cumulative
args["histtype"] = self.ViewObject.Type
args["label"] = labels
if Version(mpl.__version__) >= Version("3.10.0"):
args["hatch_linewidth"] = self.ViewObject.HatchLineWidth
n, b, patches = self._plot.axes.hist(full_data, bins, **args)
# set the patches view properties.
if len(full_args) == 1:
for patch in patches:
patch.set(**full_args[0])
elif len(full_args) > 1:
for i, args in enumerate(full_args):
for patch in patches[i]:
patch.set(**full_args[i])
# axes decoration
if self.ViewObject.Title:
self._plot.axes.set_title(self.ViewObject.Title)
if self.ViewObject.XLabel:
self._plot.axes.set_xlabel(self.ViewObject.XLabel)
if self.ViewObject.YLabel:
self._plot.axes.set_ylabel(self.ViewObject.YLabel)
if self.ViewObject.Legend and labels:
self._plot.axes.legend(loc=self.ViewObject.LegendLocation)
self._plot.update()
def get_next_default_color(self):
# we use the next color in order. We do not check (yet) if this
# color is already taken
i = len(self.Object.Group)
cmap = mpl.pyplot.get_cmap("tab10")
return cmap(i)

View File

@@ -0,0 +1,583 @@
# ***************************************************************************
# * 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
from PySide import QtGui, QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
import io
import numpy as np
import matplotlib as mpl
from vtkmodules.numpy_interface.dataset_adapter import VTKArray
from . import view_base_fempostextractors
from . import view_base_fempostvisualization
from femtaskpanels import task_post_lineplot
from . import view_base_femobject
_GuiPropHelper = view_base_femobject._GuiPropHelper
class EditViewWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldViewEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
vobj = self._object.ViewObject
self.widget.Legend.setText(vobj.Legend)
self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle)
self._post_dialog._enumPropertyToCombobox(vobj, "MarkerStyle", self.widget.MarkerStyle)
self.widget.LineWidth.setValue(vobj.LineWidth)
self.widget.MarkerSize.setValue(vobj.MarkerSize)
self._setup_color_button(self.widget.Color, vobj.Color, self.colorChanged)
self.widget.Legend.editingFinished.connect(self.legendChanged)
self.widget.MarkerStyle.activated.connect(self.markerStyleChanged)
self.widget.LineStyle.activated.connect(self.lineStyleChanged)
self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged)
self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged)
# sometimes wierd sizes occur with spinboxes
self.widget.MarkerSize.setMaximumHeight(self.widget.MarkerStyle.sizeHint().height())
self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height())
def _setup_color_button(self, button, fcColor, callback):
barColor = QtGui.QColor(*[v * 255 for v in fcColor])
icon_size = button.iconSize()
icon_size.setWidth(icon_size.width() * 2)
button.setIconSize(icon_size)
pixmap = QtGui.QPixmap(icon_size)
pixmap.fill(barColor)
button.setIcon(pixmap)
action = QtGui.QWidgetAction(button)
diag = QtGui.QColorDialog(barColor, parent=button)
diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True)
diag.accepted.connect(action.trigger)
diag.rejected.connect(action.trigger)
diag.colorSelected.connect(callback)
action.setDefaultWidget(diag)
button.addAction(action)
button.setPopupMode(QtGui.QToolButton.InstantPopup)
@QtCore.Slot(QtGui.QColor)
def colorChanged(self, color):
pixmap = QtGui.QPixmap(self.widget.Color.iconSize())
pixmap.fill(color)
self.widget.Color.setIcon(pixmap)
self._object.ViewObject.Color = color.getRgb()
@QtCore.Slot(float)
def lineWidthChanged(self, value):
self._object.ViewObject.LineWidth = value
@QtCore.Slot(float)
def markerSizeChanged(self, value):
self._object.ViewObject.MarkerSize = value
@QtCore.Slot(int)
def markerStyleChanged(self, index):
self._object.ViewObject.MarkerStyle = index
@QtCore.Slot(int)
def lineStyleChanged(self, index):
self._object.ViewObject.LineStyle = index
@QtCore.Slot()
def legendChanged(self):
self._object.ViewObject.Legend = self.widget.Legend.text()
class EditFieldAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.XField)
self._post_dialog._enumPropertyToCombobox(
self._object, "XComponent", self.widget.XComponent
)
self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField)
self._post_dialog._enumPropertyToCombobox(
self._object, "YComponent", self.widget.YComponent
)
self.widget.Extract.setChecked(self._object.ExtractFrames)
self.widget.XField.activated.connect(self.xFieldChanged)
self.widget.XComponent.activated.connect(self.xComponentChanged)
self.widget.YField.activated.connect(self.yFieldChanged)
self.widget.YComponent.activated.connect(self.yComponentChanged)
self.widget.Extract.toggled.connect(self.extractionChanged)
@QtCore.Slot(int)
def xFieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(
self._object, "XComponent", self.widget.XComponent
)
self._post_dialog._recompute()
@QtCore.Slot(int)
def xComponentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(int)
def yFieldChanged(self, index):
self._object.YField = index
self._post_dialog._enumPropertyToCombobox(
self._object, "YComponent", self.widget.YComponent
)
self._post_dialog._recompute()
@QtCore.Slot(int)
def yComponentChanged(self, index):
self._object.YComponent = index
self._post_dialog._recompute()
@QtCore.Slot(bool)
def extractionChanged(self, extract):
self._object.ExtractFrames = extract
self._post_dialog._recompute()
class EditIndexAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotIndexAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self.widget.Index.setValue(self._object.Index)
self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField)
self._post_dialog._enumPropertyToCombobox(
self._object, "YComponent", self.widget.YComponent
)
self.widget.Index.valueChanged.connect(self.indexChanged)
self.widget.YField.activated.connect(self.yFieldChanged)
self.widget.YComponent.activated.connect(self.yComponentChanged)
# sometimes wierd sizes occur with spinboxes
self.widget.Index.setMaximumHeight(self.widget.YField.sizeHint().height())
@QtCore.Slot(int)
def indexChanged(self, value):
self._object.Index = value
self._post_dialog._recompute()
@QtCore.Slot(int)
def yFieldChanged(self, index):
self._object.YField = index
self._post_dialog._enumPropertyToCombobox(
self._object, "YComponent", self.widget.YComponent
)
self._post_dialog._recompute()
@QtCore.Slot(int)
def yComponentChanged(self, index):
self._object.YComponent = index
self._post_dialog._recompute()
class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor):
"""
A View Provider for extraction of 2D field data specialy for histograms
"""
def __init__(self, vobj):
super().__init__(vobj)
vobj.Proxy = self
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyString",
name="Legend",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"),
value="",
),
_GuiPropHelper(
type="App::PropertyColor",
name="Color",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The color the line and the markers are drawn with"),
value=(0, 85, 255, 255),
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="LineStyle",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"),
value=["-", "--", "-.", ":", "None"],
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="LineWidth",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The width the line is drawn with"),
value=(1, 0.1, 99, 0.1),
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="MarkerStyle",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The style the data markers are drawn with"),
value=["None", "*", "+", "s", ".", "o", "x"],
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="MarkerSize",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The size the data markers are drawn in"),
value=(5, 0.1, 99, 0.1),
),
]
return super()._get_properties() + prop
def attach(self, vobj):
self.Object = vobj.Object
self.ViewObject = vobj
def getIcon(self):
return ":/icons/FEM_PostField.svg"
def get_app_edit_widget(self, post_dialog):
return EditFieldAppWidget(self.Object, post_dialog)
def get_view_edit_widget(self, post_dialog):
return EditViewWidget(self.Object, post_dialog)
def get_preview(self):
# Returns the preview tuple of icon and label: (QPixmap, str)
# Note: QPixmap in ratio 2:1
fig = mpl.pyplot.figure(figsize=(0.2, 0.1), dpi=1000)
ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 1.0, 1.0])
ax.set_axis_off()
fig.add_axes(ax)
kwargs = self.get_kw_args()
kwargs["markevery"] = [1]
ax.plot([0, 0.5, 1], [0.5, 0.5, 0.5], **kwargs)
data = io.BytesIO()
mpl.pyplot.savefig(data, bbox_inches=0, transparent=True)
mpl.pyplot.close()
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data.getvalue())
return (pixmap, self.ViewObject.Legend)
def get_kw_args(self):
# builds kw args from the properties
kwargs = {}
# colors need a workaround, some error occurs with rgba tuple
kwargs["color"] = self.ViewObject.Color
kwargs["markeredgecolor"] = self.ViewObject.Color
kwargs["markerfacecolor"] = self.ViewObject.Color
kwargs["linestyle"] = self.ViewObject.LineStyle
kwargs["linewidth"] = self.ViewObject.LineWidth
kwargs["marker"] = self.ViewObject.MarkerStyle
kwargs["markersize"] = self.ViewObject.MarkerSize
return kwargs
def get_default_color_property(self):
return "Color"
class VPPostLineplotIndexOverFrames(VPPostLineplotFieldData):
"""
A View Provider for extraction of 2D index over frames data
"""
def __init__(self, vobj):
super().__init__(vobj)
def getIcon(self):
return ":/icons/FEM_PostIndex.svg"
def get_app_edit_widget(self, post_dialog):
return EditIndexAppWidget(self.Object, post_dialog)
class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
"""
A View Provider for Lineplot plots
"""
def __init__(self, vobj):
super().__init__(vobj)
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyBool",
name="Grid",
group="Lineplot",
doc=QT_TRANSLATE_NOOP(
"FEM", "If be the bars shoud show the cumulative sum left to rigth"
),
value=True,
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="Scale",
group="Lineplot",
doc=QT_TRANSLATE_NOOP("FEM", "The scale the axis are drawn in"),
value=["linear", "semi-log x", "semi-log y", "log"],
),
_GuiPropHelper(
type="App::PropertyString",
name="Title",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"),
value="",
),
_GuiPropHelper(
type="App::PropertyString",
name="XLabel",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"),
value="",
),
_GuiPropHelper(
type="App::PropertyString",
name="YLabel",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"),
value="",
),
_GuiPropHelper(
type="App::PropertyBool",
name="Legend",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"),
value=True,
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="LegendLocation",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"),
value=[
"best",
"upper right",
"upper left",
"lower left",
"lower right",
"right",
"center left",
"center right",
"lower center",
"upper center",
"center",
],
),
]
return prop
def getIcon(self):
return ":/icons/FEM_PostLineplot.svg"
def setEdit(self, vobj, mode):
# build up the task panel
taskd = task_post_lineplot._TaskPanel(vobj)
# show it
FreeCADGui.Control.showDialog(taskd)
return True
def show_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
main = FreeCADGui.getMainWindow()
self._plot = Plot.Plot()
self._plot.setWindowTitle(self.Object.Label)
self._plot.setParent(main)
self._plot.setWindowFlags(QtGui.Qt.Dialog)
self._plot.resize(
main.size().height() / 2, main.size().height() / 3
) # keep the aspect ratio
self.update_visualization()
self._plot.show()
def get_kw_args(self, obj):
view = obj.ViewObject
if not view or not hasattr(view, "Proxy"):
return {}
if not hasattr(view.Proxy, "get_kw_args"):
return {}
return view.Proxy.get_kw_args()
def update_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
return
self._plot.axes.clear()
# we do not iterate the table, but iterate the children. This makes it possible
# to attribute the correct styles
plotted = False
for child in self.Object.Group:
table = child.Table
kwargs = self.get_kw_args(child)
# iterate over the table and plot all (note: column 0 is always X!)
color_factor = np.linspace(1, 0.5, int(table.GetNumberOfColumns() / 2))
legend_multiframe = table.GetNumberOfColumns() > 2
for i in range(0, table.GetNumberOfColumns(), 2):
plotted = True
# add the kw args, with some slide change over color for multiple frames
tmp_args = {}
for key in kwargs:
if "color" in key:
value = np.array(kwargs[key]) * color_factor[int(i / 2)]
tmp_args[key] = mpl.colors.to_hex(value)
else:
tmp_args[key] = kwargs[key]
xdata = VTKArray(table.GetColumn(i))
ydata = VTKArray(table.GetColumn(i + 1))
# ensure points are visible if it is a single datapoint
if len(xdata) == 1 and tmp_args["marker"] == "None":
tmp_args["marker"] = "o"
# legend labels
if child.ViewObject.Legend:
if not legend_multiframe:
label = child.ViewObject.Legend
else:
postfix = table.GetColumnName(i + 1).split("-")[-1]
label = child.ViewObject.Legend + " - " + postfix
else:
legend_prefix = ""
if len(self.Object.Group) > 1:
legend_prefix = child.Source.Label + ": "
label = legend_prefix + table.GetColumnName(i + 1)
match self.ViewObject.Scale:
case "log":
self._plot.axes.loglog(xdata, ydata, **tmp_args, label=label)
case "semi-log x":
self._plot.axes.semilogx(xdata, ydata, **tmp_args, label=label)
case "semi-log y":
self._plot.axes.semilogy(xdata, ydata, **tmp_args, label=label)
case _:
self._plot.axes.plot(xdata, ydata, **tmp_args, label=label)
if self.ViewObject.Title:
self._plot.axes.set_title(self.ViewObject.Title)
if self.ViewObject.XLabel:
self._plot.axes.set_xlabel(self.ViewObject.XLabel)
if self.ViewObject.YLabel:
self._plot.axes.set_ylabel(self.ViewObject.YLabel)
if self.ViewObject.Legend and plotted:
self._plot.axes.legend(loc=self.ViewObject.LegendLocation)
self._plot.axes.grid(self.ViewObject.Grid)
self._plot.update()
def get_next_default_color(self):
# we use the next color in order. We do not check (yet) if this
# color is already taken
i = len(self.Object.Group)
cmap = mpl.pyplot.get_cmap("tab10")
return cmap(i)

View File

@@ -0,0 +1,291 @@
# ***************************************************************************
# * 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 table ViewProvider for the document object"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package view_post_table
# \ingroup FEM
# \brief view provider for post table object
import FreeCAD
import FreeCADGui
from PySide import QtGui, QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
from . import view_base_fempostextractors
from . import view_base_fempostvisualization
from femtaskpanels import task_post_table
from femguiutils import vtk_table_view as vtv
from . import view_base_femobject
_GuiPropHelper = view_base_femobject._GuiPropHelper
class EditViewWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostTableFieldViewEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self.widget.Name.setText(self._object.ViewObject.Name)
self.widget.Name.editingFinished.connect(self.legendChanged)
@QtCore.Slot()
def legendChanged(self):
self._object.ViewObject.Name = self.widget.Name.text()
class EditFieldAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up (we reuse histogram, as we need the exact same)
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self.widget.Extract.setChecked(self._object.ExtractFrames)
self.widget.Field.activated.connect(self.fieldChanged)
self.widget.Component.activated.connect(self.componentChanged)
self.widget.Extract.toggled.connect(self.extractionChanged)
@QtCore.Slot(int)
def fieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self._post_dialog._recompute()
@QtCore.Slot(int)
def componentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(bool)
def extractionChanged(self, extract):
self._object.ExtractFrames = extract
self._post_dialog._recompute()
class EditIndexAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up (we reuse histogram, as we need the exact same)
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self.widget.Index.setValue(self._object.Index)
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self.widget.Index.valueChanged.connect(self.indexChanged)
self.widget.Field.activated.connect(self.fieldChanged)
self.widget.Component.activated.connect(self.componentChanged)
# sometimes wierd sizes occur with spinboxes
self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height())
@QtCore.Slot(int)
def fieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self._post_dialog._recompute()
@QtCore.Slot(int)
def componentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(int)
def indexChanged(self, value):
self._object.Index = value
self._post_dialog._recompute()
class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor):
"""
A View Provider for extraction of 1D field data specialy for tables
"""
def __init__(self, vobj):
super().__init__(vobj)
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyString",
name="Name",
group="Table",
doc=QT_TRANSLATE_NOOP(
"FEM", "The name used in the table header. Default name is used if empty"
),
value="",
),
]
return super()._get_properties() + prop
def attach(self, vobj):
self.Object = vobj.Object
self.ViewObject = vobj
def getIcon(self):
return ":/icons/FEM_PostField.svg"
def get_app_edit_widget(self, post_dialog):
return EditFieldAppWidget(self.Object, post_dialog)
def get_view_edit_widget(self, post_dialog):
return EditViewWidget(self.Object, post_dialog)
def get_preview(self):
name = QT_TRANSLATE_NOOP("FEM", "default")
if self.ViewObject.Name:
name = self.ViewObject.Name
return (QtGui.QPixmap(), name)
def get_default_color_property(self):
return None
class VPPostTableIndexOverFrames(VPPostTableFieldData):
"""
A View Provider for extraction of 1D index over frames data
"""
def __init__(self, vobj):
super().__init__(vobj)
def getIcon(self):
return ":/icons/FEM_PostIndex.svg"
def get_app_edit_widget(self, post_dialog):
return EditIndexAppWidget(self.Object, post_dialog)
class VPPostTable(view_base_fempostvisualization.VPPostVisualization):
"""
A View Provider for Table plots
"""
def __init__(self, vobj):
super().__init__(vobj)
def getIcon(self):
return ":/icons/FEM_PostSpreadsheet.svg"
def setEdit(self, vobj, mode):
# build up the task panel
taskd = task_post_table._TaskPanel(vobj)
# show it
FreeCADGui.Control.showDialog(taskd)
return True
def show_visualization(self):
if not hasattr(self, "_tableview") or not self._tableview:
main = FreeCADGui.getMainWindow()
self._tableModel = vtv.VtkTableModel()
self._tableview = vtv.VtkTableView(self._tableModel)
self._tableview.setWindowTitle(self.Object.Label)
self._tableview.setParent(main)
self._tableview.setWindowFlags(QtGui.Qt.Dialog)
self._tableview.resize(
main.size().height() / 2, main.size().height() / 3
) # keep the aspect ratio
self.update_visualization()
self._tableview.show()
def update_visualization(self):
if not hasattr(self, "_tableModel") or not self._tableModel:
return
# we collect the header names from the viewproviders
table = self.Object.Table
header = {}
for child in self.Object.Group:
if not child.Source:
continue
new_name = child.ViewObject.Name
if new_name:
# this child uses a custom name. We try to find all
# columns that are from this child and use custom header for it
for i in range(table.GetNumberOfColumns()):
if child.Source.Name in table.GetColumnName(i):
header[table.GetColumnName(i)] = new_name
self._tableModel.setTable(self.Object.Table, header)

View File

@@ -28,7 +28,7 @@ import sys
try:
import matplotlib
matplotlib.use("Qt5Agg")
matplotlib.use("QtAgg")
# Force matplotlib to use PySide backend by temporarily unloading PyQt
if "PyQt5.QtCore" in sys.modules:
@@ -36,10 +36,11 @@ try:
import matplotlib.pyplot as plt
import PyQt5.QtCore
else:
print("default matplotlib import")
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
except ImportError: