Fem: Implement lineplot visualization

This commit is contained in:
Stefan Tröger
2025-04-16 18:47:01 +02:00
parent 6e4fab1f50
commit c1b11a19f7
22 changed files with 1995 additions and 346 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -14,6 +14,18 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">

View File

@@ -14,6 +14,18 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
@@ -76,9 +88,6 @@
<property name="text">
<string>Lines:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="3">
@@ -102,9 +111,6 @@
<property name="text">
<string>Bars:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
@@ -141,9 +147,6 @@
<property name="text">
<string>Legend:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
@@ -157,6 +160,15 @@
<header>Gui/Widgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>Legend</tabstop>
<tabstop>BarColor</tabstop>
<tabstop>Hatch</tabstop>
<tabstop>HatchDensity</tabstop>
<tabstop>LineColor</tabstop>
<tabstop>LineStyle</tabstop>
<tabstop>LineWidth</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>296</width>
<height>186</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>X Field:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="XField">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="XComponent">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Y Field:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="YField">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="YComponent">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Frames:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="Extract">
<property name="text">
<string>One Y field for each frames</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PostHistogramEdit</class>
<widget class="QWidget" name="PostHistogramEdit">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>335</width>
<height>124</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="4">
<widget class="QDoubleSpinBox" name="MarkerSize">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Width of all lines (outline and hatch)</string>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Marker:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QComboBox" name="LineStyle">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Hatch pattern</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1" colspan="4">
<widget class="QLineEdit" name="Legend"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Legend:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="MarkerStyle">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Outline draw style (None does not draw outlines)</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Line:</string>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QDoubleSpinBox" name="LineWidth">
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::ColorButton" name="Color">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Color of the bars in histogram</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::ColorButton</class>
<extends>QPushButton</extends>
<header>Gui/Widgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>Legend</tabstop>
<tabstop>Color</tabstop>
<tabstop>LineStyle</tabstop>
<tabstop>LineWidth</tabstop>
<tabstop>MarkerStyle</tabstop>
<tabstop>MarkerSize</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -14,9 +14,21 @@
<string notr="true">Glyph settings</string>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
<enum>Qt::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
@@ -38,7 +50,7 @@
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
<enum>Qt::LeftToRight</enum>
</property>
<property name="minimum">
<number>2</number>
@@ -94,7 +106,7 @@
<item>
<widget class="QCheckBox" name="LegendShow">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Show</string>
@@ -195,9 +207,6 @@
<property name="text">
<string>Hatch Line Width</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
@@ -205,9 +214,6 @@
<property name="text">
<string>Bar width</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
@@ -218,6 +224,18 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>Bins</tabstop>
<tabstop>Type</tabstop>
<tabstop>Cumulative</tabstop>
<tabstop>LegendShow</tabstop>
<tabstop>LegendPos</tabstop>
<tabstop>Title</tabstop>
<tabstop>XLabel</tabstop>
<tabstop>YLabel</tabstop>
<tabstop>BarWidth</tabstop>
<tabstop>HatchWidth</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskPostGlyph</class>
<widget class="QWidget" name="TaskPostGlyph">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>302</width>
<height>302</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Glyph settings</string>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>The form of the glyph</string>
</property>
<property name="text">
<string>Grid</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="Grid">
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="LegendShow">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Legend</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="LegendPos">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_10">
<property name="toolTip">
<string>Which vector field is used to orient the glyphs</string>
</property>
<property name="text">
<string>Scale</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="Scale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Which vector field is used to orient the glyphs</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="Form">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Labels</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="Title"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Y Axis</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="YLabel"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components</string>
</property>
<property name="text">
<string>X Axis</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string>A constant multiplier the glyphs are scaled with</string>
</property>
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="XLabel"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>Grid</tabstop>
<tabstop>LegendShow</tabstop>
<tabstop>LegendPos</tabstop>
<tabstop>Scale</tabstop>
<tabstop>Title</tabstop>
<tabstop>XLabel</tabstop>
<tabstop>YLabel</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -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"):

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,153 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD post extractors 2D"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_histogram
# \ingroup FEM
# \brief Post processing plot displaying lines
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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,211 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD post line plot"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package post_lineplot
# \ingroup FEM
# \brief Post processing plot displaying lines
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

View File

@@ -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]

View File

@@ -0,0 +1,163 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD FEM 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

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,3 @@
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
@@ -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