FEM: Add table post data visualization

This commit is contained in:
Stefan Tröger
2025-04-20 14:18:30 +02:00
parent d3fa7ad8f0
commit d86040dd58
21 changed files with 777 additions and 405 deletions

View File

@@ -223,6 +223,7 @@ if(BUILD_FEM_VTK_PYTHON)
femobjects/post_extract2D.py
femobjects/post_histogram.py
femobjects/post_lineplot.py
femobjects/post_table.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -637,6 +638,7 @@ if(BUILD_FEM_VTK_PYTHON)
femtaskpanels/task_post_glyphfilter.py
femtaskpanels/task_post_histogram.py
femtaskpanels/task_post_lineplot.py
femtaskpanels/task_post_table.py
femtaskpanels/task_post_extractor.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -666,6 +668,7 @@ SET(FemGuiViewProvider_SRCS
femviewprovider/view_base_femmeshelement.py
femviewprovider/view_base_femobject.py
femviewprovider/view_base_fempostvisualization.py
femviewprovider/view_base_fempostextractors.py
femviewprovider/view_constant_vacuumpermittivity.py
femviewprovider/view_constraint_bodyheatsource.py
femviewprovider/view_constraint_centrif.py
@@ -704,6 +707,7 @@ if(BUILD_FEM_VTK_PYTHON)
femviewprovider/view_post_extract.py
femviewprovider/view_post_histogram.py
femviewprovider/view_post_lineplot.py
femviewprovider/view_post_table.py
)
endif(BUILD_FEM_VTK_PYTHON)

View File

