diff --git a/src/Mod/Fem/App/FemPostFilterPy.xml b/src/Mod/Fem/App/FemPostFilterPy.xml index 28d1823f69..3fe0e4fd88 100644 --- a/src/Mod/Fem/App/FemPostFilterPy.xml +++ b/src/Mod/Fem/App/FemPostFilterPy.xml @@ -49,6 +49,13 @@ Note: Can lead to a full recompute of the whole pipeline, hence best to call thi Returns the names of all scalar fields available on this filter's input. Note: Can lead to a full recompute of the whole pipeline, hence best to call this only in "execute", where the user expects long calculation cycles. + + + + + + +Returns the filters vtk algorithm currently used as output (the one generating the Data field). Note that the output algorithm may change depending on filter settings. " diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index dddf9048e1..097915f78e 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -38,6 +38,7 @@ #ifdef FC_USE_VTK_PYTHON #include #include +#include #endif // BUILD_FEM_VTK using namespace Fem; @@ -129,6 +130,9 @@ PyObject* FemPostFilterPy::getInputData(PyObject* args) case VTK_UNSTRUCTURED_GRID: copy = vtkUnstructuredGrid::New(); break; + case VTK_POLY_DATA: + copy = vtkPolyData::New(); + break; default: PyErr_SetString(PyExc_TypeError, "cannot return datatype object; not unstructured grid"); @@ -183,6 +187,25 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) return Py::new_reference_to(list); } +PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the algorithm + auto algorithm = getFemPostFilterPtr()->getFilterOutput(); + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); + + return Py::new_reference_to(py_algorithm); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostFilterPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/FemPostObjectPy.xml b/src/Mod/Fem/App/FemPostObjectPy.xml index cc4d4dacef..8f5603234b 100644 --- a/src/Mod/Fem/App/FemPostObjectPy.xml +++ b/src/Mod/Fem/App/FemPostObjectPy.xml @@ -23,6 +23,13 @@ filename: str File extension is automatically detected from data type. + + + getDataset() -> vtkDataSet + +Returns the current output dataset. For normal filters this is equal to the objects Data property output. However, a pipelines Data property could store multiple frames, and hence Data can be of type vtkCompositeData, which is not a vtkDataset. To simplify implementations this function always returns a vtkDataSet, and for a pipeline it will be the dataset of the currently selected frame. Note that the returned value could be None, if no data is set at all. + + diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index 81ee5119ac..27a1204bc0 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -29,6 +29,10 @@ #include "FemPostObjectPy.h" #include "FemPostObjectPy.cpp" +#ifdef BUILD_FEM_VTK_WRAPPER + #include + #include +#endif //BUILD_FEM_VTK using namespace Fem; @@ -55,6 +59,27 @@ PyObject* FemPostObjectPy::writeVTK(PyObject* args) Py_Return; } +PyObject* FemPostObjectPy::getDataSet(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the dataset + auto dataset = getFemPostObjectPtr()->getDataSet(); + if (dataset) { + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset); + return Py::new_reference_to(py_algorithm); + } + return Py_None; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostObjectPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/FemPostPipeline.h b/src/Mod/Fem/App/FemPostPipeline.h index 15d9149705..d590915cb4 100644 --- a/src/Mod/Fem/App/FemPostPipeline.h +++ b/src/Mod/Fem/App/FemPostPipeline.h @@ -118,6 +118,12 @@ public: unsigned int getFrameNumber(); std::vector getFrameValues(); + // output algorithm handling + vtkSmartPointer getOutputAlgorithm() + { + return m_source_algorithm; + } + protected: void onChanged(const App::Property* prop) override; bool allowObject(App::DocumentObject* obj) override; diff --git a/src/Mod/Fem/App/FemPostPipelinePy.xml b/src/Mod/Fem/App/FemPostPipelinePy.xml index c71981393b..ab15496be9 100644 --- a/src/Mod/Fem/App/FemPostPipelinePy.xml +++ b/src/Mod/Fem/App/FemPostPipelinePy.xml @@ -71,5 +71,12 @@ Load a single result object or create a multiframe result by loading multiple re Change name of data arrays + + + +Returns the pipeline vtk algorithm, which generates the data passed to the pipelines filters. Note that the output algorithm may change depending on pipeline settings. + + + " diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index be59cdefb2..3154800802 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -34,6 +34,10 @@ #include "FemPostPipelinePy.cpp" // clang-format on +#ifdef BUILD_FEM_VTK_WRAPPER + #include +#endif //BUILD_FEM_VTK + using namespace Fem; @@ -313,6 +317,25 @@ PyObject* FemPostPipelinePy::renameArrays(PyObject* args) Py_Return; } +PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args) +{ +#ifdef BUILD_FEM_VTK_WRAPPER + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + // return python object for the algorithm + auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm(); + PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); + + return Py::new_reference_to(py_algorithm); +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif +} + PyObject* FemPostPipelinePy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/App/PropertyPostDataObject.cpp b/src/Mod/Fem/App/PropertyPostDataObject.cpp index 3ab5e1cbbf..83648350ad 100644 --- a/src/Mod/Fem/App/PropertyPostDataObject.cpp +++ b/src/Mod/Fem/App/PropertyPostDataObject.cpp @@ -32,8 +32,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -243,6 +246,9 @@ void PropertyPostDataObject::createDataObjectByExternalType(vtkSmartPointer::New(); break; + case VTK_TABLE: + m_dataObject = vtkSmartPointer::New(); + break; default: throw Base::TypeError("Unsupported VTK data type"); }; @@ -313,6 +319,9 @@ void PropertyPostDataObject::Save(Base::Writer& writer) const case VTK_MULTIBLOCK_DATA_SET: extension = "zip"; break; + case VTK_TABLE: + extension = ".vtt"; + break; default: break; }; @@ -382,13 +391,16 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const xmlWriter = vtkSmartPointer::New(); xmlWriter->SetInputDataObject(m_dataObject); xmlWriter->SetFileName(datafile.filePath().c_str()); - xmlWriter->SetDataModeToBinary(); + } + else if (m_dataObject->IsA("vtkTable")) { + xmlWriter = vtkSmartPointer::New(); + xmlWriter->SetInputDataObject(m_dataObject); + xmlWriter->SetFileName(fi.filePath().c_str()); } else { xmlWriter = vtkSmartPointer::New(); xmlWriter->SetInputDataObject(m_dataObject); xmlWriter->SetFileName(fi.filePath().c_str()); - xmlWriter->SetDataModeToBinary(); #ifdef VTK_CELL_ARRAY_V2 // Looks like an invalid data object that causes a crash with vtk9 @@ -399,6 +411,7 @@ void PropertyPostDataObject::SaveDocFile(Base::Writer& writer) const } #endif } + xmlWriter->SetDataModeToBinary(); if (xmlWriter->Write() != 1) { // Note: Do NOT throw an exception here because if the tmp. file could @@ -481,6 +494,9 @@ void PropertyPostDataObject::RestoreDocFile(Base::Reader& reader) else if (extension == "vti") { xmlReader = vtkSmartPointer::New(); } + else if (extension == "vtt") { + xmlReader = vtkSmartPointer::New(); + } else if (extension == "zip") { // first unzip the file into a datafolder diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index a0390b1035..e5df8521ea 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 ) @@ -182,6 +182,8 @@ SET(FemObjects_SRCS femobjects/base_femelement.py femobjects/base_femmeshelement.py femobjects/base_fempythonobject.py + femobjects/base_fempostextractors.py + femobjects/base_fempostvisualizations.py femobjects/constant_vacuumpermittivity.py femobjects/constraint_bodyheatsource.py femobjects/constraint_centrif.py @@ -217,6 +219,8 @@ if(BUILD_FEM_VTK_PYTHON) SET(FemObjects_SRCS ${FemObjects_SRCS} femobjects/post_glyphfilter.py + femobjects/post_extract1D.py + femobjects/post_histogram.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -597,6 +601,7 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/__init__.py femtaskpanels/base_femtaskpanel.py femtaskpanels/base_femlogtaskpanel.py + femtaskpanels/base_fempostpanel.py femtaskpanels/task_constraint_bodyheatsource.py femtaskpanels/task_constraint_centrif.py femtaskpanels/task_constraint_currentdensity.py @@ -628,6 +633,8 @@ if(BUILD_FEM_VTK_PYTHON) SET(FemGuiTaskPanels_SRCS ${FemGuiTaskPanels_SRCS} femtaskpanels/task_post_glyphfilter.py + femtaskpanels/task_post_histogram.py + femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -642,6 +649,10 @@ SET(FemGuiUtils_SRCS femguiutils/migrate_gui.py femguiutils/selection_widgets.py femguiutils/vtk_module_handling.py + femguiutils/vtk_table_view.py + femguiutils/data_extraction.py + femguiutils/extract_link_view.py + femguiutils/post_visualization.py ) SET(FemGuiViewProvider_SRCS @@ -651,6 +662,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_base_femmaterial.py femviewprovider/view_base_femmeshelement.py femviewprovider/view_base_femobject.py + femviewprovider/view_base_fempostvisualization.py femviewprovider/view_constant_vacuumpermittivity.py femviewprovider/view_constraint_bodyheatsource.py femviewprovider/view_constraint_centrif.py @@ -686,6 +698,8 @@ if(BUILD_FEM_VTK_PYTHON) SET(FemGuiViewProvider_SRCS ${FemGuiViewProvider_SRCS} femviewprovider/view_post_glyphfilter.py + femviewprovider/view_post_extract.py + femviewprovider/view_post_histogram.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 7abc3b4df0..f624778753 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -291,6 +291,8 @@ if(BUILD_FEM_VTK) SphereWidget.ui TaskPostBoxes.h TaskPostBoxes.cpp + TaskPostExtraction.h + TaskPostExtraction.cpp TaskPostCalculator.ui TaskPostClip.ui TaskPostContours.ui @@ -440,6 +442,10 @@ SET(FemGuiPythonUI_SRCS Resources/ui/SolverCalculiX.ui Resources/ui/SolverCcxTools.ui Resources/ui/TaskPostGlyph.ui + Resources/ui/TaskPostExtraction.ui + Resources/ui/TaskPostHistogram.ui + Resources/ui/PostHistogramFieldViewEdit.ui + Resources/ui/PostHistogramFieldAppEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index 7e15fdf17e..8777f6b4dc 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -86,6 +86,12 @@ icons/FEM_PostFrames.svg icons/FEM_PostBranchFilter.svg icons/FEM_PostPipelineFromResult.svg + icons/FEM_PostLineplot.svg + icons/FEM_PostPlotline.svg + icons/FEM_PostHistogram.svg + icons/FEM_PostSpreadsheet.svg + icons/FEM_PostField.svg + icons/FEM_PostIndex.svg icons/FEM_ResultShow.svg icons/FEM_ResultsPurge.svg diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg new file mode 100644 index 0000000000..5a42219430 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg @@ -0,0 +1,60 @@ + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg new file mode 100644 index 0000000000..4e6d52d4a1 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg new file mode 100644 index 0000000000..36c93c04ba --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg new file mode 100644 index 0000000000..637dac60be --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg new file mode 100644 index 0000000000..a788318bac --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg new file mode 100644 index 0000000000..6220e8e87f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui new file mode 100644 index 0000000000..a89c7ef39b --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -0,0 +1,66 @@ + + + Form + + + + 0 + 0 + 317 + 118 + + + + Form + + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One field for all frames + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui new file mode 100644 index 0000000000..bc26238b94 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -0,0 +1,162 @@ + + + PostHistogramEdit + + + + 0 + 0 + 293 + 126 + + + + Form + + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + Lines: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + Density of hatch pattern + + + 1 + + + + + + + Bars: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + Color of all lines (bar outline and hatches) + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + + + Legend: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+
+ + +
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui new file mode 100644 index 0000000000..8f082da23f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui @@ -0,0 +1,54 @@ + + + TaskPostExtraction + + + + 0 + 0 + 515 + 36 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Data Summary + + + + + + + Show Data + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui new file mode 100644 index 0000000000..70e2f3ecba --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui @@ -0,0 +1,223 @@ + + + TaskPostGlyph + + + + 0 + 0 + 343 + 498 + + + + Glyph settings + + + Qt::LayoutDirection::LeftToRight + + + + + + + + The form of the glyph + + + Bins + + + + + + + + 0 + 0 + + + + Qt::LayoutDirection::LeftToRight + + + 2 + + + 1000 + + + + + + + Which vector field is used to orient the glyphs + + + Type + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + Cumulative + + + + + + + + + Legend + + + + + + Qt::LayoutDirection::LeftToRight + + + Show + + + + + + + + 0 + 0 + + + + + + + + + + + + 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 + + + + + + + + + + + + + Visuals + + + + + + 1.000000000000000 + + + 0.050000000000000 + + + + + + + Hatch Line Width + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Bar width + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 805977f6d2..d880f73d33 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -64,7 +64,6 @@ #include "ui_TaskPostFrames.h" #include "ui_TaskPostBranch.h" - #include "FemSettings.h" #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" @@ -72,6 +71,9 @@ #include "ViewProviderFemPostObject.h" #include "ViewProviderFemPostBranchFilter.h" +#include +#include +#include using namespace FemGui; using namespace Gui; @@ -214,9 +216,14 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, setWindowTitle(title); setWindowIcon(icon); m_icon = icon; + + m_connection = m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, this, boost::placeholders::_1, boost::placeholders::_2)); } -TaskPostWidget::~TaskPostWidget() = default; +TaskPostWidget::~TaskPostWidget() +{ + m_connection.disconnect(); +}; bool TaskPostWidget::autoApply() { @@ -256,6 +263,14 @@ void TaskPostWidget::updateEnumerationList(App::PropertyEnumeration& prop, QComb box->setCurrentIndex(index); } +void TaskPostWidget::handlePropertyChange(const App::DocumentObject& obj, const App::Property& prop) +{ + if (auto postobj = m_object.get()) { + if (&prop == &postobj->Data) { + this->onPostDataChanged(postobj); + } + } +} // *************************************************************************** // simulation dialog for the TaskView @@ -475,7 +490,6 @@ void TaskPostDisplay::onTransparencyValueChanged(int i) void TaskPostDisplay::applyPythonCode() {} - // *************************************************************************** // functions TaskPostFunction::TaskPostFunction(ViewProviderFemPostFunction* view, QWidget* parent) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index d37742dd27..9b24eb314f 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -42,6 +42,7 @@ class Ui_TaskPostWarpVector; class Ui_TaskPostCut; class Ui_TaskPostFrames; class Ui_TaskPostBranch; +class Ui_TaskPostExtraction; class SoFontStyle; class SoText2; @@ -187,10 +188,15 @@ protected: static void updateEnumerationList(App::PropertyEnumeration&, QComboBox* box); + // object update handling + void handlePropertyChange(const App::DocumentObject&, const App::Property&); + virtual void onPostDataChanged(Fem::FemPostObject*) {}; + private: QPixmap m_icon; App::DocumentObjectWeakPtrT m_object; Gui::ViewProviderWeakPtrT m_view; + boost::signals2::connection m_connection; }; @@ -267,7 +273,6 @@ private: std::unique_ptr ui; }; - // *************************************************************************** // functions class ViewProviderFemPostFunction; diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp new file mode 100644 index 0000000000..ef70109462 --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -0,0 +1,169 @@ +/*************************************************************************** + * Copyright (c) 2015 Stefan Tröger * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ + +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ViewProviderFemPostObject.h" +#include "TaskPostExtraction.h" + +using namespace FemGui; +using namespace Gui; + + +// *************************************************************************** +// box to handle data extractions + +TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent) + : TaskPostWidget(view, + Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), + parent) +{ + // we load the python implementation, and try to get the widget from it, to add + // directly our widget + + setWindowTitle(tr("Data and extractions")); + + Base::PyGILStateLocker lock; + + Py::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true); + if (mod.isNull()) + throw Base::ImportError("Unable to import data extraction widget"); + + try { + Py::Callable method(mod.getAttr(std::string("DataExtraction"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(view->getPyObject())); + m_panel = Py::Object(method.apply(args)); + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } + + if (m_panel.hasAttr(std::string("widget"))) { + Py::Object pywidget(m_panel.getAttr(std::string("widget"))); + + Gui::PythonWrapper wrap; + if (wrap.loadCoreModule()) { + QObject* object = wrap.toQObject(pywidget); + if (object) { + QWidget* widget = qobject_cast(object); + if (widget) { + // finally we have the usable QWidget. Add to us! + + auto layout = new QVBoxLayout(); + layout->addWidget(widget); + setLayout(layout); + return; + } + } + } + } + // if we are here somethign went wrong! + throw Base::ImportError("Unable to import data extraction widget"); +}; + +TaskPostExtraction::~TaskPostExtraction() { + + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("widget"))) { + m_panel.setAttr(std::string("widget"), Py::None()); + } + m_panel = Py::None(); + } + catch (Py::AttributeError& e) { + e.clear(); + } +} + +void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("onPostDataChanged"))) { + Py::Callable method(m_panel.getAttr(std::string("onPostDataChanged"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(obj->getPyObject())); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +}; + +bool TaskPostExtraction::isGuiTaskOnly() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("isGuiTaskOnly"))) { + Py::Callable method(m_panel.getAttr(std::string("isGuiTaskOnly"))); + auto result = Py::Boolean(method.apply()); + return result.as_bool(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } + + return false; +}; + +void TaskPostExtraction::apply() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("apply"))) { + Py::Callable method(m_panel.getAttr(std::string("apply"))); + method.apply(); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +#include "moc_TaskPostExtraction.cpp" diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.h b/src/Mod/Fem/Gui/TaskPostExtraction.h new file mode 100644 index 0000000000..5423a83d00 --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.h @@ -0,0 +1,67 @@ +/*************************************************************************** + * Copyright (c) 2025 Stefan Tröger * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#ifndef GUI_TASKVIEW_TaskPostExtraction_H +#define GUI_TASKVIEW_TaskPostExtraction_H + +#include +#include +#include +#include + +#include + +#include "TaskPostBoxes.h" + +#include +#include + +class Ui_TaskPostExtraction; + + +namespace FemGui +{ + +// *************************************************************************** +// box to handle data extractions: It is implemented in python, the c++ +// code is used to access it and manage it for the c++ task panels +class TaskPostExtraction: public TaskPostWidget +{ + Q_OBJECT + +public: + explicit TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent = nullptr); + ~TaskPostExtraction(); + +protected: + bool isGuiTaskOnly() override; + void apply() override; + void onPostDataChanged(Fem::FemPostObject* obj) override; + +private: + Py::Object m_panel; +}; + + +} // namespace FemGui + +#endif // GUI_TASKVIEW_TaskPostExtraction_H diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.ui b/src/Mod/Fem/Gui/TaskPostExtraction.ui new file mode 100644 index 0000000000..7387ffb7de --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostExtraction.ui @@ -0,0 +1,135 @@ + + + TaskPostExtraction + + + + 0 + 0 + 375 + 302 + + + + Form + + + + + + + + Data Summary + + + + + + + Show Data + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 20 + 10 + + + + + + + + + + + 0 + 0 + + + + Data used in: + + + Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + Add data to + + + + + + + + + 0 + 0 + + + + + Create and add + + + + + + + + + + true + + + + + 0 + 0 + 359 + 188 + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml index c41959e24d..9a41e8e972 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPy.xml @@ -20,5 +20,10 @@ Returns the display option task panel for a post processing edit task dialog. + + + Returns the data extraction task panel for a post processing edit task dialog. + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp index c922d76840..7caff695eb 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -27,6 +27,7 @@ #include #include "ViewProviderFemPostFilter.h" #include "TaskPostBoxes.h" +#include "TaskPostExtraction.h" // inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml) #include "ViewProviderFemPostFilterPy.h" #include "ViewProviderFemPostFilterPy.cpp" @@ -60,6 +61,24 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) return nullptr; } +PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args) +{ + // we take no arguments + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + auto panel = new TaskPostExtraction(getViewProviderFemPostObjectPtr()); + + Gui::PythonWrapper wrap; + if (wrap.loadCoreModule()) { + return Py::new_reference_to(wrap.fromQWidget(panel)); + } + + PyErr_SetString(PyExc_TypeError, "creating the panel failed"); + return nullptr; +} + PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const { return nullptr; diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 0d44e6486e..bc4dd1d953 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -67,6 +67,7 @@ #include #include "TaskPostBoxes.h" +#include "TaskPostExtraction.h" #include "ViewProviderAnalysis.h" #include "ViewProviderFemPostObject.h" @@ -1019,8 +1020,11 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { assert(dlg->getView() == this); - auto panel = new TaskPostDisplay(this); - dlg->addTaskBox(panel->windowIcon().pixmap(32), panel); + auto disp_panel = new TaskPostDisplay(this); + dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel); + + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); } void ViewProviderFemPostObject::unsetEdit(int ModNum) diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 3ae3219705..acd7202acc 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -214,7 +214,11 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterDataAtPoint" << "FEM_PostFilterCalculator" << "Separator" - << "FEM_PostCreateFunctions"; + << "FEM_PostCreateFunctions" +#ifdef BUILD_FEM_VTK_WRAPPER + << "FEM_PostVisualization" +#endif + ; #endif Gui::ToolBarItem* utils = new Gui::ToolBarItem(root); @@ -366,7 +370,11 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterDataAtPoint" << "FEM_PostFilterCalculator" << "Separator" - << "FEM_PostCreateFunctions"; + << "FEM_PostCreateFunctions" +#ifdef BUILD_FEM_VTK_WRAPPER + << "FEM_PostVisualization" +#endif + ; #endif Gui::MenuItem* utils = new Gui::MenuItem; diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c05ecc6108..1dce3db5f7 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -686,6 +686,48 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): return obj +def makePostVtkLinePlot(doc, name="Lineplot"): + """makePostVtkLineplot(document, [name]): + creates a FEM post processing line plot + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_lineplot + + 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) + return obj + + +def makePostVtkHistogram(doc, name="Histogram"): + """makePostVtkHistogram(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogram(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogram(obj.ViewObject) + return obj + + # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): """makeEquationDeformation(document, [base_solver], [name]): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 0c61dcff2b..98c620a96c 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -40,6 +40,7 @@ from .manager import CommandManager from femtools.femutils import expandParentObject from femtools.femutils import is_of_type from femsolver.settings import get_default_solver +from femguiutils import post_visualization # Python command definitions: # for C++ command definitions see src/Mod/Fem/Command.cpp @@ -1231,7 +1232,6 @@ class _PostFilterGlyph(CommandManager): self.is_active = "with_vtk_selresult" self.do_activated = "add_filter_set_edit" - # the string in add command will be the page name on FreeCAD wiki FreeCADGui.addCommand("FEM_Analysis", _Analysis()) FreeCADGui.addCommand("FEM_ClippingPlaneAdd", _ClippingPlaneAdd()) @@ -1289,3 +1289,8 @@ FreeCADGui.addCommand("FEM_SolverZ88", _SolverZ88()) if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) + + # setup all visualization commands (register by importing) + import femobjects.post_histogram + post_visualization.setup_commands("FEM_PostVisualization") + diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py new file mode 100644 index 0000000000..4eeffbcef4 --- /dev/null +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -0,0 +1,139 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing ldata view and extraction widget" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package data_extraction +# \ingroup FEM +# \brief A widget for data extraction. Used in the PostObject task panel. + +from . import vtk_table_view + +from PySide import QtCore, QtGui + +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter +from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents + +import FreeCAD +import FreeCADGui + +from femtaskpanels.base_fempostpanel import _BasePostTaskPanel + +from . import extract_link_view +ExtractLinkView = extract_link_view.ExtractLinkView + +class DataExtraction(_BasePostTaskPanel): + # The class is not a widget itself, but provides a widget. It implements + # all required callbacks for the widget and the task dialog. + # Note: This object is created and used from c++! See PostTaskExtraction + + def __init__(self, vobj): + + super().__init__(vobj.Object) + + self.ViewObject = vobj + self.Object = vobj.Object + + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostExtraction.ui" + ) + + # connect all signals as required + self.widget.Data.clicked.connect(self.showData) + self.widget.Summary.clicked.connect(self.showSummary) + + # setup the data models + self.data_model = vtk_table_view.VtkTableModel() + self.summary_model = vtk_table_view.VtkTableSummaryModel() + + # generate the data + self.onPostDataChanged(self.Object) + + # setup the extraction widget + self._extraction_view = ExtractLinkView(self.Object, True, self) + self.widget.layout().addSpacing(self.widget.Data.size().height()/3) + self.widget.layout().addWidget(self._extraction_view) + self._extraction_view.repopulate() + + + @QtCore.Slot() + def showData(self): + + dialog = QtGui.QDialog(self.widget) + widget = vtk_table_view.VtkTableView(self.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() + def showSummary(self): + + dialog = QtGui.QDialog(self.widget) + widget = vtk_table_view.VtkTableView(self.summary_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0,0,0,0) + + dialog.setLayout(layout) + dialog.resize(600, 900) + dialog.show() + + def isGuiTaskOnly(self): + # If all panels return true it omits the Apply button in the dialog + return True + + def onPostDataChanged(self, obj): + + algo = obj.getOutputAlgorithm() + if not algo: + self.data_model.setTable(vtkTable()) + + filter = vtkAttributeDataToTableFilter() + filter.SetInputConnection(0, algo.GetOutputPort(0)) + filter.Update() + table = filter.GetOutputDataObject(0) + + # add the points + points = algo.GetOutputDataObject(0).GetPoints().GetData() + table.InsertColumn(points, 0) + + # split the components + splitter = vtkSplitColumnComponents() + splitter.SetNamingModeToNamesWithParens() + splitter.SetInputData(0, table) + + splitter.Update() + table = splitter.GetOutputDataObject(0) + + self.data_model.setTable(table) + self.summary_model.setTable(table) + + def apply(self): + pass diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py new file mode 100644 index 0000000000..60baecd9a4 --- /dev/null +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -0,0 +1,490 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing view for summarizing extractor links" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package data_extraction +# \ingroup FEM +# \brief A widget that shows summaries of all available links to extractors + +from PySide import QtGui, QtCore + +import femobjects.base_fempostextractors as extr +import femobjects.base_fempostvisualizations as vis + +import FreeCAD +import FreeCADGui + +from . import post_visualization as pv + +# a model showing available visualizations and possible extractions +# ################################################################# + +def build_new_visualization_tree_model(): + # model that shows all options to create new visualizations + + model = QtGui.QStandardItemModel() + + visualizations = pv.get_registered_visualizations() + for vis_name in visualizations: + vis_icon = FreeCADGui.getIcon(visualizations[vis_name].icon) + vis_item = QtGui.QStandardItem(vis_icon, f"New {vis_name}") + vis_item.setFlags(QtGui.Qt.ItemIsEnabled) + vis_item.setData(visualizations[vis_name]) + + for ext in visualizations[vis_name].extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_name) + ext_item = QtGui.QStandardItem(icon, f"with {name}") + ext_item.setData(ext) + vis_item.appendRow(ext_item) + model.appendRow(vis_item) + + return model + +def build_add_to_visualization_tree_model(): + # model that shows all possible visualization objects to add data to + + visualizations = pv.get_registered_visualizations() + model = QtGui.QStandardItemModel() + + for obj in FreeCAD.ActiveDocument.Objects: + if obj.isDerivedFrom("Fem::FemAnalysis"): + ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label) + ana_item.setFlags(QtGui.Qt.ItemIsEnabled) + + # check all children it it is a visualization + for child in obj.Group: + if vis.is_visualization_object(child): + + vis_item = QtGui.QStandardItem(child.ViewObject.Icon, child.Label) + vis_type = vis.get_visualization_type(child) + vis_item.setFlags(QtGui.Qt.ItemIsEnabled) + vis_item.setData(child) + ana_item.appendRow(vis_item) + + # add extractor items + for ext in visualizations[vis_type].extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_type) + ext_item = QtGui.QStandardItem(icon, f"Add {name}") + ext_item.setData(ext) + vis_item.appendRow(ext_item) + + if ana_item.rowCount(): + model.appendRow(ana_item) + + return model + +def build_post_object_item(post_object, extractions, vis_type): + + # definitely build a item and add the extractions + post_item = QtGui.QStandardItem(post_object.ViewObject.Icon, f"From {post_object.Label}") + post_item.setFlags(QtGui.Qt.ItemIsEnabled) + post_item.setData(post_object) + + # add extractor items + for ext in extractions: + icon = FreeCADGui.getIcon(ext.icon) + name = ext.name.removeprefix(vis_type) + ext_item = QtGui.QStandardItem(icon, f"add {name}") + ext_item.setData(ext) + post_item.appendRow(ext_item) + + # if we are a post group, we need to add the children + if post_object.hasExtension("Fem::FemPostGroupExtension"): + + for child in post_object.Group: + if child.isDerivedFrom("Fem::FemPostObject"): + item = build_post_object_item(child, extractions, vis_type) + post_item.appendRow(item) + + return post_item + + +def build_add_from_data_tree_model(vis_type): + # model that shows all Post data objects from which data can be extracted + extractions = pv.get_registered_visualizations()[vis_type].extractions + + model = QtGui.QStandardItemModel() + for obj in FreeCAD.ActiveDocument.Objects: + if obj.isDerivedFrom("Fem::FemAnalysis"): + ana_item = QtGui.QStandardItem(obj.ViewObject.Icon, obj.Label) + ana_item.setFlags(QtGui.Qt.ItemIsEnabled) + + # check all children if it is a post object + for child in obj.Group: + if child.isDerivedFrom("Fem::FemPostObject"): + item = build_post_object_item(child, extractions, vis_type) + ana_item.appendRow(item) + + if ana_item.rowCount(): + model.appendRow(ana_item) + + return model + +class TreeChoiceButton(QtGui.QToolButton): + + selection = QtCore.Signal(object,object) + + def __init__(self, model): + super().__init__() + + self.model = model + self.setEnabled(bool(model.rowCount())) + + self.__skip_next_hide = False + + self.tree_view = QtGui.QTreeView(self) + self.tree_view.setModel(model) + + self.tree_view.setFrameShape(QtGui.QFrame.NoFrame) + self.tree_view.setHeaderHidden(True) + self.tree_view.setEditTriggers(QtGui.QTreeView.EditTriggers.NoEditTriggers) + self.tree_view.setSelectionBehavior(QtGui.QTreeView.SelectionBehavior.SelectRows) + self.tree_view.expandAll() + self.tree_view.activated.connect(self.selectIndex) + + # set a complex menu + self.popup = QtGui.QWidgetAction(self) + self.popup.setDefaultWidget(self.tree_view) + self.setPopupMode(QtGui.QToolButton.InstantPopup) + self.addAction(self.popup); + + QtCore.Slot(QtCore.QModelIndex) + def selectIndex(self, index): + item = self.model.itemFromIndex(index) + + if item and not item.hasChildren(): + extraction = item.data() + parent = item.parent().data() + self.selection.emit(parent, extraction) + self.popup.trigger() + + def setModel(self, model): + self.model = model + self.tree_view.setModel(model) + self.tree_view.expandAll() + + # check if we should be disabled + self.setEnabled(bool(model.rowCount())) + + +# implementationof GUI and its functionality +# ########################################## + +class _ShowVisualization: + def __init__(self, st_object): + self._st_object = st_object + + def __call__(self): + if vis.is_visualization_object(self._st_object): + # show the visualization + self._st_object.ViewObject.Proxy.show_visualization() + else: + # for now just select the thing + 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 + + widgets = self._extractor.ViewObject.Proxy.get_edit_widgets(self._post_dialog) + vbox = QtGui.QVBoxLayout() + + buttonBox = QtGui.QDialogButtonBox() + buttonBox.setCenterButtons(True) + buttonBox.setStandardButtons(self._post_dialog.getStandardButtons()) + vbox.addWidget(buttonBox) + + started = False + for widget in widgets: + + if started: + # add a seperator line + frame = QtGui.QFrame() + frame.setFrameShape(QtGui.QFrame.HLine); + vbox.addWidget(frame); + else: + started = True + + vbox.addWidget(widget) + + vbox.addStretch() + + 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) + + + 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): + + def __init__(self, obj, is_source, post_dialog): + # initializes the view. + # obj: The object for which the links should be shown / summarized + # is_source: Bool, if the object is the data source (e.g. postobject), or the target (e.g. plots) + + super().__init__() + + self._object = obj + self._is_source = is_source + self._post_dialog = post_dialog + self._widgets = [] + + # build the layout: + self._scroll_view = QtGui.QScrollArea(self) + self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff) + self._scroll_view.setWidgetResizable(True) + + hbox = QtGui.QHBoxLayout() + label = QtGui.QLabel("Data used in:") + if not self._is_source: + label.setText("Data used from:") + + label.setAlignment(QtGui.Qt.AlignBottom) + hbox.addWidget(label) + hbox.addStretch() + + if self._is_source: + + 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.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.setText("Add data from") + self._add.selection.connect(self.addExtractionToPostObject) + hbox.addWidget(self._add) + + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0,0,0,0) + vbox.addItem(hbox) + vbox.addWidget(self._scroll_view) + + 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)) + + return widget + + def repopulate(self): + # collect all links that are available and shows them + + # clear the view + for widget in self._widgets: + widget.hide() + widget.deleteLater() + + self._widgets = [] + + # rebuild the widgets + + if self._is_source: + candidates = self._object.InList + else: + candidates = self._object.OutList + + # get all widgets from the candidates + extractors = [] + for candidate in candidates: + if extr.is_extractor_object(candidate): + summary = self._build_summary_widget(candidate) + self._widgets.append(summary) + + # fill the scroll area + vbox = QtGui.QVBoxLayout() + for widget in self._widgets: + vbox.addWidget(widget) + + vbox.addStretch() + widget = QtGui.QWidget() + widget.setLayout(vbox) + + self._scroll_view.setWidget(widget) + + # also reset the add button model + if self._is_source: + self._add.setModel(build_add_to_visualization_tree_model()) + + def _find_parent_analysis(self, obj): + # iterate upwards, till we find a analysis + for parent in obj.InList: + if parent.isDerivedFrom("Fem::FemAnalysis"): + return parent + + analysis = self._find_parent_analysis(parent) + if analysis: + return analysis + + return None + + QtCore.Slot(object, object) # visualization data, extraction data + def newVisualization(self, vis_data, ext_data): + + doc = self._object.Document + + FreeCADGui.addModule(vis_data.module) + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create visualization + FreeCADGui.doCommand( + f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)" + ) + analysis = self._find_parent_analysis(self._object) + if analysis: + FreeCADGui.doCommand( + f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)" + ) + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" + ) + FreeCADGui.doCommand( + f"visualization.addObject(extraction)" + ) + + self._post_dialog._recompute() + self.repopulate() + + QtCore.Slot(object, object) # visualization object, extraction data + def addExtractionToVisualization(self, vis_obj, ext_data): + + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" + ) + FreeCADGui.doCommand( + f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)" + ) + + self._post_dialog._recompute() + self.repopulate() + + QtCore.Slot(object, object) # post object, extraction data + def addExtractionToPostObject(self, post_obj, ext_data): + + FreeCADGui.addModule(ext_data.module) + FreeCADGui.addModule("FemGui") + + # create extraction and add it + FreeCADGui.doCommand( + f"extraction = {ext_data.module}.{ext_data.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}" + ) + FreeCADGui.doCommand( + f"App.ActiveDocument.{self._object.Name}.addObject(extraction)" + ) + + self._post_dialog._recompute() + self.repopulate() + diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py new file mode 100644 index 0000000000..d1bfc93898 --- /dev/null +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -0,0 +1,162 @@ +# *************************************************************************** +# * 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 visualization registry" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_visualization +# \ingroup FEM +# \brief A registry to collect visualizations for use in menus + +import copy +from dataclasses import dataclass + +from PySide import QtGui, QtCore + +import FreeCAD +import FreeCADGui +import FemGui + +# Registry to handle visulization commands +# ######################################## + +_registry = {} + +@dataclass +class _Extraction: + + name: str + icon: str + dimension: str + extracttype: str + module: str + factory: str + +@dataclass +class _Visualization: + + name: str + icon: str + module: str + factory: str + extractions: list[_Extraction] + +# Register a visualization by type, icon and factory function +def register_visualization(visualization_type, icon, module, factory): + if visualization_type in _registry: + raise ValueError("Visualization type already registered") + + _registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, []) + +def register_extractor(visualization_type, extraction_type, icon, dimension, etype, module, factory): + + if not visualization_type in _registry: + raise ValueError("visualization not registered yet") + + extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory) + _registry[visualization_type].extractions.append(extraction) + +def get_registered_visualizations(): + return copy.deepcopy(_registry) + + +def _to_command_name(name): + return "FEM_PostVisualization" + name + +class _VisualizationGroupCommand: + + def GetCommands(self): + visus = _registry.keys() + cmds = [_to_command_name(v) for v in visus] + return cmds + + def GetDefaultCommand(self): + return 0 + + def GetResources(self): + return { 'MenuText': 'Data Visualizations', 'ToolTip': 'Different visualizations to show post processing data in'} + + def IsActive(self): + if not FreeCAD.ActiveDocument: + return False + + return bool(FemGui.getActiveAnalysis()) + + +class _VisualizationCommand: + + def __init__(self, visualization_type): + self._visualization_type = visualization_type + + def GetResources(self): + + cmd = _to_command_name(self._visualization_type) + vis = _registry[self._visualization_type] + tooltip = f"Create a {self._visualization_type} post processing data visualization" + + return { + "Pixmap": vis.icon, + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, f"{self._visualization_type}"), + "Accel": "", + "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), + "CmdType": "AlterDoc" + } + + def IsActive(self): + # active analysis available + if not FreeCAD.ActiveDocument: + return False + + return bool(FemGui.getActiveAnalysis()) + + def Activated(self): + + vis = _registry[self._visualization_type] + FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}") + + FreeCADGui.addModule(vis.module) + FreeCADGui.addModule("FemGui") + + FreeCADGui.doCommand( + f"obj = {vis.module}.{vis.factory}(FreeCAD.ActiveDocument)" + ) + FreeCADGui.doCommand( + f"FemGui.getActiveAnalysis().addObject(obj)" + ) + + FreeCADGui.Selection.clearSelection() + FreeCADGui.doCommand( + "FreeCADGui.ActiveDocument.setEdit(obj)" + ) + +def setup_commands(toplevel_name): + # creates all visualization commands and registers them. The + # toplevel group command will have the name provided to this function. + + # first all visualization and extraction commands + for vis in _registry: + FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis)) + + # build the group command! + FreeCADGui.addCommand("FEM_PostVisualization", _VisualizationGroupCommand()) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py new file mode 100644 index 0000000000..df06c51ee0 --- /dev/null +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -0,0 +1,138 @@ +# *************************************************************************** +# * 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 table view widget to visualize vtkTable" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package vtk_table_view +# \ingroup FEM +# \brief A Qt widget to show a vtkTable + +from PySide import QtGui +from PySide import QtCore + +class VtkTableModel(QtCore.QAbstractTableModel): + # Simple table model. Only supports single component columns + + def __init__(self): + super().__init__() + self._table = None + + def setTable(self, table): + self.beginResetModel() + self._table = table + self.endResetModel() + + def rowCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfRows() + + def columnCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfColumns() + + def data(self, index, role): + + if not self._table: + return None + + if role == QtCore.Qt.DisplayRole: + col = self._table.GetColumn(index.column()) + return col.GetTuple(index.row())[0] + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return self._table.GetColumnName(section) + + if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: + return section + +class VtkTableSummaryModel(QtCore.QAbstractTableModel): + # Simple model showing a summary of the table. + # Only supports single component columns + + def __init__(self): + super().__init__() + self._table = None + + def setTable(self, table): + self.beginResetModel() + self._table = table + self.endResetModel() + + def rowCount(self, index): + + if not self._table: + return 0 + + return self._table.GetNumberOfColumns() + + def columnCount(self, index): + return 2 # min, max + + def data(self, index, role): + + if not self._table: + return None + + if role == QtCore.Qt.DisplayRole: + col = self._table.GetColumn(index.row()) + range = col.GetRange() + return range[index.column()] + + def headerData(self, section, orientation, role): + + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return ["Min","Max"][section] + + if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: + return self._table.GetColumnName(section) + + +class VtkTableView(QtGui.QWidget): + + def __init__(self, model): + super().__init__() + + self.model = model + self.table_view = QtGui.QTableView() + self.table_view.setModel(model) + + # fast initial resize and manual resizing still allowed! + header = self.table_view.horizontalHeader() + header.setResizeContentsPrecision(10) + self.table_view.resizeColumnsToContents() + + layout = QtGui.QVBoxLayout() + layout.setContentsMargins(0,0,0,0) + layout.addWidget(self.table_view) + self.setLayout(layout) + diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py new file mode 100644 index 0000000000..4ccef7018a --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -0,0 +1,199 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \brief base objects for data extractors + +from vtkmodules.vtkCommonDataModel import vtkTable + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +# helper functions +# ################ + +def is_extractor_object(obj): + if not hasattr(obj, "Proxy"): + return False + + return hasattr(obj.Proxy, "ExtractionType") + +def get_extraction_type(obj): + # returns the extractor type string, or throws exception if + # not a extractor + return obj.Proxy.ExtractionType + +def get_extraction_dimension(obj): + # returns the extractor dimension string, or throws exception if + # not a extractor + return obj.Proxy.ExtractionDimension + + +# Base class for all extractors with common source and table handling functionality +# Note: Never use directly, always subclass! This class does not create a +# ExtractionType/Dimension variable, hence will not work correctly. +class Extractor(base_fempythonobject.BaseFemPythonObject): + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + 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="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the extracted data", + value=vtkTable(), + ), + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Base", + doc="The data source from which the data is extracted", + value=None, + ), + ] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(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.Source = None + + + def get_vtk_table(self, obj): + if not obj.DataTable: + obj.DataTable = vtkTable() + + return obj.DataTable + + def component_options(self, num): + + match num: + case 2: + return ["X", "Y"] + case 3: + return ["X", "Y", "Z"] + case _: + return ["Not a vector"] + + +class Extractor1D(Extractor): + + ExtractionDimension = "1D" + + def __init__(self, obj): + super().__init__(obj) + + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="XField", + group="X Data", + doc="The field to use as X data", + value=[], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XComponent", + group="X Data", + doc="Which part of the X field vector to use for the X axis", + value=[], + ), + ] + + return super()._get_properties() + prop + + + def onChanged(self, obj, prop): + + super().onChanged(obj, prop) + + if prop == "XField" and obj.Source and obj.Source.getDataSet(): + point_data = obj.Source.getDataSet().GetPointData() + self._setup_x_component_property(obj, point_data) + + if prop == "Source": + if obj.Source: + dset = obj.Source.getDataSet() + if dset: + self._setup_x_properties(obj, dset) + else: + self._clear_x_properties(obj) + else: + self._clear_x_properties(obj) + + def _setup_x_component_property(self, obj, point_data): + + if obj.XField == "Index": + obj.XComponent = self.component_options(1) + elif obj.XField == "Position": + obj.XComponent = self.component_options(3) + else: + array = point_data.GetAbstractArray(obj.XField) + obj.XComponent = self.component_options(array.GetNumberOfComponents()) + + def _clear_x_properties(self, obj): + if hasattr(obj, "XComponent"): + obj.XComponent = [] + if hasattr(obj, "XField"): + obj.XField = [] + + def _setup_x_properties(self, obj, dataset): + # Set all X Data properties correctly for the given dataset + fields = ["Index", "Position"] + point_data = dataset.GetPointData() + + for i in range(point_data.GetNumberOfArrays()): + fields.append(point_data.GetArrayName(i)) + + current_field = obj.XField + obj.XField = fields + if current_field in fields: + obj.XField = current_field + + self._setup_x_component_property(obj, point_data) + + diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py new file mode 100644 index 0000000000..fae9c58b6c --- /dev/null +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -0,0 +1,71 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostextractors +# \ingroup FEM +# \brief base objects for data extractors + +from vtkmodules.vtkCommonDataModel import vtkTable + +from . import base_fempythonobject + +# helper functions +# ################ + +def is_visualization_object(obj): + if not hasattr(obj, "Proxy"): + return False + + return hasattr(obj.Proxy, "VisualizationType") + +def get_visualization_type(obj): + # returns the extractor type string, or throws exception if + # not a extractor + return obj.Proxy.VisualizationType + + +# Base class for all visualizations +# Note: Never use directly, always subclass! This class does not create a +# Visualization variable, hence will not work correctly. +class PostVisualization(base_fempythonobject.BaseFemPythonObject): + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(obj) + + + def onDocumentRestored(self, obj): + self._setup_properties(obj) + + def _get_properties(self): + return [] diff --git a/src/Mod/Fem/femobjects/base_fempythonobject.py b/src/Mod/Fem/femobjects/base_fempythonobject.py index 45b8de4e7d..48003443c2 100644 --- a/src/Mod/Fem/femobjects/base_fempythonobject.py +++ b/src/Mod/Fem/femobjects/base_fempythonobject.py @@ -54,6 +54,7 @@ class _PropHelper: Helper class to manage property data inside proxy objects. Initialization keywords are the same used with PropertyContainer to add dynamics properties plus "value" for the initial value. + Note: Is used as base for a GUI version, be aware when refactoring """ def __init__(self, **kwds): diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py new file mode 100644 index 0000000000..5a9404e149 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -0,0 +1,178 @@ +# *************************************************************************** +# * 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_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +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): + """ + A post processing extraction of one 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 __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 + 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 + array = self.__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) + + else: + algo = obj.Source.getOutputAlgorithm() + for timestep in timesteps: + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self.__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) + + # set the final table + obj.Table = table + + +class PostIndexData1D(base_fempostextractors.Extractor1D): + """ + 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 new file mode 100644 index 0000000000..df238c6e08 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -0,0 +1,142 @@ +# *************************************************************************** +# * 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_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract1D + +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable + + +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization("Histogram", + ":/icons/FEM_PostHistogram.svg", + "ObjectsFem", + "makePostVtkHistogram") + +post_visualization.register_extractor("Histogram", + "HistogramFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostVtkHistogramFieldData") + + +# Implementation +# ############## + +def is_histogram_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Histogram" + + +class PostHistogramFieldData(post_extract1D.PostFieldData1D): + """ + A 1D Field extraction for histograms. + """ + VisualizationType = "Histogram" + + + + +class PostHistogram(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as histograms + """ + + VisualizationType = "Histogram" + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtensionPython") + + def _get_properties(self): + prop = [ + _PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the plotted data, one column per histogram", + value=vtkTable(), + ), + ] + return super()._get_properties() + prop + + + def onChanged(self, obj, prop): + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if not is_histogram_extractor(child): + FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added") + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + def execute(self, obj): + + # during execution we collect all child data into our table + table = vtkTable() + for child in obj.Group: + + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + # TODO: check which array type it is and use that one + array = vtkDoubleArray() + array.DeepCopy(c_array) + array.SetName(f"{child.Source.Label}: {c_array.GetName()}") + table.AddColumn(array) + + obj.Table = table + return False + + + + + diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py new file mode 100644 index 0000000000..f8798fbc23 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_lineplot.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/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py new file mode 100644 index 0000000000..f90af0e260 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -0,0 +1,83 @@ +# *************************************************************************** +# * 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 task panel base for post object task panels" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package base_fempostpanel +# \ingroup FEM +# \brief task panel base for post objects + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from femguiutils import selection_widgets +from . import base_femtaskpanel + + +class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): + """ + The TaskPanel for post objects, mimicing the c++ functionality + """ + + def __init__(self, obj): + super().__init__(obj) + + # get the settings group + self.__settings_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem") + + # Implement parent functions + # ########################## + + def getStandardButtons(self): + return QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + + def clicked(self, button): + # apply button hit? + if button == QtGui.QDialogButtonBox.Apply: + self.obj.Document.recompute() + + + # Helper functions + # ################ + + def _recompute(self): + # only recompute if the user wants automatic recompute + if self.__settings_grp.GetBool("PostAutoRecompute", True): + self.obj.Document.recompute() + + def _enumPropertyToCombobox(self, obj, prop, cbox): + cbox.blockSignals(True) + cbox.clear() + entries = obj.getEnumerationsOfProperty(prop) + for entry in entries: + cbox.addItem(entry) + + cbox.setCurrentText(getattr(obj, prop)) + cbox.blockSignals(False) + + + diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py new file mode 100644 index 0000000000..5a56077c3e --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -0,0 +1,54 @@ +# *************************************************************************** +# * 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 post extractor object task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_extractor +# \ingroup FEM +# \brief universal task dialog for extractor objects. + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from femguiutils import selection_widgets +from . import base_fempostpanel + + +class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties extractor objects. The actual UI is + provided by the viewproviders. This allows using a universal task panel + """ + + def __init__(self, obj): + super().__init__(obj) + + # form is used to display individual task panels + self.form = obj.ViewObject.Proxy.get_edit_widgets(self) + + + diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index a0658812e6..8804951067 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -35,10 +35,10 @@ import FreeCAD import FreeCADGui from femguiutils import selection_widgets -from . import base_femtaskpanel +from . import base_fempostpanel -class _TaskPanel(base_femtaskpanel._BaseTaskPanel): +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter """ diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py new file mode 100644 index 0000000000..593f177a94 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -0,0 +1,180 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + 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("Histogram data") + + + # histogram parameter widget + self.view_widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" + ) + self.view_widget.setWindowTitle("Histogram 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.view_widget.Bins.setValue(viewObj.Bins) + self._enumPropertyToCombobox(viewObj, "Type", self.view_widget.Type) + self.view_widget.Cumulative.setChecked(viewObj.Cumulative) + + 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) + self.view_widget.BarWidth.setValue(viewObj.BarWidth) + self.view_widget.HatchWidth.setValue(viewObj.HatchLineWidth) + + # connect callbacks + self.view_widget.Bins.valueChanged.connect(self.binsChanged) + self.view_widget.Type.activated.connect(self.typeChanged) + self.view_widget.Cumulative.toggled.connect(self.comulativeChanged) + + 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) + self.view_widget.BarWidth.valueChanged.connect(self.barWidthChanged) + self.view_widget.HatchWidth.valueChanged.connect(self.hatchWidthChanged) + + + 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 binsChanged(self, bins): + self.obj.ViewObject.Bins = bins + + QtCore.Slot(int) + def typeChanged(self, idx): + self.obj.ViewObject.Type = idx + + QtCore.Slot(bool) + def comulativeChanged(self, state): + self.obj.ViewObject.Cumulative = 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 + + QtCore.Slot(float) + def barWidthChanged(self, value): + self.obj.ViewObject.BarWidth = value + + QtCore.Slot(float) + def hatchWidthChanged(self, value): + self.obj.ViewObject.HatchLineWidth = value diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index dc7e6ba8ba..a86c1288a2 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -36,8 +36,25 @@ import FreeCADGui import FemGui # needed to display the icons in TreeView +from femobjects.base_fempythonobject import _PropHelper + False if FemGui.__name__ else True # flake8, dummy FemGui usage +class _GuiPropHelper(_PropHelper): + """ + Helper class to manage property data inside proxy objects. + Based on the App verison, but viewprovider addProperty does + not take keyword args, hence we use positional arguments here + """ + + def __init__(self, **kwds): + super().__init__(**kwds) + + def add_to_object(self, obj): + obj.addProperty(self.info["type"], self.info["name"], self.info["group"], self.info["doc"]) + obj.setPropertyStatus(self.name, "LockDynamic") + setattr(obj, self.name, self.value) + class VPBaseFemObject: """Proxy View Provider for FEM FeaturePythons base constraint.""" diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py new file mode 100644 index 0000000000..153537d669 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -0,0 +1,91 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing visualization base ViewProvider" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_base_fempostvisualizations +# \ingroup FEM +# \brief view provider for post visualization object + +from PySide import QtGui, QtCore + +import Plot +import FreeCADGui + +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper + +class VPPostVisualization: + """ + A View Provider for visualization objects + """ + + def __init__(self, vobj): + vobj.Proxy = self + self._setup_properties(vobj) + + def _setup_properties(self, vobj): + pl = vobj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(vobj) + + def _get_properties(self): + return [] + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def isShow(self): + return True + + def doubleClicked(self,vobj): + + guidoc = FreeCADGui.getDocument(vobj.Object.Document) + + # check if another VP is in edit mode and close it then + if guidoc.getInEdit(): + FreeCADGui.Control.closeDialog() + guidoc.resetEdit() + + guidoc.setEdit(vobj.Object.Name) + return True + + def show_visualization(self): + # shows the visualization without going into edit mode + # to be implemented by subclasses + pass + + def get_kw_args(self, obj): + # returns a dictionary with all visualization options needed for plotting + # based on the view provider properties + return {} + + def dumps(self): + return None + + def loads(self, state): + return None diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_post_extract.py new file mode 100644 index 0000000000..c75dd4bc8b --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_extract.py @@ -0,0 +1,129 @@ + +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import FemGui +from PySide import QtGui + +import femobjects.base_fempostextractors as fpe +from femtaskpanels import task_post_extractor + +class VPPostExtractor: + """ + A View Provider for extraction of data + """ + + def __init__(self, vobj): + vobj.Proxy = self + self._setup_properties(vobj) + + def _setup_properties(self, vobj): + pl = vobj.PropertiesList + for prop in self._get_properties(): + if not prop.name in pl: + prop.add_to_object(vobj) + + def _get_properties(self): + return [] + + def attach(self, vobj): + self.Object = vobj.Object # used on various places, claim childreens, get icon, etc. + self.ViewObject = vobj + + def isShow(self): + return True + + def onChanged(self, vobj, prop): + + # one of our view properties was changed. Lets inform our parent plot + # that this happend, as this is the one that needs to redraw + + if prop == "Proxy": + return + + group = vobj.Object.getParentGroup() + if not group: + return + + if (hasattr(group.ViewObject, "Proxy") and + hasattr(group.ViewObject.Proxy, "childViewPropertyChanged")): + + group.ViewObject.Proxy.childViewPropertyChanged(vobj, prop) + + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_extractor._ExtractorTaskPanel(vobj.Object) + + #show it + FreeCADGui.Control.showDialog(taskd) + + return True + + def doubleClicked(self, vobj): + guidoc = FreeCADGui.getDocument(vobj.Object.Document) + + # check if another VP is in edit mode and close it then + if guidoc.getInEdit(): + FreeCADGui.Control.closeDialog() + guidoc.resetEdit() + + guidoc.setEdit(vobj.Object.Name) + + return True + + def get_kw_args(self): + # should return the plot keyword arguments that represent the properties + # of the object + return {} + + def get_edit_widgets(self, post_dialog): + # Returns a list of widgets for editing the object/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. + # 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 dumps(self): + return None + + def loads(self, state): + return None diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py new file mode 100644 index 0000000000..5a433f17bc --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -0,0 +1,476 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import Plot +import FemGui +from PySide import QtGui, QtCore + +import numpy as np +import matplotlib as mpl + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_post_extract +from . import view_base_fempostvisualization +from femtaskpanels import task_post_histogram + +_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/PostHistogramFieldViewEdit.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, "Hatch", self.widget.Hatch) + self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) + self.widget.LineWidth.setValue(vobj.LineWidth) + self.widget.HatchDensity.setValue(vobj.HatchDensity) + self.widget.BarColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.BarColor])) + self.widget.LineColor.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.LineColor])) + + self.widget.Legend.editingFinished.connect(self.legendChanged) + self.widget.Hatch.activated.connect(self.hatchPatternChanged) + self.widget.LineStyle.activated.connect(self.lineStyleChanged) + self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged) + self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) + self.widget.LineColor.changed.connect(self.lineColorChanged) + self.widget.BarColor.changed.connect(self.barColorChanged) + + @QtCore.Slot() + def lineColorChanged(self): + color = self.widget.LineColor.property("color") + self._object.ViewObject.LineColor = color.getRgb() + + @QtCore.Slot() + def barColorChanged(self): + color = self.widget.BarColor.property("color") + self._object.ViewObject.BarColor = color.getRgb() + + @QtCore.Slot(float) + def lineWidthChanged(self, value): + self._object.ViewObject.LineWidth = value + + @QtCore.Slot(float) + def hatchDensityChanged(self, value): + self._object.ViewObject.HatchDensity = value + + @QtCore.Slot(int) + def hatchPatternChanged(self, index): + self._object.ViewObject.Hatch = 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 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/PostHistogramFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): + """ + 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="HistogramPlot", + doc="The name used in the plots legend", + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="BarColor", + group="HistogramBar", + doc="The color the data bin area is drawn with", + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Hatch", + group="HistogramBar", + doc="The hatch pattern drawn in the bar", + value=['None', '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'], + ), + _GuiPropHelper( + type="App::PropertyIntegerConstraint", + name="HatchDensity", + group="HistogramBar", + doc="The line width of the hatch", + value=(1, 1, 99, 1), + ), + _GuiPropHelper( + type="App::PropertyColor", + name="LineColor", + group="HistogramLine", + doc="The color the data bin area is drawn with", + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="HistogramLine", + doc="The width of the bar, between 0 and 1 (1 being without gaps)", + value=(1, 0, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LineStyle", + group="HistogramLine", + doc="The style the line is drawn in", + value=['None', '-', '--', '-.', ':'], + ), + ] + 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_edit_widgets(self, post_dialog): + return [ EditAppWidget(self.Object, post_dialog), + EditViewWidget(self.Object, post_dialog)] + + def get_preview_widget(self, post_dialog): + return QtGui.QComboBox() + + def get_kw_args(self): + # builds kw args from the properties + kwargs = {} + + # colors need a workaround, some error occurs with rgba tuple + kwargs["edgecolor"] = self.ViewObject.LineColor + kwargs["facecolor"] = self.ViewObject.BarColor + kwargs["linestyle"] = self.ViewObject.LineStyle + kwargs["linewidth"] = self.ViewObject.LineWidth + if self.ViewObject.Hatch != "None": + kwargs["hatch"] = self.ViewObject.Hatch*self.ViewObject.HatchDensity + + return kwargs + + +class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Histogram plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Cumulative", + group="Histogram", + doc="If be the bars shoud show the cumulative sum left to rigth", + value=False, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Type", + group="Histogram", + doc="The type of histogram plotted", + value=["bar","barstacked", "step", "stepfilled"], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="BarWidth", + group="Histogram", + doc="The width of the bar, between 0 and 1 (1 being without gaps)", + value=(0.9, 0, 1, 0.05), + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="HatchLineWidth", + group="Histogram", + doc="The line width of all drawn hatch patterns", + value=(1, 0, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyInteger", + name="Bins", + group="Histogram", + doc="The number of bins the data is split into", + value=10, + ), + _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_PostHistogram.svg" + + def doubleClicked(self,vobj): + + self.show_visualization() + super().doubleClicked(vobj) + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_histogram._TaskPanel(vobj) + + #show it + FreeCADGui.Control.showDialog(taskd) + + 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() + bins = self.ViewObject.Bins + + # we do not iterate the table, but iterate the children. This makes it possible + # to attribute the correct styles + full_args = {} + full_data = [] + labels = [] + for child in self.Object.Group: + + table = child.Table + kwargs = self.get_kw_args(child) + + # iterate over the table and plot all + color_factor = np.linspace(1,0.5,table.GetNumberOfColumns()) + legend_multiframe = table.GetNumberOfColumns() > 1 + for i in range(table.GetNumberOfColumns()): + + # add the kw args, with some slide change over color for multiple frames + for key in kwargs: + if not (key in full_args): + full_args[key] = [] + + if "color" in key: + value = np.array(kwargs[key])*color_factor[i] + full_args[key].append(mpl.colors.to_hex(value)) + else: + full_args[key].append(kwargs[key]) + + data = VTKArray(table.GetColumn(i)) + full_data.append(data) + + # legend labels + if child.ViewObject.Legend: + if not legend_multiframe: + labels.append(child.ViewObject.Legend) + else: + postfix = table.GetColumnName(i).split("-")[-1] + labels.append(child.ViewObject.Legend + " - " + postfix) + else: + legend_prefix = "" + if len(self.Object.Group) > 1: + legend_prefix = child.Source.Label + ": " + labels.append(legend_prefix + table.GetColumnName(i)) + + + full_args["hatch_linewidth"] = self.ViewObject.HatchLineWidth + full_args["rwidth"] = self.ViewObject.BarWidth + full_args["cumulative"] = self.ViewObject.Cumulative + full_args["histtype"] = self.ViewObject.Type + full_args["label"] = labels + + self._plot.axes.hist(full_data, bins, **full_args) + + 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 labels: + self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + + self._plot.update() + + + def updateData(self, obj, prop): + # we only react if the table changed, as then know that new data is available + if prop == "Table": + self.drawPlot() + + + def onChanged(self, vobj, prop): + + # for all property changes we need to redraw the plot + self.drawPlot() + + def childViewPropertyChanged(self, vobj, prop): + + # on of our extractors has a changed view property. + self.drawPlot() + + def dumps(self): + return None + + + def loads(self, state): + return None diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py new file mode 100644 index 0000000000..0ce5ec8954 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -0,0 +1,71 @@ + +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_lineplot +# \ingroup FEM +# \brief view provider for post line plot object + +import FreeCAD +import FreeCADGui + +import FemGui +from PySide import QtGui + + +class VPPostLinePlot: + """ + A View Provider for the Post LinePlot object + """ + + def __init__(self, vobj): + vobj.Proxy = self + + def getIcon(self): + return ":/icons/FEM_PostLineplot.svg" + + def setEdit(self, vobj, mode): + # make sure we see what we edit + vobj.show() + + # build up the task panel + #taskd = task_post_glyphfilter._TaskPanel(vobj) + + #show it + #FreeCADGui.Control.showDialog(taskd) + + return True + + def unsetEdit(self, vobj, mode): + FreeCADGui.Control.closeDialog() + return True + + def dumps(self): + return None + + def loads(self, state): + return None