diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt
index e5df8521ea..0178bffe84 100755
--- a/src/Mod/Fem/CMakeLists.txt
+++ b/src/Mod/Fem/CMakeLists.txt
@@ -36,7 +36,7 @@ SET(FemBaseModules_SRCS
coding_conventions.md
Init.py
InitGui.py
- # ObjectsFem.py
+ ObjectsFem.py
TestFemApp.py
CreateLabels.py
)
@@ -220,7 +220,9 @@ if(BUILD_FEM_VTK_PYTHON)
${FemObjects_SRCS}
femobjects/post_glyphfilter.py
femobjects/post_extract1D.py
+ femobjects/post_extract2D.py
femobjects/post_histogram.py
+ femobjects/post_lineplot.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -634,6 +636,7 @@ if(BUILD_FEM_VTK_PYTHON)
${FemGuiTaskPanels_SRCS}
femtaskpanels/task_post_glyphfilter.py
femtaskpanels/task_post_histogram.py
+ femtaskpanels/task_post_lineplot.py
femtaskpanels/task_post_extractor.py
)
endif(BUILD_FEM_VTK_PYTHON)
@@ -700,6 +703,7 @@ if(BUILD_FEM_VTK_PYTHON)
femviewprovider/view_post_glyphfilter.py
femviewprovider/view_post_extract.py
femviewprovider/view_post_histogram.py
+ femviewprovider/view_post_lineplot.py
)
endif(BUILD_FEM_VTK_PYTHON)
diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt
index f624778753..e1523956ea 100755
--- a/src/Mod/Fem/Gui/CMakeLists.txt
+++ b/src/Mod/Fem/Gui/CMakeLists.txt
@@ -444,8 +444,12 @@ SET(FemGuiPythonUI_SRCS
Resources/ui/TaskPostGlyph.ui
Resources/ui/TaskPostExtraction.ui
Resources/ui/TaskPostHistogram.ui
+ Resources/ui/TaskPostLineplot.ui
+ Resources/ui/PostExtractionSummaryWidget.ui
Resources/ui/PostHistogramFieldViewEdit.ui
Resources/ui/PostHistogramFieldAppEdit.ui
+ Resources/ui/PostLineplotFieldViewEdit.ui
+ Resources/ui/PostLineplotFieldAppEdit.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 a89c7ef39b..d100b81ab3 100644
--- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui
+++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui
@@ -14,6 +14,18 @@
Form
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
-
diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui
index bc26238b94..744e5a6240 100644
--- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui
+++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui
@@ -14,6 +14,18 @@
Form
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
-
@@ -76,9 +88,6 @@
Lines:
-
- Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
-
@@ -102,9 +111,6 @@
Bars:
-
- Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
-
@@ -141,9 +147,6 @@
Legend:
-
- Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
@@ -157,6 +160,15 @@
+
+ Legend
+ BarColor
+ Hatch
+ HatchDensity
+ LineColor
+ LineStyle
+ LineWidth
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui
new file mode 100644
index 0000000000..00b8ab4f55
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui
@@ -0,0 +1,101 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 296
+ 186
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ X Field:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Y Field:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Frames:
+
+
+
+ -
+
+
+ One Y field for each frames
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui
new file mode 100644
index 0000000000..720eb96c6e
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui
@@ -0,0 +1,154 @@
+
+
+ PostHistogramEdit
+
+
+
+ 0
+ 0
+ 335
+ 124
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Width of all lines (outline and hatch)
+
+
+ 99.000000000000000
+
+
+ 0.100000000000000
+
+
+
+ -
+
+
+ Marker:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Hatch pattern
+
+
-
+
+ None
+
+
+
+
+ -
+
+
+ -
+
+
+ Legend:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Outline draw style (None does not draw outlines)
+
+
-
+
+ None
+
+
+
+
+ -
+
+
+ Line:
+
+
+
+ -
+
+
+ 99.000000000000000
+
+
+ 0.100000000000000
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Color of the bars in histogram
+
+
+
+
+
+
+
+
+
+ Gui::ColorButton
+ QPushButton
+
+
+
+
+ Legend
+ Color
+ LineStyle
+ LineWidth
+ MarkerStyle
+ MarkerSize
+
+
+
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui
index 70e2f3ecba..a753071f9a 100644
--- a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui
+++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui
@@ -14,9 +14,21 @@
Glyph settings
- Qt::LayoutDirection::LeftToRight
+ Qt::LeftToRight
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
-
@@ -38,7 +50,7 @@
- Qt::LayoutDirection::LeftToRight
+ Qt::LeftToRight
2
@@ -94,7 +106,7 @@
-
- Qt::LayoutDirection::LeftToRight
+ Qt::LeftToRight
Show
@@ -195,9 +207,6 @@
Hatch Line Width
-
- Qt::AlignmentFlag::AlignCenter
-
-
@@ -205,9 +214,6 @@
Bar width
-
- Qt::AlignmentFlag::AlignCenter
-
-
@@ -218,6 +224,18 @@
+
+ Bins
+ Type
+ Cumulative
+ LegendShow
+ LegendPos
+ Title
+ XLabel
+ YLabel
+ BarWidth
+ HatchWidth
+
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui
new file mode 100644
index 0000000000..bec95e063f
--- /dev/null
+++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui
@@ -0,0 +1,181 @@
+
+
+ TaskPostGlyph
+
+
+
+ 0
+ 0
+ 302
+ 302
+
+
+
+ Glyph settings
+
+
+ Qt::LeftToRight
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+ The form of the glyph
+
+
+ Grid
+
+
+
+ -
+
+
+ Show
+
+
+
+ -
+
+
+ Qt::LeftToRight
+
+
+ Show
+
+
+
+ -
+
+
+ Legend
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Which vector field is used to orient the glyphs
+
+
+ Scale
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Which vector field is used to orient the glyphs
+
+
-
+
+ None
+
+
+
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+ Labels
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
-
+
+
+ -
+
+
+ Y Axis
+
+
+
+ -
+
+
+ -
+
+
+ If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components
+
+
+ X Axis
+
+
+
+ -
+
+
+ A constant multiplier the glyphs are scaled with
+
+
+ Title
+
+
+
+ -
+
+
+
+
+
+
+
+
+ Grid
+ LegendShow
+ LegendPos
+ Scale
+ Title
+ XLabel
+ YLabel
+
+
+
+
diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py
index 1dce3db5f7..c01cc8f8e6 100644
--- a/src/Mod/Fem/ObjectsFem.py
+++ b/src/Mod/Fem/ObjectsFem.py
@@ -686,36 +686,34 @@ def makePostVtkResult(doc, result_data, name="VtkResult"):
return obj
-def makePostVtkLinePlot(doc, name="Lineplot"):
- """makePostVtkLineplot(document, [name]):
+def makePostLineplot(doc, name="Lineplot"):
+ """makePostLineplot(document, [name]):
creates a FEM post processing line plot
"""
obj = doc.addObject("App::FeaturePython", name)
from femobjects import post_lineplot
- post_lineplot.PostLinePlot(obj)
+ post_lineplot.PostLineplot(obj)
if FreeCAD.GuiUp:
from femviewprovider import view_post_lineplot
- view_post_lineplot.VPPostLinePlot(obj.ViewObject)
- return
-
-
-def makePostVtkHistogramFieldData(doc, name="FieldData1D"):
- """makePostVtkFieldData1D(document, [name]):
- creates a FEM post processing data extractor for 1D Field data
- """
- obj = doc.addObject("App::FeaturePython", name)
- from femobjects import post_histogram
-
- post_histogram.PostHistogramFieldData(obj)
- if FreeCAD.GuiUp:
- from femviewprovider import view_post_histogram
- view_post_histogram.VPPostHistogramFieldData(obj.ViewObject)
+ 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("App::FeaturePython", name)
+ from femobjects import post_lineplot
-def makePostVtkHistogram(doc, name="Histogram"):
- """makePostVtkHistogram(document, [name]):
+ post_lineplot.PostLineplotFieldData(obj)
+ if FreeCAD.GuiUp:
+ from femviewprovider import view_post_lineplot
+ view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject)
+ return obj
+
+def makePostHistogram(doc, name="Histogram"):
+ """makePostHistogram(document, [name]):
creates a FEM post processing histogram plot
"""
obj = doc.addObject("App::FeaturePython", name)
@@ -727,6 +725,19 @@ def makePostVtkHistogram(doc, name="Histogram"):
view_post_histogram.VPPostHistogram(obj.ViewObject)
return obj
+def makePostHistogramFieldData(doc, name="FieldData1D"):
+ """makePostHistogramFieldData1D(document, [name]):
+ creates a FEM post processing data extractor for 1D Field data
+ """
+ obj = doc.addObject("App::FeaturePython", name)
+ from femobjects import post_histogram
+
+ post_histogram.PostHistogramFieldData(obj)
+ if FreeCAD.GuiUp:
+ from femviewprovider import view_post_histogram
+ view_post_histogram.VPPostHistogramFieldData(obj.ViewObject)
+ return obj
+
# ********* solver objects ***********************************************************************
def makeEquationDeformation(doc, base_solver=None, name="Deformation"):
diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py
index 98c620a96c..befcd48f30 100644
--- a/src/Mod/Fem/femcommands/commands.py
+++ b/src/Mod/Fem/femcommands/commands.py
@@ -1292,5 +1292,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__:
# setup all visualization commands (register by importing)
import femobjects.post_histogram
+ import femobjects.post_lineplot
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 60baecd9a4..9c984b537e 100644
--- a/src/Mod/Fem/femguiutils/extract_link_view.py
+++ b/src/Mod/Fem/femguiutils/extract_link_view.py
@@ -145,7 +145,10 @@ def build_add_from_data_tree_model(vis_type):
return model
-class TreeChoiceButton(QtGui.QToolButton):
+# implementation of GUI and its functionality
+# ###########################################
+
+class _TreeChoiceButton(QtGui.QToolButton):
selection = QtCore.Signal(object,object)
@@ -191,15 +194,157 @@ class TreeChoiceButton(QtGui.QToolButton):
# check if we should be disabled
self.setEnabled(bool(model.rowCount()))
+class _SettingsPopup(QtGui.QGroupBox):
-# implementationof GUI and its functionality
-# ##########################################
+ close = QtCore.Signal()
+
+ def __init__(self, setting):
+
+ toplevel = QtGui.QApplication.topLevelWidgets()
+ for i in toplevel:
+ if i.metaObject().className() == "Gui::MainWindow":
+ main = i
+ break
+
+ super().__init__(main)
+
+ self.setFocusPolicy(QtGui.Qt.ClickFocus)
+
+ vbox = QtGui.QVBoxLayout()
+ vbox.addWidget(setting)
+
+ buttonBox = QtGui.QDialogButtonBox()
+ buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok)
+ vbox.addWidget(buttonBox)
+
+ buttonBox.accepted.connect(self.accept)
+ self.setLayout(vbox)
+
+ @QtCore.Slot()
+ def accept(self):
+ self.close.emit()
+
+ def showEvent(self, event):
+ self.setFocus()
+
+ def keyPressEvent(self, event):
+ if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return:
+ self.accept()
+
+
+
+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__()
-class _ShowVisualization:
- def __init__(self, st_object):
self._st_object = st_object
+ self._extractor = extractor
+ self._post_dialog = post_dialog
- def __call__(self):
+ extr_label = extractor.Proxy.get_representive_fieldname(extractor)
+ extr_repr = extractor.ViewObject.Proxy.get_preview()
+
+ # build the UI
+
+ self.stButton = self._button(st_object.Label)
+ self.stButton.setIcon(st_object.ViewObject.Icon)
+
+ self.extrButton = self._button(extr_label)
+ 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])
+
+
+ self.rmButton = QtGui.QToolButton(self)
+ self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete"))
+ self.rmButton.setAutoRaise(True)
+
+ # add the separation line
+ self.frame = QtGui.QFrame(self)
+ self.frame.setFrameShape(QtGui.QFrame.HLine);
+
+ policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
+ self.setSizePolicy(policy)
+ self.setMinimumSize(self.stButton.sizeHint()+self.frame.sizeHint()*3)
+
+ # 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
+ 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, text):
+ btn = QtGui.QPushButton(self)
+ btn.full_text = text
+
+ #size = btn.sizeHint()
+ #size.setWidth(size.width()*2)
+ btn.setMinimumSize(btn.sizeHint())
+
+ btn.setFlat(True)
+ btn.setText(text)
+ btn.setStyleSheet("text-align:left;padding:6px");
+ btn.setToolTip(text)
+
+ return btn
+
+ def _redraw(self):
+
+ btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton
+ btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width()
+ fm = self.fontMetrics()
+ min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2
+
+ pos = 0
+ btns = [self.stButton, self.extrButton, self.viewButton]
+ btn_rel_size = [0.4, 0.4, 0.2]
+ btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight]
+ for i, btn in enumerate(btns):
+
+ btn_size = btn_total_size*btn_rel_size[i]
+ txt_size = btn_size - btn.iconSize().width() - btn_margin/2*3
+
+ # we elide only if there is enough space for a meaningful text
+ if txt_size >= min_text_width:
+
+ text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size)
+ btn.setText(text)
+ btn.setStyleSheet("text-align:left;padding:6px");
+ else:
+ btn.setText("")
+ btn.setStyleSheet("text-align:center;");
+
+ rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height())
+ btn.setGeometry(rect)
+ pos+=btn_size
+
+ rmsize = self.stButton.height()
+ pos = self.size().width() - rmsize
+ self.rmButton.setGeometry(pos, 0, rmsize, rmsize)
+
+ frame_hint = self.frame.sizeHint()
+ rect = QtCore.QRect(0, self.stButton.height()+frame_hint.height(), self.size().width(), frame_hint.height())
+ self.frame.setGeometry(rect)
+
+ def resizeEvent(self, event):
+
+ # calculate the allowed text length
+ self._redraw()
+ super().resizeEvent(event)
+
+ @QtCore.Slot()
+ def showVisualization(self):
if vis.is_visualization_object(self._st_object):
# show the visualization
self._st_object.ViewObject.Proxy.show_visualization()
@@ -208,67 +353,76 @@ class _ShowVisualization:
FreeCADGui.Selection.clearSelection()
FreeCADGui.Selection.addSelection(self._st_object)
-class _ShowEditDialog:
- def __init__(self, extractor, post_dialog, widget):
- self._extractor = extractor
- self._post_dialog = post_dialog
- self._widget = widget
+ def _position_dialog(self, dialog):
- widgets = self._extractor.ViewObject.Proxy.get_edit_widgets(self._post_dialog)
- vbox = QtGui.QVBoxLayout()
+ main = dialog.parent()
+ list_widget = self.parent().parent().parent()
+ widget_rect = list_widget.geometry()
+ diag_size = dialog.sizeHint()
+ # default is towards main window center
+ if main.geometry().center().x() >= list_widget.mapToGlobal(widget_rect.center()).x():
+ rigth_point = list_widget.mapToGlobal(widget_rect.topRight())
+ dialog.setGeometry(QtCore.QRect(rigth_point, diag_size))
+ else:
+ left_point = list_widget.mapToGlobal(widget_rect.topLeft())
+ left_point -= QtCore.QPoint(diag_size.width(), 0)
+ dialog.setGeometry(QtCore.QRect(left_point, diag_size))
- buttonBox = QtGui.QDialogButtonBox()
- buttonBox.setCenterButtons(True)
- buttonBox.setStandardButtons(self._post_dialog.getStandardButtons())
- vbox.addWidget(buttonBox)
+ @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.appDialog.close.connect(self.appAccept)
- started = False
- for widget in widgets:
+ if not self.appDialog.isVisible():
+ # position correctly and show
+ self._position_dialog(self.appDialog)
+ self.appDialog.show()
+ #self.appDialog.raise_()
- if started:
- # add a seperator line
- frame = QtGui.QFrame()
- frame.setFrameShape(QtGui.QFrame.HLine);
- vbox.addWidget(frame);
- else:
- started = True
+ @QtCore.Slot()
+ def editView(self):
- vbox.addWidget(widget)
+ if not hasattr(self, "viewDialog"):
+ widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog)
+ self.viewDialog = _SettingsPopup(widget)
+ self.viewDialog.close.connect(self.viewAccept)
- vbox.addStretch()
+ if not self.viewDialog.isVisible():
+ # position correctly and show
+ self._position_dialog(self.viewDialog)
+ self.viewDialog.show()
+ #self.viewDialog.raise_()
- self.dialog = QtGui.QDialog(self._widget)
- buttonBox.accepted.connect(self.accept)
- buttonBox.rejected.connect(self.dialog.close)
- buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.apply)
- self.dialog.setLayout(vbox)
+ @QtCore.Slot()
+ def deleteTriggered(self):
+ self.delete.emit(self._extractor, self)
+
+ @QtCore.Slot()
+ def viewAccept(self):
+
+ self.viewDialog.hide()
+
+ # update the preview
+ extr_repr = self._extractor.ViewObject.Proxy.get_preview()
+ self.viewButton.setIcon(extr_repr[0])
+ self.viewButton.full_text = extr_repr[1]
+ self.viewButton.setToolTip(extr_repr[1])
+ self._redraw()
+
+ @QtCore.Slot()
+ def appAccept(self):
+
+ self.appDialog.hide()
+
+ # update the preview
+ extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor)
+ self.extrButton.full_text = extr_label
+ self.extrButton.setToolTip(extr_label)
+ self._redraw()
- def accept(self):
- # recompute and close
- self._extractor.Document.recompute()
- self.dialog.close()
-
- def apply(self):
- self._extractor.Document.recompute()
-
- def __call__(self):
- # create the widgets, add it to dialog
- self.dialog.show()
-
-class _DeleteExtractor:
- def __init__(self, extractor, widget):
- self._extractor = extractor
- self._widget = widget
-
- def __call__(self):
- # remove the document object
- doc = self._extractor.Document
- doc.removeObject(self._extractor.Name)
- doc.recompute()
-
- # remove the widget
- self._widget.deleteLater()
class ExtractLinkView(QtGui.QWidget):
@@ -300,19 +454,19 @@ class ExtractLinkView(QtGui.QWidget):
if self._is_source:
- self._add = TreeChoiceButton(build_add_to_visualization_tree_model())
+ self._add = _TreeChoiceButton(build_add_to_visualization_tree_model())
self._add.setText("Add data to")
self._add.selection.connect(self.addExtractionToVisualization)
hbox.addWidget(self._add)
- self._create = TreeChoiceButton(build_new_visualization_tree_model())
+ self._create = _TreeChoiceButton(build_new_visualization_tree_model())
self._create.setText("New")
self._create.selection.connect(self.newVisualization)
hbox.addWidget(self._create)
else:
vis_type = vis.get_visualization_type(self._object)
- self._add = TreeChoiceButton(build_add_from_data_tree_model(vis_type))
+ self._add = _TreeChoiceButton(build_add_from_data_tree_model(vis_type))
self._add.setText("Add data from")
self._add.selection.connect(self.addExtractionToPostObject)
hbox.addWidget(self._add)
@@ -324,46 +478,31 @@ class ExtractLinkView(QtGui.QWidget):
self.setLayout(vbox)
-
-
# add the content
self.repopulate()
def _build_summary_widget(self, extractor):
- widget = FreeCADGui.PySideUic.loadUi(
- FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostExtractionSummaryWidget.ui"
- )
-
- # add the separation line
- frame = QtGui.QFrame()
- frame.setFrameShape(QtGui.QFrame.HLine);
- widget.layout().addWidget(frame);
-
if self._is_source:
st_object = extractor.getParentGroup()
else:
st_object = extractor.Source
- widget.RemoveButton.setIcon(QtGui.QIcon.fromTheme("delete"))
-
- widget.STButton.setIcon(st_object.ViewObject.Icon)
- widget.STButton.setText(st_object.Label)
-
- widget.ExtractButton.setIcon(extractor.ViewObject.Icon)
-
- extr_label = extr.get_extraction_dimension(extractor)
- extr_label += " " + extr.get_extraction_type(extractor)
- widget.ExtractButton.setText(extr_label)
-
- # connect actions. We add functions to widget, as well as the data we need,
- # and use those as callback. This way every widget knows which objects to use
- widget.STButton.clicked.connect(_ShowVisualization(st_object))
- widget.ExtractButton.clicked.connect(_ShowEditDialog(extractor, self._post_dialog, widget))
- widget.RemoveButton.clicked.connect(_DeleteExtractor(extractor, widget))
+ 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
diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py
index 4ccef7018a..33ef4b4935 100644
--- a/src/Mod/Fem/femobjects/base_fempostextractors.py
+++ b/src/Mod/Fem/femobjects/base_fempostextractors.py
@@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief base objects for data extractors
+from vtkmodules.vtkCommonCore import vtkIntArray
+from vtkmodules.vtkCommonCore import vtkDoubleArray
from vtkmodules.vtkCommonDataModel import vtkTable
from . import base_fempythonobject
@@ -117,6 +119,10 @@ class Extractor(base_fempythonobject.BaseFemPythonObject):
case _:
return ["Not a vector"]
+ def get_representive_fieldname(self):
+ # should return the representive field name, e.g. Position (X)
+ return ""
+
class Extractor1D(Extractor):
@@ -196,4 +202,179 @@ class Extractor1D(Extractor):
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):
+ # extracts the relevant array from the dataset and returns a copy
+
+ match obj.XField:
+ case "Index":
+ num = dataset.GetPoints().GetNumberOfPoints()
+ array = vtkIntArray()
+ array.SetNumberOfTuples(num)
+ array.SetNumberOfComponents(1)
+ for i in range(num):
+ array.SetValue(i,i)
+
+ case "Position":
+ orig_array = dataset.GetPoints().GetData()
+ array = vtkDoubleArray()
+ array.DeepCopy(orig_array)
+
+ case _:
+ point_data = dataset.GetPointData()
+ orig_array = point_data.GetAbstractArray(obj.XField)
+ array = vtkDoubleArray()
+ array.DeepCopy(orig_array)
+
+ return array
+
+ def 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="The field to use as Y data",
+ value=[],
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="YComponent",
+ group="Y Data",
+ doc="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):
+ # extracts the relevant array from the dataset and returns a copy
+
+ match obj.YField:
+ case "Index":
+ num = dataset.GetPoints().GetNumberOfPoints()
+ array = vtkIntArray()
+ array.SetNumberOfTuples(num)
+ array.SetNumberOfComponents(1)
+ for i in range(num):
+ array.SetValue(i,i)
+
+ case "Position":
+ orig_array = dataset.GetPoints().GetData()
+ array = vtkDoubleArray()
+ array.DeepCopy(orig_array)
+
+ case _:
+ point_data = dataset.GetPointData()
+ orig_array = point_data.GetAbstractArray(obj.YField)
+ array = vtkDoubleArray()
+ array.DeepCopy(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
diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py
index 5a9404e149..f7a450c181 100644
--- a/src/Mod/Fem/femobjects/post_extract1D.py
+++ b/src/Mod/Fem/femobjects/post_extract1D.py
@@ -33,10 +33,7 @@ from . import base_fempostextractors
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
-from vtkmodules.vtkCommonCore import vtkDoubleArray
-from vtkmodules.vtkCommonCore import vtkIntArray
from vtkmodules.vtkCommonDataModel import vtkTable
-from vtkmodules.vtkCommonDataModel import vtkDataObject
from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline
class PostFieldData1D(base_fempostextractors.Extractor1D):
@@ -60,44 +57,6 @@ class PostFieldData1D(base_fempostextractors.Extractor1D):
]
return super()._get_properties() + prop
- def __array_to_table(self, obj, array, table):
- if array.GetNumberOfComponents() == 1:
- table.AddColumn(array)
- else:
- component_array = vtkDoubleArray();
- component_array.SetNumberOfComponents(1)
- component_array.SetNumberOfTuples(array.GetNumberOfTuples())
- c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent)
- component_array.CopyComponent(0, array, c_idx)
- component_array.SetName(array.GetName())
- table.AddColumn(component_array)
-
- def __array_from_dataset(self, obj, dataset):
- # extracts the relevant array from the dataset and returns a copy
-
- match obj.XField:
- case "Index":
- num = dataset.GetPoints().GetNumberOfPoints()
- array = vtkIntArray()
- array.SetNumberOfTuples(num)
- array.SetNumberOfComponents(1)
- for i in range(num):
- array.SetValue(i,i)
-
- case "Position":
- orig_array = dataset.GetPoints().GetData()
- array = vtkDoubleArray()
- array.DeepCopy(orig_array)
-
- case _:
- point_data = dataset.GetPointData()
- orig_array = point_data.GetAbstractArray(obj.XField)
- array = vtkDoubleArray()
- array.DeepCopy(orig_array)
-
- return array
-
-
def execute(self, obj):
# on execution we populate the vtk table
@@ -124,26 +83,26 @@ class PostFieldData1D(base_fempostextractors.Extractor1D):
if not frames:
# get the dataset and extract the correct array
- array = self.__array_from_dataset(obj, dataset)
+ array = self._x_array_from_dataset(obj, dataset)
if array.GetNumberOfComponents() > 1:
array.SetName(obj.XField + " (" + obj.XComponent + ")")
else:
array.SetName(obj.XField)
- self.__array_to_table(obj, array, table)
+ 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.__array_from_dataset(obj, dataset)
+ 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.__array_to_table(obj, array, table)
+ self._x_array_component_to_table(obj, array, table)
# set the final table
obj.Table = table
diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py
new file mode 100644
index 0000000000..b25b809655
--- /dev/null
+++ b/src/Mod/Fem/femobjects/post_extract2D.py
@@ -0,0 +1,153 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD post extractors 2D"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package post_histogram
+# \ingroup FEM
+# \brief Post processing plot displaying lines
+
+from . import base_fempostextractors
+from . import base_fempythonobject
+_PropHelper = base_fempythonobject._PropHelper
+
+from vtkmodules.vtkCommonDataModel import vtkTable
+from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline
+
+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="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
+
+ frames = False
+ if obj.ExtractFrames:
+ # check if we have timesteps
+ info = obj.Source.getOutputAlgorithm().GetOutputInformation(0)
+ if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()):
+ timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS())
+ frames = True
+ else:
+ FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property")
+
+ if not frames:
+ # get the dataset and extract the correct array
+ 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 PostIndexData2D(base_fempostextractors.Extractor2D):
+ """
+ A post processing extraction of one dimensional index data
+ """
+
+ ExtractionType = "Index"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ def _get_properties(self):
+ prop =[ _PropHelper(
+ type="App::PropertyBool",
+ name="ExtractFrames",
+ group="Multiframe",
+ doc="Specify if the data at the index should be extracted for each frame",
+ value=False,
+ ),
+ _PropHelper(
+ type="App::PropertyInteger",
+ name="XIndex",
+ group="X Data",
+ doc="Specify for which point index the data should be extracted",
+ value=0,
+ ),
+ ]
+ return super()._get_properties() + prop
diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py
index df238c6e08..8cbd72b41c 100644
--- a/src/Mod/Fem/femobjects/post_histogram.py
+++ b/src/Mod/Fem/femobjects/post_histogram.py
@@ -21,13 +21,13 @@
# * *
# ***************************************************************************
-__title__ = "FreeCAD post line plot"
+__title__ = "FreeCAD post histogram"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_histogram
# \ingroup FEM
-# \brief Post processing plot displaying lines
+# \brief Post processing plot displaying histograms
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
@@ -46,7 +46,7 @@ from femguiutils import post_visualization
post_visualization.register_visualization("Histogram",
":/icons/FEM_PostHistogram.svg",
"ObjectsFem",
- "makePostVtkHistogram")
+ "makePostHistogram")
post_visualization.register_extractor("Histogram",
"HistogramFieldData",
@@ -54,7 +54,7 @@ post_visualization.register_extractor("Histogram",
"1D",
"Field",
"ObjectsFem",
- "makePostVtkHistogramFieldData")
+ "makePostHistogramFieldData")
# Implementation
diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py
index f8798fbc23..272aaea74c 100644
--- a/src/Mod/Fem/femobjects/post_lineplot.py
+++ b/src/Mod/Fem/femobjects/post_lineplot.py
@@ -32,41 +32,76 @@ __url__ = "https://www.freecad.org"
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
+from . import base_fempostextractors
+from . import base_fempostvisualizations
+from . import post_extract2D
- return "unknown"
+from vtkmodules.vtkCommonCore import vtkDoubleArray
+from vtkmodules.vtkCommonDataModel import vtkTable
-class PostLinePlot(base_fempythonobject.BaseFemPythonObject):
+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")
+
+
+# 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 post processing extraction for plotting lines
+ A 2D Field extraction for lineplot.
+ """
+ VisualizationType = "Lineplot"
+
+
+
+class PostLineplot(base_fempostvisualizations.PostVisualization):
+ """
+ A post processing plot for showing extracted data as line plots
"""
- Type = "App::FeaturePython"
+ VisualizationType = "Lineplot"
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)
+ obj.addExtension("App::GroupExtensionPython")
def _get_properties(self):
- prop = []
- return prop
+ 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 onDocumentRestored(self, obj):
- self._setup_properties(self, obj):
def onChanged(self, obj, prop):
@@ -75,137 +110,29 @@ class PostLinePlot(base_fempythonobject.BaseFemPythonObject):
children = obj.Group
for child in obj.Group:
- if _get_extraction_subtype(child) not in ["Line"]:
+ 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):
-class PostPlotLine(base_fempythonobject.BaseFemPythonObject):
+ # during execution we collect all child data into our table
+ table = vtkTable()
+ for child in obj.Group:
- Type = "App::FeaturePython"
+ 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)
- def __init__(self, obj):
- super().__init__(obj)
- self._setup_properties(obj)
+ obj.Table = table
+ return False
- 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/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py
new file mode 100644
index 0000000000..f8798fbc23
--- /dev/null
+++ b/src/Mod/Fem/femobjects/post_table.py
@@ -0,0 +1,211 @@
+# ***************************************************************************
+# * Copyright (c) 2025 Stefan Tröger *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD post line plot"
+__author__ = "Stefan Tröger"
+__url__ = "https://www.freecad.org"
+
+## @package post_lineplot
+# \ingroup FEM
+# \brief Post processing plot displaying lines
+
+from . import base_fempythonobject
+_PropHelper = base_fempythonobject._PropHelper
+
+# helper function to extract plot object type
+def _get_extraction_subtype(obj):
+ if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"):
+ return obj.Proxy.Type
+
+ return "unknown"
+
+
+class PostLinePlot(base_fempythonobject.BaseFemPythonObject):
+ """
+ A post processing extraction for plotting lines
+ """
+
+ Type = "App::FeaturePython"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ obj.addExtension("App::GroupExtension")
+ self._setup_properties(obj)
+
+ def _setup_properties(self, obj):
+
+ self.ExtractionType = "LinePlot"
+
+ pl = obj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.Name in pl:
+ prop.add_to_object(obj)
+
+ def _get_properties(self):
+ prop = []
+ return prop
+
+ def onDocumentRestored(self, obj):
+ self._setup_properties(self, obj):
+
+ def onChanged(self, obj, prop):
+
+ if prop == "Group":
+ # check if all objects are allowed
+
+ children = obj.Group
+ for child in obj.Group:
+ if _get_extraction_subtype(child) not in ["Line"]:
+ children.remove(child)
+
+ if len(obj.Group) != len(children):
+ obj.Group = children
+
+
+class PostPlotLine(base_fempythonobject.BaseFemPythonObject):
+
+ Type = "App::FeaturePython"
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ self._setup_properties(obj)
+
+ def _setup_properties(self, obj):
+
+ self.ExtractionType = "Line"
+
+ pl = obj.PropertiesList
+ for prop in self._get_properties():
+ if not prop.Name in pl:
+ prop.add_to_object(obj)
+
+ def _get_properties(self):
+
+ prop = [
+ _PropHelper(
+ type="App::PropertyLink",
+ name="Source",
+ group="Line",
+ doc="The data source, the line uses",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="XField",
+ group="X Data",
+ doc="The field to use as X data",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="XComponent",
+ group="X Data",
+ doc="Which part of the X field vector to use for the X axis",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="YField",
+ group="Y Data",
+ doc="The field to use as Y data for the line plot",
+ value=None,
+ ),
+ _PropHelper(
+ type="App::PropertyEnumeration",
+ name="YComponent",
+ group="Y Data",
+ doc="Which part of the Y field vector to use for the X axis",
+ value=None,
+ ),
+ ]
+ return prop
+
+ def onDocumentRestored(self, obj):
+ self._setup_properties(self, obj):
+
+ def onChanged(self, obj, prop):
+
+ if prop == "Source":
+ # check if the source is a Post object
+ if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"):
+ FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject")
+ obj.XField = []
+ obj.YField = []
+ obj.Source = None
+
+ if prop == "XField":
+ if not obj.Source:
+ obj.XComponent = []
+ return
+
+ point_data = obj.Source.Data.GetPointData()
+ if not point_data.HasArray(obj.XField):
+ obj.XComponent = []
+ return
+
+ match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents:
+ case 1:
+ obj.XComponent = ["Not a vector"]
+ case 2:
+ obj.XComponent = ["Magnitude", "X", "Y"]
+ case 3:
+ obj.XComponent = ["Magnitude", "X", "Y", "Z"]
+
+ if prop == "YField":
+ if not obj.Source:
+ obj.YComponent = []
+ return
+
+ point_data = obj.Source.Data.GetPointData()
+ if not point_data.HasArray(obj.YField):
+ obj.YComponent = []
+ return
+
+ match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents:
+ case 1:
+ obj.YComponent = ["Not a vector"]
+ case 2:
+ obj.YComponent = ["Magnitude", "X", "Y"]
+ case 3:
+ obj.YComponent = ["Magnitude", "X", "Y", "Z"]
+
+ def onExecute(self, obj):
+ # we need to make sure that we show the correct fields to the user as option for data extraction
+
+ fields = []
+ if obj.Source:
+ point_data = obj.Source.Data.GetPointData()
+ fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())]
+
+ current_X = obj.XField
+ obj.XField = fields
+ if current_X in fields:
+ obj.XField = current_X
+
+ current_Y = obj.YField
+ obj.YField = fields
+ if current_Y in fields:
+ obj.YField = current_Y
+
+ return True
+
diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py
index 5a56077c3e..6d29a305e8 100644
--- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py
+++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py
@@ -48,7 +48,14 @@ class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel):
super().__init__(obj)
# form is used to display individual task panels
- self.form = obj.ViewObject.Proxy.get_edit_widgets(self)
+ 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]
diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py
new file mode 100644
index 0000000000..650cdd70a1
--- /dev/null
+++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py
@@ -0,0 +1,163 @@
+# ***************************************************************************
+# * 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 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
+
+class _TaskPanel(base_fempostpanel._BasePostTaskPanel):
+ """
+ The TaskPanel for editing properties of glyph filter
+ """
+
+ def __init__(self, vobj):
+ super().__init__(vobj.Object)
+
+ # data widget
+ self.data_widget = QtGui.QWidget()
+ hbox = QtGui.QHBoxLayout()
+ self.data_widget.show_plot = QtGui.QPushButton()
+ self.data_widget.show_plot.setText("Show plot")
+ hbox.addWidget(self.data_widget.show_plot)
+ self.data_widget.show_table = QtGui.QPushButton()
+ self.data_widget.show_table.setText("Show data")
+ hbox.addWidget(self.data_widget.show_table)
+ vbox = QtGui.QVBoxLayout()
+ vbox.addItem(hbox)
+ vbox.addSpacing(10)
+
+ extracts = elv.ExtractLinkView(self.obj, False, self)
+ vbox.addWidget(extracts)
+
+ self.data_widget.setLayout(vbox)
+ self.data_widget.setWindowTitle("Lineplot data")
+
+
+ # lineplot parameter widget
+ self.view_widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui"
+ )
+ self.view_widget.setWindowTitle("Lineplot view settings")
+ self.__init_widgets()
+
+ # form made from param and selection widget
+ self.form = [self.data_widget, self.view_widget]
+
+
+ # Setup functions
+ # ###############
+
+ def __init_widgets(self):
+
+ # connect data widget
+ self.data_widget.show_plot.clicked.connect(self.showPlot)
+ self.data_widget.show_table.clicked.connect(self.showTable)
+
+ # set current values to view widget
+ viewObj = self.obj.ViewObject
+
+ self._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
diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_post_extract.py
index c75dd4bc8b..b91c65cb45 100644
--- a/src/Mod/Fem/femviewprovider/view_post_extract.py
+++ b/src/Mod/Fem/femviewprovider/view_post_extract.py
@@ -109,18 +109,23 @@ class VPPostExtractor:
# of the object
return {}
- def get_edit_widgets(self, post_dialog):
- # Returns a list of widgets for editing the object/viewprovider.
+ 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_preview_widget(self, post_dialog):
- # Returns a widget for editing the object/viewprovider.
+ 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")
+
def dumps(self):
return None
diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py
index 5a433f17bc..8fe2fa6b4c 100644
--- a/src/Mod/Fem/femviewprovider/view_post_histogram.py
+++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py
@@ -232,12 +232,14 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor):
def getIcon(self):
return ":/icons/FEM_PostField.svg"
- def get_edit_widgets(self, post_dialog):
- return [ EditAppWidget(self.Object, post_dialog),
- EditViewWidget(self.Object, post_dialog)]
+ def get_app_edit_widget(self, post_dialog):
+ return EditAppWidget(self.Object, post_dialog)
- def get_preview_widget(self, post_dialog):
- return QtGui.QComboBox()
+ def get_view_edit_widget(self, post_dialog):
+ return EditViewWidget(self.Object, post_dialog)
+
+ def get_preview(self):
+ return (QtGui.QPixmap(), self.ViewObject.Legend)
def get_kw_args(self):
# builds kw args from the properties
diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py
index 0ce5ec8954..54fe5439f3 100644
--- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py
+++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py
@@ -1,4 +1,3 @@
-
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger *
# * *
@@ -33,39 +32,445 @@ __url__ = "https://www.freecad.org"
import FreeCAD
import FreeCADGui
+import Plot
import FemGui
-from PySide import QtGui
+from PySide import QtGui, QtCore
+
+import io
+import numpy as np
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+
+from vtkmodules.numpy_interface.dataset_adapter import VTKArray
+
+from . import view_post_extract
+from . import view_base_fempostvisualization
+from femtaskpanels import task_post_lineplot
+
+_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/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.widget.Color.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.Color]))
+
+ 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)
+ self.widget.Color.changed.connect(self.colorChanged)
+
+ @QtCore.Slot()
+ def colorChanged(self):
+ color = self.widget.Color.property("color")
+ 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 VPPostLinePlot:
+class EditAppWidget(QtGui.QWidget):
+
+ def __init__(self, obj, post_dialog):
+ super().__init__()
+
+ self._object = obj
+ self._post_dialog = post_dialog
+
+ # load the ui and set it up
+ self.widget = FreeCADGui.PySideUic.loadUi(
+ FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/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 VPPostLineplotFieldData(view_post_extract.VPPostExtractor):
"""
- A View Provider for the Post LinePlot object
+ 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="Lineplot",
+ doc="The name used in the plots legend",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyColor",
+ name="Color",
+ group="Lineplot",
+ doc="The color the line and the markers are drawn with",
+ value=(0, 85, 255, 255),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="LineStyle",
+ group="Lineplot",
+ doc="The style the line is drawn in",
+ value=['-', '--', '-.', ':', 'None'],
+ ),
+ _GuiPropHelper(
+ type="App::PropertyFloatConstraint",
+ name="LineWidth",
+ group="Lineplot",
+ doc="The width the line is drawn with",
+ value=(1, 0.1, 99, 0.1),
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="MarkerStyle",
+ group="Lineplot",
+ doc="The style the data markers are drawn with",
+ value=['None', '*', '+', 's', '.', 'o', 'x'],
+ ),
+ _GuiPropHelper(
+ type="App::PropertyFloatConstraint",
+ name="MarkerSize",
+ group="Lineplot",
+ doc="The size the data markers are drawn in",
+ value=(10, 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 EditAppWidget(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 = plt.figure(figsize=(0.2,0.1), dpi=1000)
+ ax = plt.Axes(fig, [0., 0., 1., 1.])
+ 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()
+ plt.savefig(data, bbox_inches=0, transparent=True)
+ plt.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
+
+
+class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization):
+ """
+ A View Provider for Lineplot plots
+ """
+
+ def __init__(self, vobj):
+ super().__init__(vobj)
+ vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
+
+ def _get_properties(self):
+
+ prop = [
+ _GuiPropHelper(
+ type="App::PropertyBool",
+ name="Grid",
+ group="Lineplot",
+ doc="If be the bars shoud show the cumulative sum left to rigth",
+ value=False,
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="Scale",
+ group="Lineplot",
+ doc="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="The histogram plot title",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyString",
+ name="XLabel",
+ group="Plot",
+ doc="The label shown for the histogram X axis",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyString",
+ name="YLabel",
+ group="Plot",
+ doc="The label shown for the histogram Y axis",
+ value="",
+ ),
+ _GuiPropHelper(
+ type="App::PropertyBool",
+ name="Legend",
+ group="Plot",
+ doc="Determines if the legend is plotted",
+ value=True,
+ ),
+ _GuiPropHelper(
+ type="App::PropertyEnumeration",
+ name="LegendLocation",
+ group="Plot",
+ doc="Determines if the legend is plotted",
+ value=['best','upper right','upper left','lower left','lower right','right',
+ 'center left','center right','lower center','upper center','center'],
+ ),
+
+ ]
+ return prop
+
def getIcon(self):
return ":/icons/FEM_PostLineplot.svg"
- def setEdit(self, vobj, mode):
- # make sure we see what we edit
- vobj.show()
+ def doubleClicked(self,vobj):
+
+ self.show_visualization()
+ super().doubleClicked(vobj)
+
+ def setEdit(self, vobj, mode):
# build up the task panel
- #taskd = task_post_glyphfilter._TaskPanel(vobj)
+ taskd = task_post_lineplot._TaskPanel(vobj)
#show it
- #FreeCADGui.Control.showDialog(taskd)
+ FreeCADGui.Control.showDialog(taskd)
return True
- def unsetEdit(self, vobj, mode):
- FreeCADGui.Control.closeDialog()
- return True
+
+ def show_visualization(self):
+
+ if not hasattr(self, "_plot") or not self._plot:
+ self._plot = Plot.Plot()
+ self._dialog = QtGui.QDialog(Plot.getMainWindow())
+ box = QtGui.QVBoxLayout()
+ box.addWidget(self._plot)
+ self._dialog.setLayout(box)
+
+ self.drawPlot()
+ self._dialog.show()
+
+ def get_kw_args(self, obj):
+ view = obj.ViewObject
+ if not view or not hasattr(view, "Proxy"):
+ return {}
+ if not hasattr(view.Proxy, "get_kw_args"):
+ return {}
+ return view.Proxy.get_kw_args()
+
+ def drawPlot(self):
+
+ if not hasattr(self, "_plot") or not self._plot:
+ return
+
+ self._plot.axes.clear()
+
+ # we do not iterate the table, but iterate the children. This makes it possible
+ # to attribute the correct styles
+ 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):
+
+ # 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))
+
+ # 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 self.Object.Group:
+ self._plot.axes.legend(loc = self.ViewObject.LegendLocation)
+
+ self._plot.axes.grid(self.ViewObject.Grid)
+
+ 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