@@ -451,6 +451,7 @@ SET(FemGuiPythonUI_SRCS
Resources/ui/PostLineplotFieldViewEdit.ui
Resources/ui/PostLineplotFieldAppEdit.ui
Resources/ui/PostLineplotIndexAppEdit.ui
Resources/ui/PostTableFieldViewEdit.ui
)
ADD_CUSTOM_TARGET(FemPythonUi ALL

View File

@@ -65,7 +65,7 @@
<item row="2" column="1">
<widget class="QCheckBox" name="Extract">
<property name="text">
<string>One field for all frames</string>
<string>One field for each frames</string>
</property>
</widget>
</item>

View File

@@ -0,0 +1,43 @@
<?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>259</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"/>
</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

@@ -770,6 +770,48 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"):
return obj
def makePostTable(doc, name="Table"):
"""makePostTable(document, [name]):
creates a FEM post processing histogram plot
"""
obj = doc.addObject("App::FeaturePython", name)
from femobjects import post_table
post_table.PostTable(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_table
view_post_table.VPPostTable(obj.ViewObject)
return obj
def makePostTableFieldData(doc, name="FieldData1D"):
"""makePostTableFieldData(document, [name]):
creates a FEM post processing data extractor for 1D Field data
"""
obj = doc.addObject("App::FeaturePython", name)
from femobjects import post_table
post_table.PostTableFieldData(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_table
view_post_table.VPPostTableFieldData(obj.ViewObject)
return obj
def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"):
"""makePostTableIndexOverFrames(document, [name]):
creates a FEM post processing data extractor for 1D Field data
"""
obj = doc.addObject("App::FeaturePython", name)
from femobjects import post_table
post_table.PostTableIndexOverFrames(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_table
view_post_table.VPPostTableIndexOverFrames(obj.ViewObject)
return obj
# ********* solver objects ***********************************************************************
def makeEquationDeformation(doc, base_solver=None, name="Deformation"):
"""makeEquationDeformation(document, [base_solver], [name]):

View File

@@ -1293,4 +1293,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
# setup all visualization commands (register by importing)
import femobjects.post_lineplot
import femobjects.post_histogram
import femobjects.post_table
post_visualization.setup_commands("FEM_PostVisualization")

View File

@@ -252,11 +252,13 @@ class _SummaryWidget(QtGui.QWidget):
self.extrButton.setIcon(extractor.ViewObject.Icon)
self.viewButton = self._button(extr_repr[1])
size = self.viewButton.iconSize()
size.setWidth(size.width()*2)
self.viewButton.setIconSize(size)
self.viewButton.setIcon(extr_repr[0])
if not extr_repr[0].isNull():
size = self.viewButton.iconSize()
size.setWidth(size.width()*2)
self.viewButton.setIconSize(size)
self.viewButton.setIcon(extr_repr[0])
else:
self.viewButton.setIconSize(QtCore.QSize(0,0))
self.rmButton = QtGui.QToolButton(self)
self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete"))

View File

@@ -34,14 +34,23 @@ from PySide import QtCore
class VtkTableModel(QtCore.QAbstractTableModel):
# Simple table model. Only supports single component columns
# One can supply a header_names dict to replace the table column names
# in the header. It is a dict "column_idx (int)" to "new name"" or
# "orig_name (str)" to "new name"
def __init__(self):
def __init__(self, header_names = None):
super().__init__()
self._table = None
if header_names:
self._header = header_names
else:
self._header = {}
def setTable(self, table):
def setTable(self, table, header_names = None):
self.beginResetModel()
self._table = table
if header_names:
self._header = header_names
self.endResetModel()
def rowCount(self, index):
@@ -70,7 +79,14 @@ class VtkTableModel(QtCore.QAbstractTableModel):
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._table.GetColumnName(section)
if section in self._header:
return self._header[section]
name = self._table.GetColumnName(section)
if name in self._header:
return self._header[name]
return name
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return section

View File

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

View File

@@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief Post processing plot displaying lines
import FreeCAD
from . import base_fempostextractors
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
@@ -176,9 +178,9 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D):
frame_array.SetTuple(i, idx, array)
if frame_array.GetNumberOfComponents() > 1:
frame_array.SetName(f"{obj.XField} ({obj.XComponent})")
frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}")
else:
frame_array.SetName(f"{obj.XField}")
frame_array.SetName(f"{obj.XField} @Idx {obj.Index}")
self._x_array_component_to_table(obj, frame_array, table)

View File

@@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief Post processing plot displaying lines
import FreeCAD
from . import base_fempostextractors
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
@@ -207,9 +209,9 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D):
frame_x_array.SetName("Frames")
if frame_y_array.GetNumberOfComponents() > 1:
frame_y_array.SetName(f"{obj.YField} ({obj.YComponent})")
frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}")
else:
frame_y_array.SetName(obj.YField)
frame_y_array.SetName(f"{obj.YField} @Idx {obj.Index}")
table.AddColumn(frame_x_array)
self._y_array_component_to_table(obj, frame_y_array, table)

View File

@@ -29,17 +29,11 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief Post processing plot displaying histograms
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
from . import base_fempostextractors
from . import base_fempostvisualizations
from . import post_extract1D
from vtkmodules.vtkCommonCore import vtkDoubleArray
from vtkmodules.vtkCommonDataModel import vtkTable
from femguiutils import post_visualization
# register visualization and extractors
@@ -85,6 +79,7 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D):
"""
VisualizationType = "Histogram"
class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D):
"""
A 1D index extraction for histogram.
@@ -96,57 +91,11 @@ class PostHistogram(base_fempostvisualizations.PostVisualization):
"""
A post processing plot for showing extracted data as histograms
"""
VisualizationType = "Histogram"
def __init__(self, obj):
super().__init__(obj)
obj.addExtension("App::GroupExtensionPython")
def _get_properties(self):
prop = [
_PropHelper(
type="Fem::PropertyPostDataObject",
name="Table",
group="Base",
doc="The data table that stores the plotted data, one column per histogram",
value=vtkTable(),
),
]
return super()._get_properties() + prop
def onChanged(self, obj, prop):
if prop == "Group":
# check if all objects are allowed
children = obj.Group
for child in obj.Group:
if not is_histogram_extractor(child):
FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added")
children.remove(child)
if len(obj.Group) != len(children):
obj.Group = children
def execute(self, obj):
# during execution we collect all child data into our table
table = vtkTable()
for child in obj.Group:
c_table = child.Table
for i in range(c_table.GetNumberOfColumns()):
c_array = c_table.GetColumn(i)
# TODO: check which array type it is and use that one
array = vtkDoubleArray()
array.DeepCopy(c_array)
array.SetName(f"{child.Source.Label}: {c_array.GetName()}")
table.AddColumn(array)
obj.Table = table
return False

View File

@@ -29,17 +29,10 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief Post processing plot displaying lines
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
from . import base_fempostextractors
from . import base_fempostvisualizations
from . import post_extract2D
from vtkmodules.vtkCommonCore import vtkDoubleArray
from vtkmodules.vtkCommonDataModel import vtkTable
from femguiutils import post_visualization
# register visualization and extractors
@@ -85,6 +78,7 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D):
"""
VisualizationType = "Lineplot"
class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D):
"""
A 2D index extraction for lineplot.
@@ -97,56 +91,7 @@ class PostLineplot(base_fempostvisualizations.PostVisualization):
"""
A post processing plot for showing extracted data as line plots
"""
VisualizationType = "Lineplot"
def __init__(self, obj):
super().__init__(obj)
obj.addExtension("App::GroupExtensionPython")
def _get_properties(self):
prop = [
_PropHelper(
type="Fem::PropertyPostDataObject",
name="Table",
group="Base",
doc="The data table that stores the plotted data, two columns per lineplot (x,y)",
value=vtkTable(),
),
]
return super()._get_properties() + prop
def onChanged(self, obj, prop):
if prop == "Group":
# check if all objects are allowed
children = obj.Group
for child in obj.Group:
if not is_lineplot_extractor(child):
FreeCAD.Console.PrintWarning(f"{child.Label} is not a data lineplot data extraction object, cannot be added")
children.remove(child)
if len(obj.Group) != len(children):
obj.Group = children
def execute(self, obj):
# during execution we collect all child data into our table
table = vtkTable()
for child in obj.Group:
c_table = child.Table
for i in range(c_table.GetNumberOfColumns()):
c_array = c_table.GetColumn(i)
# TODO: check which array type it is and use that one
array = vtkDoubleArray()
array.DeepCopy(c_array)
array.SetName(f"{child.Source.Label}: {c_array.GetName()}")
table.AddColumn(array)
obj.Table = table
return False

View File

@@ -21,191 +21,76 @@
# * *
# ***************************************************************************
__title__ = "FreeCAD post line plot"
__title__ = "FreeCAD post table"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_lineplot
## @package post_table
# \ingroup FEM
# \brief Post processing plot displaying lines
# \brief Post processing plot displaying tables
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
# helper function to extract plot object type
def _get_extraction_subtype(obj):
if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"):
return obj.Proxy.Type
return "unknown"
from . import base_fempostextractors
from . import base_fempostvisualizations
from . import post_extract1D
class PostLinePlot(base_fempythonobject.BaseFemPythonObject):
from femguiutils import post_visualization
# register visualization and extractors
post_visualization.register_visualization("Table",
":/icons/FEM_PostSpreadsheet.svg",
"ObjectsFem",
"makePostTable")
post_visualization.register_extractor("Table",
"TableFieldData",
":/icons/FEM_PostField.svg",
"1D",
"Field",
"ObjectsFem",
"makePostTableFieldData")
post_visualization.register_extractor("Table",
"TableIndexOverFrames",
":/icons/FEM_PostIndex.svg",
"1D",
"Index",
"ObjectsFem",
"makePostTableIndexOverFrames")
# Implementation
# ##############
def is_table_extractor(obj):
if not base_fempostextractors.is_extractor_object(obj):
return False
if not hasattr(obj.Proxy, "VisualizationType"):
return False
return obj.Proxy.VisualizationType == "Table"
class PostTableFieldData(post_extract1D.PostFieldData1D):
"""
A post processing extraction for plotting lines
A 1D Field extraction for tables.
"""
Type = "App::FeaturePython"
def __init__(self, obj):
super().__init__(obj)
obj.addExtension("App::GroupExtension")
self._setup_properties(obj)
def _setup_properties(self, obj):
self.ExtractionType = "LinePlot"
pl = obj.PropertiesList
for prop in self._get_properties():
if not prop.Name in pl:
prop.add_to_object(obj)
def _get_properties(self):
prop = []
return prop
def onDocumentRestored(self, obj):
self._setup_properties(self, obj):
def onChanged(self, obj, prop):
if prop == "Group":
# check if all objects are allowed
children = obj.Group
for child in obj.Group:
if _get_extraction_subtype(child) not in ["Line"]:
children.remove(child)
if len(obj.Group) != len(children):
obj.Group = children
VisualizationType = "Table"
class PostPlotLine(base_fempythonobject.BaseFemPythonObject):
class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D):
"""
A 1D index extraction for table.
"""
VisualizationType = "Table"
Type = "App::FeaturePython"
def __init__(self, obj):
super().__init__(obj)
self._setup_properties(obj)
class PostTable(base_fempostvisualizations.PostVisualization):
"""
A post processing plot for showing extracted data as tables
"""
VisualizationType = "Table"
def _setup_properties(self, obj):
self.ExtractionType = "Line"
pl = obj.PropertiesList
for prop in self._get_properties():
if not prop.Name in pl:
prop.add_to_object(obj)
def _get_properties(self):
prop = [
_PropHelper(
type="App::PropertyLink",
name="Source",
group="Line",
doc="The data source, the line uses",
value=None,
),
_PropHelper(
type="App::PropertyEnumeration",
name="XField",
group="X Data",
doc="The field to use as X data",
value=None,
),
_PropHelper(
type="App::PropertyEnumeration",
name="XComponent",
group="X Data",
doc="Which part of the X field vector to use for the X axis",
value=None,
),
_PropHelper(
type="App::PropertyEnumeration",
name="YField",
group="Y Data",
doc="The field to use as Y data for the line plot",
value=None,
),
_PropHelper(
type="App::PropertyEnumeration",
name="YComponent",
group="Y Data",
doc="Which part of the Y field vector to use for the X axis",
value=None,
),
]
return prop
def onDocumentRestored(self, obj):
self._setup_properties(self, obj):
def onChanged(self, obj, prop):
if prop == "Source":
# check if the source is a Post object
if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"):
FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject")
obj.XField = []
obj.YField = []
obj.Source = None
if prop == "XField":
if not obj.Source:
obj.XComponent = []
return
point_data = obj.Source.Data.GetPointData()
if not point_data.HasArray(obj.XField):
obj.XComponent = []
return
match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents:
case 1:
obj.XComponent = ["Not a vector"]
case 2:
obj.XComponent = ["Magnitude", "X", "Y"]
case 3:
obj.XComponent = ["Magnitude", "X", "Y", "Z"]
if prop == "YField":
if not obj.Source:
obj.YComponent = []
return
point_data = obj.Source.Data.GetPointData()
if not point_data.HasArray(obj.YField):
obj.YComponent = []
return
match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents:
case 1:
obj.YComponent = ["Not a vector"]
case 2:
obj.YComponent = ["Magnitude", "X", "Y"]
case 3:
obj.YComponent = ["Magnitude", "X", "Y", "Z"]
def onExecute(self, obj):
# we need to make sure that we show the correct fields to the user as option for data extraction
fields = []
if obj.Source:
point_data = obj.Source.Data.GetPointData()
fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())]
current_X = obj.XField
obj.XField = fields
if current_X in fields:
obj.XField = current_X
current_Y = obj.YField
obj.YField = fields
if current_Y in fields:
obj.YField = current_Y
return True

View File

@@ -60,6 +60,13 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel):
if button == QtGui.QDialogButtonBox.Apply:
self.obj.Document.recompute()
def accept(self):
print("accept")
return super().accept()
def reject(self):
print("reject")
return super().reject()
# Helper functions
# ################

View File

@@ -0,0 +1,94 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD FEM histogram plot task panel"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package task_post_histogram
# \ingroup FEM
# \brief task panel for post histogram plot
from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
from . import base_fempostpanel
from femguiutils import extract_link_view as elv
from femguiutils import vtk_table_view
class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
"""
The TaskPanel for editing properties of glyph filter
"""
def __init__(self, vobj):
super().__init__(vobj.Object)
# data widget
self.data_widget = QtGui.QWidget()
self.data_widget.show_table = QtGui.QPushButton()
self.data_widget.show_table.setText("Show table")
vbox = QtGui.QVBoxLayout()
vbox.addWidget(self.data_widget.show_table)
vbox.addSpacing(10)
extracts = elv.ExtractLinkView(self.obj, False, self)
vbox.addWidget(extracts)
self.data_widget.setLayout(vbox)
self.data_widget.setWindowTitle("Table data")
self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg"))
# histogram parameter widget
#self.view_widget = FreeCADGui.PySideUic.loadUi(
# FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostTable.ui"
#)
#self.view_widget.setWindowTitle("Table view settings")
#self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostTable.svg"))
self.__init_widgets()
# form made from param and selection widget
self.form = [self.data_widget]
# Setup functions
# ###############
def __init_widgets(self):
# connect data widget
self.data_widget.show_table.clicked.connect(self.showTable)
# set current values to view widget
viewObj = self.obj.ViewObject
@QtCore.Slot()
def showTable(self):
self.obj.ViewObject.Proxy.show_visualization()

View File

@@ -66,7 +66,7 @@ class VPPostExtractor:
def onChanged(self, vobj, prop):
# one of our view properties was changed. Lets inform our parent plot
# one of our view properties was changed. Lets inform our parent visualization
# that this happend, as this is the one that needs to redraw
if prop == "Proxy":
@@ -92,6 +92,10 @@ class VPPostExtractor:
return True
def unsetEdit(self, vobj, mode=0):
FreeCADGui.Control.closeDialog()
return True
def doubleClicked(self, vobj):
guidoc = FreeCADGui.getDocument(vobj.Object.Document)
@@ -104,10 +108,20 @@ class VPPostExtractor:
return True
def dumps(self):
return None
def loads(self, state):
return None
# To be implemented by subclasses:
# ################################
def get_kw_args(self):
# should return the plot keyword arguments that represent the properties
# of the object
return {}
# Returns the matplotlib plot keyword arguments that represent the
# properties of the object.
raise FreeCAD.Base.FreeCADError("Not implemented")
def get_app_edit_widget(self, post_dialog):
# Returns a widgets for editing the object (not viewprovider!)
@@ -125,10 +139,3 @@ class VPPostExtractor:
# Returns the preview tuple of icon and label: (QPixmap, str)
# Note: QPixmap in ratio 2:1
raise FreeCAD.Base.FreeCADError("Not implemented")
def dumps(self):
return None
def loads(self, state):
return None

View File

@@ -45,6 +45,8 @@ class VPPostVisualization:
def __init__(self, vobj):
vobj.Proxy = self
self._setup_properties(vobj)
vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
def _setup_properties(self, vobj):
pl = vobj.PropertiesList
@@ -52,16 +54,21 @@ class VPPostVisualization:
if not prop.name in pl:
prop.add_to_object(vobj)
def _get_properties(self):
return []
def attach(self, vobj):
self.Object = vobj.Object
self.ViewObject = vobj
def isShow(self):
# Mark ourself as visible in the tree
return True
def doubleClicked(self,vobj):
guidoc = FreeCADGui.getDocument(vobj.Object.Document)
@@ -71,21 +78,47 @@ class VPPostVisualization:
FreeCADGui.Control.closeDialog()
guidoc.resetEdit()
# open task dialog
guidoc.setEdit(vobj.Object.Name)
# show visualization
self.show_visualization()
return True
def show_visualization(self):
# shows the visualization without going into edit mode
# to be implemented by subclasses
pass
def unsetEdit(self, vobj, mode=0):
FreeCADGui.Control.closeDialog()
return True
def get_kw_args(self, obj):
# returns a dictionary with all visualization options needed for plotting
# based on the view provider properties
return {}
def updateData(self, obj, prop):
# If the data changed we need to update the visualization
if prop == "Table":
self.update_visualization()
def onChanged(self, vobj, prop):
# for all property changes we need to update the visualization
self.update_visualization()
def childViewPropertyChanged(self, vobj, prop):
# One of the extractors view properties has changed, we need to
# update the visualization
self.update_visualization()
def dumps(self):
return None
def loads(self, state):
return None
# To be implemented by subclasses:
# ################################
def update_visualization(self):
# The visualization data or any relevant view property has changed,
# and the visualization itself needs to update to reflect that
raise FreeCAD.Base.FreeCADError("Not implemented")
def show_visualization(self):
# Shows the visualization without going into edit mode
raise FreeCAD.Base.FreeCADError("Not implemented")

View File

@@ -42,7 +42,7 @@ import matplotlib as mpl
from vtkmodules.numpy_interface.dataset_adapter import VTKArray
from . import view_post_extract
from . import view_base_fempostextractors
from . import view_base_fempostvisualization
from femtaskpanels import task_post_histogram
@@ -244,7 +244,7 @@ class EditIndexAppWidget(QtGui.QWidget):
self._post_dialog._recompute()
class VPPostHistogramFieldData(view_post_extract.VPPostExtractor):
class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor):
"""
A View Provider for extraction of 1D field data specialy for histograms
"""
@@ -378,7 +378,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
def __init__(self, vobj):
super().__init__(vobj)
vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
def _get_properties(self):
@@ -458,13 +458,10 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
]
return prop
def getIcon(self):
return ":/icons/FEM_PostHistogram.svg"
def doubleClicked(self,vobj):
self.show_visualization()
super().doubleClicked(vobj)
def setEdit(self, vobj, mode):
@@ -482,24 +479,12 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
if not hasattr(self, "_plot") or not self._plot:
main = Plot.getMainWindow()
self._plot = Plot.Plot()
self._plot.destroyed.connect(self.destroyed)
self._dialog = QtGui.QDialog(main)
box = QtGui.QVBoxLayout()
box.addWidget(self._plot)
self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep it square
self._dialog.setLayout(box)
self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square
self.update_visualization()
self.drawPlot()
self._dialog.show()
self._plot.show()
def destroyed(self, obj):
print("*********************************************************")
print("**************** ******************")
print("**************** destroy ******************")
print("**************** ******************")
print("*********************************************************")
def get_kw_args(self, obj):
view = obj.ViewObject
if not view or not hasattr(view, "Proxy"):
@@ -508,7 +493,8 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
return {}
return view.Proxy.get_kw_args()
def drawPlot(self):
def update_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
return
@@ -579,26 +565,3 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
self._plot.update()
def updateData(self, obj, prop):
# we only react if the table changed, as then know that new data is available
if prop == "Table":
self.drawPlot()
def onChanged(self, vobj, prop):
# for all property changes we need to redraw the plot
self.drawPlot()
def childViewPropertyChanged(self, vobj, prop):
# on of our extractors has a changed view property.
self.drawPlot()
def dumps(self):
return None
def loads(self, state):
return None

View File

@@ -42,9 +42,10 @@ import matplotlib as mpl
from vtkmodules.numpy_interface.dataset_adapter import VTKArray
from . import view_post_extract
from . import view_base_fempostextractors
from . import view_base_fempostvisualization
from femtaskpanels import task_post_lineplot
from femguiutils import post_visualization as pv
_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper
@@ -248,7 +249,7 @@ class EditIndexAppWidget(QtGui.QWidget):
self._post_dialog._recompute()
class VPPostLineplotFieldData(view_post_extract.VPPostExtractor):
class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor):
"""
A View Provider for extraction of 2D field data specialy for histograms
"""
@@ -376,7 +377,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
def __init__(self, vobj):
super().__init__(vobj)
vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
def _get_properties(self):
@@ -435,13 +436,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
]
return prop
def getIcon(self):
return ":/icons/FEM_PostLineplot.svg"
def doubleClicked(self,vobj):
self.show_visualization()
super().doubleClicked(vobj)
def setEdit(self, vobj, mode):
@@ -457,16 +455,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
def show_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
self._plot = Plot.Plot()
main = Plot.getMainWindow()
self._dialog = QtGui.QDialog(main)
box = QtGui.QVBoxLayout()
box.addWidget(self._plot)
self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep aspect ratio constant
self._dialog.setLayout(box)
self._plot = Plot.Plot()
self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio
self.update_visualization()
self._plot.show()
self.drawPlot()
self._dialog.show()
def get_kw_args(self, obj):
view = obj.ViewObject
@@ -476,7 +471,8 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
return {}
return view.Proxy.get_kw_args()
def drawPlot(self):
def update_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
return
@@ -545,26 +541,3 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
self._plot.update()
def updateData(self, obj, prop):
# we only react if the table changed, as then know that new data is available
if prop == "Table":
self.drawPlot()
def onChanged(self, vobj, prop):
# for all property changes we need to redraw the plot
self.drawPlot()
def childViewPropertyChanged(self, vobj, prop):
# on of our extractors has a changed view property.
self.drawPlot()
def dumps(self):
return None
def loads(self, state):
return None

View File

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