From c1b11a19f7fffd18a52cf841a5ddd1985916245d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 16 Apr 2025 18:47:01 +0200 Subject: [PATCH] Fem: Implement lineplot visualization --- src/Mod/Fem/CMakeLists.txt | 6 +- src/Mod/Fem/Gui/CMakeLists.txt | 4 + .../Resources/ui/PostHistogramFieldAppEdit.ui | 12 + .../ui/PostHistogramFieldViewEdit.ui | 30 +- .../Resources/ui/PostLineplotFieldAppEdit.ui | 101 +++++ .../Resources/ui/PostLineplotFieldViewEdit.ui | 154 +++++++ .../Fem/Gui/Resources/ui/TaskPostHistogram.ui | 36 +- .../Fem/Gui/Resources/ui/TaskPostLineplot.ui | 181 ++++++++ src/Mod/Fem/ObjectsFem.py | 51 ++- src/Mod/Fem/femcommands/commands.py | 2 +- src/Mod/Fem/femguiutils/extract_link_view.py | 315 +++++++++---- .../Fem/femobjects/base_fempostextractors.py | 181 ++++++++ src/Mod/Fem/femobjects/post_extract1D.py | 49 +- src/Mod/Fem/femobjects/post_extract2D.py | 153 +++++++ src/Mod/Fem/femobjects/post_histogram.py | 8 +- src/Mod/Fem/femobjects/post_lineplot.py | 221 +++------ src/Mod/Fem/femobjects/post_table.py | 211 +++++++++ .../Fem/femtaskpanels/task_post_extractor.py | 9 +- .../Fem/femtaskpanels/task_post_lineplot.py | 163 +++++++ .../Fem/femviewprovider/view_post_extract.py | 13 +- .../femviewprovider/view_post_histogram.py | 12 +- .../Fem/femviewprovider/view_post_lineplot.py | 429 +++++++++++++++++- 22 files changed, 1995 insertions(+), 346 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui create mode 100644 src/Mod/Fem/femobjects/post_extract2D.py create mode 100644 src/Mod/Fem/femobjects/post_table.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_lineplot.py 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 @@
Gui/Widgets.h
+ + 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 +
Gui/Widgets.h
+
+
+ + 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