diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 0178bffe84..3ba5a83c3e 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -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) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 7337afd833..7729f8af55 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -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 diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui index d100b81ab3..8e611e7790 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -65,7 +65,7 @@ - One field for all frames + One field for each frames diff --git a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui new file mode 100644 index 0000000000..ada74b69b4 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -0,0 +1,43 @@ + + + PostHistogramEdit + + + + 0 + 0 + 259 + 38 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Name: + + + + + + + + diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index d40558f4bd..c21c219dda 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -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]): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index f4edc1586e..50461484a2 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -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") diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index e1611f8609..acd409765c 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -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")) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index df06c51ee0..95ebf1007e 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -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 diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index fae9c58b6c..b16f41451f 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -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 diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 3540b6706a..425a594fae 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -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) diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 60c9ac2df2..0b9e5c528e 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -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) diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index bdcb4ad553..0a6277f5fe 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -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 + + + diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 06f844ebac..486241b367 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -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 diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index f8798fbc23..3d7d7be689 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -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 diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index f90af0e260..3e26ac1ce5 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -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 # ################ diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py new file mode 100644 index 0000000000..e17f584c01 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -0,0 +1,94 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + 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() + diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py similarity index 92% rename from src/Mod/Fem/femviewprovider/view_post_extract.py rename to src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index b91c65cb45..46313ba890 100644 --- a/src/Mod/Fem/femviewprovider/view_post_extract.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -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 diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 153537d669..20714b67c5 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -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") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 777e3d15c0..2a6d817e70 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -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 diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index f98a5bf1e4..29c0165275 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -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 diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py new file mode 100644 index 0000000000..443667e31a --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -0,0 +1,287 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing table ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_table +# \ingroup FEM +# \brief view provider for post table object + +import FreeCAD +import FreeCADGui + +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) + +