From a07303025a958ae10c7722a417ad8ab231928623 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Thu, 12 Jun 2025 17:01:36 +0200 Subject: [PATCH 001/126] CAM: move DetachedDocumentObject into a dedicated module for better reuse --- .../TestPathToolBitPropertyEditorWidget.py | 2 +- .../TestPathToolDocumentObjectEditorWidget.py | 2 +- src/Mod/CAM/CMakeLists.txt | 44 ++++++++++++++----- src/Mod/CAM/Path/Tool/docobject/__init__.py | 3 ++ .../Tool/{ui => docobject/models}/__init__.py | 0 .../models}/docobject.py | 0 .../CAM/Path/Tool/docobject/ui/__init__.py | 3 ++ .../Path/Tool/{ => docobject}/ui/docobject.py | 0 .../Path/Tool/{ => docobject}/ui/property.py | 0 src/Mod/CAM/Path/Tool/toolbit/models/base.py | 4 +- src/Mod/CAM/Path/Tool/toolbit/ui/editor.py | 5 +-- 11 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 src/Mod/CAM/Path/Tool/docobject/__init__.py rename src/Mod/CAM/Path/Tool/{ui => docobject/models}/__init__.py (100%) rename src/Mod/CAM/Path/Tool/{toolbit => docobject/models}/docobject.py (100%) create mode 100644 src/Mod/CAM/Path/Tool/docobject/ui/__init__.py rename src/Mod/CAM/Path/Tool/{ => docobject}/ui/docobject.py (100%) rename src/Mod/CAM/Path/Tool/{ => docobject}/ui/property.py (100%) diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py index 1361115a6d..ba1f02a698 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py @@ -32,7 +32,7 @@ from Path.Tool.ui.property import ( EnumPropertyEditorWidget, LabelPropertyEditorWidget, ) -from Path.Tool.toolbit.docobject import DetachedDocumentObject +from Path.Tool.docobject import DetachedDocumentObject class TestPropertyEditorFactory(unittest.TestCase): diff --git a/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py index 22c14625fb..49efdf786d 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py @@ -34,8 +34,8 @@ from Path.Tool.ui.property import ( EnumPropertyEditorWidget, LabelPropertyEditorWidget, ) +from Path.Tool.docobject import DetachedDocumentObject from Path.Tool.ui.docobject import DocumentObjectEditorWidget, _get_label_text -from Path.Tool.toolbit.docobject import DetachedDocumentObject class TestDocumentObjectEditorWidget(unittest.TestCase): diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index d9eff68df5..cd380cd5ab 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -142,20 +142,28 @@ SET(PathPythonToolsAssetsUi_SRCS Path/Tool/assets/ui/util.py ) +SET(PathPythonToolsDocObject_SRCS + Path/Tool/docobject/__init__.py +) + +SET(PathPythonToolsDocObjectModels_SRCS + Path/Tool/docobject/models/__init__.py + Path/Tool/docobject/models/docobject.py +) + +SET(PathPythonToolsDocObjectUi_SRCS + Path/Tool/docobject/ui/__init__.py + Path/Tool/docobject/ui/docobject.py + Path/Tool/docobject/ui/property.py +) + SET(PathPythonToolsGui_SRCS Path/Tool/Gui/__init__.py Path/Tool/Gui/Controller.py ) -SET(PathPythonToolsUi_SRCS - Path/Tool/ui/__init__.py - Path/Tool/ui/docobject.py - Path/Tool/ui/property.py -) - SET(PathPythonToolsToolBit_SRCS Path/Tool/toolbit/__init__.py - Path/Tool/toolbit/docobject.py Path/Tool/toolbit/util.py ) @@ -598,8 +606,10 @@ SET(all_files ${PathPythonToolsAssets_SRCS} ${PathPythonToolsAssetsStore_SRCS} ${PathPythonToolsAssetsUi_SRCS} + ${PathPythonToolsDocObject_SRCS} + ${PathPythonToolsDocObjectModels_SRCS} + ${PathPythonToolsDocObjectUi_SRCS} ${PathPythonToolsGui_SRCS} - ${PathPythonToolsUi_SRCS} ${PathPythonToolsShape_SRCS} ${PathPythonToolsShapeModels_SRCS} ${PathPythonToolsShapeUi_SRCS} @@ -772,9 +782,23 @@ INSTALL( INSTALL( FILES - ${PathPythonToolsUi_SRCS} + ${PathPythonToolsDocObject_SRCS} DESTINATION - Mod/CAM/Path/Tool/ui + Mod/CAM/Path/Tool/docobject +) + +INSTALL( + FILES + ${PathPythonToolsDocObjectModels_SRCS} + DESTINATION + Mod/CAM/Path/Tool/docobject/models +) + +INSTALL( + FILES + ${PathPythonToolsDocObjectUi_SRCS} + DESTINATION + Mod/CAM/Path/Tool/docobject/ui ) INSTALL( diff --git a/src/Mod/CAM/Path/Tool/docobject/__init__.py b/src/Mod/CAM/Path/Tool/docobject/__init__.py new file mode 100644 index 0000000000..34c6eeb233 --- /dev/null +++ b/src/Mod/CAM/Path/Tool/docobject/__init__.py @@ -0,0 +1,3 @@ +from .models.docobject import DetachedDocumentObject + +__all__ = ["DetachedDocumentObject"] diff --git a/src/Mod/CAM/Path/Tool/ui/__init__.py b/src/Mod/CAM/Path/Tool/docobject/models/__init__.py similarity index 100% rename from src/Mod/CAM/Path/Tool/ui/__init__.py rename to src/Mod/CAM/Path/Tool/docobject/models/__init__.py diff --git a/src/Mod/CAM/Path/Tool/toolbit/docobject.py b/src/Mod/CAM/Path/Tool/docobject/models/docobject.py similarity index 100% rename from src/Mod/CAM/Path/Tool/toolbit/docobject.py rename to src/Mod/CAM/Path/Tool/docobject/models/docobject.py diff --git a/src/Mod/CAM/Path/Tool/docobject/ui/__init__.py b/src/Mod/CAM/Path/Tool/docobject/ui/__init__.py new file mode 100644 index 0000000000..cc014a3a35 --- /dev/null +++ b/src/Mod/CAM/Path/Tool/docobject/ui/__init__.py @@ -0,0 +1,3 @@ +from .docobject import DocumentObjectEditorWidget + +__all__ = ["DocumentObjectEditorWidget"] diff --git a/src/Mod/CAM/Path/Tool/ui/docobject.py b/src/Mod/CAM/Path/Tool/docobject/ui/docobject.py similarity index 100% rename from src/Mod/CAM/Path/Tool/ui/docobject.py rename to src/Mod/CAM/Path/Tool/docobject/ui/docobject.py diff --git a/src/Mod/CAM/Path/Tool/ui/property.py b/src/Mod/CAM/Path/Tool/docobject/ui/property.py similarity index 100% rename from src/Mod/CAM/Path/Tool/ui/property.py rename to src/Mod/CAM/Path/Tool/docobject/ui/property.py diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index ec81cf3ccc..6a8d85bd98 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -33,10 +33,10 @@ from lazy_loader.lazy_loader import LazyLoader from typing import Any, List, Optional, Tuple, Type, Union, Mapping, cast from PySide.QtCore import QT_TRANSLATE_NOOP from Path.Base.Generator import toolchange -from ...assets import Asset +from ...docobject import DetachedDocumentObject +from ...assets.asset import Asset from ...camassets import cam_assets from ...shape import ToolBitShape, ToolBitShapeCustom, ToolBitShapeIcon -from ..docobject import DetachedDocumentObject from ..util import to_json, format_value diff --git a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py index eb4d77065c..ba6f2ab787 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py @@ -22,13 +22,12 @@ """Widget for editing a ToolBit object.""" -from functools import partial import FreeCAD import FreeCADGui from PySide import QtGui, QtCore -from ..models.base import ToolBit from ...shape.ui.shapewidget import ShapeWidget -from ...ui.docobject import DocumentObjectEditorWidget +from ...docobject.ui import DocumentObjectEditorWidget +from ..models.base import ToolBit class ToolBitPropertiesWidget(QtGui.QWidget): From 8b408552de47e2e8d1322ad8394788bd4c68f764 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Thu, 12 Jun 2025 17:05:23 +0200 Subject: [PATCH 002/126] CAM: Remove obsolete images --- src/Mod/CAM/CMakeLists.txt | 14 - src/Mod/CAM/Images/Tools/drill.svg | 221 -------------- src/Mod/CAM/Images/Tools/endmill.svg | 401 ------------------------ src/Mod/CAM/Images/Tools/reamer.svg | 433 -------------------------- src/Mod/CAM/Images/Tools/v-bit.svg | 439 --------------------------- 5 files changed, 1508 deletions(-) delete mode 100644 src/Mod/CAM/Images/Tools/drill.svg delete mode 100644 src/Mod/CAM/Images/Tools/endmill.svg delete mode 100644 src/Mod/CAM/Images/Tools/reamer.svg delete mode 100644 src/Mod/CAM/Images/Tools/v-bit.svg diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index cd380cd5ab..6ba07c39af 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -562,15 +562,8 @@ SET(PathImages_Ops Images/Ops/chamfer.svg ) -SET(PathImages_Tools - Images/Tools/drill.svg - Images/Tools/endmill.svg - Images/Tools/v-bit.svg -) - SET(Path_Images ${PathImages_Ops} - ${PathImages_Tools} ) SET(PathData_Threads @@ -963,13 +956,6 @@ INSTALL( Mod/CAM/Images/Ops ) -INSTALL( - FILES - ${PathImages_Tools} - DESTINATION - Mod/CAM/Images/Tools -) - INSTALL( FILES ${PathData_Threads} diff --git a/src/Mod/CAM/Images/Tools/drill.svg b/src/Mod/CAM/Images/Tools/drill.svg deleted file mode 100644 index 5e4a0f177b..0000000000 --- a/src/Mod/CAM/Images/Tools/drill.svg +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - H - D - α - - - - diff --git a/src/Mod/CAM/Images/Tools/endmill.svg b/src/Mod/CAM/Images/Tools/endmill.svg deleted file mode 100644 index 3982fe1c12..0000000000 --- a/src/Mod/CAM/Images/Tools/endmill.svg +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - S - D - H - - - - - - - diff --git a/src/Mod/CAM/Images/Tools/reamer.svg b/src/Mod/CAM/Images/Tools/reamer.svg deleted file mode 100644 index 737600c528..0000000000 --- a/src/Mod/CAM/Images/Tools/reamer.svg +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - D - H - - - - - - - - diff --git a/src/Mod/CAM/Images/Tools/v-bit.svg b/src/Mod/CAM/Images/Tools/v-bit.svg deleted file mode 100644 index d1f4f22e25..0000000000 --- a/src/Mod/CAM/Images/Tools/v-bit.svg +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - S - D - - α - d - - - - - - - - - H - - From 2c39ba622abff846049eef85e9f4858d42f4a966 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Thu, 12 Jun 2025 17:41:12 +0200 Subject: [PATCH 003/126] CAM: fix: broken import in tests --- src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py | 2 +- src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py | 4 ++-- .../CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py index f3102bf6c1..dad32fe85c 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py @@ -27,7 +27,7 @@ from unittest.mock import MagicMock from Path.Tool.toolbit.ui.editor import ToolBitPropertiesWidget from Path.Tool.toolbit.models.base import ToolBit from Path.Tool.shape.ui.shapewidget import ShapeWidget -from Path.Tool.ui.property import BasePropertyEditorWidget +from Path.Tool.docobject.ui.property import BasePropertyEditorWidget from .PathTestUtils import PathTestWithAssets diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py index ba1f02a698..6b9c60207e 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py @@ -24,7 +24,8 @@ import unittest import FreeCAD -from Path.Tool.ui.property import ( +from Path.Tool.docobject import DetachedDocumentObject +from Path.Tool.docobject.ui.property import ( BasePropertyEditorWidget, QuantityPropertyEditorWidget, BoolPropertyEditorWidget, @@ -32,7 +33,6 @@ from Path.Tool.ui.property import ( EnumPropertyEditorWidget, LabelPropertyEditorWidget, ) -from Path.Tool.docobject import DetachedDocumentObject class TestPropertyEditorFactory(unittest.TestCase): diff --git a/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py b/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py index 49efdf786d..0112c5d385 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py @@ -26,7 +26,9 @@ import unittest from unittest.mock import MagicMock import FreeCAD from PySide import QtGui -from Path.Tool.ui.property import ( +from Path.Tool.docobject import DetachedDocumentObject +from Path.Tool.docobject.ui.docobject import DocumentObjectEditorWidget, _get_label_text +from Path.Tool.docobject.ui.property import ( BasePropertyEditorWidget, QuantityPropertyEditorWidget, BoolPropertyEditorWidget, @@ -34,8 +36,6 @@ from Path.Tool.ui.property import ( EnumPropertyEditorWidget, LabelPropertyEditorWidget, ) -from Path.Tool.docobject import DetachedDocumentObject -from Path.Tool.ui.docobject import DocumentObjectEditorWidget, _get_label_text class TestDocumentObjectEditorWidget(unittest.TestCase): From ac02a222ffa00279364ba68cd71fa66421225199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 31 Mar 2025 20:11:09 +0200 Subject: [PATCH 004/126] FEM: Draft architecture of post data extraction with histogram example --- src/Mod/Fem/App/FemPostFilterPy.xml | 7 + src/Mod/Fem/App/FemPostFilterPyImp.cpp | 23 + src/Mod/Fem/App/FemPostObjectPy.xml | 7 + src/Mod/Fem/App/FemPostObjectPyImp.cpp | 25 + src/Mod/Fem/App/FemPostPipeline.h | 6 + src/Mod/Fem/App/FemPostPipelinePy.xml | 7 + src/Mod/Fem/App/FemPostPipelinePyImp.cpp | 23 + src/Mod/Fem/App/PropertyPostDataObject.cpp | 20 +- src/Mod/Fem/CMakeLists.txt | 16 +- src/Mod/Fem/Gui/CMakeLists.txt | 6 + src/Mod/Fem/Gui/Resources/Fem.qrc | 6 + .../Fem/Gui/Resources/icons/FEM_PostField.svg | 60 +++ .../Gui/Resources/icons/FEM_PostHistogram.svg | 40 ++ .../Fem/Gui/Resources/icons/FEM_PostIndex.svg | 42 ++ .../Gui/Resources/icons/FEM_PostLineplot.svg | 41 ++ .../Gui/Resources/icons/FEM_PostPlotline.svg | 41 ++ .../Resources/icons/FEM_PostSpreadsheet.svg | 40 ++ .../Resources/ui/PostHistogramFieldAppEdit.ui | 66 +++ .../ui/PostHistogramFieldViewEdit.ui | 162 ++++++ .../Gui/Resources/ui/TaskPostExtraction.ui | 54 ++ .../Fem/Gui/Resources/ui/TaskPostHistogram.ui | 223 ++++++++ src/Mod/Fem/Gui/TaskPostBoxes.cpp | 20 +- src/Mod/Fem/Gui/TaskPostBoxes.h | 7 +- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 169 ++++++ src/Mod/Fem/Gui/TaskPostExtraction.h | 67 +++ src/Mod/Fem/Gui/TaskPostExtraction.ui | 135 +++++ .../Fem/Gui/ViewProviderFemPostFilterPy.xml | 5 + .../Gui/ViewProviderFemPostFilterPyImp.cpp | 19 + src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 8 +- src/Mod/Fem/Gui/Workbench.cpp | 12 +- src/Mod/Fem/ObjectsFem.py | 42 ++ src/Mod/Fem/femcommands/commands.py | 7 +- src/Mod/Fem/femguiutils/data_extraction.py | 139 +++++ src/Mod/Fem/femguiutils/extract_link_view.py | 490 ++++++++++++++++++ src/Mod/Fem/femguiutils/post_visualization.py | 162 ++++++ src/Mod/Fem/femguiutils/vtk_table_view.py | 138 +++++ .../Fem/femobjects/base_fempostextractors.py | 199 +++++++ .../femobjects/base_fempostvisualizations.py | 71 +++ .../Fem/femobjects/base_fempythonobject.py | 1 + src/Mod/Fem/femobjects/post_extract1D.py | 178 +++++++ src/Mod/Fem/femobjects/post_histogram.py | 142 +++++ src/Mod/Fem/femobjects/post_lineplot.py | 211 ++++++++ .../Fem/femtaskpanels/base_fempostpanel.py | 83 +++ .../Fem/femtaskpanels/task_post_extractor.py | 54 ++ .../femtaskpanels/task_post_glyphfilter.py | 4 +- .../Fem/femtaskpanels/task_post_histogram.py | 180 +++++++ .../femviewprovider/view_base_femobject.py | 17 + .../view_base_fempostvisualization.py | 91 ++++ .../Fem/femviewprovider/view_post_extract.py | 129 +++++ .../femviewprovider/view_post_histogram.py | 476 +++++++++++++++++ .../Fem/femviewprovider/view_post_lineplot.py | 71 +++ 51 files changed, 4228 insertions(+), 14 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg create mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostExtraction.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui create mode 100644 src/Mod/Fem/Gui/TaskPostExtraction.cpp create mode 100644 src/Mod/Fem/Gui/TaskPostExtraction.h create mode 100644 src/Mod/Fem/Gui/TaskPostExtraction.ui create mode 100644 src/Mod/Fem/femguiutils/data_extraction.py create mode 100644 src/Mod/Fem/femguiutils/extract_link_view.py create mode 100644 src/Mod/Fem/femguiutils/post_visualization.py create mode 100644 src/Mod/Fem/femguiutils/vtk_table_view.py create mode 100644 src/Mod/Fem/femobjects/base_fempostextractors.py create mode 100644 src/Mod/Fem/femobjects/base_fempostvisualizations.py create mode 100644 src/Mod/Fem/femobjects/post_extract1D.py create mode 100644 src/Mod/Fem/femobjects/post_histogram.py create mode 100644 src/Mod/Fem/femobjects/post_lineplot.py create mode 100644 src/Mod/Fem/femtaskpanels/base_fempostpanel.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_extractor.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_histogram.py create mode 100644 src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_extract.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_histogram.py create mode 100644 src/Mod/Fem/femviewprovider/view_post_lineplot.py 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 From 7694594338e515b166d769855c7c15a75892e19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 16 Apr 2025 18:47:01 +0200 Subject: [PATCH 005/126] Fem: Implement lineplot visualization --- src/Mod/Fem/CMakeLists.txt | 6 +- src/Mod/Fem/Gui/CMakeLists.txt | 4 + .../Resources/ui/PostHistogramFieldAppEdit.ui | 12 + .../ui/PostHistogramFieldViewEdit.ui | 30 +- .../Resources/ui/PostLineplotFieldAppEdit.ui | 101 +++++ .../Resources/ui/PostLineplotFieldViewEdit.ui | 154 +++++++ .../Fem/Gui/Resources/ui/TaskPostHistogram.ui | 36 +- .../Fem/Gui/Resources/ui/TaskPostLineplot.ui | 181 ++++++++ src/Mod/Fem/ObjectsFem.py | 51 ++- src/Mod/Fem/femcommands/commands.py | 2 +- src/Mod/Fem/femguiutils/extract_link_view.py | 315 +++++++++---- .../Fem/femobjects/base_fempostextractors.py | 181 ++++++++ src/Mod/Fem/femobjects/post_extract1D.py | 49 +- src/Mod/Fem/femobjects/post_extract2D.py | 153 +++++++ src/Mod/Fem/femobjects/post_histogram.py | 8 +- src/Mod/Fem/femobjects/post_lineplot.py | 221 +++------ src/Mod/Fem/femobjects/post_table.py | 211 +++++++++ .../Fem/femtaskpanels/task_post_extractor.py | 9 +- .../Fem/femtaskpanels/task_post_lineplot.py | 163 +++++++ .../Fem/femviewprovider/view_post_extract.py | 13 +- .../femviewprovider/view_post_histogram.py | 12 +- .../Fem/femviewprovider/view_post_lineplot.py | 429 +++++++++++++++++- 22 files changed, 1995 insertions(+), 346 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui create mode 100644 src/Mod/Fem/femobjects/post_extract2D.py create mode 100644 src/Mod/Fem/femobjects/post_table.py create mode 100644 src/Mod/Fem/femtaskpanels/task_post_lineplot.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index e5df8521ea..0178bffe84 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -36,7 +36,7 @@ SET(FemBaseModules_SRCS coding_conventions.md Init.py InitGui.py - # ObjectsFem.py + ObjectsFem.py TestFemApp.py CreateLabels.py ) @@ -220,7 +220,9 @@ if(BUILD_FEM_VTK_PYTHON) ${FemObjects_SRCS} femobjects/post_glyphfilter.py femobjects/post_extract1D.py + femobjects/post_extract2D.py femobjects/post_histogram.py + femobjects/post_lineplot.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -634,6 +636,7 @@ if(BUILD_FEM_VTK_PYTHON) ${FemGuiTaskPanels_SRCS} femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_post_histogram.py + femtaskpanels/task_post_lineplot.py femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -700,6 +703,7 @@ if(BUILD_FEM_VTK_PYTHON) femviewprovider/view_post_glyphfilter.py femviewprovider/view_post_extract.py femviewprovider/view_post_histogram.py + femviewprovider/view_post_lineplot.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index f624778753..e1523956ea 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -444,8 +444,12 @@ SET(FemGuiPythonUI_SRCS Resources/ui/TaskPostGlyph.ui Resources/ui/TaskPostExtraction.ui Resources/ui/TaskPostHistogram.ui + Resources/ui/TaskPostLineplot.ui + Resources/ui/PostExtractionSummaryWidget.ui Resources/ui/PostHistogramFieldViewEdit.ui Resources/ui/PostHistogramFieldAppEdit.ui + Resources/ui/PostLineplotFieldViewEdit.ui + Resources/ui/PostLineplotFieldAppEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui index a89c7ef39b..d100b81ab3 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui index bc26238b94..744e5a6240 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + @@ -76,9 +88,6 @@ Lines: - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - @@ -102,9 +111,6 @@ Bars: - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - @@ -141,9 +147,6 @@ Legend: - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - @@ -157,6 +160,15 @@
Gui/Widgets.h
+ + Legend + BarColor + Hatch + HatchDensity + LineColor + LineStyle + LineWidth + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui new file mode 100644 index 0000000000..00b8ab4f55 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui @@ -0,0 +1,101 @@ + + + Form + + + + 0 + 0 + 296 + 186 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + X Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Y Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Frames: + + + + + + + One Y field for each frames + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui new file mode 100644 index 0000000000..720eb96c6e --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui @@ -0,0 +1,154 @@ + + + PostHistogramEdit + + + + 0 + 0 + 335 + 124 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + Marker: + + + + + + + + 0 + 0 + + + + Hatch pattern + + + + None + + + + + + + + + + + Legend: + + + + + + + + 0 + 0 + + + + Outline draw style (None does not draw outlines) + + + + None + + + + + + + + Line: + + + + + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Color of the bars in histogram + + + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+
+ + Legend + Color + LineStyle + LineWidth + MarkerStyle + MarkerSize + + + +
diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui index 70e2f3ecba..a753071f9a 100644 --- a/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostHistogram.ui @@ -14,9 +14,21 @@ Glyph settings - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight + + 0 + + + 0 + + + 0 + + + 0 + @@ -38,7 +50,7 @@ - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight 2 @@ -94,7 +106,7 @@ - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight Show @@ -195,9 +207,6 @@ Hatch Line Width - - Qt::AlignmentFlag::AlignCenter - @@ -205,9 +214,6 @@ Bar width - - Qt::AlignmentFlag::AlignCenter - @@ -218,6 +224,18 @@ + + Bins + Type + Cumulative + LegendShow + LegendPos + Title + XLabel + YLabel + BarWidth + HatchWidth + diff --git a/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui new file mode 100644 index 0000000000..bec95e063f --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/TaskPostLineplot.ui @@ -0,0 +1,181 @@ + + + TaskPostGlyph + + + + 0 + 0 + 302 + 302 + + + + Glyph settings + + + Qt::LeftToRight + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + The form of the glyph + + + Grid + + + + + + + Show + + + + + + + Qt::LeftToRight + + + Show + + + + + + + Legend + + + + + + + + 0 + 0 + + + + + + + + Which vector field is used to orient the glyphs + + + Scale + + + + + + + + 0 + 0 + + + + Which vector field is used to orient the glyphs + + + + None + + + + + + + + + + + 1 + 0 + + + + Labels + + + false + + + false + + + false + + + + + + + + + Y Axis + + + + + + + + + + If the scale data is a vector this property decides if the glyph is scaled by vector magnitude or by the individual components + + + X Axis + + + + + + + A constant multiplier the glyphs are scaled with + + + Title + + + + + + + + + + + + + Grid + LegendShow + LegendPos + Scale + Title + XLabel + YLabel + + + + diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 1dce3db5f7..c01cc8f8e6 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -686,36 +686,34 @@ def makePostVtkResult(doc, result_data, name="VtkResult"): return obj -def makePostVtkLinePlot(doc, name="Lineplot"): - """makePostVtkLineplot(document, [name]): +def makePostLineplot(doc, name="Lineplot"): + """makePostLineplot(document, [name]): creates a FEM post processing line plot """ obj = doc.addObject("App::FeaturePython", name) from femobjects import post_lineplot - post_lineplot.PostLinePlot(obj) + post_lineplot.PostLineplot(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot - view_post_lineplot.VPPostLinePlot(obj.ViewObject) - return - - -def makePostVtkHistogramFieldData(doc, name="FieldData1D"): - """makePostVtkFieldData1D(document, [name]): - creates a FEM post processing data extractor for 1D Field data - """ - obj = doc.addObject("App::FeaturePython", name) - from femobjects import post_histogram - - post_histogram.PostHistogramFieldData(obj) - if FreeCAD.GuiUp: - from femviewprovider import view_post_histogram - view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) + view_post_lineplot.VPPostLineplot(obj.ViewObject) return obj +def makePostLineplotFieldData(doc, name="FieldData2D"): + """makePostLineplotFieldData(document, [name]): + creates a FEM post processing data extractor for 2D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_lineplot -def makePostVtkHistogram(doc, name="Histogram"): - """makePostVtkHistogram(document, [name]): + post_lineplot.PostLineplotFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) + return obj + +def makePostHistogram(doc, name="Histogram"): + """makePostHistogram(document, [name]): creates a FEM post processing histogram plot """ obj = doc.addObject("App::FeaturePython", name) @@ -727,6 +725,19 @@ def makePostVtkHistogram(doc, name="Histogram"): view_post_histogram.VPPostHistogram(obj.ViewObject) return obj +def makePostHistogramFieldData(doc, name="FieldData1D"): + """makePostHistogramFieldData1D(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_histogram + + post_histogram.PostHistogramFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) + return obj + # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 98c620a96c..befcd48f30 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1292,5 +1292,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: # setup all visualization commands (register by importing) import femobjects.post_histogram + import femobjects.post_lineplot post_visualization.setup_commands("FEM_PostVisualization") - diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 60baecd9a4..9c984b537e 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -145,7 +145,10 @@ def build_add_from_data_tree_model(vis_type): return model -class TreeChoiceButton(QtGui.QToolButton): +# implementation of GUI and its functionality +# ########################################### + +class _TreeChoiceButton(QtGui.QToolButton): selection = QtCore.Signal(object,object) @@ -191,15 +194,157 @@ class TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) +class _SettingsPopup(QtGui.QGroupBox): -# implementationof GUI and its functionality -# ########################################## + close = QtCore.Signal() + + def __init__(self, setting): + + toplevel = QtGui.QApplication.topLevelWidgets() + for i in toplevel: + if i.metaObject().className() == "Gui::MainWindow": + main = i + break + + super().__init__(main) + + self.setFocusPolicy(QtGui.Qt.ClickFocus) + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(setting) + + buttonBox = QtGui.QDialogButtonBox() + buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + vbox.addWidget(buttonBox) + + buttonBox.accepted.connect(self.accept) + self.setLayout(vbox) + + @QtCore.Slot() + def accept(self): + self.close.emit() + + def showEvent(self, event): + self.setFocus() + + def keyPressEvent(self, event): + if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: + self.accept() + + + +class _SummaryWidget(QtGui.QWidget): + + delete = QtCore.Signal(object, object) # to delete: document object, summary widget + + def __init__(self, st_object, extractor, post_dialog): + super().__init__() -class _ShowVisualization: - def __init__(self, st_object): self._st_object = st_object + self._extractor = extractor + self._post_dialog = post_dialog - def __call__(self): + extr_label = extractor.Proxy.get_representive_fieldname(extractor) + extr_repr = extractor.ViewObject.Proxy.get_preview() + + # build the UI + + self.stButton = self._button(st_object.Label) + self.stButton.setIcon(st_object.ViewObject.Icon) + + self.extrButton = self._button(extr_label) + self.extrButton.setIcon(extractor.ViewObject.Icon) + + self.viewButton = self._button(extr_repr[1]) + size = self.viewButton.iconSize() + size.setWidth(size.width()*2) + self.viewButton.setIconSize(size) + self.viewButton.setIcon(extr_repr[0]) + + + self.rmButton = QtGui.QToolButton(self) + self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) + self.rmButton.setAutoRaise(True) + + # add the separation line + self.frame = QtGui.QFrame(self) + self.frame.setFrameShape(QtGui.QFrame.HLine); + + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + self.setSizePolicy(policy) + self.setMinimumSize(self.stButton.sizeHint()+self.frame.sizeHint()*3) + + # connect actions. We add functions to widget, as well as the data we need, + # and use those as callback. This way every widget knows which objects to use + self.stButton.clicked.connect(self.showVisualization) + self.extrButton.clicked.connect(self.editApp) + self.viewButton.clicked.connect(self.editView) + self.rmButton.clicked.connect(self.deleteTriggered) + + # make sure initial drawing happened + self._redraw() + + def _button(self, text): + btn = QtGui.QPushButton(self) + btn.full_text = text + + #size = btn.sizeHint() + #size.setWidth(size.width()*2) + btn.setMinimumSize(btn.sizeHint()) + + btn.setFlat(True) + btn.setText(text) + btn.setStyleSheet("text-align:left;padding:6px"); + btn.setToolTip(text) + + return btn + + def _redraw(self): + + btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton + btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() + fm = self.fontMetrics() + min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 + + pos = 0 + btns = [self.stButton, self.extrButton, self.viewButton] + btn_rel_size = [0.4, 0.4, 0.2] + btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] + for i, btn in enumerate(btns): + + btn_size = btn_total_size*btn_rel_size[i] + txt_size = btn_size - btn.iconSize().width() - btn_margin/2*3 + + # we elide only if there is enough space for a meaningful text + if txt_size >= min_text_width: + + text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) + btn.setText(text) + btn.setStyleSheet("text-align:left;padding:6px"); + else: + btn.setText("") + btn.setStyleSheet("text-align:center;"); + + rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) + btn.setGeometry(rect) + pos+=btn_size + + rmsize = self.stButton.height() + pos = self.size().width() - rmsize + self.rmButton.setGeometry(pos, 0, rmsize, rmsize) + + frame_hint = self.frame.sizeHint() + rect = QtCore.QRect(0, self.stButton.height()+frame_hint.height(), self.size().width(), frame_hint.height()) + self.frame.setGeometry(rect) + + def resizeEvent(self, event): + + # calculate the allowed text length + self._redraw() + super().resizeEvent(event) + + @QtCore.Slot() + def showVisualization(self): if vis.is_visualization_object(self._st_object): # show the visualization self._st_object.ViewObject.Proxy.show_visualization() @@ -208,67 +353,76 @@ class _ShowVisualization: FreeCADGui.Selection.clearSelection() FreeCADGui.Selection.addSelection(self._st_object) -class _ShowEditDialog: - def __init__(self, extractor, post_dialog, widget): - self._extractor = extractor - self._post_dialog = post_dialog - self._widget = widget + def _position_dialog(self, dialog): - widgets = self._extractor.ViewObject.Proxy.get_edit_widgets(self._post_dialog) - vbox = QtGui.QVBoxLayout() + main = dialog.parent() + list_widget = self.parent().parent().parent() + widget_rect = list_widget.geometry() + diag_size = dialog.sizeHint() + # default is towards main window center + if main.geometry().center().x() >= list_widget.mapToGlobal(widget_rect.center()).x(): + rigth_point = list_widget.mapToGlobal(widget_rect.topRight()) + dialog.setGeometry(QtCore.QRect(rigth_point, diag_size)) + else: + left_point = list_widget.mapToGlobal(widget_rect.topLeft()) + left_point -= QtCore.QPoint(diag_size.width(), 0) + dialog.setGeometry(QtCore.QRect(left_point, diag_size)) - buttonBox = QtGui.QDialogButtonBox() - buttonBox.setCenterButtons(True) - buttonBox.setStandardButtons(self._post_dialog.getStandardButtons()) - vbox.addWidget(buttonBox) + @QtCore.Slot() + def editApp(self): + if not hasattr(self, "appDialog"): + widget = self._extractor.ViewObject.Proxy.get_app_edit_widget(self._post_dialog) + self.appDialog = _SettingsPopup(widget) + self.appDialog.close.connect(self.appAccept) - started = False - for widget in widgets: + if not self.appDialog.isVisible(): + # position correctly and show + self._position_dialog(self.appDialog) + self.appDialog.show() + #self.appDialog.raise_() - if started: - # add a seperator line - frame = QtGui.QFrame() - frame.setFrameShape(QtGui.QFrame.HLine); - vbox.addWidget(frame); - else: - started = True + @QtCore.Slot() + def editView(self): - vbox.addWidget(widget) + if not hasattr(self, "viewDialog"): + widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog) + self.viewDialog = _SettingsPopup(widget) + self.viewDialog.close.connect(self.viewAccept) - vbox.addStretch() + if not self.viewDialog.isVisible(): + # position correctly and show + self._position_dialog(self.viewDialog) + self.viewDialog.show() + #self.viewDialog.raise_() - self.dialog = QtGui.QDialog(self._widget) - buttonBox.accepted.connect(self.accept) - buttonBox.rejected.connect(self.dialog.close) - buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.apply) - self.dialog.setLayout(vbox) + @QtCore.Slot() + def deleteTriggered(self): + self.delete.emit(self._extractor, self) + + @QtCore.Slot() + def viewAccept(self): + + self.viewDialog.hide() + + # update the preview + extr_repr = self._extractor.ViewObject.Proxy.get_preview() + self.viewButton.setIcon(extr_repr[0]) + self.viewButton.full_text = extr_repr[1] + self.viewButton.setToolTip(extr_repr[1]) + self._redraw() + + @QtCore.Slot() + def appAccept(self): + + self.appDialog.hide() + + # update the preview + extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) + self.extrButton.full_text = extr_label + self.extrButton.setToolTip(extr_label) + self._redraw() - def accept(self): - # recompute and close - self._extractor.Document.recompute() - self.dialog.close() - - def apply(self): - self._extractor.Document.recompute() - - def __call__(self): - # create the widgets, add it to dialog - self.dialog.show() - -class _DeleteExtractor: - def __init__(self, extractor, widget): - self._extractor = extractor - self._widget = widget - - def __call__(self): - # remove the document object - doc = self._extractor.Document - doc.removeObject(self._extractor.Name) - doc.recompute() - - # remove the widget - self._widget.deleteLater() class ExtractLinkView(QtGui.QWidget): @@ -300,19 +454,19 @@ class ExtractLinkView(QtGui.QWidget): if self._is_source: - self._add = TreeChoiceButton(build_add_to_visualization_tree_model()) + self._add = _TreeChoiceButton(build_add_to_visualization_tree_model()) self._add.setText("Add data to") self._add.selection.connect(self.addExtractionToVisualization) hbox.addWidget(self._add) - self._create = TreeChoiceButton(build_new_visualization_tree_model()) + self._create = _TreeChoiceButton(build_new_visualization_tree_model()) self._create.setText("New") self._create.selection.connect(self.newVisualization) hbox.addWidget(self._create) else: vis_type = vis.get_visualization_type(self._object) - self._add = TreeChoiceButton(build_add_from_data_tree_model(vis_type)) + self._add = _TreeChoiceButton(build_add_from_data_tree_model(vis_type)) self._add.setText("Add data from") self._add.selection.connect(self.addExtractionToPostObject) hbox.addWidget(self._add) @@ -324,46 +478,31 @@ class ExtractLinkView(QtGui.QWidget): self.setLayout(vbox) - - # add the content self.repopulate() def _build_summary_widget(self, extractor): - widget = FreeCADGui.PySideUic.loadUi( - FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostExtractionSummaryWidget.ui" - ) - - # add the separation line - frame = QtGui.QFrame() - frame.setFrameShape(QtGui.QFrame.HLine); - widget.layout().addWidget(frame); - if self._is_source: st_object = extractor.getParentGroup() else: st_object = extractor.Source - widget.RemoveButton.setIcon(QtGui.QIcon.fromTheme("delete")) - - widget.STButton.setIcon(st_object.ViewObject.Icon) - widget.STButton.setText(st_object.Label) - - widget.ExtractButton.setIcon(extractor.ViewObject.Icon) - - extr_label = extr.get_extraction_dimension(extractor) - extr_label += " " + extr.get_extraction_type(extractor) - widget.ExtractButton.setText(extr_label) - - # connect actions. We add functions to widget, as well as the data we need, - # and use those as callback. This way every widget knows which objects to use - widget.STButton.clicked.connect(_ShowVisualization(st_object)) - widget.ExtractButton.clicked.connect(_ShowEditDialog(extractor, self._post_dialog, widget)) - widget.RemoveButton.clicked.connect(_DeleteExtractor(extractor, widget)) + widget = _SummaryWidget(st_object, extractor, self._post_dialog) + widget.delete.connect(self._delete_extraction) return widget + def _delete_extraction(self, extractor, widget): + # remove the document object + doc = extractor.Document + doc.removeObject(extractor.Name) + doc.recompute() + + # remove the widget + self._widgets.remove(widget) + widget.deleteLater() + def repopulate(self): # collect all links that are available and shows them diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 4ccef7018a..33ef4b4935 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief base objects for data extractors +from vtkmodules.vtkCommonCore import vtkIntArray +from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from . import base_fempythonobject @@ -117,6 +119,10 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): case _: return ["Not a vector"] + def get_representive_fieldname(self): + # should return the representive field name, e.g. Position (X) + return "" + class Extractor1D(Extractor): @@ -196,4 +202,179 @@ class Extractor1D(Extractor): self._setup_x_component_property(obj, point_data) + def _x_array_component_to_table(self, obj, array, table): + # extracts the component out of the array according to XComponent setting + # Note: Uses the array name unchanged + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray(); + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def _x_array_from_dataset(self, obj, dataset): + # extracts the relevant array from the dataset and returns a copy + + match obj.XField: + case "Index": + num = dataset.GetPoints().GetNumberOfPoints() + array = vtkIntArray() + array.SetNumberOfTuples(num) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i,i) + + case "Position": + orig_array = dataset.GetPoints().GetData() + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.XField) + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + return array + + def get_representive_fieldname(self, obj): + # representive field is the x field + label = obj.XField + if not label: + return "" + + if len(obj.getEnumerationsOfProperty("XComponent")) > 1: + label += f" ({obj.XComponent})" + + return label + +class Extractor2D(Extractor1D): + + ExtractionDimension = "2D" + + def __init__(self, obj): + super().__init__(obj) + + + def _get_properties(self): + prop = [ + _PropHelper( + type="App::PropertyEnumeration", + name="YField", + group="Y Data", + doc="The field to use as Y data", + value=[], + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YComponent", + group="Y Data", + doc="Which part of the Y field vector to use for the Y axis", + value=[], + ), + ] + + return super()._get_properties() + prop + + + def onChanged(self, obj, prop): + + super().onChanged(obj, prop) + + if prop == "YField" and obj.Source and obj.Source.getDataSet(): + point_data = obj.Source.getDataSet().GetPointData() + self._setup_y_component_property(obj, point_data) + + if prop == "Source": + if obj.Source: + dset = obj.Source.getDataSet() + if dset: + self._setup_y_properties(obj, dset) + else: + self._clear_y_properties(obj) + else: + self._clear_y_properties(obj) + + def _setup_y_component_property(self, obj, point_data): + + if obj.YField == "Position": + obj.YComponent = self.component_options(3) + else: + array = point_data.GetAbstractArray(obj.YField) + obj.YComponent = self.component_options(array.GetNumberOfComponents()) + + def _clear_y_properties(self, obj): + if hasattr(obj, "YComponent"): + obj.YComponent = [] + if hasattr(obj, "YField"): + obj.YField = [] + + def _setup_y_properties(self, obj, dataset): + # Set all X Data properties correctly for the given dataset + fields = ["Position"] + point_data = dataset.GetPointData() + + for i in range(point_data.GetNumberOfArrays()): + fields.append(point_data.GetArrayName(i)) + + current_field = obj.YField + obj.YField = fields + if current_field in fields: + obj.YField = current_field + + self._setup_y_component_property(obj, point_data) + + def _y_array_component_to_table(self, obj, array, table): + # extracts the component out of the array according to XComponent setting + + if array.GetNumberOfComponents() == 1: + table.AddColumn(array) + else: + component_array = vtkDoubleArray(); + component_array.SetNumberOfComponents(1) + component_array.SetNumberOfTuples(array.GetNumberOfTuples()) + c_idx = obj.getEnumerationsOfProperty("YComponent").index(obj.YComponent) + component_array.CopyComponent(0, array, c_idx) + component_array.SetName(array.GetName()) + table.AddColumn(component_array) + + def _y_array_from_dataset(self, obj, dataset): + # extracts the relevant array from the dataset and returns a copy + + match obj.YField: + case "Index": + num = dataset.GetPoints().GetNumberOfPoints() + array = vtkIntArray() + array.SetNumberOfTuples(num) + array.SetNumberOfComponents(1) + for i in range(num): + array.SetValue(i,i) + + case "Position": + orig_array = dataset.GetPoints().GetData() + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + case _: + point_data = dataset.GetPointData() + orig_array = point_data.GetAbstractArray(obj.YField) + array = vtkDoubleArray() + array.DeepCopy(orig_array) + + return array + + def get_representive_fieldname(self, obj): + # representive field is the y field + label = obj.YField + if not label: + return "" + + if len(obj.getEnumerationsOfProperty("YComponent")) > 1: + label += f" ({obj.YComponent})" + + return label diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 5a9404e149..f7a450c181 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -33,10 +33,7 @@ from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper -from vtkmodules.vtkCommonCore import vtkDoubleArray -from vtkmodules.vtkCommonCore import vtkIntArray from vtkmodules.vtkCommonDataModel import vtkTable -from vtkmodules.vtkCommonDataModel import vtkDataObject from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline class PostFieldData1D(base_fempostextractors.Extractor1D): @@ -60,44 +57,6 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): ] return super()._get_properties() + prop - def __array_to_table(self, obj, array, table): - if array.GetNumberOfComponents() == 1: - table.AddColumn(array) - else: - component_array = vtkDoubleArray(); - component_array.SetNumberOfComponents(1) - component_array.SetNumberOfTuples(array.GetNumberOfTuples()) - c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) - component_array.CopyComponent(0, array, c_idx) - component_array.SetName(array.GetName()) - table.AddColumn(component_array) - - def __array_from_dataset(self, obj, dataset): - # extracts the relevant array from the dataset and returns a copy - - match obj.XField: - case "Index": - num = dataset.GetPoints().GetNumberOfPoints() - array = vtkIntArray() - array.SetNumberOfTuples(num) - array.SetNumberOfComponents(1) - for i in range(num): - array.SetValue(i,i) - - case "Position": - orig_array = dataset.GetPoints().GetData() - array = vtkDoubleArray() - array.DeepCopy(orig_array) - - case _: - point_data = dataset.GetPointData() - orig_array = point_data.GetAbstractArray(obj.XField) - array = vtkDoubleArray() - array.DeepCopy(orig_array) - - return array - - def execute(self, obj): # on execution we populate the vtk table @@ -124,26 +83,26 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): if not frames: # get the dataset and extract the correct array - array = self.__array_from_dataset(obj, dataset) + array = self._x_array_from_dataset(obj, dataset) if array.GetNumberOfComponents() > 1: array.SetName(obj.XField + " (" + obj.XComponent + ")") else: array.SetName(obj.XField) - self.__array_to_table(obj, array, table) + self._x_array_component_to_table(obj, array, table) else: algo = obj.Source.getOutputAlgorithm() for timestep in timesteps: algo.UpdateTimeStep(timestep) dataset = algo.GetOutputDataObject(0) - array = self.__array_from_dataset(obj, dataset) + array = self._x_array_from_dataset(obj, dataset) if array.GetNumberOfComponents() > 1: array.SetName(f"{obj.XField} ({obj.XComponent}) - {timestep}") else: array.SetName(f"{obj.XField} - {timestep}") - self.__array_to_table(obj, array, table) + self._x_array_component_to_table(obj, array, table) # set the final table obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py new file mode 100644 index 0000000000..b25b809655 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -0,0 +1,153 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post extractors 2D" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_histogram +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempostextractors +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline + +class PostFieldData2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction of two dimensional field data + """ + + ExtractionType = "Field" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop =[ _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc="Specify if the field shall be extracted for every available frame", + value=False, + ), + ] + return super()._get_properties() + prop + + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + frames = False + if obj.ExtractFrames: + # check if we have timesteps + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + frames = True + else: + FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + + if not frames: + # get the dataset and extract the correct array + xarray = self._x_array_from_dataset(obj, dataset) + if xarray.GetNumberOfComponents() > 1: + xarray.SetName(obj.XField + " (" + obj.XComponent + ")") + else: + xarray.SetName(obj.XField) + + self._x_array_component_to_table(obj, xarray, table) + + yarray = self._y_array_from_dataset(obj, dataset) + if yarray.GetNumberOfComponents() > 1: + yarray.SetName(obj.YField + " (" + obj.YComponent + ")") + else: + yarray.SetName(obj.YField) + + self._y_array_component_to_table(obj, yarray, table) + + else: + algo = obj.Source.getOutputAlgorithm() + for timestep in timesteps: + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + + xarray = self._x_array_from_dataset(obj, dataset) + if xarray.GetNumberOfComponents() > 1: + xarray.SetName(f"X - {obj.XField} ({obj.XComponent}) - {timestep}") + else: + xarray.SetName(f"X - {obj.XField} - {timestep}") + self._x_array_component_to_table(obj, xarray, table) + + yarray = self._y_array_from_dataset(obj, dataset) + if yarray.GetNumberOfComponents() > 1: + yarray.SetName(f"{obj.YField} ({obj.YComponent}) - {timestep}") + else: + yarray.SetName(f"{obj.YField} - {timestep}") + self._y_array_component_to_table(obj, yarray, table) + + # set the final table + obj.Table = table + + +class PostIndexData2D(base_fempostextractors.Extractor2D): + """ + A post processing extraction of one dimensional index data + """ + + ExtractionType = "Index" + + def __init__(self, obj): + super().__init__(obj) + + def _get_properties(self): + prop =[ _PropHelper( + type="App::PropertyBool", + name="ExtractFrames", + group="Multiframe", + doc="Specify if the data at the index should be extracted for each frame", + value=False, + ), + _PropHelper( + type="App::PropertyInteger", + name="XIndex", + group="X Data", + doc="Specify for which point index the data should be extracted", + value=0, + ), + ] + return super()._get_properties() + prop diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index df238c6e08..8cbd72b41c 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -21,13 +21,13 @@ # * * # *************************************************************************** -__title__ = "FreeCAD post line plot" +__title__ = "FreeCAD post histogram" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" ## @package post_histogram # \ingroup FEM -# \brief Post processing plot displaying lines +# \brief Post processing plot displaying histograms from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -46,7 +46,7 @@ from femguiutils import post_visualization post_visualization.register_visualization("Histogram", ":/icons/FEM_PostHistogram.svg", "ObjectsFem", - "makePostVtkHistogram") + "makePostHistogram") post_visualization.register_extractor("Histogram", "HistogramFieldData", @@ -54,7 +54,7 @@ post_visualization.register_extractor("Histogram", "1D", "Field", "ObjectsFem", - "makePostVtkHistogramFieldData") + "makePostHistogramFieldData") # Implementation diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index f8798fbc23..272aaea74c 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -32,41 +32,76 @@ __url__ = "https://www.freecad.org" from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper -# helper function to extract plot object type -def _get_extraction_subtype(obj): - if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): - return obj.Proxy.Type +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract2D - return "unknown" +from vtkmodules.vtkCommonCore import vtkDoubleArray +from vtkmodules.vtkCommonDataModel import vtkTable -class PostLinePlot(base_fempythonobject.BaseFemPythonObject): +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization("Lineplot", + ":/icons/FEM_PostLineplot.svg", + "ObjectsFem", + "makePostLineplot") + +post_visualization.register_extractor("Lineplot", + "LineplotFieldData", + ":/icons/FEM_PostField.svg", + "2D", + "Field", + "ObjectsFem", + "makePostLineplotFieldData") + + +# Implementation +# ############## + +def is_lineplot_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Lineplot" + + +class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ - A post processing extraction for plotting lines + A 2D Field extraction for lineplot. + """ + VisualizationType = "Lineplot" + + + +class PostLineplot(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as line plots """ - Type = "App::FeaturePython" + VisualizationType = "Lineplot" def __init__(self, obj): super().__init__(obj) - obj.addExtension("App::GroupExtension") - self._setup_properties(obj) - - def _setup_properties(self, obj): - - self.ExtractionType = "LinePlot" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) + obj.addExtension("App::GroupExtensionPython") def _get_properties(self): - prop = [] - return prop + prop = [ + _PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the plotted data, two columns per lineplot (x,y)", + value=vtkTable(), + ), + ] + return super()._get_properties() + prop - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): def onChanged(self, obj, prop): @@ -75,137 +110,29 @@ class PostLinePlot(base_fempythonobject.BaseFemPythonObject): children = obj.Group for child in obj.Group: - if _get_extraction_subtype(child) not in ["Line"]: + if not is_lineplot_extractor(child): + FreeCAD.Console.PrintWarning(f"{child.Label} is not a data lineplot data extraction object, cannot be added") children.remove(child) if len(obj.Group) != len(children): obj.Group = children + def execute(self, obj): -class PostPlotLine(base_fempythonobject.BaseFemPythonObject): + # during execution we collect all child data into our table + table = vtkTable() + for child in obj.Group: - Type = "App::FeaturePython" + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + # TODO: check which array type it is and use that one + array = vtkDoubleArray() + array.DeepCopy(c_array) + array.SetName(f"{child.Source.Label}: {c_array.GetName()}") + table.AddColumn(array) - def __init__(self, obj): - super().__init__(obj) - self._setup_properties(obj) + obj.Table = table + return False - def _setup_properties(self, obj): - - self.ExtractionType = "Line" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) - - def _get_properties(self): - - prop = [ - _PropHelper( - type="App::PropertyLink", - name="Source", - group="Line", - doc="The data source, the line uses", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XField", - group="X Data", - doc="The field to use as X data", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XComponent", - group="X Data", - doc="Which part of the X field vector to use for the X axis", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YField", - group="Y Data", - doc="The field to use as Y data for the line plot", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YComponent", - group="Y Data", - doc="Which part of the Y field vector to use for the X axis", - value=None, - ), - ] - return prop - - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): - - def onChanged(self, obj, prop): - - if prop == "Source": - # check if the source is a Post object - if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): - FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") - obj.XField = [] - obj.YField = [] - obj.Source = None - - if prop == "XField": - if not obj.Source: - obj.XComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.XField): - obj.XComponent = [] - return - - match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: - case 1: - obj.XComponent = ["Not a vector"] - case 2: - obj.XComponent = ["Magnitude", "X", "Y"] - case 3: - obj.XComponent = ["Magnitude", "X", "Y", "Z"] - - if prop == "YField": - if not obj.Source: - obj.YComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.YField): - obj.YComponent = [] - return - - match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: - case 1: - obj.YComponent = ["Not a vector"] - case 2: - obj.YComponent = ["Magnitude", "X", "Y"] - case 3: - obj.YComponent = ["Magnitude", "X", "Y", "Z"] - - def onExecute(self, obj): - # we need to make sure that we show the correct fields to the user as option for data extraction - - fields = [] - if obj.Source: - point_data = obj.Source.Data.GetPointData() - fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] - - current_X = obj.XField - obj.XField = fields - if current_X in fields: - obj.XField = current_X - - current_Y = obj.YField - obj.YField = fields - if current_Y in fields: - obj.YField = current_Y - - return True diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py new file mode 100644 index 0000000000..f8798fbc23 --- /dev/null +++ b/src/Mod/Fem/femobjects/post_table.py @@ -0,0 +1,211 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD post line plot" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package post_lineplot +# \ingroup FEM +# \brief Post processing plot displaying lines + +from . import base_fempythonobject +_PropHelper = base_fempythonobject._PropHelper + +# helper function to extract plot object type +def _get_extraction_subtype(obj): + if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): + return obj.Proxy.Type + + return "unknown" + + +class PostLinePlot(base_fempythonobject.BaseFemPythonObject): + """ + A post processing extraction for plotting lines + """ + + Type = "App::FeaturePython" + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtension") + self._setup_properties(obj) + + def _setup_properties(self, obj): + + self.ExtractionType = "LinePlot" + + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.Name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + prop = [] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(self, obj): + + def onChanged(self, obj, prop): + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if _get_extraction_subtype(child) not in ["Line"]: + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + +class PostPlotLine(base_fempythonobject.BaseFemPythonObject): + + Type = "App::FeaturePython" + + def __init__(self, obj): + super().__init__(obj) + self._setup_properties(obj) + + def _setup_properties(self, obj): + + self.ExtractionType = "Line" + + pl = obj.PropertiesList + for prop in self._get_properties(): + if not prop.Name in pl: + prop.add_to_object(obj) + + def _get_properties(self): + + prop = [ + _PropHelper( + type="App::PropertyLink", + name="Source", + group="Line", + doc="The data source, the line uses", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XField", + group="X Data", + doc="The field to use as X data", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="XComponent", + group="X Data", + doc="Which part of the X field vector to use for the X axis", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YField", + group="Y Data", + doc="The field to use as Y data for the line plot", + value=None, + ), + _PropHelper( + type="App::PropertyEnumeration", + name="YComponent", + group="Y Data", + doc="Which part of the Y field vector to use for the X axis", + value=None, + ), + ] + return prop + + def onDocumentRestored(self, obj): + self._setup_properties(self, obj): + + def onChanged(self, obj, prop): + + if prop == "Source": + # check if the source is a Post object + if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): + FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") + obj.XField = [] + obj.YField = [] + obj.Source = None + + if prop == "XField": + if not obj.Source: + obj.XComponent = [] + return + + point_data = obj.Source.Data.GetPointData() + if not point_data.HasArray(obj.XField): + obj.XComponent = [] + return + + match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: + case 1: + obj.XComponent = ["Not a vector"] + case 2: + obj.XComponent = ["Magnitude", "X", "Y"] + case 3: + obj.XComponent = ["Magnitude", "X", "Y", "Z"] + + if prop == "YField": + if not obj.Source: + obj.YComponent = [] + return + + point_data = obj.Source.Data.GetPointData() + if not point_data.HasArray(obj.YField): + obj.YComponent = [] + return + + match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: + case 1: + obj.YComponent = ["Not a vector"] + case 2: + obj.YComponent = ["Magnitude", "X", "Y"] + case 3: + obj.YComponent = ["Magnitude", "X", "Y", "Z"] + + def onExecute(self, obj): + # we need to make sure that we show the correct fields to the user as option for data extraction + + fields = [] + if obj.Source: + point_data = obj.Source.Data.GetPointData() + fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] + + current_X = obj.XField + obj.XField = fields + if current_X in fields: + obj.XField = current_X + + current_Y = obj.YField + obj.YField = fields + if current_Y in fields: + obj.YField = current_Y + + return True + diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py index 5a56077c3e..6d29a305e8 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -48,7 +48,14 @@ class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): super().__init__(obj) # form is used to display individual task panels - self.form = obj.ViewObject.Proxy.get_edit_widgets(self) + app = obj.ViewObject.Proxy.get_app_edit_widget(self) + app.setWindowTitle("Data extraction") + app.setWindowIcon(obj.ViewObject.Icon) + view = obj.ViewObject.Proxy.get_view_edit_widget(self) + view.setWindowTitle("Visualization settings") + view.setWindowIcon(obj.ViewObject.Icon) + + self.form = [app, view] diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py new file mode 100644 index 0000000000..650cdd70a1 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -0,0 +1,163 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM lineplot plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_lineplot +# \ingroup FEM +# \brief task panel for post lineplot plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + hbox = QtGui.QHBoxLayout() + self.data_widget.show_plot = QtGui.QPushButton() + self.data_widget.show_plot.setText("Show plot") + hbox.addWidget(self.data_widget.show_plot) + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText("Show data") + hbox.addWidget(self.data_widget.show_table) + vbox = QtGui.QVBoxLayout() + vbox.addItem(hbox) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle("Lineplot data") + + + # lineplot parameter widget + self.view_widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" + ) + self.view_widget.setWindowTitle("Lineplot view settings") + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget, self.view_widget] + + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_plot.clicked.connect(self.showPlot) + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + self._enumPropertyToCombobox(viewObj, "Scale", self.view_widget.Scale) + self.view_widget.Grid.setChecked(viewObj.Grid) + + self.view_widget.Title.setText(viewObj.Title) + self.view_widget.XLabel.setText(viewObj.XLabel) + self.view_widget.YLabel.setText(viewObj.YLabel) + + self.view_widget.LegendShow.setChecked(viewObj.Legend) + self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) + + + # connect callbacks + self.view_widget.Scale.activated.connect(self.scaleChanged) + self.view_widget.Grid.toggled.connect(self.gridChanged) + + self.view_widget.Title.editingFinished.connect(self.titleChanged) + self.view_widget.XLabel.editingFinished.connect(self.xLabelChanged) + self.view_widget.YLabel.editingFinished.connect(self.yLabelChanged) + + self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) + self.view_widget.LegendPos.activated.connect(self.legendPosChanged) + + + QtCore.Slot() + def showPlot(self): + self.obj.ViewObject.Proxy.show_visualization() + + QtCore.Slot() + def showTable(self): + + # TODO: make data model update when object is recomputed + data_model = vtk_table_view.VtkTableModel() + data_model.setTable(self.obj.Table) + + dialog = QtGui.QDialog(self.data_widget) + widget = vtk_table_view.VtkTableView(data_model) + layout = QtGui.QVBoxLayout() + layout.addWidget(widget) + layout.setContentsMargins(0,0,0,0) + + dialog.setLayout(layout) + dialog.resize(1500, 900) + dialog.show() + + + QtCore.Slot(int) + def scaleChanged(self, idx): + self.obj.ViewObject.Scale = idx + + QtCore.Slot(bool) + def gridChanged(self, state): + self.obj.ViewObject.Grid = state + + QtCore.Slot() + def titleChanged(self): + self.obj.ViewObject.Title = self.view_widget.Title.text() + + QtCore.Slot() + def xLabelChanged(self): + self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() + + QtCore.Slot() + def yLabelChanged(self): + self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() + + QtCore.Slot(int) + def legendPosChanged(self, idx): + self.obj.ViewObject.LegendLocation = idx + + QtCore.Slot(bool) + def legendShowChanged(self, state): + self.obj.ViewObject.Legend = state diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_post_extract.py index c75dd4bc8b..b91c65cb45 100644 --- a/src/Mod/Fem/femviewprovider/view_post_extract.py +++ b/src/Mod/Fem/femviewprovider/view_post_extract.py @@ -109,18 +109,23 @@ class VPPostExtractor: # of the object return {} - def get_edit_widgets(self, post_dialog): - # Returns a list of widgets for editing the object/viewprovider. + def get_app_edit_widget(self, post_dialog): + # Returns a widgets for editing the object (not viewprovider!) # The widget will be part of the provided post_dialog, and # should use its functionality to inform of changes. raise FreeCAD.Base.FreeCADError("Not implemented") - def get_preview_widget(self, post_dialog): - # Returns a widget for editing the object/viewprovider. + def get_view_edit_widget(self, post_dialog): + # Returns a widgets for editing the viewprovider (not object!) # The widget will be part of the provided post_dialog, and # should use its functionality to inform of changes. raise FreeCAD.Base.FreeCADError("Not implemented") + def get_preview(self): + # Returns the preview tuple of icon and label: (QPixmap, str) + # Note: QPixmap in ratio 2:1 + raise FreeCAD.Base.FreeCADError("Not implemented") + def dumps(self): return None diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 5a433f17bc..8fe2fa6b4c 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -232,12 +232,14 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): def getIcon(self): return ":/icons/FEM_PostField.svg" - def get_edit_widgets(self, post_dialog): - return [ EditAppWidget(self.Object, post_dialog), - EditViewWidget(self.Object, post_dialog)] + def get_app_edit_widget(self, post_dialog): + return EditAppWidget(self.Object, post_dialog) - def get_preview_widget(self, post_dialog): - return QtGui.QComboBox() + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + return (QtGui.QPixmap(), self.ViewObject.Legend) def get_kw_args(self): # builds kw args from the properties diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 0ce5ec8954..54fe5439f3 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -1,4 +1,3 @@ - # *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * @@ -33,39 +32,445 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui +import Plot import FemGui -from PySide import QtGui +from PySide import QtGui, QtCore + +import io +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_post_extract +from . import view_base_fempostvisualization +from femtaskpanels import task_post_lineplot + +_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + vobj = self._object.ViewObject + + self.widget.Legend.setText(vobj.Legend) + self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle) + self._post_dialog._enumPropertyToCombobox(vobj, "MarkerStyle", self.widget.MarkerStyle) + self.widget.LineWidth.setValue(vobj.LineWidth) + self.widget.MarkerSize.setValue(vobj.MarkerSize) + self.widget.Color.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.Color])) + + self.widget.Legend.editingFinished.connect(self.legendChanged) + self.widget.MarkerStyle.activated.connect(self.markerStyleChanged) + self.widget.LineStyle.activated.connect(self.lineStyleChanged) + self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged) + self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) + self.widget.Color.changed.connect(self.colorChanged) + + @QtCore.Slot() + def colorChanged(self): + color = self.widget.Color.property("color") + self._object.ViewObject.Color = color.getRgb() + + @QtCore.Slot(float) + def lineWidthChanged(self, value): + self._object.ViewObject.LineWidth = value + + @QtCore.Slot(float) + def markerSizeChanged(self, value): + self._object.ViewObject.MarkerSize = value + + @QtCore.Slot(int) + def markerStyleChanged(self, index): + self._object.ViewObject.MarkerStyle = index + + @QtCore.Slot(int) + def lineStyleChanged(self, index): + self._object.ViewObject.LineStyle = index + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Legend = self.widget.Legend.text() -class VPPostLinePlot: +class EditAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.XField) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.XField.activated.connect(self.xFieldChanged) + self.widget.XComponent.activated.connect(self.xComponentChanged) + self.widget.YField.activated.connect(self.yFieldChanged) + self.widget.YComponent.activated.connect(self.yComponentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def xFieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def xComponentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yFieldChanged(self, index): + self._object.YField = index + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yComponentChanged(self, index): + self._object.YComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + + +class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): """ - A View Provider for the Post LinePlot object + A View Provider for extraction of 1D field data specialy for histograms """ def __init__(self, vobj): + super().__init__(vobj) vobj.Proxy = self + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Legend", + group="Lineplot", + doc="The name used in the plots legend", + value="", + ), + _GuiPropHelper( + type="App::PropertyColor", + name="Color", + group="Lineplot", + doc="The color the line and the markers are drawn with", + value=(0, 85, 255, 255), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LineStyle", + group="Lineplot", + doc="The style the line is drawn in", + value=['-', '--', '-.', ':', 'None'], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="LineWidth", + group="Lineplot", + doc="The width the line is drawn with", + value=(1, 0.1, 99, 0.1), + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="MarkerStyle", + group="Lineplot", + doc="The style the data markers are drawn with", + value=['None', '*', '+', 's', '.', 'o', 'x'], + ), + _GuiPropHelper( + type="App::PropertyFloatConstraint", + name="MarkerSize", + group="Lineplot", + doc="The size the data markers are drawn in", + value=(10, 0.1, 99, 0.1), + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + # Returns the preview tuple of icon and label: (QPixmap, str) + # Note: QPixmap in ratio 2:1 + + fig = plt.figure(figsize=(0.2,0.1), dpi=1000) + ax = plt.Axes(fig, [0., 0., 1., 1.]) + ax.set_axis_off() + fig.add_axes(ax) + kwargs = self.get_kw_args() + kwargs["markevery"] = [1] + ax.plot([0,0.5,1],[0.5,0.5,0.5], **kwargs) + data = io.BytesIO() + plt.savefig(data, bbox_inches=0, transparent=True) + plt.close() + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data.getvalue()) + + return (pixmap, self.ViewObject.Legend) + + + def get_kw_args(self): + # builds kw args from the properties + kwargs = {} + + # colors need a workaround, some error occurs with rgba tuple + kwargs["color"] = self.ViewObject.Color + kwargs["markeredgecolor"] = self.ViewObject.Color + kwargs["markerfacecolor"] = self.ViewObject.Color + kwargs["linestyle"] = self.ViewObject.LineStyle + kwargs["linewidth"] = self.ViewObject.LineWidth + kwargs["marker"] = self.ViewObject.MarkerStyle + kwargs["markersize"] = self.ViewObject.MarkerSize + return kwargs + + +class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Lineplot plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyBool", + name="Grid", + group="Lineplot", + doc="If be the bars shoud show the cumulative sum left to rigth", + value=False, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="Scale", + group="Lineplot", + doc="The scale the axis are drawn in", + value=["linear","semi-log x", "semi-log y", "log"], + ), + _GuiPropHelper( + type="App::PropertyString", + name="Title", + group="Plot", + doc="The histogram plot title", + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="XLabel", + group="Plot", + doc="The label shown for the histogram X axis", + value="", + ), + _GuiPropHelper( + type="App::PropertyString", + name="YLabel", + group="Plot", + doc="The label shown for the histogram Y axis", + value="", + ), + _GuiPropHelper( + type="App::PropertyBool", + name="Legend", + group="Plot", + doc="Determines if the legend is plotted", + value=True, + ), + _GuiPropHelper( + type="App::PropertyEnumeration", + name="LegendLocation", + group="Plot", + doc="Determines if the legend is plotted", + value=['best','upper right','upper left','lower left','lower right','right', + 'center left','center right','lower center','upper center','center'], + ), + + ] + return prop + def getIcon(self): return ":/icons/FEM_PostLineplot.svg" - def setEdit(self, vobj, mode): - # make sure we see what we edit - vobj.show() + def doubleClicked(self,vobj): + + self.show_visualization() + super().doubleClicked(vobj) + + def setEdit(self, vobj, mode): # build up the task panel - #taskd = task_post_glyphfilter._TaskPanel(vobj) + taskd = task_post_lineplot._TaskPanel(vobj) #show it - #FreeCADGui.Control.showDialog(taskd) + FreeCADGui.Control.showDialog(taskd) return True - def unsetEdit(self, vobj, mode): - FreeCADGui.Control.closeDialog() - return True + + def show_visualization(self): + + if not hasattr(self, "_plot") or not self._plot: + self._plot = Plot.Plot() + self._dialog = QtGui.QDialog(Plot.getMainWindow()) + box = QtGui.QVBoxLayout() + box.addWidget(self._plot) + self._dialog.setLayout(box) + + self.drawPlot() + self._dialog.show() + + def get_kw_args(self, obj): + view = obj.ViewObject + if not view or not hasattr(view, "Proxy"): + return {} + if not hasattr(view.Proxy, "get_kw_args"): + return {} + return view.Proxy.get_kw_args() + + def drawPlot(self): + + if not hasattr(self, "_plot") or not self._plot: + return + + self._plot.axes.clear() + + # we do not iterate the table, but iterate the children. This makes it possible + # to attribute the correct styles + for child in self.Object.Group: + + table = child.Table + kwargs = self.get_kw_args(child) + + # iterate over the table and plot all (note: column 0 is always X!) + color_factor = np.linspace(1,0.5,int(table.GetNumberOfColumns()/2)) + legend_multiframe = table.GetNumberOfColumns() > 2 + + for i in range(0,table.GetNumberOfColumns(),2): + + # add the kw args, with some slide change over color for multiple frames + tmp_args = {} + for key in kwargs: + if "color" in key: + value = np.array(kwargs[key])*color_factor[int(i/2)] + tmp_args[key] = mpl.colors.to_hex(value) + else: + tmp_args[key] = kwargs[key] + + xdata = VTKArray(table.GetColumn(i)) + ydata = VTKArray(table.GetColumn(i+1)) + + # legend labels + if child.ViewObject.Legend: + if not legend_multiframe: + label = child.ViewObject.Legend + else: + postfix = table.GetColumnName(i+1).split("-")[-1] + label = child.ViewObject.Legend + " - " + postfix + else: + legend_prefix = "" + if len(self.Object.Group) > 1: + legend_prefix = child.Source.Label + ": " + label = legend_prefix + table.GetColumnName(i+1) + + match self.ViewObject.Scale: + case "log": + self._plot.axes.loglog(xdata, ydata, **tmp_args, label=label) + case "semi-log x": + self._plot.axes.semilogx(xdata, ydata, **tmp_args, label=label) + case "semi-log y": + self._plot.axes.semilogy(xdata, ydata, **tmp_args, label=label) + case _: + self._plot.axes.plot(xdata, ydata, **tmp_args, label=label) + + if self.ViewObject.Title: + self._plot.axes.set_title(self.ViewObject.Title) + if self.ViewObject.XLabel: + self._plot.axes.set_xlabel(self.ViewObject.XLabel) + if self.ViewObject.YLabel: + self._plot.axes.set_ylabel(self.ViewObject.YLabel) + + if self.ViewObject.Legend and self.Object.Group: + self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + + self._plot.axes.grid(self.ViewObject.Grid) + + self._plot.update() + + + def updateData(self, obj, prop): + # we only react if the table changed, as then know that new data is available + if prop == "Table": + self.drawPlot() + + + def onChanged(self, vobj, prop): + + # for all property changes we need to redraw the plot + self.drawPlot() + + def childViewPropertyChanged(self, vobj, prop): + + # on of our extractors has a changed view property. + self.drawPlot() def dumps(self): return None + def loads(self, state): return None From 2c983ce75e231ad31f6d53812e3f290e4dbcd3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sat, 19 Apr 2025 11:23:04 +0200 Subject: [PATCH 006/126] FEM: Add index over frames visualizations --- src/Mod/Fem/Gui/CMakeLists.txt | 3 +- .../ui/PostHistogramFieldViewEdit.ui | 41 ++--- .../Resources/ui/PostHistogramIndexAppEdit.ui | 81 +++++++++ .../Resources/ui/PostLineplotFieldAppEdit.ui | 4 +- .../Resources/ui/PostLineplotFieldViewEdit.ui | 81 +++++---- .../Resources/ui/PostLineplotIndexAppEdit.ui | 85 ++++++++++ src/Mod/Fem/Gui/TaskPostBoxes.cpp | 19 +++ src/Mod/Fem/Gui/TaskPostBoxes.h | 10 ++ src/Mod/Fem/Gui/TaskPostExtraction.cpp | 18 ++ src/Mod/Fem/Gui/TaskPostExtraction.h | 1 + src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 1 + src/Mod/Fem/ObjectsFem.py | 33 +++- src/Mod/Fem/femcommands/commands.py | 2 +- src/Mod/Fem/femguiutils/data_extraction.py | 11 ++ src/Mod/Fem/femguiutils/extract_link_view.py | 72 ++++---- .../Fem/femobjects/base_fempostextractors.py | 48 ++++-- src/Mod/Fem/femobjects/post_extract1D.py | 71 ++++++-- src/Mod/Fem/femobjects/post_extract2D.py | 89 ++++++++-- src/Mod/Fem/femobjects/post_histogram.py | 14 +- src/Mod/Fem/femobjects/post_lineplot.py | 14 ++ .../Fem/femtaskpanels/task_post_histogram.py | 3 + .../Fem/femtaskpanels/task_post_lineplot.py | 3 + .../femviewprovider/view_post_histogram.py | 154 ++++++++++++++++-- .../Fem/femviewprovider/view_post_lineplot.py | 122 ++++++++++++-- src/Mod/Plot/Plot.py | 7 +- 25 files changed, 804 insertions(+), 183 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index e1523956ea..7337afd833 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -445,11 +445,12 @@ SET(FemGuiPythonUI_SRCS Resources/ui/TaskPostExtraction.ui Resources/ui/TaskPostHistogram.ui Resources/ui/TaskPostLineplot.ui - Resources/ui/PostExtractionSummaryWidget.ui Resources/ui/PostHistogramFieldViewEdit.ui Resources/ui/PostHistogramFieldAppEdit.ui + Resources/ui/PostHistogramIndexAppEdit.ui Resources/ui/PostLineplotFieldViewEdit.ui Resources/ui/PostLineplotFieldAppEdit.ui + Resources/ui/PostLineplotIndexAppEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui index 744e5a6240..5fe4a7d3dc 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldViewEdit.ui @@ -6,8 +6,8 @@ 0 0 - 293 - 126 + 278 + 110 @@ -68,7 +68,7 @@ - + 0 0 @@ -93,7 +93,7 @@ - + 0 0 @@ -113,10 +113,20 @@ + + + + + + + Legend: + + + - + - + 0 0 @@ -127,7 +137,7 @@ - + 0 @@ -139,27 +149,10 @@ - - - - - - - Legend: - - -
- - - Gui::ColorButton - QPushButton -
Gui/Widgets.h
-
-
Legend BarColor diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui new file mode 100644 index 0000000000..e9dd2a2b3d --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui @@ -0,0 +1,81 @@ + + + Form + + + + 0 + 0 + 261 + 110 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui index 00b8ab4f55..b0d1830852 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldAppEdit.ui @@ -6,8 +6,8 @@ 0 0 - 296 - 186 + 271 + 174 diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui index 720eb96c6e..f197016d12 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotFieldViewEdit.ui @@ -6,8 +6,8 @@ 0 0 - 335 - 124 + 274 + 114 @@ -28,25 +28,6 @@ - - - - - 0 - 0 - - - - Width of all lines (outline and hatch) - - - 99.000000000000000 - - - 0.100000000000000 - - - @@ -57,7 +38,7 @@ - + 0 0 @@ -107,18 +88,8 @@ - - - - 99.000000000000000 - - - 0.100000000000000 - - - - + 0 @@ -130,24 +101,50 @@ + + + + + 0 + 0 + + + + 99.000000000000000 + + + 0.100000000000000 + + + + + + + + 0 + 0 + + + + Width of all lines (outline and hatch) + + + 99.000000000000000 + + + 0.100000000000000 + + +
- - - Gui::ColorButton - QPushButton -
Gui/Widgets.h
-
-
Legend Color LineStyle - LineWidth MarkerStyle - MarkerSize diff --git a/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui new file mode 100644 index 0000000000..ba4ab0ead3 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostLineplotIndexAppEdit.ui @@ -0,0 +1,85 @@ + + + Form + + + + 0 + 0 + 310 + 108 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Y Field: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + 99999999 + + + + + + + Index + YField + YComponent + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index d880f73d33..1a2c244943 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -408,6 +408,21 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) } } +void TaskDlgPost::processCollapsedWidgets() { + + for (auto& widget : Content) { + if(auto task_box = dynamic_cast(widget)) { + // get the task widget and check if it is a post widget + auto widget = task_box->groupLayout()->itemAt(0)->widget(); + if(auto post_widget = dynamic_cast(widget)) { + if(post_widget->initiallyCollapsed()) { + post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); + task_box->hideGroupBox(); + } + } + } + } +} // *************************************************************************** // box to set the coloring @@ -571,6 +586,10 @@ void TaskPostFrames::applyPythonCode() // we apply the views widgets python code } +bool TaskPostFrames::initiallyCollapsed() { + + return (ui->FrameTable->rowCount() == 0); +} // *************************************************************************** // in the following, the different filters sorted alphabetically diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index 9b24eb314f..3c60fe8ddf 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -156,6 +156,11 @@ public: // executed when the apply button is pressed in the task dialog virtual void apply() {}; + // returns if the widget shall be collapsed when opening the task dialog + virtual bool initiallyCollapsed() { + return false; + }; + protected: App::DocumentObject* getObject() const { @@ -235,6 +240,9 @@ public: /// returns for Close and Help button QDialogButtonBox::StandardButtons getStandardButtons() const override; + /// makes sure all widgets are collapsed, if they want to be + void processCollapsedWidgets(); + protected: void recompute(); @@ -300,6 +308,8 @@ public: void applyPythonCode() override; + bool initiallyCollapsed() override; + private: void setupConnections(); void onSelectionChanged(); diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index ef70109462..57f39a70a2 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -166,4 +166,22 @@ void TaskPostExtraction::apply() } } +bool TaskPostExtraction::initiallyCollapsed() +{ + Base::PyGILStateLocker lock; + try { + if (m_panel.hasAttr(std::string("initiallyCollapsed"))) { + Py::Callable method(m_panel.getAttr(std::string("initiallyCollapsed"))); + 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; +} + #include "moc_TaskPostExtraction.cpp" diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.h b/src/Mod/Fem/Gui/TaskPostExtraction.h index 5423a83d00..5fe2518760 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.h +++ b/src/Mod/Fem/Gui/TaskPostExtraction.h @@ -56,6 +56,7 @@ protected: bool isGuiTaskOnly() override; void apply() override; void onPostDataChanged(Fem::FemPostObject* obj) override; + bool initiallyCollapsed() override; private: Py::Object m_panel; diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index bc4dd1d953..2a34070c72 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -1007,6 +1007,7 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) postDlg = new TaskDlgPost(this); setupTaskDialog(postDlg); postDlg->connectSlots(); + postDlg->processCollapsedWidgets(); Gui::Control().showDialog(postDlg); } diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c01cc8f8e6..d40558f4bd 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -699,6 +699,7 @@ def makePostLineplot(doc, name="Lineplot"): view_post_lineplot.VPPostLineplot(obj.ViewObject) return obj + def makePostLineplotFieldData(doc, name="FieldData2D"): """makePostLineplotFieldData(document, [name]): creates a FEM post processing data extractor for 2D Field data @@ -712,6 +713,21 @@ def makePostLineplotFieldData(doc, name="FieldData2D"): view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) return obj + +def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): + """makePostLineplotIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 2D index data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_lineplot + + post_lineplot.PostLineplotIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotIndexOverFrames(obj.ViewObject) + return obj + + def makePostHistogram(doc, name="Histogram"): """makePostHistogram(document, [name]): creates a FEM post processing histogram plot @@ -725,8 +741,9 @@ def makePostHistogram(doc, name="Histogram"): view_post_histogram.VPPostHistogram(obj.ViewObject) return obj + def makePostHistogramFieldData(doc, name="FieldData1D"): - """makePostHistogramFieldData1D(document, [name]): + """makePostHistogramFieldData(document, [name]): creates a FEM post processing data extractor for 1D Field data """ obj = doc.addObject("App::FeaturePython", name) @@ -739,6 +756,20 @@ def makePostHistogramFieldData(doc, name="FieldData1D"): return obj +def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostHistogramIndexOverFrames(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.PostHistogramIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramIndexOverFrames(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 befcd48f30..f4edc1586e 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1291,6 +1291,6 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: FreeCADGui.addCommand("FEM_PostFilterGlyph", _PostFilterGlyph()) # setup all visualization commands (register by importing) - import femobjects.post_histogram import femobjects.post_lineplot + 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 index 4eeffbcef4..23a1bb784b 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -40,11 +40,13 @@ from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents import FreeCAD import FreeCADGui +import femobjects.base_fempostextractors as extr 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. @@ -137,3 +139,12 @@ class DataExtraction(_BasePostTaskPanel): def apply(self): pass + + def initiallyCollapsed(self): + # if we do not have any extractions to show we hide initially to remove clutter + + for obj in self.Object.InList: + if extr.is_extractor_object(obj): + return False + + return True diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 9c984b537e..e1611f8609 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -198,16 +198,10 @@ class _SettingsPopup(QtGui.QGroupBox): close = QtCore.Signal() - def __init__(self, setting): - - toplevel = QtGui.QApplication.topLevelWidgets() - for i in toplevel: - if i.metaObject().className() == "Gui::MainWindow": - main = i - break - - super().__init__(main) + def __init__(self, setting, parent): + super().__init__(parent) + self.setWindowFlags(QtGui.Qt.Popup) self.setFocusPolicy(QtGui.Qt.ClickFocus) vbox = QtGui.QVBoxLayout() @@ -217,20 +211,22 @@ class _SettingsPopup(QtGui.QGroupBox): buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) vbox.addWidget(buttonBox) - buttonBox.accepted.connect(self.accept) + buttonBox.accepted.connect(self.hide) self.setLayout(vbox) - @QtCore.Slot() - def accept(self): - self.close.emit() - def showEvent(self, event): + # required to get keyboard events self.setFocus() - def keyPressEvent(self, event): - if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: - self.accept() + def hideEvent(self, event): + # emit on hide: this happens for OK button as well as + # "click away" closing of the popup + self.close.emit() + def keyPressEvent(self, event): + # close on hitting enter + if event.key() == QtGui.Qt.Key_Enter or event.key() == QtGui.Qt.Key_Return: + self.hide() class _SummaryWidget(QtGui.QWidget): @@ -284,14 +280,12 @@ class _SummaryWidget(QtGui.QWidget): # make sure initial drawing happened self._redraw() + def _button(self, text): btn = QtGui.QPushButton(self) btn.full_text = text - #size = btn.sizeHint() - #size.setWidth(size.width()*2) btn.setMinimumSize(btn.sizeHint()) - btn.setFlat(True) btn.setText(text) btn.setStyleSheet("text-align:left;padding:6px"); @@ -313,7 +307,7 @@ class _SummaryWidget(QtGui.QWidget): for i, btn in enumerate(btns): btn_size = btn_total_size*btn_rel_size[i] - txt_size = btn_size - btn.iconSize().width() - btn_margin/2*3 + txt_size = btn_size - btn.iconSize().width() - btn_margin # we elide only if there is enough space for a meaningful text if txt_size >= min_text_width: @@ -355,24 +349,28 @@ class _SummaryWidget(QtGui.QWidget): def _position_dialog(self, dialog): - main = dialog.parent() - list_widget = self.parent().parent().parent() - widget_rect = list_widget.geometry() - diag_size = dialog.sizeHint() - # default is towards main window center - if main.geometry().center().x() >= list_widget.mapToGlobal(widget_rect.center()).x(): - rigth_point = list_widget.mapToGlobal(widget_rect.topRight()) - dialog.setGeometry(QtCore.QRect(rigth_point, diag_size)) - else: - left_point = list_widget.mapToGlobal(widget_rect.topLeft()) - left_point -= QtCore.QPoint(diag_size.width(), 0) - dialog.setGeometry(QtCore.QRect(left_point, diag_size)) + # the scroll area does mess the mapping to global up, somehow + # the transformation from the widget ot the scroll area gives + # very weird values. Hence we build the coords of the widget + # ourself + + summary = dialog.parent() # == self + base_widget = summary.parent() + viewport = summary.parent() + scroll = viewport.parent() + + top_left = summary.geometry().topLeft() + base_widget.geometry().topLeft() + viewport.geometry().topLeft() + delta = (summary.width() - dialog.sizeHint().width())/2 + local_point = QtCore.QPoint(top_left.x()+delta, top_left.y()+summary.height()) + global_point = scroll.mapToGlobal(local_point) + + dialog.setGeometry(QtCore.QRect(global_point, dialog.sizeHint())) @QtCore.Slot() def editApp(self): if not hasattr(self, "appDialog"): widget = self._extractor.ViewObject.Proxy.get_app_edit_widget(self._post_dialog) - self.appDialog = _SettingsPopup(widget) + self.appDialog = _SettingsPopup(widget, self) self.appDialog.close.connect(self.appAccept) if not self.appDialog.isVisible(): @@ -386,7 +384,7 @@ class _SummaryWidget(QtGui.QWidget): if not hasattr(self, "viewDialog"): widget = self._extractor.ViewObject.Proxy.get_view_edit_widget(self._post_dialog) - self.viewDialog = _SettingsPopup(widget) + self.viewDialog = _SettingsPopup(widget, self) self.viewDialog.close.connect(self.viewAccept) if not self.viewDialog.isVisible(): @@ -402,8 +400,6 @@ class _SummaryWidget(QtGui.QWidget): @QtCore.Slot() def viewAccept(self): - self.viewDialog.hide() - # update the preview extr_repr = self._extractor.ViewObject.Proxy.get_preview() self.viewButton.setIcon(extr_repr[0]) @@ -414,8 +410,6 @@ class _SummaryWidget(QtGui.QWidget): @QtCore.Slot() def appAccept(self): - self.appDialog.hide() - # update the preview extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) self.extrButton.full_text = extr_label diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 33ef4b4935..9e4ddac24f 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -116,6 +116,8 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): return ["X", "Y"] case 3: return ["X", "Y", "Z"] + case 6: + return ["XX", "YY", "ZZ", "XY", "XZ", "YZ"] case _: return ["Not a vector"] @@ -217,11 +219,13 @@ class Extractor1D(Extractor): component_array.SetName(array.GetName()) table.AddColumn(component_array) - def _x_array_from_dataset(self, obj, dataset): + def _x_array_from_dataset(self, obj, dataset, copy=True): # extracts the relevant array from the dataset and returns a copy + # indices = None uses all indices, otherwise the values in this list match obj.XField: case "Index": + # index needs always to be build, ignore copy argument num = dataset.GetPoints().GetNumberOfPoints() array = vtkIntArray() array.SetNumberOfTuples(num) @@ -230,15 +234,22 @@ class Extractor1D(Extractor): array.SetValue(i,i) case "Position": + orig_array = dataset.GetPoints().GetData() - array = vtkDoubleArray() - array.DeepCopy(orig_array) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array case _: point_data = dataset.GetPointData() orig_array = point_data.GetAbstractArray(obj.XField) - array = vtkDoubleArray() - array.DeepCopy(orig_array) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array return array @@ -343,28 +354,29 @@ class Extractor2D(Extractor1D): component_array.SetName(array.GetName()) table.AddColumn(component_array) - def _y_array_from_dataset(self, obj, dataset): + def _y_array_from_dataset(self, obj, dataset, copy=True): # extracts the relevant array from the dataset and returns a copy + # indices = None uses all indices, otherwise the values in this list match obj.YField: - case "Index": - num = dataset.GetPoints().GetNumberOfPoints() - array = vtkIntArray() - array.SetNumberOfTuples(num) - array.SetNumberOfComponents(1) - for i in range(num): - array.SetValue(i,i) - case "Position": + orig_array = dataset.GetPoints().GetData() - array = vtkDoubleArray() - array.DeepCopy(orig_array) + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array case _: point_data = dataset.GetPointData() orig_array = point_data.GetAbstractArray(obj.YField) - array = vtkDoubleArray() - array.DeepCopy(orig_array) + + if copy: + array = vtkDoubleArray() + array.DeepCopy(orig_array) + else: + array = orig_array return array diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index f7a450c181..3540b6706a 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -33,6 +33,7 @@ from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper +from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline @@ -108,7 +109,7 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): obj.Table = table -class PostIndexData1D(base_fempostextractors.Extractor1D): +class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): """ A post processing extraction of one dimensional index data """ @@ -119,19 +120,67 @@ class PostIndexData1D(base_fempostextractors.Extractor1D): 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( + prop =[_PropHelper( type="App::PropertyInteger", - name="XIndex", + name="Index", group="X Data", - doc="Specify for which point index the data should be extracted", + doc="Specify for which index the data should be extracted", value=0, ), ] return super()._get_properties() + prop + + def execute(self, obj): + + # on execution we populate the vtk table + table = vtkTable() + + if not obj.Source: + obj.Table = table + return + + dataset = obj.Source.getDataSet() + if not dataset: + obj.Table = table + return + + # check if we have timesteps (required!) + abort = True + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + if len(timesteps) > 1: + abort = False + + if abort: + FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") + obj.Table = table + return + + algo = obj.Source.getOutputAlgorithm() + setup = False + frame_array = vtkDoubleArray() + + idx = obj.Index + for i, timestep in enumerate(timesteps): + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset, copy=False) + + if not setup: + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_array.SetTuple(i, idx, array) + + if frame_array.GetNumberOfComponents() > 1: + frame_array.SetName(f"{obj.XField} ({obj.XComponent})") + else: + frame_array.SetName(f"{obj.XField}") + + self._x_array_component_to_table(obj, frame_array, table) + + # set the final table + obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index b25b809655..60c9ac2df2 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -33,6 +33,7 @@ from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper +from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline @@ -124,9 +125,9 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): obj.Table = table -class PostIndexData2D(base_fempostextractors.Extractor2D): +class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): """ - A post processing extraction of one dimensional index data + A post processing extraction for two dimensional data with X always being the frames """ ExtractionType = "Index" @@ -135,19 +136,83 @@ class PostIndexData2D(base_fempostextractors.Extractor2D): 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( + prop =[_PropHelper( type="App::PropertyInteger", - name="XIndex", - group="X Data", + name="Index", + group="Data", doc="Specify for which point index the data should be extracted", value=0, ), ] return super()._get_properties() + prop + + def _setup_x_component_property(self, obj, point_data): + # override to only allow "Frames" as X data + obj.XComponent = ["Not a vector"] + + def _setup_x_properties(self, obj, dataset): + # override to only allow "Frames" as X data + obj.XField = ["Frames"] + + 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 + + # check if we have timesteps (required!) + abort = True + info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) + if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): + timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) + if len(timesteps) > 1: + abort = False + + if abort: + FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") + obj.Table = table + return + + algo = obj.Source.getOutputAlgorithm() + + frame_x_array = vtkDoubleArray() + frame_x_array.SetNumberOfTuples(len(timesteps)) + frame_x_array.SetNumberOfComponents(1) + + + frame_y_array = vtkDoubleArray() + idx = obj.Index + setup = False + for i, timestep in enumerate(timesteps): + + frame_x_array.SetTuple1(i, timestep) + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._y_array_from_dataset(obj, dataset, copy=False) + if not setup: + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_y_array.SetTuple(i, idx, array) + + frame_x_array.SetName("Frames") + if frame_y_array.GetNumberOfComponents() > 1: + frame_y_array.SetName(f"{obj.YField} ({obj.YComponent})") + else: + frame_y_array.SetName(obj.YField) + + table.AddColumn(frame_x_array) + self._y_array_component_to_table(obj, frame_y_array, table) + + # set the final table + obj.Table = table diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index 8cbd72b41c..bdcb4ad553 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -57,6 +57,14 @@ post_visualization.register_extractor("Histogram", "makePostHistogramFieldData") +post_visualization.register_extractor("Histogram", + "HistogramIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostHistogramIndexOverFrames") + # Implementation # ############## @@ -77,7 +85,11 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D): """ VisualizationType = "Histogram" - +class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for histogram. + """ + VisualizationType = "Histogram" class PostHistogram(base_fempostvisualizations.PostVisualization): diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 272aaea74c..06f844ebac 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -56,6 +56,14 @@ post_visualization.register_extractor("Lineplot", "ObjectsFem", "makePostLineplotFieldData") +post_visualization.register_extractor("Lineplot", + "LineplotIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "2D", + "Index", + "ObjectsFem", + "makePostLineplotIndexOverFrames") + # Implementation # ############## @@ -77,6 +85,12 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ VisualizationType = "Lineplot" +class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): + """ + A 2D index extraction for lineplot. + """ + VisualizationType = "Lineplot" + class PostLineplot(base_fempostvisualizations.PostVisualization): diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py index 593f177a94..79b4e4eeab 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_histogram.py +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -64,6 +64,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setLayout(vbox) self.data_widget.setWindowTitle("Histogram data") + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) # histogram parameter widget @@ -71,6 +72,8 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" ) self.view_widget.setWindowTitle("Histogram view settings") + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) + self.__init_widgets() # form made from param and selection widget diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py index 650cdd70a1..507e6b3cbf 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -64,6 +64,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setLayout(vbox) self.data_widget.setWindowTitle("Lineplot data") + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) # lineplot parameter widget @@ -71,6 +72,8 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" ) self.view_widget.setWindowTitle("Lineplot view settings") + self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) + self.__init_widgets() # form made from param and selection widget diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 8fe2fa6b4c..777e3d15c0 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -36,6 +36,7 @@ import Plot import FemGui from PySide import QtGui, QtCore +import io import numpy as np import matplotlib as mpl @@ -73,25 +74,59 @@ class EditViewWidget(QtGui.QWidget): 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])) + + # setup the color buttons (don't use FreeCADs color button, as this does not work in popups!) + self._setup_color_button(self.widget.BarColor, vobj.BarColor, self.barColorChanged) + self._setup_color_button(self.widget.LineColor, vobj.LineColor, self.lineColorChanged) 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") + # sometimes wierd sizes occur with spinboxes + self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height()) + self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) + + + def _setup_color_button(self, button, fcColor, callback): + + barColor = QtGui.QColor(*[v*255 for v in fcColor]) + icon_size = button.iconSize() + icon_size.setWidth(icon_size.width()*2) + button.setIconSize(icon_size) + pixmap = QtGui.QPixmap(icon_size) + pixmap.fill(barColor) + button.setIcon(pixmap) + + action = QtGui.QWidgetAction(button) + diag = QtGui.QColorDialog(barColor, parent=button) + diag.accepted.connect(action.trigger) + diag.rejected.connect(action.trigger) + diag.colorSelected.connect(callback) + + action.setDefaultWidget(diag) + button.addAction(action) + button.setPopupMode(QtGui.QToolButton.InstantPopup) + + + @QtCore.Slot(QtGui.QColor) + def lineColorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.LineColor.iconSize()) + pixmap.fill(color) + self.widget.LineColor.setIcon(pixmap) + self._object.ViewObject.LineColor = color.getRgb() - @QtCore.Slot() - def barColorChanged(self): - color = self.widget.BarColor.property("color") + @QtCore.Slot(QtGui.QColor) + def barColorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.BarColor.iconSize()) + pixmap.fill(color) + self.widget.BarColor.setIcon(pixmap) + self._object.ViewObject.BarColor = color.getRgb() @QtCore.Slot(float) @@ -115,7 +150,7 @@ class EditViewWidget(QtGui.QWidget): self._object.ViewObject.Legend = self.widget.Legend.text() -class EditAppWidget(QtGui.QWidget): +class EditFieldAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): super().__init__() @@ -160,6 +195,54 @@ class EditAppWidget(QtGui.QWidget): self._object.ExtractFrames = extract self._post_dialog._recompute() +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + + # sometimes wierd sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): """ @@ -233,13 +316,30 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): return ":/icons/FEM_PostField.svg" def get_app_edit_widget(self, post_dialog): - return EditAppWidget(self.Object, post_dialog) + return EditFieldAppWidget(self.Object, post_dialog) def get_view_edit_widget(self, post_dialog): return EditViewWidget(self.Object, post_dialog) def get_preview(self): - return (QtGui.QPixmap(), self.ViewObject.Legend) + + fig = mpl.pyplot.figure(figsize=(0.4,0.2), dpi=500) + ax = mpl.pyplot.Axes(fig, [0., 0., 2, 1]) + ax.set_axis_off() + fig.add_axes(ax) + + kwargs = self.get_kw_args() + patch = mpl.patches.Rectangle(xy=(0,0), width=2, height=1, **kwargs) + ax.add_patch(patch) + + data = io.BytesIO() + mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) + mpl.pyplot.close() + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data.getvalue()) + + return (pixmap, self.ViewObject.Legend) def get_kw_args(self): # builds kw args from the properties @@ -256,6 +356,21 @@ class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): return kwargs +class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData): + """ + A View Provider for extraction of 1D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): """ A View Provider for Histogram plots @@ -365,15 +480,26 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: + main = Plot.getMainWindow() self._plot = Plot.Plot() - self._dialog = QtGui.QDialog(Plot.getMainWindow()) + self._plot.destroyed.connect(self.destroyed) + self._dialog = QtGui.QDialog(main) box = QtGui.QVBoxLayout() box.addWidget(self._plot) + self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep it square self._dialog.setLayout(box) self.drawPlot() self._dialog.show() + + def destroyed(self, obj): + print("*********************************************************") + print("**************** ******************") + print("**************** destroy ******************") + print("**************** ******************") + print("*********************************************************") + def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 54fe5439f3..f98a5bf1e4 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -39,7 +39,6 @@ from PySide import QtGui, QtCore import io import numpy as np import matplotlib as mpl -import matplotlib.pyplot as plt from vtkmodules.numpy_interface.dataset_adapter import VTKArray @@ -75,18 +74,47 @@ class EditViewWidget(QtGui.QWidget): self._post_dialog._enumPropertyToCombobox(vobj, "MarkerStyle", self.widget.MarkerStyle) self.widget.LineWidth.setValue(vobj.LineWidth) self.widget.MarkerSize.setValue(vobj.MarkerSize) - self.widget.Color.setProperty("color", QtGui.QColor(*[v*255 for v in vobj.Color])) + + self._setup_color_button(self.widget.Color, vobj.Color, self.colorChanged) self.widget.Legend.editingFinished.connect(self.legendChanged) self.widget.MarkerStyle.activated.connect(self.markerStyleChanged) self.widget.LineStyle.activated.connect(self.lineStyleChanged) self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged) self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) - self.widget.Color.changed.connect(self.colorChanged) - @QtCore.Slot() - def colorChanged(self): - color = self.widget.Color.property("color") + # sometimes wierd sizes occur with spinboxes + self.widget.MarkerSize.setMaximumHeight(self.widget.MarkerStyle.sizeHint().height()) + self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) + + def _setup_color_button(self, button, fcColor, callback): + + barColor = QtGui.QColor(*[v*255 for v in fcColor]) + icon_size = button.iconSize() + icon_size.setWidth(icon_size.width()*2) + button.setIconSize(icon_size) + pixmap = QtGui.QPixmap(icon_size) + pixmap.fill(barColor) + button.setIcon(pixmap) + + action = QtGui.QWidgetAction(button) + diag = QtGui.QColorDialog(barColor, parent=button) + diag.accepted.connect(action.trigger) + diag.rejected.connect(action.trigger) + diag.colorSelected.connect(callback) + + action.setDefaultWidget(diag) + button.addAction(action) + button.setPopupMode(QtGui.QToolButton.InstantPopup) + + + @QtCore.Slot(QtGui.QColor) + def colorChanged(self, color): + + pixmap = QtGui.QPixmap(self.widget.Color.iconSize()) + pixmap.fill(color) + self.widget.Color.setIcon(pixmap) + self._object.ViewObject.Color = color.getRgb() @QtCore.Slot(float) @@ -110,7 +138,7 @@ class EditViewWidget(QtGui.QWidget): self._object.ViewObject.Legend = self.widget.Legend.text() -class EditAppWidget(QtGui.QWidget): +class EditFieldAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): super().__init__() @@ -171,9 +199,58 @@ class EditAppWidget(QtGui.QWidget): self._post_dialog._recompute() +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostLineplotIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.YField.activated.connect(self.yFieldChanged) + self.widget.YComponent.activated.connect(self.yComponentChanged) + + # sometimes wierd sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.YField.sizeHint().height()) + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yFieldChanged(self, index): + self._object.YField = index + self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def yComponentChanged(self, index): + self._object.YComponent = index + self._post_dialog._recompute() + + class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): """ - A View Provider for extraction of 1D field data specialy for histograms + A View Provider for extraction of 2D field data specialy for histograms """ def __init__(self, vobj): @@ -236,7 +313,7 @@ class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): return ":/icons/FEM_PostField.svg" def get_app_edit_widget(self, post_dialog): - return EditAppWidget(self.Object, post_dialog) + return EditFieldAppWidget(self.Object, post_dialog) def get_view_edit_widget(self, post_dialog): return EditViewWidget(self.Object, post_dialog) @@ -245,16 +322,16 @@ class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): # Returns the preview tuple of icon and label: (QPixmap, str) # Note: QPixmap in ratio 2:1 - fig = plt.figure(figsize=(0.2,0.1), dpi=1000) - ax = plt.Axes(fig, [0., 0., 1., 1.]) + fig = mpl.pyplot.figure(figsize=(0.2,0.1), dpi=1000) + ax = mpl.pyplot.Axes(fig, [0., 0., 1., 1.]) ax.set_axis_off() fig.add_axes(ax) kwargs = self.get_kw_args() kwargs["markevery"] = [1] ax.plot([0,0.5,1],[0.5,0.5,0.5], **kwargs) data = io.BytesIO() - plt.savefig(data, bbox_inches=0, transparent=True) - plt.close() + mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) + mpl.pyplot.close() pixmap = QtGui.QPixmap() pixmap.loadFromData(data.getvalue()) @@ -277,6 +354,21 @@ class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): return kwargs +class VPPostLineplotIndexOverFrames(VPPostLineplotFieldData): + """ + A View Provider for extraction of 2D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): """ A View Provider for Lineplot plots @@ -366,9 +458,11 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: self._plot = Plot.Plot() - self._dialog = QtGui.QDialog(Plot.getMainWindow()) + main = Plot.getMainWindow() + self._dialog = QtGui.QDialog(main) box = QtGui.QVBoxLayout() box.addWidget(self._plot) + self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep aspect ratio constant self._dialog.setLayout(box) self.drawPlot() diff --git a/src/Mod/Plot/Plot.py b/src/Mod/Plot/Plot.py index 4f5a360745..a3423ae93d 100644 --- a/src/Mod/Plot/Plot.py +++ b/src/Mod/Plot/Plot.py @@ -28,7 +28,7 @@ import sys try: import matplotlib - matplotlib.use("Qt5Agg") + matplotlib.use("QtAgg") # Force matplotlib to use PySide backend by temporarily unloading PyQt if "PyQt5.QtCore" in sys.modules: @@ -36,10 +36,11 @@ try: import matplotlib.pyplot as plt import PyQt5.QtCore else: + print("default matplotlib import") import matplotlib.pyplot as plt - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas - from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas + from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure except ImportError: From 0a4dd0c31dda0977168fac949777debe6d0da7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 20 Apr 2025 14:18:30 +0200 Subject: [PATCH 007/126] FEM: Add table post data visualization --- src/Mod/Fem/CMakeLists.txt | 4 + src/Mod/Fem/Gui/CMakeLists.txt | 1 + .../Resources/ui/PostHistogramFieldAppEdit.ui | 2 +- .../Resources/ui/PostTableFieldViewEdit.ui | 43 +++ src/Mod/Fem/ObjectsFem.py | 42 +++ src/Mod/Fem/femcommands/commands.py | 1 + src/Mod/Fem/femguiutils/extract_link_view.py | 12 +- src/Mod/Fem/femguiutils/vtk_table_view.py | 22 +- .../femobjects/base_fempostvisualizations.py | 124 +++++++- src/Mod/Fem/femobjects/post_extract1D.py | 6 +- src/Mod/Fem/femobjects/post_extract2D.py | 6 +- src/Mod/Fem/femobjects/post_histogram.py | 59 +--- src/Mod/Fem/femobjects/post_lineplot.py | 57 +--- src/Mod/Fem/femobjects/post_table.py | 231 ++++---------- .../Fem/femtaskpanels/base_fempostpanel.py | 7 + src/Mod/Fem/femtaskpanels/task_post_table.py | 94 ++++++ ...ract.py => view_base_fempostextractors.py} | 29 +- .../view_base_fempostvisualization.py | 49 ++- .../femviewprovider/view_post_histogram.py | 55 +--- .../Fem/femviewprovider/view_post_lineplot.py | 51 +--- .../Fem/femviewprovider/view_post_table.py | 287 ++++++++++++++++++ 21 files changed, 777 insertions(+), 405 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui create mode 100644 src/Mod/Fem/femtaskpanels/task_post_table.py rename src/Mod/Fem/femviewprovider/{view_post_extract.py => view_base_fempostextractors.py} (92%) create mode 100644 src/Mod/Fem/femviewprovider/view_post_table.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 0178bffe84..3ba5a83c3e 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -223,6 +223,7 @@ if(BUILD_FEM_VTK_PYTHON) femobjects/post_extract2D.py femobjects/post_histogram.py femobjects/post_lineplot.py + femobjects/post_table.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -637,6 +638,7 @@ if(BUILD_FEM_VTK_PYTHON) femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_post_histogram.py femtaskpanels/task_post_lineplot.py + femtaskpanels/task_post_table.py femtaskpanels/task_post_extractor.py ) endif(BUILD_FEM_VTK_PYTHON) @@ -666,6 +668,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_base_femmeshelement.py femviewprovider/view_base_femobject.py femviewprovider/view_base_fempostvisualization.py + femviewprovider/view_base_fempostextractors.py femviewprovider/view_constant_vacuumpermittivity.py femviewprovider/view_constraint_bodyheatsource.py femviewprovider/view_constraint_centrif.py @@ -704,6 +707,7 @@ if(BUILD_FEM_VTK_PYTHON) femviewprovider/view_post_extract.py femviewprovider/view_post_histogram.py femviewprovider/view_post_lineplot.py + femviewprovider/view_post_table.py ) endif(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 7337afd833..7729f8af55 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -451,6 +451,7 @@ SET(FemGuiPythonUI_SRCS Resources/ui/PostLineplotFieldViewEdit.ui Resources/ui/PostLineplotFieldAppEdit.ui Resources/ui/PostLineplotIndexAppEdit.ui + Resources/ui/PostTableFieldViewEdit.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui index d100b81ab3..8e611e7790 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramFieldAppEdit.ui @@ -65,7 +65,7 @@ - One field for all frames + One field for each frames diff --git a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui new file mode 100644 index 0000000000..ada74b69b4 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -0,0 +1,43 @@ + + + PostHistogramEdit + + + + 0 + 0 + 259 + 38 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Name: + + + + + + + + diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index d40558f4bd..c21c219dda 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -770,6 +770,48 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): return obj +def makePostTable(doc, name="Table"): + """makePostTable(document, [name]): + creates a FEM post processing histogram plot + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_table + + post_table.PostTable(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + view_post_table.VPPostTable(obj.ViewObject) + return obj + + +def makePostTableFieldData(doc, name="FieldData1D"): + """makePostTableFieldData(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_table + + post_table.PostTableFieldData(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + view_post_table.VPPostTableFieldData(obj.ViewObject) + return obj + + +def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): + """makePostTableIndexOverFrames(document, [name]): + creates a FEM post processing data extractor for 1D Field data + """ + obj = doc.addObject("App::FeaturePython", name) + from femobjects import post_table + + post_table.PostTableIndexOverFrames(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_post_table + view_post_table.VPPostTableIndexOverFrames(obj.ViewObject) + return obj + + # ********* solver objects *********************************************************************** def makeEquationDeformation(doc, base_solver=None, name="Deformation"): """makeEquationDeformation(document, [base_solver], [name]): diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index f4edc1586e..50461484a2 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1293,4 +1293,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: # setup all visualization commands (register by importing) import femobjects.post_lineplot import femobjects.post_histogram + import femobjects.post_table post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index e1611f8609..acd409765c 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -252,11 +252,13 @@ class _SummaryWidget(QtGui.QWidget): self.extrButton.setIcon(extractor.ViewObject.Icon) self.viewButton = self._button(extr_repr[1]) - size = self.viewButton.iconSize() - size.setWidth(size.width()*2) - self.viewButton.setIconSize(size) - self.viewButton.setIcon(extr_repr[0]) - + if not extr_repr[0].isNull(): + size = self.viewButton.iconSize() + size.setWidth(size.width()*2) + self.viewButton.setIconSize(size) + self.viewButton.setIcon(extr_repr[0]) + else: + self.viewButton.setIconSize(QtCore.QSize(0,0)) self.rmButton = QtGui.QToolButton(self) self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index df06c51ee0..95ebf1007e 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -34,14 +34,23 @@ from PySide import QtCore class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns + # One can supply a header_names dict to replace the table column names + # in the header. It is a dict "column_idx (int)" to "new name"" or + # "orig_name (str)" to "new name" - def __init__(self): + def __init__(self, header_names = None): super().__init__() self._table = None + if header_names: + self._header = header_names + else: + self._header = {} - def setTable(self, table): + def setTable(self, table, header_names = None): self.beginResetModel() self._table = table + if header_names: + self._header = header_names self.endResetModel() def rowCount(self, index): @@ -70,7 +79,14 @@ class VtkTableModel(QtCore.QAbstractTableModel): def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: - return self._table.GetColumnName(section) + if section in self._header: + return self._header[section] + + name = self._table.GetColumnName(section) + if name in self._header: + return self._header[name] + + return name if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return section diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index fae9c58b6c..b16f41451f 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -21,42 +21,70 @@ # * * # *************************************************************************** -__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts" +__title__ = "FreeCAD FEM postprocessing data visualization base object" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" ## @package base_fempostextractors # \ingroup FEM -# \brief base objects for data extractors +# \brief base objects for data visualizations from vtkmodules.vtkCommonDataModel import vtkTable +from vtkmodules.vtkCommonCore import vtkDoubleArray from . import base_fempythonobject +from . import base_fempostextractors # helper functions # ################ def is_visualization_object(obj): + if not obj: + return False + if not hasattr(obj, "Proxy"): return False return hasattr(obj.Proxy, "VisualizationType") + def get_visualization_type(obj): # returns the extractor type string, or throws exception if # not a extractor return obj.Proxy.VisualizationType +def is_visualization_extractor_type(obj, vistype): + + # must be extractor + if not base_fempostextractors.is_extractor_object(obj): + return False + + # must be visualization object + if not is_visualization_object(obj): + return False + + # must be correct type + if get_visualization_type(obj) != vistype: + return False + + return True + + + # Base class for all visualizations +# It collects all data from its extraction objects into a table. # Note: Never use directly, always subclass! This class does not create a # Visualization variable, hence will not work correctly. class PostVisualization(base_fempythonobject.BaseFemPythonObject): + def __init__(self, obj): super().__init__(obj) + obj.addExtension("App::GroupExtensionPython") self._setup_properties(obj) + def _setup_properties(self, obj): pl = obj.PropertiesList for prop in self._get_properties(): @@ -64,8 +92,96 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): prop.add_to_object(obj) + def _get_properties(self): + # override if subclass wants to add additional properties + + prop = [ + base_fempostextractors._PropHelper( + type="Fem::PropertyPostDataObject", + name="Table", + group="Base", + doc="The data table that stores the data for visualization", + value=vtkTable(), + ), + ] + return prop + + def onDocumentRestored(self, obj): + # if a new property was added we handle it by setup + # Override if subclass needs to handle changed property type + self._setup_properties(obj) - def _get_properties(self): - return [] + + def onChanged(self, obj, prop): + # Ensure only correct child object types are in the group + + if prop == "Group": + # check if all objects are allowed + + children = obj.Group + for child in obj.Group: + if not is_visualization_extractor_type(child, self.VisualizationType): + FreeCAD.Console.PrintWarning(f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added") + children.remove(child) + + if len(obj.Group) != len(children): + obj.Group = children + + + def execute(self, obj): + # Collect all extractor child data into our table + # Note: Each childs table can have different number of rows. We need + # to pad the date for our table in this case + + rows = self.getLongestColumnLength(obj) + table = vtkTable() + for child in obj.Group: + + # If child has no Source, its table should be empty. However, + # it would theoretical be possible that child source was set + # to none without recompute, and the visualization was manually + # recomputed afterwards + if not child.Source and (c_table.GetNumberOfColumns() > 0): + FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored") + continue + + c_table = child.Table + for i in range(c_table.GetNumberOfColumns()): + c_array = c_table.GetColumn(i) + array = vtkDoubleArray() + + if c_array.GetNumberOfTuples() == rows: + # simple deep copy is enough + array.DeepCopy(c_array) + + else: + array.SetNumberOfComponents(c_array.GetNumberOfComponents()) + array.SetNumberOfTuples(rows) + array.Fill(0) # so that all non-used entries are set to 0 + for i in range(c_array.GetNumberOfTuples()): + array.SetTuple(i, c_array.GetTuple(i)) + + array.SetName(f"{child.Source.Name}: {c_array.GetName()}") + table.AddColumn(array) + + + obj.Table = table + return False + + + def getLongestColumnLength(self, obj): + # iterate all extractor children and get the column lengths + + length = 0 + for child in obj.Group: + if base_fempostextractors.is_extractor_object(child): + table = child.Table + if table.GetNumberOfColumns() > 0: + # we assume all columns of an extractor have same length + num = table.GetColumn(0).GetNumberOfTuples() + if num > length: + length = num + + return length diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 3540b6706a..425a594fae 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines +import FreeCAD + from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -176,9 +178,9 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): frame_array.SetTuple(i, idx, array) if frame_array.GetNumberOfComponents() > 1: - frame_array.SetName(f"{obj.XField} ({obj.XComponent})") + frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") else: - frame_array.SetName(f"{obj.XField}") + frame_array.SetName(f"{obj.XField} @Idx {obj.Index}") self._x_array_component_to_table(obj, frame_array, table) diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 60c9ac2df2..0b9e5c528e 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines +import FreeCAD + from . import base_fempostextractors from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -207,9 +209,9 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): frame_x_array.SetName("Frames") if frame_y_array.GetNumberOfComponents() > 1: - frame_y_array.SetName(f"{obj.YField} ({obj.YComponent})") + frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}") else: - frame_y_array.SetName(obj.YField) + frame_y_array.SetName(f"{obj.YField} @Idx {obj.Index}") table.AddColumn(frame_x_array) self._y_array_component_to_table(obj, frame_y_array, table) diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index bdcb4ad553..0a6277f5fe 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -29,17 +29,11 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying histograms -from . import base_fempythonobject -_PropHelper = base_fempythonobject._PropHelper from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract1D -from vtkmodules.vtkCommonCore import vtkDoubleArray -from vtkmodules.vtkCommonDataModel import vtkTable - - from femguiutils import post_visualization # register visualization and extractors @@ -85,6 +79,7 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D): """ VisualizationType = "Histogram" + class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): """ A 1D index extraction for histogram. @@ -96,57 +91,11 @@ class PostHistogram(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as histograms """ - VisualizationType = "Histogram" - def __init__(self, obj): - super().__init__(obj) - obj.addExtension("App::GroupExtensionPython") - - def _get_properties(self): - prop = [ - _PropHelper( - type="Fem::PropertyPostDataObject", - name="Table", - group="Base", - doc="The data table that stores the plotted data, one column per histogram", - value=vtkTable(), - ), - ] - return super()._get_properties() + prop - - - def onChanged(self, obj, prop): - - if prop == "Group": - # check if all objects are allowed - - children = obj.Group - for child in obj.Group: - if not is_histogram_extractor(child): - FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added") - children.remove(child) - - if len(obj.Group) != len(children): - obj.Group = children - - def execute(self, obj): - - # during execution we collect all child data into our table - table = vtkTable() - for child in obj.Group: - - c_table = child.Table - for i in range(c_table.GetNumberOfColumns()): - c_array = c_table.GetColumn(i) - # TODO: check which array type it is and use that one - array = vtkDoubleArray() - array.DeepCopy(c_array) - array.SetName(f"{child.Source.Label}: {c_array.GetName()}") - table.AddColumn(array) - - obj.Table = table - return False + + + diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 06f844ebac..486241b367 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -29,17 +29,10 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines -from . import base_fempythonobject -_PropHelper = base_fempythonobject._PropHelper - from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract2D -from vtkmodules.vtkCommonCore import vtkDoubleArray -from vtkmodules.vtkCommonDataModel import vtkTable - - from femguiutils import post_visualization # register visualization and extractors @@ -85,6 +78,7 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ VisualizationType = "Lineplot" + class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): """ A 2D index extraction for lineplot. @@ -97,56 +91,7 @@ class PostLineplot(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as line plots """ - VisualizationType = "Lineplot" - def __init__(self, obj): - super().__init__(obj) - obj.addExtension("App::GroupExtensionPython") - - def _get_properties(self): - prop = [ - _PropHelper( - type="Fem::PropertyPostDataObject", - name="Table", - group="Base", - doc="The data table that stores the plotted data, two columns per lineplot (x,y)", - value=vtkTable(), - ), - ] - return super()._get_properties() + prop - - - def onChanged(self, obj, prop): - - if prop == "Group": - # check if all objects are allowed - - children = obj.Group - for child in obj.Group: - if not is_lineplot_extractor(child): - FreeCAD.Console.PrintWarning(f"{child.Label} is not a data lineplot data extraction object, cannot be added") - children.remove(child) - - if len(obj.Group) != len(children): - obj.Group = children - - def execute(self, obj): - - # during execution we collect all child data into our table - table = vtkTable() - for child in obj.Group: - - c_table = child.Table - for i in range(c_table.GetNumberOfColumns()): - c_array = c_table.GetColumn(i) - # TODO: check which array type it is and use that one - array = vtkDoubleArray() - array.DeepCopy(c_array) - array.SetName(f"{child.Source.Label}: {c_array.GetName()}") - table.AddColumn(array) - - obj.Table = table - return False diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index f8798fbc23..3d7d7be689 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -21,191 +21,76 @@ # * * # *************************************************************************** -__title__ = "FreeCAD post line plot" +__title__ = "FreeCAD post table" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" -## @package post_lineplot +## @package post_table # \ingroup FEM -# \brief Post processing plot displaying lines +# \brief Post processing plot displaying tables -from . import base_fempythonobject -_PropHelper = base_fempythonobject._PropHelper - -# helper function to extract plot object type -def _get_extraction_subtype(obj): - if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"): - return obj.Proxy.Type - - return "unknown" +from . import base_fempostextractors +from . import base_fempostvisualizations +from . import post_extract1D -class PostLinePlot(base_fempythonobject.BaseFemPythonObject): +from femguiutils import post_visualization + +# register visualization and extractors +post_visualization.register_visualization("Table", + ":/icons/FEM_PostSpreadsheet.svg", + "ObjectsFem", + "makePostTable") + +post_visualization.register_extractor("Table", + "TableFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostTableFieldData") + + +post_visualization.register_extractor("Table", + "TableIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostTableIndexOverFrames") + +# Implementation +# ############## + +def is_table_extractor(obj): + + if not base_fempostextractors.is_extractor_object(obj): + return False + + if not hasattr(obj.Proxy, "VisualizationType"): + return False + + return obj.Proxy.VisualizationType == "Table" + + +class PostTableFieldData(post_extract1D.PostFieldData1D): """ - A post processing extraction for plotting lines + A 1D Field extraction for tables. """ - - Type = "App::FeaturePython" - - def __init__(self, obj): - super().__init__(obj) - obj.addExtension("App::GroupExtension") - self._setup_properties(obj) - - def _setup_properties(self, obj): - - self.ExtractionType = "LinePlot" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) - - def _get_properties(self): - prop = [] - return prop - - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): - - def onChanged(self, obj, prop): - - if prop == "Group": - # check if all objects are allowed - - children = obj.Group - for child in obj.Group: - if _get_extraction_subtype(child) not in ["Line"]: - children.remove(child) - - if len(obj.Group) != len(children): - obj.Group = children + VisualizationType = "Table" -class PostPlotLine(base_fempythonobject.BaseFemPythonObject): +class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D): + """ + A 1D index extraction for table. + """ + VisualizationType = "Table" - Type = "App::FeaturePython" - def __init__(self, obj): - super().__init__(obj) - self._setup_properties(obj) +class PostTable(base_fempostvisualizations.PostVisualization): + """ + A post processing plot for showing extracted data as tables + """ + VisualizationType = "Table" - def _setup_properties(self, obj): - - self.ExtractionType = "Line" - - pl = obj.PropertiesList - for prop in self._get_properties(): - if not prop.Name in pl: - prop.add_to_object(obj) - - def _get_properties(self): - - prop = [ - _PropHelper( - type="App::PropertyLink", - name="Source", - group="Line", - doc="The data source, the line uses", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XField", - group="X Data", - doc="The field to use as X data", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="XComponent", - group="X Data", - doc="Which part of the X field vector to use for the X axis", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YField", - group="Y Data", - doc="The field to use as Y data for the line plot", - value=None, - ), - _PropHelper( - type="App::PropertyEnumeration", - name="YComponent", - group="Y Data", - doc="Which part of the Y field vector to use for the X axis", - value=None, - ), - ] - return prop - - def onDocumentRestored(self, obj): - self._setup_properties(self, obj): - - def onChanged(self, obj, prop): - - if prop == "Source": - # check if the source is a Post object - if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"): - FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject") - obj.XField = [] - obj.YField = [] - obj.Source = None - - if prop == "XField": - if not obj.Source: - obj.XComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.XField): - obj.XComponent = [] - return - - match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents: - case 1: - obj.XComponent = ["Not a vector"] - case 2: - obj.XComponent = ["Magnitude", "X", "Y"] - case 3: - obj.XComponent = ["Magnitude", "X", "Y", "Z"] - - if prop == "YField": - if not obj.Source: - obj.YComponent = [] - return - - point_data = obj.Source.Data.GetPointData() - if not point_data.HasArray(obj.YField): - obj.YComponent = [] - return - - match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents: - case 1: - obj.YComponent = ["Not a vector"] - case 2: - obj.YComponent = ["Magnitude", "X", "Y"] - case 3: - obj.YComponent = ["Magnitude", "X", "Y", "Z"] - - def onExecute(self, obj): - # we need to make sure that we show the correct fields to the user as option for data extraction - - fields = [] - if obj.Source: - point_data = obj.Source.Data.GetPointData() - fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())] - - current_X = obj.XField - obj.XField = fields - if current_X in fields: - obj.XField = current_X - - current_Y = obj.YField - obj.YField = fields - if current_Y in fields: - obj.YField = current_Y - - return True diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index f90af0e260..3e26ac1ce5 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -60,6 +60,13 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): if button == QtGui.QDialogButtonBox.Apply: self.obj.Document.recompute() + def accept(self): + print("accept") + return super().accept() + + def reject(self): + print("reject") + return super().reject() # Helper functions # ################ diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py new file mode 100644 index 0000000000..e17f584c01 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -0,0 +1,94 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM histogram plot task panel" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package task_post_histogram +# \ingroup FEM +# \brief task panel for post histogram plot + +from PySide import QtCore, QtGui + +import FreeCAD +import FreeCADGui + +from . import base_fempostpanel +from femguiutils import extract_link_view as elv +from femguiutils import vtk_table_view + +class _TaskPanel(base_fempostpanel._BasePostTaskPanel): + """ + The TaskPanel for editing properties of glyph filter + """ + + def __init__(self, vobj): + super().__init__(vobj.Object) + + # data widget + self.data_widget = QtGui.QWidget() + self.data_widget.show_table = QtGui.QPushButton() + self.data_widget.show_table.setText("Show table") + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(self.data_widget.show_table) + vbox.addSpacing(10) + + extracts = elv.ExtractLinkView(self.obj, False, self) + vbox.addWidget(extracts) + + self.data_widget.setLayout(vbox) + self.data_widget.setWindowTitle("Table data") + self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg")) + + + # histogram parameter widget + #self.view_widget = FreeCADGui.PySideUic.loadUi( + # FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostTable.ui" + #) + #self.view_widget.setWindowTitle("Table view settings") + #self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostTable.svg")) + + self.__init_widgets() + + # form made from param and selection widget + self.form = [self.data_widget] + + + # Setup functions + # ############### + + def __init_widgets(self): + + # connect data widget + self.data_widget.show_table.clicked.connect(self.showTable) + + # set current values to view widget + viewObj = self.obj.ViewObject + + + @QtCore.Slot() + def showTable(self): + self.obj.ViewObject.Proxy.show_visualization() + diff --git a/src/Mod/Fem/femviewprovider/view_post_extract.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py similarity index 92% rename from src/Mod/Fem/femviewprovider/view_post_extract.py rename to src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index b91c65cb45..46313ba890 100644 --- a/src/Mod/Fem/femviewprovider/view_post_extract.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -66,7 +66,7 @@ class VPPostExtractor: def onChanged(self, vobj, prop): - # one of our view properties was changed. Lets inform our parent plot + # one of our view properties was changed. Lets inform our parent visualization # that this happend, as this is the one that needs to redraw if prop == "Proxy": @@ -92,6 +92,10 @@ class VPPostExtractor: return True + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True + def doubleClicked(self, vobj): guidoc = FreeCADGui.getDocument(vobj.Object.Document) @@ -104,10 +108,20 @@ class VPPostExtractor: return True + def dumps(self): + return None + + def loads(self, state): + return None + + + # To be implemented by subclasses: + # ################################ + def get_kw_args(self): - # should return the plot keyword arguments that represent the properties - # of the object - return {} + # Returns the matplotlib plot keyword arguments that represent the + # properties of the object. + raise FreeCAD.Base.FreeCADError("Not implemented") def get_app_edit_widget(self, post_dialog): # Returns a widgets for editing the object (not viewprovider!) @@ -125,10 +139,3 @@ class VPPostExtractor: # Returns the preview tuple of icon and label: (QPixmap, str) # Note: QPixmap in ratio 2:1 raise FreeCAD.Base.FreeCADError("Not implemented") - - - def dumps(self): - return None - - def loads(self, state): - return None diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 153537d669..20714b67c5 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -45,6 +45,8 @@ class VPPostVisualization: def __init__(self, vobj): vobj.Proxy = self self._setup_properties(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + def _setup_properties(self, vobj): pl = vobj.PropertiesList @@ -52,16 +54,21 @@ class VPPostVisualization: if not prop.name in pl: prop.add_to_object(vobj) + def _get_properties(self): return [] + def attach(self, vobj): self.Object = vobj.Object self.ViewObject = vobj + def isShow(self): + # Mark ourself as visible in the tree return True + def doubleClicked(self,vobj): guidoc = FreeCADGui.getDocument(vobj.Object.Document) @@ -71,21 +78,47 @@ class VPPostVisualization: FreeCADGui.Control.closeDialog() guidoc.resetEdit() + # open task dialog guidoc.setEdit(vobj.Object.Name) + + # show visualization + self.show_visualization() + return True - def show_visualization(self): - # shows the visualization without going into edit mode - # to be implemented by subclasses - pass + def unsetEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + return True - def get_kw_args(self, obj): - # returns a dictionary with all visualization options needed for plotting - # based on the view provider properties - return {} + def updateData(self, obj, prop): + # If the data changed we need to update the visualization + if prop == "Table": + self.update_visualization() + + def onChanged(self, vobj, prop): + # for all property changes we need to update the visualization + self.update_visualization() + + def childViewPropertyChanged(self, vobj, prop): + # One of the extractors view properties has changed, we need to + # update the visualization + self.update_visualization() def dumps(self): return None def loads(self, state): return None + + + # To be implemented by subclasses: + # ################################ + + def update_visualization(self): + # The visualization data or any relevant view property has changed, + # and the visualization itself needs to update to reflect that + raise FreeCAD.Base.FreeCADError("Not implemented") + + def show_visualization(self): + # Shows the visualization without going into edit mode + raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 777e3d15c0..2a6d817e70 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -42,7 +42,7 @@ import matplotlib as mpl from vtkmodules.numpy_interface.dataset_adapter import VTKArray -from . import view_post_extract +from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_histogram @@ -244,7 +244,7 @@ class EditIndexAppWidget(QtGui.QWidget): self._post_dialog._recompute() -class VPPostHistogramFieldData(view_post_extract.VPPostExtractor): +class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): """ A View Provider for extraction of 1D field data specialy for histograms """ @@ -378,7 +378,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + def _get_properties(self): @@ -458,13 +458,10 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): ] return prop + def getIcon(self): return ":/icons/FEM_PostHistogram.svg" - def doubleClicked(self,vobj): - - self.show_visualization() - super().doubleClicked(vobj) def setEdit(self, vobj, mode): @@ -482,24 +479,12 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: main = Plot.getMainWindow() self._plot = Plot.Plot() - self._plot.destroyed.connect(self.destroyed) - self._dialog = QtGui.QDialog(main) - box = QtGui.QVBoxLayout() - box.addWidget(self._plot) - self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep it square - self._dialog.setLayout(box) + self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square + self.update_visualization() - self.drawPlot() - self._dialog.show() + self._plot.show() - def destroyed(self, obj): - print("*********************************************************") - print("**************** ******************") - print("**************** destroy ******************") - print("**************** ******************") - print("*********************************************************") - def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): @@ -508,7 +493,8 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def drawPlot(self): + + def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: return @@ -579,26 +565,3 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.update() - - def updateData(self, obj, prop): - # we only react if the table changed, as then know that new data is available - if prop == "Table": - self.drawPlot() - - - def onChanged(self, vobj, prop): - - # for all property changes we need to redraw the plot - self.drawPlot() - - def childViewPropertyChanged(self, vobj, prop): - - # on of our extractors has a changed view property. - self.drawPlot() - - def dumps(self): - return None - - - def loads(self, state): - return None diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index f98a5bf1e4..29c0165275 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -42,9 +42,10 @@ import matplotlib as mpl from vtkmodules.numpy_interface.dataset_adapter import VTKArray -from . import view_post_extract +from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_lineplot +from femguiutils import post_visualization as pv _GuiPropHelper = view_base_fempostvisualization._GuiPropHelper @@ -248,7 +249,7 @@ class EditIndexAppWidget(QtGui.QWidget): self._post_dialog._recompute() -class VPPostLineplotFieldData(view_post_extract.VPPostExtractor): +class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): """ A View Provider for extraction of 2D field data specialy for histograms """ @@ -376,7 +377,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + def _get_properties(self): @@ -435,13 +436,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): ] return prop + def getIcon(self): return ":/icons/FEM_PostLineplot.svg" - def doubleClicked(self,vobj): - - self.show_visualization() - super().doubleClicked(vobj) def setEdit(self, vobj, mode): @@ -457,16 +455,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: - self._plot = Plot.Plot() main = Plot.getMainWindow() - self._dialog = QtGui.QDialog(main) - box = QtGui.QVBoxLayout() - box.addWidget(self._plot) - self._dialog.resize(main.size().height()/2, main.size().height()/3) # keep aspect ratio constant - self._dialog.setLayout(box) + self._plot = Plot.Plot() + self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self.update_visualization() + + self._plot.show() - self.drawPlot() - self._dialog.show() def get_kw_args(self, obj): view = obj.ViewObject @@ -476,7 +471,8 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def drawPlot(self): + + def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: return @@ -545,26 +541,3 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): self._plot.update() - - def updateData(self, obj, prop): - # we only react if the table changed, as then know that new data is available - if prop == "Table": - self.drawPlot() - - - def onChanged(self, vobj, prop): - - # for all property changes we need to redraw the plot - self.drawPlot() - - def childViewPropertyChanged(self, vobj, prop): - - # on of our extractors has a changed view property. - self.drawPlot() - - def dumps(self): - return None - - - def loads(self, state): - return None diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py new file mode 100644 index 0000000000..443667e31a --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -0,0 +1,287 @@ +# *************************************************************************** +# * Copyright (c) 2025 Stefan Tröger * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM postprocessing table ViewProvider for the document object" +__author__ = "Stefan Tröger" +__url__ = "https://www.freecad.org" + +## @package view_post_table +# \ingroup FEM +# \brief view provider for post table object + +import FreeCAD +import FreeCADGui + +import Plot +import FemGui +from PySide import QtGui, QtCore + +import io +import numpy as np +import matplotlib as mpl + +from vtkmodules.numpy_interface.dataset_adapter import VTKArray + +from . import view_base_fempostextractors +from . import view_base_fempostvisualization +from femtaskpanels import task_post_table +from femguiutils import vtk_table_view as vtv + +_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + +class EditViewWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostTableFieldViewEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + self.widget.Name.setText(self._object.ViewObject.Name) + self.widget.Name.editingFinished.connect(self.legendChanged) + + @QtCore.Slot() + def legendChanged(self): + self._object.ViewObject.Name = self.widget.Name.text() + + +class EditFieldAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up (we reuse histogram, as we need the exact same) + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self.widget.Extract.setChecked(self._object.ExtractFrames) + + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + self.widget.Extract.toggled.connect(self.extractionChanged) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(bool) + def extractionChanged(self, extract): + self._object.ExtractFrames = extract + self._post_dialog._recompute() + +class EditIndexAppWidget(QtGui.QWidget): + + def __init__(self, obj, post_dialog): + super().__init__() + + self._object = obj + self._post_dialog = post_dialog + + # load the ui and set it up (we reuse histogram, as we need the exact same) + self.widget = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui" + ) + layout = QtGui.QVBoxLayout() + layout.addWidget(self.widget) + self.setLayout(layout) + + self.__init_widget() + + def __init_widget(self): + # set the other properties + + self.widget.Index.setValue(self._object.Index) + self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field) + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + + self.widget.Index.valueChanged.connect(self.indexChanged) + self.widget.Field.activated.connect(self.fieldChanged) + self.widget.Component.activated.connect(self.componentChanged) + + # sometimes wierd sizes occur with spinboxes + self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) + + @QtCore.Slot(int) + def fieldChanged(self, index): + self._object.XField = index + self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component) + self._post_dialog._recompute() + + @QtCore.Slot(int) + def componentChanged(self, index): + self._object.XComponent = index + self._post_dialog._recompute() + + @QtCore.Slot(int) + def indexChanged(self, value): + self._object.Index = value + self._post_dialog._recompute() + + +class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): + """ + A View Provider for extraction of 1D field data specialy for tables + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def _get_properties(self): + + prop = [ + _GuiPropHelper( + type="App::PropertyString", + name="Name", + group="Table", + doc="The name used in the table header. Default name is used if empty", + value="", + ), + ] + return super()._get_properties() + prop + + def attach(self, vobj): + self.Object = vobj.Object + self.ViewObject = vobj + + def getIcon(self): + return ":/icons/FEM_PostField.svg" + + def get_app_edit_widget(self, post_dialog): + return EditFieldAppWidget(self.Object, post_dialog) + + def get_view_edit_widget(self, post_dialog): + return EditViewWidget(self.Object, post_dialog) + + def get_preview(self): + name = "----" + if self.ViewObject.Name: + name = self.ViewObject.Name + return (QtGui.QPixmap(), name) + + +class VPPostTableIndexOverFrames(VPPostTableFieldData): + """ + A View Provider for extraction of 1D index over frames data + """ + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_PostIndex.svg" + + def get_app_edit_widget(self, post_dialog): + return EditIndexAppWidget(self.Object, post_dialog) + + +class VPPostTable(view_base_fempostvisualization.VPPostVisualization): + """ + A View Provider for Table plots + """ + + def __init__(self, vobj): + super().__init__(vobj) + + + def getIcon(self): + return ":/icons/FEM_PostSpreadsheet.svg" + + + def setEdit(self, vobj, mode): + + # build up the task panel + taskd = task_post_table._TaskPanel(vobj) + + #show it + FreeCADGui.Control.showDialog(taskd) + + return True + + + def show_visualization(self): + + if not hasattr(self, "_tableview") or not self._tableview: + self._tableModel = vtv.VtkTableModel() + self._tableview = vtv.VtkTableView(self._tableModel) + self.update_visualization() + + self._tableview.show() + + + def update_visualization(self): + + if not hasattr(self, "_tableModel") or not self._tableModel: + return + + # we collect the header names from the viewproviders + table = self.Object.Table + header = {} + for child in self.Object.Group: + + if not child.Source: + continue + + new_name = child.ViewObject.Name + if new_name: + # this child uses a custom name. We try to find all + # columns that are from this child and use custom header for it + for i in range(table.GetNumberOfColumns()): + if child.Source.Name in table.GetColumnName(i): + header[table.GetColumnName(i)] = new_name + + self._tableModel.setTable(self.Object.Table, header) + + From a5ac5571b7c9d146db1dbbdadcf4bedc2b81d465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 11:22:31 +0200 Subject: [PATCH 008/126] FEM: Add extraction task panel to data plot filters --- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 9 +++++++++ src/Mod/Fem/femobjects/base_fempostvisualizations.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 4cbacb5cad..4ea5f3e36a 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -31,6 +31,7 @@ #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" #include "ViewProviderFemPostFilterPy.h" +#include "TaskPostExtraction.h" using namespace FemGui; @@ -89,6 +90,10 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); auto panel = new TaskPostDataAlongLine(this); dlg->addTaskBox(panel->getIcon(), panel); + + // and the extraction + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); } @@ -138,6 +143,10 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) assert(dlg->getView() == this); auto panel = new TaskPostDataAtPoint(this); dlg->addTaskBox(panel->getIcon(), panel); + + // and the extraction + auto extr_panel = new TaskPostExtraction(this); + dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); } diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index b16f41451f..396694a652 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -143,7 +143,7 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): # it would theoretical be possible that child source was set # to none without recompute, and the visualization was manually # recomputed afterwards - if not child.Source and (c_table.GetNumberOfColumns() > 0): + if not child.Source and (child.Table.GetNumberOfColumns() > 0): FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored") continue From 0fb7c3cc5c50f65266bc51714deec43e320ae5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 12:41:37 +0200 Subject: [PATCH 009/126] FEM: Post data visualization bug fixes and quality of life updates --- src/Mod/Fem/femguiutils/extract_link_view.py | 103 +++++++++++++----- .../Fem/femtaskpanels/base_fempostpanel.py | 8 -- .../view_base_fempostextractors.py | 10 ++ .../view_base_fempostvisualization.py | 10 ++ .../femviewprovider/view_post_histogram.py | 15 ++- .../Fem/femviewprovider/view_post_lineplot.py | 22 +++- .../Fem/femviewprovider/view_post_table.py | 8 ++ 7 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index acd409765c..619ea358f1 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -245,9 +245,6 @@ class _SummaryWidget(QtGui.QWidget): # build the UI - self.stButton = self._button(st_object.Label) - self.stButton.setIcon(st_object.ViewObject.Icon) - self.extrButton = self._button(extr_label) self.extrButton.setIcon(extractor.ViewObject.Icon) @@ -260,6 +257,19 @@ class _SummaryWidget(QtGui.QWidget): else: self.viewButton.setIconSize(QtCore.QSize(0,0)) + if st_object: + self.stButton = self._button(st_object.Label) + self.stButton.setIcon(st_object.ViewObject.Icon) + + else: + # that happens if the source of the extractor was deleted and now + # that property is set to None + self.extrButton.hide() + self.viewButton.hide() + + self.warning = QtGui.QLabel(self) + self.warning.full_text = f"{extractor.Label}: Data source not available" + self.rmButton = QtGui.QToolButton(self) self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) self.rmButton.setAutoRaise(True) @@ -270,13 +280,15 @@ class _SummaryWidget(QtGui.QWidget): policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) self.setSizePolicy(policy) - self.setMinimumSize(self.stButton.sizeHint()+self.frame.sizeHint()*3) + self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) # connect actions. We add functions to widget, as well as the data we need, # and use those as callback. This way every widget knows which objects to use - self.stButton.clicked.connect(self.showVisualization) - self.extrButton.clicked.connect(self.editApp) - self.viewButton.clicked.connect(self.editView) + if st_object: + self.stButton.clicked.connect(self.showVisualization) + self.extrButton.clicked.connect(self.editApp) + self.viewButton.clicked.connect(self.editView) + self.rmButton.clicked.connect(self.deleteTriggered) # make sure initial drawing happened @@ -300,37 +312,47 @@ class _SummaryWidget(QtGui.QWidget): btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() fm = self.fontMetrics() - min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 - pos = 0 - btns = [self.stButton, self.extrButton, self.viewButton] - btn_rel_size = [0.4, 0.4, 0.2] - btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] - for i, btn in enumerate(btns): + if self._st_object: - btn_size = btn_total_size*btn_rel_size[i] - txt_size = btn_size - btn.iconSize().width() - btn_margin + min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 - # we elide only if there is enough space for a meaningful text - if txt_size >= min_text_width: + pos = 0 + btns = [self.stButton, self.extrButton, self.viewButton] + btn_rel_size = [0.4, 0.4, 0.2] + btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] + for i, btn in enumerate(btns): - text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) - btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px"); - else: - btn.setText("") - btn.setStyleSheet("text-align:center;"); + btn_size = btn_total_size*btn_rel_size[i] + txt_size = btn_size - btn.iconSize().width() - btn_margin - rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) - btn.setGeometry(rect) - pos+=btn_size + # we elide only if there is enough space for a meaningful text + if txt_size >= min_text_width: - rmsize = self.stButton.height() + text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) + btn.setText(text) + btn.setStyleSheet("text-align:left;padding:6px"); + else: + btn.setText("") + btn.setStyleSheet("text-align:center;"); + + rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) + btn.setGeometry(rect) + pos+=btn_size + + else: + warning_txt = fm.elidedText(self.warning.full_text, QtGui.Qt.ElideRight, btn_total_size) + self.warning.setText(warning_txt) + rect = QtCore.QRect(0,0, btn_total_size, self.extrButton.sizeHint().height()) + self.warning.setGeometry(rect) + + + rmsize = self.extrButton.sizeHint().height() pos = self.size().width() - rmsize self.rmButton.setGeometry(pos, 0, rmsize, rmsize) frame_hint = self.frame.sizeHint() - rect = QtCore.QRect(0, self.stButton.height()+frame_hint.height(), self.size().width(), frame_hint.height()) + rect = QtCore.QRect(0, self.extrButton.sizeHint().height()+frame_hint.height(), self.size().width(), frame_hint.height()) self.frame.setGeometry(rect) def resizeEvent(self, event): @@ -563,6 +585,7 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"visualization = {vis_data.module}.{vis_data.factory}(FreeCAD.ActiveDocument)" ) + analysis = self._find_parent_analysis(self._object) if analysis: FreeCADGui.doCommand( @@ -576,10 +599,18 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" ) + # default values: color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = visualization.ViewObject.Proxy.get_next_default_color()" + ) + FreeCADGui.doCommand( f"visualization.addObject(extraction)" ) + self._post_dialog._recompute() self.repopulate() @@ -596,6 +627,14 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}" ) + + # default values: color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = (Gui.ActiveDocument.{vis_obj.Name}.Proxy.get_next_default_color())" + ) + FreeCADGui.doCommand( f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)" ) @@ -616,6 +655,14 @@ class ExtractLinkView(QtGui.QWidget): FreeCADGui.doCommand( f"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}" ) + + # default values for color + color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() + if color_prop: + FreeCADGui.doCommand( + f"extraction.ViewObject.{color_prop} = Gui.ActiveDocument.{self._object.Name}.Proxy.get_next_default_color()" + ) + FreeCADGui.doCommand( f"App.ActiveDocument.{self._object.Name}.addObject(extraction)" ) diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 3e26ac1ce5..a9edb902ec 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -60,14 +60,6 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): if button == QtGui.QDialogButtonBox.Apply: self.obj.Document.recompute() - def accept(self): - print("accept") - return super().accept() - - def reject(self): - print("reject") - return super().reject() - # Helper functions # ################ diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index 46313ba890..143cd8fba5 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -118,6 +118,16 @@ class VPPostExtractor: # To be implemented by subclasses: # ################################ + def get_default_color_property(self): + # Returns the property name to set the default color to. + # Return None if no such property + raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_default_field_properties(self): + # Returns the property name to which the default field name should be set + # ret: [FieldProperty, ComponentProperty] + raise FreeCAD.Base.FreeCADError("Not implemented") + def get_kw_args(self): # Returns the matplotlib plot keyword arguments that represent the # properties of the object. diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 20714b67c5..b3d244b1ef 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -32,6 +32,7 @@ __url__ = "https://www.freecad.org" from PySide import QtGui, QtCore import Plot +import FreeCAD import FreeCADGui from . import view_base_femobject @@ -68,6 +69,8 @@ class VPPostVisualization: # Mark ourself as visible in the tree return True + def getDisplayModes(self, obj): + return ["Dialog"] def doubleClicked(self,vobj): @@ -122,3 +125,10 @@ class VPPostVisualization: def show_visualization(self): # Shows the visualization without going into edit mode raise FreeCAD.Base.FreeCADError("Not implemented") + + def get_next_default_color(self): + # Returns the next default color a new object should use + # Returns color in FreeCAD proeprty notation (r,g,b,a) + # If the relevant extractors do not have color properties, this + # can stay unimplemented + raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 2a6d817e70..69aed4101f 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -289,7 +289,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="LineColor", group="HistogramLine", doc="The color the data bin area is drawn with", - value=(0, 85, 255, 255), + value=(0, 0, 0, 1), # black ), _GuiPropHelper( type="App::PropertyFloatConstraint", @@ -355,6 +355,9 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): return kwargs + def get_default_color_property(self): + return "BarColor" + class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData): """ @@ -477,8 +480,10 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: - main = Plot.getMainWindow() + main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setParent(main) + self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square self.update_visualization() @@ -565,3 +570,9 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.update() + def get_next_default_color(self): + # we use the next color in order. We do not check (yet) if this + # color is already taken + i = len(self.Object.Group) + cmap = mpl.pyplot.get_cmap("tab10") + return cmap(i) diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 29c0165275..dfd7f2b5b4 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -354,6 +354,9 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): kwargs["markersize"] = self.ViewObject.MarkerSize return kwargs + def get_default_color_property(self): + return "Color" + class VPPostLineplotIndexOverFrames(VPPostLineplotFieldData): """ @@ -387,7 +390,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="Grid", group="Lineplot", doc="If be the bars shoud show the cumulative sum left to rigth", - value=False, + value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", @@ -455,8 +458,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: - main = Plot.getMainWindow() + main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setParent(main) + self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio self.update_visualization() @@ -481,6 +486,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): # we do not iterate the table, but iterate the children. This makes it possible # to attribute the correct styles + plotted = False for child in self.Object.Group: table = child.Table @@ -492,6 +498,8 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): for i in range(0,table.GetNumberOfColumns(),2): + plotted = True + # add the kw args, with some slide change over color for multiple frames tmp_args = {} for key in kwargs: @@ -534,10 +542,16 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if self.ViewObject.YLabel: self._plot.axes.set_ylabel(self.ViewObject.YLabel) - if self.ViewObject.Legend and self.Object.Group: + if self.ViewObject.Legend and plotted: self._plot.axes.legend(loc = self.ViewObject.LegendLocation) self._plot.axes.grid(self.ViewObject.Grid) - self._plot.update() + def get_next_default_color(self): + # we use the next color in order. We do not check (yet) if this + # color is already taken + i = len(self.Object.Group) + cmap = mpl.pyplot.get_cmap("tab10") + return cmap(i) + diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 443667e31a..7b07cc785f 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -211,6 +211,9 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): name = self.ViewObject.Name return (QtGui.QPixmap(), name) + def get_default_color_property(self): + return None + class VPPostTableIndexOverFrames(VPPostTableFieldData): """ @@ -254,8 +257,13 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): def show_visualization(self): if not hasattr(self, "_tableview") or not self._tableview: + main = FreeCADGui.getMainWindow() self._tableModel = vtv.VtkTableModel() self._tableview = vtv.VtkTableView(self._tableModel) + self._tableview.setParent(main) + self._tableview.setWindowFlags(QtGui.Qt.Dialog) + self._tableview.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self.update_visualization() self._tableview.show() From 005d0aa8542189f26a7d3b41f17a2096af4dc353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 18:23:32 +0200 Subject: [PATCH 010/126] FEM: Allow export of post processing data tables to CSV files or to cliboard to paste into spreadsheet programs --- src/Mod/Fem/femguiutils/vtk_table_view.py | 86 ++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index 95ebf1007e..912aa2aba4 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -32,6 +32,11 @@ __url__ = "https://www.freecad.org" from PySide import QtGui from PySide import QtCore +import FreeCAD +import FreeCADGui + +from vtkmodules.vtkIOCore import vtkDelimitedTextWriter + class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns # One can supply a header_names dict to replace the table column names @@ -91,6 +96,9 @@ class VtkTableModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return section + def getTable(self): + return self._table + class VtkTableSummaryModel(QtCore.QAbstractTableModel): # Simple model showing a summary of the table. # Only supports single component columns @@ -132,6 +140,9 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return self._table.GetColumnName(section) + def getTable(self): + return self._table + class VtkTableView(QtGui.QWidget): @@ -139,16 +150,87 @@ class VtkTableView(QtGui.QWidget): super().__init__() self.model = model + + layout = QtGui.QVBoxLayout() + layout.setContentsMargins(0,0,0,0) + layout.setSpacing(0) + + # start with the toolbar + self.toolbar = QtGui.QToolBar() + csv_action = QtGui.QAction(self) + csv_action.triggered.connect(self.exportCsv) + csv_action.setIcon(FreeCADGui.getIcon("Std_Export")) + csv_action.setToolTip("Export to CSV") + self.toolbar.addAction(csv_action) + + copy_action = QtGui.QAction(self) + copy_action.triggered.connect(self.copyToClipboard) + copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) + shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) + copy_action.setToolTip(f"Copy to clipboard ({shortcut.toString()})") + copy_action.setShortcut(shortcut) + self.toolbar.addAction(copy_action) + + layout.addWidget(self.toolbar) + + # now the table view self.table_view = QtGui.QTableView() self.table_view.setModel(model) + self.model.modelReset.connect(self.modelReset) # 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) + @QtCore.Slot() + def modelReset(self): + # The model is reset, make sure the header visibility is working + # This is needed in case new data was added + self.table_view.resizeColumnsToContents() + + @QtCore.Slot(bool) + def exportCsv(self, state): + + file_path, filter = QtGui.QFileDialog.getSaveFileName(None, "Save as csv file", "", "CSV (*.csv)") + if not file_path: + FreeCAD.Console.PrintMessage("CSV file export aborted: no filename selected") + return + + writer = vtkDelimitedTextWriter() + writer.SetFileName(file_path) + writer.SetInputData(self.model.getTable()); + writer.Write(); + + @QtCore.Slot() + def copyToClipboard(self): + + sel_model = self.table_view.selectionModel() + selection = sel_model.selectedIndexes() + + if len(selection) < 1: + return + + copy_table = "" + previous = selection.pop(0) + for current in selection: + + data = self.model.data(previous, QtCore.Qt.DisplayRole); + copy_table += str(data) + + if current.row() != previous.row(): + copy_table += '\n' + else: + copy_table += '\t' + + previous = current + + copy_table += str(self.model.data(selection[-1], QtCore.Qt.DisplayRole)) + copy_table += '\n' + + clipboard = QtGui.QApplication.instance().clipboard() + clipboard.setText(copy_table) + From 3c22e30cd2b082dca79274cf1c39e8199a6b2afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 21 Apr 2025 19:29:22 +0200 Subject: [PATCH 011/126] FEM: Usability and UI improvements for data extraction Update icons for post data extraction Improve translatability of post data extraction Fix post data extraction commit handling --- src/Mod/Fem/Gui/Resources/Fem.qrc | 1 - .../Fem/Gui/Resources/icons/FEM_PostField.svg | 94 ++++++++++++++----- .../Gui/Resources/icons/FEM_PostHistogram.svg | 45 +++++++-- .../Fem/Gui/Resources/icons/FEM_PostIndex.svg | 20 ++-- .../Gui/Resources/icons/FEM_PostLineplot.svg | 21 +++-- .../Gui/Resources/icons/FEM_PostPlotline.svg | 41 -------- .../Resources/icons/FEM_PostSpreadsheet.svg | 38 ++++++-- src/Mod/Fem/femguiutils/extract_link_view.py | 24 ++--- src/Mod/Fem/femguiutils/post_visualization.py | 5 +- src/Mod/Fem/femguiutils/vtk_table_view.py | 10 +- .../Fem/femobjects/base_fempostextractors.py | 14 +-- src/Mod/Fem/femobjects/post_extract1D.py | 7 +- src/Mod/Fem/femobjects/post_extract2D.py | 7 +- .../Fem/femtaskpanels/base_fempostpanel.py | 8 ++ .../Fem/femtaskpanels/task_post_histogram.py | 10 +- .../Fem/femtaskpanels/task_post_lineplot.py | 10 +- src/Mod/Fem/femtaskpanels/task_post_table.py | 14 +-- .../femviewprovider/view_post_histogram.py | 36 +++---- .../Fem/femviewprovider/view_post_lineplot.py | 30 +++--- .../Fem/femviewprovider/view_post_table.py | 3 +- 20 files changed, 260 insertions(+), 178 deletions(-) delete mode 100644 src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index 8777f6b4dc..351dad3e48 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -87,7 +87,6 @@ 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 diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg index 5a42219430..a93343fd25 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostField.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostField.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -25,36 +25,78 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:zoom="34.537747" - inkscape:cx="8.8743484" - inkscape:cy="9.6850556" + inkscape:cx="8.8598715" + inkscape:cy="9.6561018" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> - - + + + + + + - - + cx="4.0927677" + cy="12.27616" + rx="2.7138755" + ry="2.7138758" /> + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg index 4e6d52d4a1..333e138d83 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostHistogram.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostHistogram.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,17 +24,46 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="97.6875" - inkscape:cx="8" - inkscape:cy="8" + inkscape:zoom="48.84375" + inkscape:cx="9.4996801" + inkscape:cy="6.1932182" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> - + + + + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg index 36c93c04ba..9198dcdba0 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostIndex.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostIndex.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -25,18 +25,18 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:zoom="34.537747" - inkscape:cx="8.8453946" + inkscape:cx="7.7885798" inkscape:cy="9.6561018" - inkscape:window-width="3132" - inkscape:window-height="1772" + inkscape:window-width="1960" + inkscape:window-height="1308" inkscape:window-x="0" inkscape:window-y="0" - inkscape:window-maximized="1" + inkscape:window-maximized="0" inkscape:current-layer="svg1" /> + style="fill:#e5007e;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none" + id="path1" + cx="7.9564848" + cy="7.9564848" + r="4.2860532" /> diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg index 637dac60be..6e90515778 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostLineplot.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostLineplot.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,9 +24,9 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="97.6875" - inkscape:cx="8" - inkscape:cy="8" + inkscape:zoom="45.254834" + inkscape:cx="1.6793786" + inkscape:cy="9.534893" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" @@ -34,8 +34,13 @@ inkscape:window-maximized="1" inkscape:current-layer="svg1" /> + id="rect1" + style="fill:#e5007e;fill-opacity:1;stroke:#260013;stroke-width:0.4;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" + d="M 0.5234375,0.5390625 V 14.324219 15.548828 H 2.0332031 15.248047 V 14.324219 H 2.0332031 V 0.5390625 Z" + sodipodi:nodetypes="ccccccccc" /> + diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg deleted file mode 100644 index a788318bac..0000000000 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostPlotline.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg index 6220e8e87f..b8453c0756 100644 --- a/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostSpreadsheet.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="FEM_PostSpreadsheet.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.1 (93de688d07, 2025-03-30)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,17 +24,41 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="97.6875" - inkscape:cx="8" - inkscape:cy="8" + inkscape:zoom="48.84375" + inkscape:cx="2.9277031" + inkscape:cy="8.4248241" inkscape:window-width="3132" inkscape:window-height="1772" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> + + style="fill:#e5007e;stroke:#260013;stroke-width:0.694259;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none" + d="M 1.013422,5.2704734 H 15.11965" + id="path2" /> + + + + diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 619ea358f1..ba0f151375 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -39,6 +39,8 @@ import FreeCADGui from . import post_visualization as pv +translate = FreeCAD.Qt.translate + # a model showing available visualizations and possible extractions # ################################################################# @@ -50,14 +52,14 @@ def build_new_visualization_tree_model(): 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 = QtGui.QStandardItem(vis_icon, translate("FEM", "New {}").format(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 = QtGui.QStandardItem(icon, translate("FEM", "with {}").format(name)) ext_item.setData(ext) vis_item.appendRow(ext_item) model.appendRow(vis_item) @@ -89,7 +91,7 @@ def build_add_to_visualization_tree_model(): 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 = QtGui.QStandardItem(icon, translate("FEM", "Add {}").format(name)) ext_item.setData(ext) vis_item.appendRow(ext_item) @@ -101,7 +103,7 @@ def build_add_to_visualization_tree_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 = QtGui.QStandardItem(post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label)) post_item.setFlags(QtGui.Qt.ItemIsEnabled) post_item.setData(post_object) @@ -109,7 +111,7 @@ def build_post_object_item(post_object, extractions, vis_type): 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 = QtGui.QStandardItem(icon, translate("FEM", "add {}").format(name)) ext_item.setData(ext) post_item.appendRow(ext_item) @@ -268,7 +270,7 @@ class _SummaryWidget(QtGui.QWidget): self.viewButton.hide() self.warning = QtGui.QLabel(self) - self.warning.full_text = f"{extractor.Label}: Data source not available" + self.warning.full_text = translate("FEM", "{}: Data source not available").format(extractor.Label) self.rmButton = QtGui.QToolButton(self) self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) @@ -462,9 +464,9 @@ class ExtractLinkView(QtGui.QWidget): self._scroll_view.setWidgetResizable(True) hbox = QtGui.QHBoxLayout() - label = QtGui.QLabel("Data used in:") + label = QtGui.QLabel(translate("FEM", "Data used in:")) if not self._is_source: - label.setText("Data used from:") + label.setText(translate("FEM", "Data used from:")) label.setAlignment(QtGui.Qt.AlignBottom) hbox.addWidget(label) @@ -473,19 +475,19 @@ class ExtractLinkView(QtGui.QWidget): if self._is_source: self._add = _TreeChoiceButton(build_add_to_visualization_tree_model()) - self._add.setText("Add data to") + self._add.setText(translate("FEM", "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.setText(translate("FEM", "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.setText(translate("FEM", "Add data from")) self._add.selection.connect(self.addExtractionToPostObject) hbox.addWidget(self._add) diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index d1bfc93898..ab895a9a8c 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -95,7 +95,8 @@ class _VisualizationGroupCommand: return 0 def GetResources(self): - return { 'MenuText': 'Data Visualizations', 'ToolTip': 'Different visualizations to show post processing data in'} + return { 'MenuText': QtCore.QT_TRANSLATE_NOOP("FEM", 'Data Visualizations'), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("FEM", 'Different visualizations to show post processing data in')} def IsActive(self): if not FreeCAD.ActiveDocument: @@ -117,7 +118,7 @@ class _VisualizationCommand: return { "Pixmap": vis.icon, - "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, f"{self._visualization_type}"), + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), "Accel": "", "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), "CmdType": "AlterDoc" diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index 912aa2aba4..c1263150ac 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -37,6 +37,8 @@ import FreeCADGui from vtkmodules.vtkIOCore import vtkDelimitedTextWriter +translate = FreeCAD.Qt.translate + class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns # One can supply a header_names dict to replace the table column names @@ -160,14 +162,14 @@ class VtkTableView(QtGui.QWidget): csv_action = QtGui.QAction(self) csv_action.triggered.connect(self.exportCsv) csv_action.setIcon(FreeCADGui.getIcon("Std_Export")) - csv_action.setToolTip("Export to CSV") + csv_action.setToolTip(translate("FEM", "Export to CSV")) self.toolbar.addAction(csv_action) copy_action = QtGui.QAction(self) copy_action.triggered.connect(self.copyToClipboard) copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) - copy_action.setToolTip(f"Copy to clipboard ({shortcut.toString()})") + copy_action.setToolTip(translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString()))) copy_action.setShortcut(shortcut) self.toolbar.addAction(copy_action) @@ -195,9 +197,9 @@ class VtkTableView(QtGui.QWidget): @QtCore.Slot(bool) def exportCsv(self, state): - file_path, filter = QtGui.QFileDialog.getSaveFileName(None, "Save as csv file", "", "CSV (*.csv)") + file_path, filter = QtGui.QFileDialog.getSaveFileName(None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)") if not file_path: - FreeCAD.Console.PrintMessage("CSV file export aborted: no filename selected") + FreeCAD.Console.PrintMessage(translate("FEM", "CSV file export aborted: no filename selected")) return writer = vtkDelimitedTextWriter() diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 9e4ddac24f..e42d2adf1b 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -33,6 +33,8 @@ from vtkmodules.vtkCommonCore import vtkIntArray from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable +from PySide.QtCore import QT_TRANSLATE_NOOP + from . import base_fempythonobject _PropHelper = base_fempythonobject._PropHelper @@ -78,14 +80,14 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): type="Fem::PropertyPostDataObject", name="Table", group="Base", - doc="The data table that stores the extracted data", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "The data source from which the data is extracted"), value=None, ), ] @@ -140,14 +142,14 @@ class Extractor1D(Extractor): type="App::PropertyEnumeration", name="XField", group="X Data", - doc="The field to use as X data", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "Which part of the X field vector to use for the X axis"), value=[], ), ] @@ -278,14 +280,14 @@ class Extractor2D(Extractor1D): type="App::PropertyEnumeration", name="YField", group="Y Data", - doc="The field to use as Y data", + doc=QT_TRANSLATE_NOOP("FEM", "The field to use as Y data"), value=[], ), _PropHelper( type="App::PropertyEnumeration", name="YComponent", group="Y Data", - doc="Which part of the Y field vector to use for the Y axis", + doc=QT_TRANSLATE_NOOP("FEM", "Which part of the Y field vector to use for the Y axis"), value=[], ), ] diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 425a594fae..987dfdabdd 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -39,6 +39,9 @@ from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline +from PySide.QtCore import QT_TRANSLATE_NOOP + + class PostFieldData1D(base_fempostextractors.Extractor1D): """ A post processing extraction of one dimensional field data @@ -54,7 +57,7 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc="Specify if the field shall be extracted for every available frame", + doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), value=False, ), ] @@ -126,7 +129,7 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): type="App::PropertyInteger", name="Index", group="X Data", - doc="Specify for which index the data should be extracted", + doc=QT_TRANSLATE_NOOP("FEM", "Specify for which index the data should be extracted"), value=0, ), ] diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 0b9e5c528e..baa5f3d8c2 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -39,6 +39,9 @@ from vtkmodules.vtkCommonCore import vtkDoubleArray from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkCommonExecutionModel import vtkStreamingDemandDrivenPipeline +from PySide.QtCore import QT_TRANSLATE_NOOP + + class PostFieldData2D(base_fempostextractors.Extractor2D): """ A post processing extraction of two dimensional field data @@ -54,7 +57,7 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc="Specify if the field shall be extracted for every available frame", + doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), value=False, ), ] @@ -142,7 +145,7 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): type="App::PropertyInteger", name="Index", group="Data", - doc="Specify for which point index the data should be extracted", + doc=QT_TRANSLATE_NOOP("FEM", "Specify for which point index the data should be extracted"), value=0, ), ] diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index a9edb902ec..08c067de7f 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -37,6 +37,8 @@ import FreeCADGui from femguiutils import selection_widgets from . import base_femtaskpanel +translate = FreeCAD.Qt.translate + class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): """ @@ -60,6 +62,12 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): if button == QtGui.QDialogButtonBox.Apply: self.obj.Document.recompute() + def open(self): + # open a new transaction if non is open + if not FreeCAD.getActiveTransaction(): + FreeCAD.ActiveDocument.openTransaction(translate("FEM", "Edit {}").format(self.obj.Label)) + + # Helper functions # ################ diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py index 79b4e4eeab..496fc1792a 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_histogram.py +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -38,6 +38,8 @@ from . import base_fempostpanel from femguiutils import extract_link_view as elv from femguiutils import vtk_table_view +translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -50,10 +52,10 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget = QtGui.QWidget() hbox = QtGui.QHBoxLayout() self.data_widget.show_plot = QtGui.QPushButton() - self.data_widget.show_plot.setText("Show plot") + self.data_widget.show_plot.setText(translate("FEM", "Show plot")) hbox.addWidget(self.data_widget.show_plot) self.data_widget.show_table = QtGui.QPushButton() - self.data_widget.show_table.setText("Show data") + self.data_widget.show_table.setText(translate("FEM", "Show data")) hbox.addWidget(self.data_widget.show_table) vbox = QtGui.QVBoxLayout() vbox.addItem(hbox) @@ -63,7 +65,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): vbox.addWidget(extracts) self.data_widget.setLayout(vbox) - self.data_widget.setWindowTitle("Histogram data") + self.data_widget.setWindowTitle(translate("FEM", "Histogram data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) @@ -71,7 +73,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" ) - self.view_widget.setWindowTitle("Histogram view settings") + self.view_widget.setWindowTitle(translate("FEM", "Histogram view settings")) self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) self.__init_widgets() diff --git a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py index 507e6b3cbf..474d84b80b 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -38,6 +38,8 @@ from . import base_fempostpanel from femguiutils import extract_link_view as elv from femguiutils import vtk_table_view +translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -50,10 +52,10 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget = QtGui.QWidget() hbox = QtGui.QHBoxLayout() self.data_widget.show_plot = QtGui.QPushButton() - self.data_widget.show_plot.setText("Show plot") + self.data_widget.show_plot.setText(translate("FEM", "Show plot")) hbox.addWidget(self.data_widget.show_plot) self.data_widget.show_table = QtGui.QPushButton() - self.data_widget.show_table.setText("Show data") + self.data_widget.show_table.setText(translate("FEM", "Show data")) hbox.addWidget(self.data_widget.show_table) vbox = QtGui.QVBoxLayout() vbox.addItem(hbox) @@ -63,7 +65,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): vbox.addWidget(extracts) self.data_widget.setLayout(vbox) - self.data_widget.setWindowTitle("Lineplot data") + self.data_widget.setWindowTitle(translate("FEM", "Lineplot data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) @@ -71,7 +73,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" ) - self.view_widget.setWindowTitle("Lineplot view settings") + self.view_widget.setWindowTitle(translate("FEM", "Lineplot view settings")) self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) self.__init_widgets() diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py index e17f584c01..b75549eeb9 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_table.py +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -38,6 +38,8 @@ from . import base_fempostpanel from femguiutils import extract_link_view as elv from femguiutils import vtk_table_view +translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -49,7 +51,7 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # data widget self.data_widget = QtGui.QWidget() self.data_widget.show_table = QtGui.QPushButton() - self.data_widget.show_table.setText("Show table") + self.data_widget.show_table.setText(translate("FEM", "Show table")) vbox = QtGui.QVBoxLayout() vbox.addWidget(self.data_widget.show_table) @@ -59,17 +61,9 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): vbox.addWidget(extracts) self.data_widget.setLayout(vbox) - self.data_widget.setWindowTitle("Table data") + self.data_widget.setWindowTitle(translate("FEM", "Table data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostSpreadsheet.svg")) - - # histogram parameter widget - #self.view_widget = FreeCADGui.PySideUic.loadUi( - # FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostTable.ui" - #) - #self.view_widget.setWindowTitle("Table view settings") - #self.view_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostTable.svg")) - self.__init_widgets() # form made from param and selection widget diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 69aed4101f..fdbd975300 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -35,6 +35,7 @@ import FreeCADGui import Plot import FemGui from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP import io import numpy as np @@ -48,6 +49,7 @@ from femtaskpanels import task_post_histogram _GuiPropHelper = view_base_fempostvisualization._GuiPropHelper + class EditViewWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -260,49 +262,49 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Legend", group="HistogramPlot", - doc="The name used in the plots legend", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), value=(0, 0, 0, 1), # black ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="LineWidth", group="HistogramLine", - doc="The width of the bar, between 0 and 1 (1 being without gaps)", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), value=['None', '-', '--', '-.', ':'], ), ] @@ -390,70 +392,70 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Cumulative", group="Histogram", - doc="If be the bars shoud show the cumulative sum left to rigth", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "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)", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "The number of bins the data is split into"), value=10, ), _GuiPropHelper( type="App::PropertyString", name="Title", group="Plot", - doc="The histogram plot title", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), value="", ), _GuiPropHelper( type="App::PropertyString", name="XLabel", group="Plot", - doc="The label shown for the histogram X axis", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), value="", ), _GuiPropHelper( type="App::PropertyBool", name="Legend", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", name="LegendLocation", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "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'], ), diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index dfd7f2b5b4..9d2d821e66 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -35,6 +35,8 @@ import FreeCADGui import Plot import FemGui from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + import io import numpy as np @@ -265,43 +267,43 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Legend", group="Lineplot", - doc="The name used in the plots legend", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"), value="", ), _GuiPropHelper( type="App::PropertyColor", name="Color", group="Lineplot", - doc="The color the line and the markers are drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The color the line and the markers are drawn with"), value=(0, 85, 255, 255), ), _GuiPropHelper( type="App::PropertyEnumeration", name="LineStyle", group="Lineplot", - doc="The style the line is drawn in", + doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), value=['-', '--', '-.', ':', 'None'], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="LineWidth", group="Lineplot", - doc="The width the line is drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The width the line is drawn with"), value=(1, 0.1, 99, 0.1), ), _GuiPropHelper( type="App::PropertyEnumeration", name="MarkerStyle", group="Lineplot", - doc="The style the data markers are drawn with", + doc=QT_TRANSLATE_NOOP("FEM", "The style the data markers are drawn with"), value=['None', '*', '+', 's', '.', 'o', 'x'], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="MarkerSize", group="Lineplot", - doc="The size the data markers are drawn in", - value=(10, 0.1, 99, 0.1), + doc=QT_TRANSLATE_NOOP("FEM", "The size the data markers are drawn in"), + value=(5, 0.1, 99, 0.1), ), ] return super()._get_properties() + prop @@ -389,49 +391,49 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Grid", group="Lineplot", - doc="If be the bars shoud show the cumulative sum left to rigth", + doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", name="Scale", group="Lineplot", - doc="The scale the axis are drawn in", + doc=QT_TRANSLATE_NOOP("FEM", "The scale the axis are drawn in"), value=["linear","semi-log x", "semi-log y", "log"], ), _GuiPropHelper( type="App::PropertyString", name="Title", group="Plot", - doc="The histogram plot title", + doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"), value="", ), _GuiPropHelper( type="App::PropertyString", name="XLabel", group="Plot", - doc="The label shown for the histogram X axis", + doc=QT_TRANSLATE_NOOP("FEM", "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", + doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"), value="", ), _GuiPropHelper( type="App::PropertyBool", name="Legend", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"), value=True, ), _GuiPropHelper( type="App::PropertyEnumeration", name="LegendLocation", group="Plot", - doc="Determines if the legend is plotted", + doc=QT_TRANSLATE_NOOP("FEM", "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'], ), diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 7b07cc785f..ac20fd2f01 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -35,6 +35,7 @@ import FreeCADGui import Plot import FemGui from PySide import QtGui, QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP import io import numpy as np @@ -186,7 +187,7 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Name", group="Table", - doc="The name used in the table header. Default name is used if empty", + doc=QT_TRANSLATE_NOOP("FEM", "The name used in the table header. Default name is used if empty"), value="", ), ] From 463c6c91494791e8d8bad4b8b11f08b5f43c137c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Tue, 6 May 2025 16:37:03 +0200 Subject: [PATCH 012/126] FEM: Adopt post extraction code to updated main --- src/Mod/Fem/App/FemPostFilterPyImp.cpp | 2 +- src/Mod/Fem/App/FemPostObjectPyImp.cpp | 4 +-- src/Mod/Fem/App/FemPostPipelinePyImp.cpp | 4 +-- src/Mod/Fem/CMakeLists.txt | 33 +++++++++++----------- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 10 +++---- src/Mod/Fem/Gui/Workbench.cpp | 4 +-- src/Mod/Fem/InitGui.py | 6 ++-- src/Mod/Fem/femcommands/commands.py | 3 +- src/Mod/Fem/femcommands/manager.py | 4 +-- src/Mod/Fem/femobjects/post_glyphfilter.py | 1 - src/Mod/Fem/femobjects/post_histogram.py | 3 ++ src/Mod/Fem/femobjects/post_lineplot.py | 4 +++ src/Mod/Fem/femobjects/post_table.py | 5 +++- 13 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index 097915f78e..2e09c9e3f0 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -189,7 +189,7 @@ PyObject* FemPostFilterPy::getInputScalarFields(PyObject* args) PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args) { -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON // we take no arguments if (!PyArg_ParseTuple(args, "")) { return nullptr; diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index 27a1204bc0..a35e0b569a 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -29,7 +29,7 @@ #include "FemPostObjectPy.h" #include "FemPostObjectPy.cpp" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON #include #include #endif //BUILD_FEM_VTK @@ -61,7 +61,7 @@ PyObject* FemPostObjectPy::writeVTK(PyObject* args) PyObject* FemPostObjectPy::getDataSet(PyObject* args) { -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON // we take no arguments if (!PyArg_ParseTuple(args, "")) { return nullptr; diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index 3154800802..388aed35de 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -34,7 +34,7 @@ #include "FemPostPipelinePy.cpp" // clang-format on -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON #include #endif //BUILD_FEM_VTK @@ -319,7 +319,7 @@ PyObject* FemPostPipelinePy::renameArrays(PyObject* args) PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args) { -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON // we take no arguments if (!PyArg_ParseTuple(args, "")) { return nullptr; diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 3ba5a83c3e..aca1b443f0 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -182,8 +182,6 @@ 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 @@ -216,8 +214,9 @@ SET(FemObjects_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemObjects_SRCS - ${FemObjects_SRCS} + list(APPEND FemObjects_SRCS + femobjects/base_fempostextractors.py + femobjects/base_fempostvisualizations.py femobjects/post_glyphfilter.py femobjects/post_extract1D.py femobjects/post_extract2D.py @@ -633,8 +632,7 @@ SET(FemGuiTaskPanels_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemGuiTaskPanels_SRCS - ${FemGuiTaskPanels_SRCS} + list(APPEND FemGuiTaskPanels_SRCS femtaskpanels/task_post_glyphfilter.py femtaskpanels/task_post_histogram.py femtaskpanels/task_post_lineplot.py @@ -653,13 +651,18 @@ SET(FemGuiUtils_SRCS femguiutils/disambiguate_solid_selection.py 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 ) +if(BUILD_FEM_VTK_PYTHON) + list(APPEND FemGuiUtils_SRCS + femguiutils/vtk_module_handling.py + femguiutils/vtk_table_view.py + femguiutils/data_extraction.py + femguiutils/extract_link_view.py + femguiutils/post_visualization.py + ) +endif(BUILD_FEM_VTK_PYTHON) + SET(FemGuiViewProvider_SRCS femviewprovider/__init__.py femviewprovider/view_base_femconstraint.py @@ -667,8 +670,6 @@ 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_base_fempostextractors.py femviewprovider/view_constant_vacuumpermittivity.py femviewprovider/view_constraint_bodyheatsource.py femviewprovider/view_constraint_centrif.py @@ -701,10 +702,10 @@ SET(FemGuiViewProvider_SRCS ) if(BUILD_FEM_VTK_PYTHON) - SET(FemGuiViewProvider_SRCS - ${FemGuiViewProvider_SRCS} + list(APPEND FemGuiViewProvider_SRCS + femviewprovider/view_base_fempostextractors.py + femviewprovider/view_base_fempostvisualization.py femviewprovider/view_post_glyphfilter.py - femviewprovider/view_post_extract.py femviewprovider/view_post_histogram.py femviewprovider/view_post_lineplot.py femviewprovider/view_post_table.py diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 57f39a70a2..45ace5bb15 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -76,7 +76,7 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } if (m_panel.hasAttr(std::string("widget"))) { @@ -129,7 +129,7 @@ void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } }; @@ -145,7 +145,7 @@ bool TaskPostExtraction::isGuiTaskOnly() } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } return false; @@ -162,7 +162,7 @@ void TaskPostExtraction::apply() } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } } @@ -178,7 +178,7 @@ bool TaskPostExtraction::initiallyCollapsed() } catch (Py::Exception&) { Base::PyException e; // extract the Python error text - e.ReportException(); + e.reportException(); } return false; diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index acd7202acc..44c1186c6f 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -215,7 +215,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterCalculator" << "Separator" << "FEM_PostCreateFunctions" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif ; @@ -371,7 +371,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterCalculator" << "Separator" << "FEM_PostCreateFunctions" -#ifdef BUILD_FEM_VTK_WRAPPER +#ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif ; diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 8ac271d379..35c835f81e 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -81,9 +81,9 @@ class FemWorkbench(Workbench): False if femcommands.commands.__name__ else True # check vtk version to potentially find missmatchs - from femguiutils.vtk_module_handling import vtk_module_handling - - vtk_module_handling() + if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: + from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() def GetClassName(self): # see https://forum.freecad.org/viewtopic.php?f=10&t=43300 diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 50461484a2..8c85fc7270 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -40,7 +40,6 @@ 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 @@ -1294,4 +1293,6 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: import femobjects.post_lineplot import femobjects.post_histogram import femobjects.post_table + + from femguiutils import post_visualization post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index d57764e54b..50fbae0548 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -34,7 +34,6 @@ import FreeCAD from femtools.femutils import expandParentObject from femtools.femutils import is_of_type -from femguiutils.vtk_module_handling import vtk_compatibility_abort if FreeCAD.GuiUp: from PySide import QtCore @@ -380,7 +379,8 @@ class CommandManager: # like add_obj_on_gui_selobj_noset_edit but the selection is kept # and the selobj is expanded in the tree to see the added obj - # check if we should use python filter + # check if we should use python fitler + from femguiutils.vtk_module_handling import vtk_compatibility_abort if vtk_compatibility_abort(True): return diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py index a783835656..51c7d480c6 100644 --- a/src/Mod/Fem/femobjects/post_glyphfilter.py +++ b/src/Mod/Fem/femobjects/post_glyphfilter.py @@ -33,7 +33,6 @@ import FreeCAD # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling - vtk_module_handling() # IMPORTANT: Never import vtk directly. Often vtk is compiled with different QT diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index 0a6277f5fe..fcbb1ce2e7 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -29,6 +29,9 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying histograms +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() from . import base_fempostextractors from . import base_fempostvisualizations diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 486241b367..8d4b725128 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -29,6 +29,10 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying lines +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() + from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract2D diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index 3d7d7be689..0a64dc733d 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -29,11 +29,14 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief Post processing plot displaying tables +# check vtk version to potentially find missmatchs +from femguiutils.vtk_module_handling import vtk_module_handling +vtk_module_handling() + from . import base_fempostextractors from . import base_fempostvisualizations from . import post_extract1D - from femguiutils import post_visualization # register visualization and extractors From 4c642e63c6533cb0aa0e4daffb1ef689f3870053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Thu, 8 May 2025 16:55:07 +0200 Subject: [PATCH 013/126] FEM: Data extraction objects are FEM::FeaturePython This allows them to be drag and droped in an analysis --- src/Mod/Fem/ObjectsFem.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index c21c219dda..1ca11e2566 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -690,7 +690,7 @@ def makePostLineplot(doc, name="Lineplot"): """makePostLineplot(document, [name]): creates a FEM post processing line plot """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_lineplot post_lineplot.PostLineplot(obj) @@ -704,7 +704,7 @@ def makePostLineplotFieldData(doc, name="FieldData2D"): """makePostLineplotFieldData(document, [name]): creates a FEM post processing data extractor for 2D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_lineplot post_lineplot.PostLineplotFieldData(obj) @@ -718,7 +718,7 @@ def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): """makePostLineplotIndexOverFrames(document, [name]): creates a FEM post processing data extractor for 2D index data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_lineplot post_lineplot.PostLineplotIndexOverFrames(obj) @@ -732,7 +732,7 @@ def makePostHistogram(doc, name="Histogram"): """makePostHistogram(document, [name]): creates a FEM post processing histogram plot """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_histogram post_histogram.PostHistogram(obj) @@ -746,7 +746,7 @@ def makePostHistogramFieldData(doc, name="FieldData1D"): """makePostHistogramFieldData(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_histogram post_histogram.PostHistogramFieldData(obj) @@ -760,7 +760,7 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): """makePostHistogramIndexOverFrames(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_histogram post_histogram.PostHistogramIndexOverFrames(obj) @@ -774,7 +774,7 @@ def makePostTable(doc, name="Table"): """makePostTable(document, [name]): creates a FEM post processing histogram plot """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_table post_table.PostTable(obj) @@ -788,7 +788,7 @@ def makePostTableFieldData(doc, name="FieldData1D"): """makePostTableFieldData(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_table post_table.PostTableFieldData(obj) @@ -802,7 +802,7 @@ def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): """makePostTableIndexOverFrames(document, [name]): creates a FEM post processing data extractor for 1D Field data """ - obj = doc.addObject("App::FeaturePython", name) + obj = doc.addObject("Fem::FeaturePython", name) from femobjects import post_table post_table.PostTableIndexOverFrames(obj) From 8dd3e908961654265fcf23e6fea2a77d435ed61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Thu, 8 May 2025 19:30:01 +0200 Subject: [PATCH 014/126] FEM: port DataAlongLine filter to use arc length. This makes it easier for the new data extraction to also plot data over line length. --- src/Mod/Fem/App/FemPostFilter.cpp | 17 +++++++---------- src/Mod/Fem/App/FemPostFilter.h | 2 ++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Mod/Fem/App/FemPostFilter.cpp b/src/Mod/Fem/App/FemPostFilter.cpp index d7d054f083..79da85c5a1 100644 --- a/src/Mod/Fem/App/FemPostFilter.cpp +++ b/src/Mod/Fem/App/FemPostFilter.cpp @@ -382,18 +382,21 @@ FemPostDataAlongLineFilter::FemPostDataAlongLineFilter() m_line->SetPoint2(vec2.x, vec2.y, vec2.z); m_line->SetResolution(Resolution.getValue()); + m_arclength = vtkSmartPointer::New(); + m_arclength->SetInputConnection(m_line->GetOutputPort(0)); + auto passthrough = vtkSmartPointer::New(); m_probe = vtkSmartPointer::New(); m_probe->SetSourceConnection(passthrough->GetOutputPort(0)); - m_probe->SetInputConnection(m_line->GetOutputPort()); - m_probe->SetValidPointMaskArrayName("ValidPointArray"); + m_probe->SetInputConnection(m_arclength->GetOutputPort()); m_probe->SetPassPointArrays(1); m_probe->SetPassCellArrays(1); m_probe->ComputeToleranceOff(); m_probe->SetTolerance(0.01); clip.source = passthrough; + clip.algorithmStorage.push_back(m_arclength); clip.target = m_probe; addFilterPipeline(clip, "DataAlongLine"); @@ -488,12 +491,7 @@ void FemPostDataAlongLineFilter::GetAxisData() return; } - vtkDataArray* tcoords = dset->GetPointData()->GetTCoords("Texture Coordinates"); - - const Base::Vector3d& vec1 = Point1.getValue(); - const Base::Vector3d& vec2 = Point2.getValue(); - const Base::Vector3d diff = vec1 - vec2; - double Len = diff.Length(); + vtkDataArray* alength = dset->GetPointData()->GetArray("arc_length"); for (vtkIdType i = 0; i < dset->GetNumberOfPoints(); ++i) { double value = 0; @@ -517,8 +515,7 @@ void FemPostDataAlongLineFilter::GetAxisData() } values.push_back(value); - double tcoord = tcoords->GetComponent(i, 0); - coords.push_back(tcoord * Len); + coords.push_back(alength->GetTuple1(i)); } YAxisData.setValues(values); diff --git a/src/Mod/Fem/App/FemPostFilter.h b/src/Mod/Fem/App/FemPostFilter.h index 273bd63c0f..38499d01d3 100644 --- a/src/Mod/Fem/App/FemPostFilter.h +++ b/src/Mod/Fem/App/FemPostFilter.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -181,6 +182,7 @@ protected: private: vtkSmartPointer m_line; + vtkSmartPointer m_arclength; vtkSmartPointer m_probe; }; From bb971c1cf73d50213e22516ba372cd70c9e1c769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 09:42:16 +0200 Subject: [PATCH 015/126] FEM: Add data extraction objects to FEM test suite --- src/Mod/Fem/femtest/app/test_object.py | 27 ++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index 93d331ea94..7b34db84e6 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -79,12 +79,16 @@ class TestObjectCreate(unittest.TestCase): # gmsh mesh children: group, region, boundary layer --> 3 # result children: mesh result --> 1 # analysis itself is not in analysis group --> 1 - # vtk post pipeline children: region, scalar, cut, wrap, glyph --> 5 - # vtk python post objects: glyph --> 1 + # vtk post pipeline children: region, scalar, cut, wrap, contour --> 5 + # vtk python post objects: glyph, 6x data extraction --> 7 subtraction = 15 if vtk_objects_used: - subtraction += 6 + subtraction += 12 + if not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__): + # remove the 3 data visualization objects that would be in the Analysis + # if they would be available (Lineplot, histogram, table) + subtraction += 3 self.assertEqual(len(doc.Analysis.Group), count_defmake - subtraction) @@ -92,7 +96,9 @@ class TestObjectCreate(unittest.TestCase): # have been counted, but will not be executed to create objects failed = 0 if vtk_objects_used and not ("BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__): - failed += 1 + # the 7 objects also counted in subtraction, +3 additional objects that are + # added directly to the analysis + failed += 10 self.assertEqual(len(doc.Objects), count_defmake - failed) @@ -1167,6 +1173,19 @@ def create_all_fem_objects_doc(doc): if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: ObjectsFem.makePostFilterGlyph(doc, vres) + # data extraction objects + lp = analysis.addObject(ObjectsFem.makePostLineplot(doc))[0] + lp.addObject(ObjectsFem.makePostLineplotFieldData(doc)) + lp.addObject(ObjectsFem.makePostLineplotIndexOverFrames(doc)) + + hp = analysis.addObject(ObjectsFem.makePostHistogram(doc))[0] + hp.addObject(ObjectsFem.makePostHistogramFieldData(doc)) + hp.addObject(ObjectsFem.makePostHistogramIndexOverFrames(doc)) + + tb = analysis.addObject(ObjectsFem.makePostTable(doc))[0] + tb.addObject(ObjectsFem.makePostTableFieldData(doc)) + tb.addObject(ObjectsFem.makePostTableIndexOverFrames(doc)) + analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc)) analysis.addObject(ObjectsFem.makeSolverCalculiX(doc)) sol = analysis.addObject(ObjectsFem.makeSolverElmer(doc))[0] From 5f4a8f7a492f3411119961992659a2930cb0b5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 10:02:50 +0200 Subject: [PATCH 016/126] FEM: Ensure post task dialogs work without VTK python build --- src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp | 8 +++++++- src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp | 7 +++++++ src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index 4ea5f3e36a..f6c60491b8 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -31,8 +31,10 @@ #include "TaskPostBoxes.h" #include "ViewProviderFemPostFilter.h" #include "ViewProviderFemPostFilterPy.h" -#include "TaskPostExtraction.h" +#ifdef FC_USE_VTK_PYTHON +#include "TaskPostExtraction.h" +#endif using namespace FemGui; @@ -91,9 +93,11 @@ void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) auto panel = new TaskPostDataAlongLine(this); dlg->addTaskBox(panel->getIcon(), panel); +#ifdef FC_USE_VTK_PYTHON // and the extraction auto extr_panel = new TaskPostExtraction(this); dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } @@ -144,9 +148,11 @@ void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) auto panel = new TaskPostDataAtPoint(this); dlg->addTaskBox(panel->getIcon(), panel); +#ifdef FC_USE_VTK_PYTHON // and the extraction auto extr_panel = new TaskPostExtraction(this); dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp index 7caff695eb..5683ce2467 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilterPyImp.cpp @@ -27,7 +27,9 @@ #include #include "ViewProviderFemPostFilter.h" #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON #include "TaskPostExtraction.h" +#endif // inclusion of the generated files (generated out of ViewProviderFemPostFilterPy.xml) #include "ViewProviderFemPostFilterPy.h" #include "ViewProviderFemPostFilterPy.cpp" @@ -63,6 +65,7 @@ PyObject* ViewProviderFemPostFilterPy::createDisplayTaskWidget(PyObject* args) PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args) { +#ifdef FC_USE_VTK_PYTHON // we take no arguments if (!PyArg_ParseTuple(args, "")) { return nullptr; @@ -77,6 +80,10 @@ PyObject* ViewProviderFemPostFilterPy::createExtractionTaskWidget(PyObject* args PyErr_SetString(PyExc_TypeError, "creating the panel failed"); return nullptr; +#else + PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); + Py_Return; +#endif } PyObject* ViewProviderFemPostFilterPy::getCustomAttributes(const char* /*attr*/) const diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 2a34070c72..b3dcbf4c7b 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -67,7 +67,9 @@ #include #include "TaskPostBoxes.h" +#ifdef FC_USE_VTK_PYTHON #include "TaskPostExtraction.h" +#endif #include "ViewProviderAnalysis.h" #include "ViewProviderFemPostObject.h" @@ -1024,8 +1026,10 @@ void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) auto disp_panel = new TaskPostDisplay(this); dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel); +#ifdef FC_USE_VTK_PYTHON auto extr_panel = new TaskPostExtraction(this); dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); +#endif } void ViewProviderFemPostObject::unsetEdit(int ModNum) From 27f1fdabd499a6f53ed6b7d967d9daf5a24a714a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 11:19:49 +0200 Subject: [PATCH 017/126] FEM: Adopt data extraction for VTK <9.3: different table filter Additionally remove unneeded includes in c++ code remaining from earlier experiments --- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 4 ---- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 5 ----- src/Mod/Fem/femguiutils/data_extraction.py | 17 +++++++++++++++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 1a2c244943..804cde7f14 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -71,10 +71,6 @@ #include "ViewProviderFemPostObject.h" #include "ViewProviderFemPostBranchFilter.h" -#include -#include -#include - using namespace FemGui; using namespace Gui; diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 45ace5bb15..4765b14c0e 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -37,11 +37,6 @@ #include #include -#include -#include -#include -#include - #include "ViewProviderFemPostObject.h" #include "TaskPostExtraction.h" diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 23a1bb784b..5b80f64d36 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -33,10 +33,17 @@ from . import vtk_table_view from PySide import QtCore, QtGui +from vtkmodules.vtkCommonCore import vtkVersion from vtkmodules.vtkCommonDataModel import vtkTable -from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents +if vtkVersion.GetVTKMajorVersion() > 9 and \ + vtkVersion.GetVTKMinorVersion() > 3: + from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter +else: + from vtkmodules.vtkInfovisCore import vtkDataObjectToTable + + import FreeCAD import FreeCADGui @@ -117,7 +124,13 @@ class DataExtraction(_BasePostTaskPanel): if not algo: self.data_model.setTable(vtkTable()) - filter = vtkAttributeDataToTableFilter() + if vtkVersion.GetVTKMajorVersion() > 9 and \ + vtkVersion.GetVTKMinorVersion() > 3: + filter = vtkAttributeDataToTableFilter() + else: + filter = vtkDataObjectToTable() + filter.SetFieldType(vtkDataObjectToTable.POINT_DATA) + filter.SetInputConnection(0, algo.GetOutputPort(0)) filter.Update() table = filter.GetOutputDataObject(0) From 820f867bf4654a9fd1e5f4974b6255566778632e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 12:09:47 +0200 Subject: [PATCH 018/126] FEM: Data extraction ui works better with stylesheets --- src/Mod/Fem/femguiutils/extract_link_view.py | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index ba0f151375..64816dda48 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -196,7 +196,7 @@ class _TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) -class _SettingsPopup(QtGui.QGroupBox): +class _SettingsPopup(QtGui.QDialog): close = QtCore.Signal() @@ -211,9 +211,9 @@ class _SettingsPopup(QtGui.QGroupBox): buttonBox = QtGui.QDialogButtonBox() buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + buttonBox.accepted.connect(self.hide) vbox.addWidget(buttonBox) - buttonBox.accepted.connect(self.hide) self.setLayout(vbox) def showEvent(self, event): @@ -301,7 +301,7 @@ class _SummaryWidget(QtGui.QWidget): btn = QtGui.QPushButton(self) btn.full_text = text - btn.setMinimumSize(btn.sizeHint()) + btn.setMinimumWidth(0) btn.setFlat(True) btn.setText(text) btn.setStyleSheet("text-align:left;padding:6px"); @@ -311,7 +311,13 @@ class _SummaryWidget(QtGui.QWidget): def _redraw(self): - btn_total_size = ((self.size() - self.rmButton.size()).width() - 20) #20 is space to rmButton + btn_spacing = 3 + btn_height = self.extrButton.sizeHint().height() + + # total size notes: + # - 5 spacing = 2x between buttons + 3 spacings to remove button + # - remove btn_height as this is used as remove button square size + btn_total_size = self.size().width() - btn_height - 5*btn_spacing btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() fm = self.fontMetrics() @@ -338,23 +344,23 @@ class _SummaryWidget(QtGui.QWidget): btn.setText("") btn.setStyleSheet("text-align:center;"); - rect = QtCore.QRect(pos,0, btn_size, btn.sizeHint().height()) + rect = QtCore.QRect(pos, 0, btn_size, btn_height) btn.setGeometry(rect) - pos+=btn_size + pos += btn_size + btn_spacing else: warning_txt = fm.elidedText(self.warning.full_text, QtGui.Qt.ElideRight, btn_total_size) self.warning.setText(warning_txt) - rect = QtCore.QRect(0,0, btn_total_size, self.extrButton.sizeHint().height()) + rect = QtCore.QRect(0,0, btn_total_size, btn_height) self.warning.setGeometry(rect) - rmsize = self.extrButton.sizeHint().height() + rmsize = btn_height pos = self.size().width() - rmsize self.rmButton.setGeometry(pos, 0, rmsize, rmsize) frame_hint = self.frame.sizeHint() - rect = QtCore.QRect(0, self.extrButton.sizeHint().height()+frame_hint.height(), self.size().width(), frame_hint.height()) + rect = QtCore.QRect(0, btn_height+frame_hint.height(), self.size().width(), frame_hint.height()) self.frame.setGeometry(rect) def resizeEvent(self, event): From 09eeb15e4a1a0bd9d8ccdf59ec65f61c82e026d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 12:48:07 +0200 Subject: [PATCH 019/126] FEM: Ensure tests run without GUI with data extraction code --- src/Mod/Fem/femguiutils/post_visualization.py | 13 ++++++++++--- src/Mod/Fem/femguiutils/vtk_module_handling.py | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index ab895a9a8c..9975db127b 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -29,14 +29,16 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief A registry to collect visualizations for use in menus +# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui +# directly to support cmd line use. + import copy from dataclasses import dataclass -from PySide import QtGui, QtCore +from PySide import QtCore import FreeCAD -import FreeCADGui -import FemGui + # Registry to handle visulization commands # ######################################## @@ -102,6 +104,7 @@ class _VisualizationGroupCommand: if not FreeCAD.ActiveDocument: return False + import FemGui return bool(FemGui.getActiveAnalysis()) @@ -129,9 +132,11 @@ class _VisualizationCommand: if not FreeCAD.ActiveDocument: return False + import FemGui return bool(FemGui.getActiveAnalysis()) def Activated(self): + import FreeCADGui vis = _registry[self._visualization_type] FreeCAD.ActiveDocument.openTransaction(f"Create {vis.name}") @@ -155,6 +160,8 @@ def setup_commands(toplevel_name): # creates all visualization commands and registers them. The # toplevel group command will have the name provided to this function. + import FreeCADGui + # first all visualization and extraction commands for vis in _registry: FreeCADGui.addCommand(_to_command_name(vis), _VisualizationCommand(vis)) diff --git a/src/Mod/Fem/femguiutils/vtk_module_handling.py b/src/Mod/Fem/femguiutils/vtk_module_handling.py index 6c834b0820..9ae5cc2fb9 100644 --- a/src/Mod/Fem/femguiutils/vtk_module_handling.py +++ b/src/Mod/Fem/femguiutils/vtk_module_handling.py @@ -47,6 +47,10 @@ __title__ = "FEM GUI vtk python module check" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" + +# Note: This file is imported from FreeCAD App files. Do not import any FreeCADGui +# directly to support cmd line use. + __user_input_received = False From 2f55e4d2760680ce7b91a4033e1a79e26aca763b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Fri, 9 May 2025 13:49:57 +0200 Subject: [PATCH 020/126] FEM: Fix impact of stylesheet min button widht --- src/Mod/Fem/femguiutils/extract_link_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 64816dda48..db524361ca 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -304,7 +304,7 @@ class _SummaryWidget(QtGui.QWidget): btn.setMinimumWidth(0) btn.setFlat(True) btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px"); + btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); btn.setToolTip(text) return btn @@ -339,10 +339,10 @@ class _SummaryWidget(QtGui.QWidget): text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px"); + btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); else: btn.setText("") - btn.setStyleSheet("text-align:center;"); + btn.setStyleSheet("text-align:center;min-width:20px;"); rect = QtCore.QRect(pos, 0, btn_size, btn_height) btn.setGeometry(rect) From fb6d0b75ac6058d3a241cdcf7ea6945fdcd4d6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 11 May 2025 19:30:44 +0200 Subject: [PATCH 021/126] FEM: Update data extraction dialog titles and spelling errors --- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 2 +- src/Mod/Fem/femguiutils/data_extraction.py | 2 ++ src/Mod/Fem/femviewprovider/view_post_histogram.py | 1 + src/Mod/Fem/femviewprovider/view_post_lineplot.py | 1 + src/Mod/Fem/femviewprovider/view_post_table.py | 1 + 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 4765b14c0e..54027dde38 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -93,7 +93,7 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* } } } - // if we are here somethign went wrong! + // if we are here something went wrong! throw Base::ImportError("Unable to import data extraction widget"); }; diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 5b80f64d36..8cfb845d17 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -92,6 +92,7 @@ class DataExtraction(_BasePostTaskPanel): def showData(self): dialog = QtGui.QDialog(self.widget) + dialog.setWindowTitle(f"Data of {self.Object.Label}") widget = vtk_table_view.VtkTableView(self.data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) @@ -105,6 +106,7 @@ class DataExtraction(_BasePostTaskPanel): def showSummary(self): dialog = QtGui.QDialog(self.widget) + dialog.setWindowTitle(f"Data summary of {self.Object.Label}") widget = vtk_table_view.VtkTableView(self.summary_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index fdbd975300..9b976d62b9 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -484,6 +484,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 9d2d821e66..fb17a2eaaf 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -462,6 +462,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if not hasattr(self, "_plot") or not self._plot: main = FreeCADGui.getMainWindow() self._plot = Plot.Plot() + self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index ac20fd2f01..cf8da7fdab 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -261,6 +261,7 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): main = FreeCADGui.getMainWindow() self._tableModel = vtv.VtkTableModel() self._tableview = vtv.VtkTableView(self._tableModel) + self._tableview.setWindowTitle(self.Object.Label) self._tableview.setParent(main) self._tableview.setWindowFlags(QtGui.Qt.Dialog) self._tableview.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio From b5a92b752ff0ed89904f65a11f8e74a0adf90ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 11 May 2025 20:57:41 +0200 Subject: [PATCH 022/126] FEM: Remove VTK 9.4 only function And make sure filters task dialogs can be used if something in python fails --- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 12 ++++++++---- src/Mod/Fem/femguiutils/data_extraction.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 54027dde38..abce2a30cf 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -59,11 +59,14 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* 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::Module mod(PyImport_ImportModule("femguiutils.data_extraction"), true); + if (mod.isNull()) { + Base::Console().error("Unable to import data extraction widget\n"); + return; + } + Py::Callable method(mod.getAttr(std::string("DataExtraction"))); Py::Tuple args(1); args.setItem(0, Py::Object(view->getPyObject())); @@ -93,8 +96,9 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* } } } + // if we are here something went wrong! - throw Base::ImportError("Unable to import data extraction widget"); + Base::Console().error("Unable to import data extraction widget\n"); }; TaskPostExtraction::~TaskPostExtraction() { diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index 8cfb845d17..cb55295703 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -139,7 +139,7 @@ class DataExtraction(_BasePostTaskPanel): # add the points points = algo.GetOutputDataObject(0).GetPoints().GetData() - table.InsertColumn(points, 0) + table.AddColumn(points) # split the components splitter = vtkSplitColumnComponents() From a7a79d6d909c41f59464369de02a53af5c558201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 14 May 2025 16:57:52 +0200 Subject: [PATCH 023/126] FEM: Adopt data extraction code to ubuntu LTS --- src/Mod/Fem/femguiutils/extract_link_view.py | 197 ++++++++++--------- 1 file changed, 105 insertions(+), 92 deletions(-) diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index db524361ca..568724ceb9 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -60,6 +60,7 @@ def build_new_visualization_tree_model(): icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_name) ext_item = QtGui.QStandardItem(icon, translate("FEM", "with {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) vis_item.appendRow(ext_item) model.appendRow(vis_item) @@ -92,6 +93,7 @@ def build_add_to_visualization_tree_model(): icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) ext_item = QtGui.QStandardItem(icon, translate("FEM", "Add {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) vis_item.appendRow(ext_item) @@ -112,6 +114,7 @@ def build_post_object_item(post_object, extractions, vis_type): icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) ext_item = QtGui.QStandardItem(icon, translate("FEM", "add {}").format(name)) + ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) post_item.appendRow(ext_item) @@ -150,6 +153,54 @@ def build_add_from_data_tree_model(vis_type): # implementation of GUI and its functionality # ########################################### +class _ElideToolButton(QtGui.QToolButton): + # tool button that elides its text, and left align icon and text + + def __init__(self, icon, text, parent): + super().__init__(parent) + + self._text = text + self._icon = icon + + def setCustomText(self, text): + self._text = text + self.repaint() + + def setCustomIcon(self, icon): + self._icon = icon + self.repaint() + + def sizeHint(self): + button_size = super().sizeHint() + return QtCore.QSize(self.iconSize().width()+10, button_size.height()) + + def paintEvent(self, event): + + # draw notmal button, without text and icon + super().paintEvent(event) + + # add icon and elided text + painter = QtGui.QPainter() + painter.begin(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing, True) + painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) + + margin = (self.height() - self.iconSize().height()) / 2 + match type(self._icon): + case QtGui.QPixmap: + painter.drawPixmap(margin, margin, self._icon.scaled(self.iconSize())) + case QtGui.QIcon: + self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + + fm = self.fontMetrics() + text_size = self.width() - self.iconSize().width() - 3*margin + text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, text_size) + if text: + painter.drawText(2*margin+self.iconSize().width(), margin + fm.ascent(), text) + + painter.end() + + class _TreeChoiceButton(QtGui.QToolButton): selection = QtCore.Signal(object,object) @@ -167,10 +218,13 @@ class _TreeChoiceButton(QtGui.QToolButton): 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) + self.tree_view.clicked.connect(self.selectIndex) + + style = self.style() + if not style.styleHint(QtGui.QStyle.SH_ItemView_ActivateItemOnSingleClick): + self.tree_view.activated.connect(self.selectIndex) # set a complex menu self.popup = QtGui.QWidgetAction(self) @@ -180,6 +234,7 @@ class _TreeChoiceButton(QtGui.QToolButton): QtCore.Slot(QtCore.QModelIndex) def selectIndex(self, index): + print("select triggered") item = self.model.itemFromIndex(index) if item and not item.hasChildren(): @@ -214,7 +269,13 @@ class _SettingsPopup(QtGui.QDialog): buttonBox.accepted.connect(self.hide) vbox.addWidget(buttonBox) - self.setLayout(vbox) + widget = QtGui.QFrame() + widget.setLayout(vbox) + + vbox2 = QtGui.QVBoxLayout() + vbox2.setContentsMargins(0,0,0,0) + vbox2.addWidget(widget) + self.setLayout(vbox2) def showEvent(self, event): # required to get keyboard events @@ -246,22 +307,22 @@ class _SummaryWidget(QtGui.QWidget): extr_repr = extractor.ViewObject.Proxy.get_preview() # build the UI + hbox = QtGui.QHBoxLayout() + hbox.setContentsMargins(6,0,6,0) + hbox.setSpacing(5) - self.extrButton = self._button(extr_label) - self.extrButton.setIcon(extractor.ViewObject.Icon) - - self.viewButton = self._button(extr_repr[1]) + self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) + self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) if not extr_repr[0].isNull(): size = self.viewButton.iconSize() size.setWidth(size.width()*2) self.viewButton.setIconSize(size) - self.viewButton.setIcon(extr_repr[0]) else: self.viewButton.setIconSize(QtCore.QSize(0,0)) if st_object: - self.stButton = self._button(st_object.Label) - self.stButton.setIcon(st_object.ViewObject.Icon) + self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) + hbox.addWidget(self.stButton) else: # that happens if the source of the extractor was deleted and now @@ -271,18 +332,30 @@ class _SummaryWidget(QtGui.QWidget): self.warning = QtGui.QLabel(self) self.warning.full_text = translate("FEM", "{}: Data source not available").format(extractor.Label) + hbox.addWidget(self.warning) self.rmButton = QtGui.QToolButton(self) - self.rmButton.setIcon(QtGui.QIcon.fromTheme("delete")) + self.rmButton.setIcon(FreeCADGui.getIcon("delete.svg")) self.rmButton.setAutoRaise(True) + hbox.addWidget(self.extrButton) + hbox.addWidget(self.viewButton) + hbox.addSpacing(15) + hbox.addWidget(self.rmButton) + # add the separation line + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0,0,0,0) + vbox.setSpacing(5) + vbox.addItem(hbox) self.frame = QtGui.QFrame(self) self.frame.setFrameShape(QtGui.QFrame.HLine); + vbox.addWidget(self.frame) - policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.setSizePolicy(policy) - self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + #self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + self.setLayout(vbox) # 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 @@ -294,80 +367,21 @@ class _SummaryWidget(QtGui.QWidget): self.rmButton.clicked.connect(self.deleteTriggered) # make sure initial drawing happened - self._redraw() + #self._redraw() - def _button(self, text): - btn = QtGui.QPushButton(self) - btn.full_text = text + def _button(self, icon, text, stretch=2): + btn = _ElideToolButton(icon, text, self) btn.setMinimumWidth(0) - btn.setFlat(True) - btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); + btn.setAutoRaise(True) btn.setToolTip(text) + policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + policy.setHorizontalStretch(stretch) + btn.setSizePolicy(policy) return btn - def _redraw(self): - - btn_spacing = 3 - btn_height = self.extrButton.sizeHint().height() - - # total size notes: - # - 5 spacing = 2x between buttons + 3 spacings to remove button - # - remove btn_height as this is used as remove button square size - btn_total_size = self.size().width() - btn_height - 5*btn_spacing - btn_margin = (self.rmButton.size() - self.rmButton.iconSize()).width() - fm = self.fontMetrics() - - if self._st_object: - - min_text_width = fm.size(QtGui.Qt.TextSingleLine, "...").width()*2 - - pos = 0 - btns = [self.stButton, self.extrButton, self.viewButton] - btn_rel_size = [0.4, 0.4, 0.2] - btn_elide_mode = [QtGui.Qt.ElideMiddle, QtGui.Qt.ElideMiddle, QtGui.Qt.ElideRight] - for i, btn in enumerate(btns): - - btn_size = btn_total_size*btn_rel_size[i] - txt_size = btn_size - btn.iconSize().width() - btn_margin - - # we elide only if there is enough space for a meaningful text - if txt_size >= min_text_width: - - text = fm.elidedText(btn.full_text, btn_elide_mode[i], txt_size) - btn.setText(text) - btn.setStyleSheet("text-align:left;padding:6px;min-width:20px;"); - else: - btn.setText("") - btn.setStyleSheet("text-align:center;min-width:20px;"); - - rect = QtCore.QRect(pos, 0, btn_size, btn_height) - btn.setGeometry(rect) - pos += btn_size + btn_spacing - - else: - warning_txt = fm.elidedText(self.warning.full_text, QtGui.Qt.ElideRight, btn_total_size) - self.warning.setText(warning_txt) - rect = QtCore.QRect(0,0, btn_total_size, btn_height) - self.warning.setGeometry(rect) - - - rmsize = btn_height - pos = self.size().width() - rmsize - self.rmButton.setGeometry(pos, 0, rmsize, rmsize) - - frame_hint = self.frame.sizeHint() - rect = QtCore.QRect(0, btn_height+frame_hint.height(), self.size().width(), frame_hint.height()) - self.frame.setGeometry(rect) - - def resizeEvent(self, event): - - # calculate the allowed text length - self._redraw() - super().resizeEvent(event) @QtCore.Slot() def showVisualization(self): @@ -434,19 +448,17 @@ class _SummaryWidget(QtGui.QWidget): # update the preview extr_repr = self._extractor.ViewObject.Proxy.get_preview() - self.viewButton.setIcon(extr_repr[0]) - self.viewButton.full_text = extr_repr[1] + self.viewButton.setCustomIcon(extr_repr[0]) + self.viewButton.setCustomText(extr_repr[1]) self.viewButton.setToolTip(extr_repr[1]) - self._redraw() @QtCore.Slot() def appAccept(self): # update the preview extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) - self.extrButton.full_text = extr_label + self.extrButton.setCustomText = extr_label self.extrButton.setToolTip(extr_label) - self._redraw() @@ -468,8 +480,15 @@ class ExtractLinkView(QtGui.QWidget): self._scroll_view = QtGui.QScrollArea(self) self._scroll_view.setHorizontalScrollBarPolicy(QtGui.Qt.ScrollBarAlwaysOff) self._scroll_view.setWidgetResizable(True) + self._scroll_widget = QtGui.QWidget(self._scroll_view) + vbox = QtGui.QVBoxLayout() + vbox.setContentsMargins(0,6,0,0) + vbox.addStretch() + self._scroll_widget.setLayout(vbox) + self._scroll_view.setWidget(self._scroll_widget) hbox = QtGui.QHBoxLayout() + hbox.setSpacing(6) label = QtGui.QLabel(translate("FEM", "Data used in:")) if not self._is_source: label.setText(translate("FEM", "Data used from:")) @@ -554,15 +573,9 @@ class ExtractLinkView(QtGui.QWidget): self._widgets.append(summary) # fill the scroll area - vbox = QtGui.QVBoxLayout() + vbox = self._scroll_widget.layout() for widget in self._widgets: - vbox.addWidget(widget) - - vbox.addStretch() - widget = QtGui.QWidget() - widget.setLayout(vbox) - - self._scroll_view.setWidget(widget) + vbox.insertWidget(0, widget) # also reset the add button model if self._is_source: From 919cc8767405d8cc0eccefe70542a135517f95a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Wed, 14 May 2025 17:44:26 +0200 Subject: [PATCH 024/126] FEM: Data extraction code version conflicts resolved: PySide, mpl, VTK --- .../Resources/ui/PostTableFieldViewEdit.ui | 11 ++- src/Mod/Fem/femguiutils/extract_link_view.py | 72 ++++++++++++------- .../femviewprovider/view_post_histogram.py | 36 ++++++---- .../Fem/femviewprovider/view_post_lineplot.py | 1 + .../Fem/femviewprovider/view_post_table.py | 2 +- 5 files changed, 83 insertions(+), 39 deletions(-) diff --git a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui index ada74b69b4..6b3000248a 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostTableFieldViewEdit.ui @@ -6,7 +6,7 @@ 0 0 - 259 + 279 38 @@ -27,7 +27,14 @@ 0 - + + + + 0 + 0 + + + diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 568724ceb9..37e8e82c62 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -172,7 +172,9 @@ class _ElideToolButton(QtGui.QToolButton): def sizeHint(self): button_size = super().sizeHint() - return QtCore.QSize(self.iconSize().width()+10, button_size.height()) + icn_size = self.iconSize() + min_margin = max((button_size - icn_size).height(), 6) + return QtCore.QSize(self.iconSize().width()+10, icn_size.height() + min_margin) def paintEvent(self, event): @@ -186,17 +188,39 @@ class _ElideToolButton(QtGui.QToolButton): painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) margin = (self.height() - self.iconSize().height()) / 2 - match type(self._icon): - case QtGui.QPixmap: - painter.drawPixmap(margin, margin, self._icon.scaled(self.iconSize())) - case QtGui.QIcon: - self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + icn_width = self.iconSize().width() + if self._icon.isNull(): + icn_width = 0; + fm = self.fontMetrics() - text_size = self.width() - self.iconSize().width() - 3*margin - text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, text_size) - if text: - painter.drawText(2*margin+self.iconSize().width(), margin + fm.ascent(), text) + txt_size = self.width() - icn_width - 2*margin + if not self._icon.isNull(): + # we add the margin between icon and text + txt_size -= margin + + txt_min = fm.boundingRect("...").width() + + # should we center the icon? + xpos = margin + if not self._icon.isNull() and txt_size < txt_min: + # center icon + xpos = self.width()/2 - self.iconSize().width()/2 + + if not self._icon.isNull(): + match type(self._icon): + case QtGui.QPixmap: + painter.drawPixmap(xpos, margin, self._icon.scaled(self.iconSize())) + xpos += self.iconSize().width() + case QtGui.QIcon: + self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + xpos += self.iconSize().width() + + xpos += margin # the margin to the text + + if txt_size >= txt_min: + text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, txt_size) + painter.drawText(xpos, margin + fm.ascent(), text) painter.end() @@ -234,7 +258,6 @@ class _TreeChoiceButton(QtGui.QToolButton): QtCore.Slot(QtCore.QModelIndex) def selectIndex(self, index): - print("select triggered") item = self.model.itemFromIndex(index) if item and not item.hasChildren(): @@ -251,13 +274,14 @@ class _TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) -class _SettingsPopup(QtGui.QDialog): +class _SettingsPopup(QtGui.QMenu): close = QtCore.Signal() def __init__(self, setting, parent): super().__init__(parent) + self._setting = setting self.setWindowFlags(QtGui.Qt.Popup) self.setFocusPolicy(QtGui.Qt.ClickFocus) @@ -277,6 +301,9 @@ class _SettingsPopup(QtGui.QDialog): vbox2.addWidget(widget) self.setLayout(vbox2) + def size(self): + return self._setting.sizeHint() + def showEvent(self, event): # required to get keyboard events self.setFocus() @@ -309,16 +336,15 @@ class _SummaryWidget(QtGui.QWidget): # build the UI hbox = QtGui.QHBoxLayout() hbox.setContentsMargins(6,0,6,0) - hbox.setSpacing(5) + hbox.setSpacing(2) self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) - if not extr_repr[0].isNull(): - size = self.viewButton.iconSize() - size.setWidth(size.width()*2) - self.viewButton.setIconSize(size) - else: - self.viewButton.setIconSize(QtCore.QSize(0,0)) + + size = self.viewButton.iconSize() + size.setWidth(size.width()*2) + self.viewButton.setIconSize(size) + if st_object: self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) @@ -406,7 +432,7 @@ class _SummaryWidget(QtGui.QWidget): scroll = viewport.parent() top_left = summary.geometry().topLeft() + base_widget.geometry().topLeft() + viewport.geometry().topLeft() - delta = (summary.width() - dialog.sizeHint().width())/2 + delta = (summary.width() - dialog.size().width())/2 local_point = QtCore.QPoint(top_left.x()+delta, top_left.y()+summary.height()) global_point = scroll.mapToGlobal(local_point) @@ -423,7 +449,6 @@ class _SummaryWidget(QtGui.QWidget): # position correctly and show self._position_dialog(self.appDialog) self.appDialog.show() - #self.appDialog.raise_() @QtCore.Slot() def editView(self): @@ -437,7 +462,6 @@ class _SummaryWidget(QtGui.QWidget): # position correctly and show self._position_dialog(self.viewDialog) self.viewDialog.show() - #self.viewDialog.raise_() @QtCore.Slot() def deleteTriggered(self): @@ -457,7 +481,7 @@ class _SummaryWidget(QtGui.QWidget): # update the preview extr_label = self._extractor.Proxy.get_representive_fieldname(self._extractor) - self.extrButton.setCustomText = extr_label + self.extrButton.setCustomText(extr_label) self.extrButton.setToolTip(extr_label) @@ -574,7 +598,7 @@ class ExtractLinkView(QtGui.QWidget): # fill the scroll area vbox = self._scroll_widget.layout() - for widget in self._widgets: + for widget in reversed(self._widgets): vbox.insertWidget(0, widget) # also reset the add button model diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 9b976d62b9..1079808fcd 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -40,6 +40,7 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import io import numpy as np import matplotlib as mpl +from packaging.version import Version from vtkmodules.numpy_interface.dataset_adapter import VTKArray @@ -104,6 +105,7 @@ class EditViewWidget(QtGui.QWidget): action = QtGui.QWidgetAction(button) diag = QtGui.QColorDialog(barColor, parent=button) + diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) diag.accepted.connect(action.trigger) diag.rejected.connect(action.trigger) diag.colorSelected.connect(callback) @@ -512,7 +514,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): # we do not iterate the table, but iterate the children. This makes it possible # to attribute the correct styles - full_args = {} + full_args = [] full_data = [] labels = [] for child in self.Object.Group: @@ -526,15 +528,14 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): for i in range(table.GetNumberOfColumns()): # add the kw args, with some slide change over color for multiple frames + args = kwargs.copy() 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]) + args[key] = mpl.colors.to_hex(value) + + full_args.append(args) data = VTKArray(table.GetColumn(i)) full_data.append(data) @@ -552,15 +553,26 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): legend_prefix = child.Source.Label + ": " labels.append(legend_prefix + table.GetColumnName(i)) + args = {} + args["rwidth"] = self.ViewObject.BarWidth + args["cumulative"] = self.ViewObject.Cumulative + args["histtype"] = self.ViewObject.Type + args["label"] = labels + if Version(mpl.__version__) >= Version("3.10.0"): + args["hatch_linewidth"] = self.ViewObject.HatchLineWidth - 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 + n, b, patches = self._plot.axes.hist(full_data, bins, **args) - self._plot.axes.hist(full_data, bins, **full_args) + # set the patches view properties. + if len(full_args) == 1: + for patch in patches: + patch.set(**full_args[0]) + elif len(full_args) > 1: + for i, args in enumerate(full_args): + for patch in patches[i]: + patch.set(**full_args[i]) + # axes decoration if self.ViewObject.Title: self._plot.axes.set_title(self.ViewObject.Title) if self.ViewObject.XLabel: diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index fb17a2eaaf..f2b03743d1 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -102,6 +102,7 @@ class EditViewWidget(QtGui.QWidget): action = QtGui.QWidgetAction(button) diag = QtGui.QColorDialog(barColor, parent=button) + diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) diag.accepted.connect(action.trigger) diag.rejected.connect(action.trigger) diag.colorSelected.connect(callback) diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index cf8da7fdab..75458ef9d7 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -207,7 +207,7 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): return EditViewWidget(self.Object, post_dialog) def get_preview(self): - name = "----" + name = QT_TRANSLATE_NOOP("FEM", "default") if self.ViewObject.Name: name = self.ViewObject.Name return (QtGui.QPixmap(), name) From 56def6c86dbf3d8f1609c5adbce30e01205c1854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 10:27:27 +0200 Subject: [PATCH 025/126] FEM: Plot single frame index data as point --- .../Resources/ui/PostHistogramIndexAppEdit.ui | 3 ++ src/Mod/Fem/femobjects/post_extract1D.py | 38 ++++++++------- src/Mod/Fem/femobjects/post_extract2D.py | 48 +++++++++++-------- src/Mod/Fem/femobjects/post_lineplot.py | 2 +- .../Fem/femviewprovider/view_post_lineplot.py | 4 ++ 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui index e9dd2a2b3d..496f42229b 100644 --- a/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui +++ b/src/Mod/Fem/Gui/Resources/ui/PostHistogramIndexAppEdit.ui @@ -70,6 +70,9 @@ 0 + + 999999999 +
diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 987dfdabdd..1ffb946acd 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -149,36 +149,40 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): obj.Table = table return - # check if we have timesteps (required!) - abort = True + # check if we have timesteps + timesteps = [] info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - if len(timesteps) > 1: - abort = False - if abort: - FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") - obj.Table = table - return algo = obj.Source.getOutputAlgorithm() - setup = False frame_array = vtkDoubleArray() - idx = obj.Index - for i, timestep in enumerate(timesteps): - algo.UpdateTimeStep(timestep) + if timesteps: + setup = False + for i, timestep in enumerate(timesteps): + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._x_array_from_dataset(obj, dataset, copy=False) + + if not setup: + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_array.SetTuple(i, idx, array) + else: + algo.Update() dataset = algo.GetOutputDataObject(0) array = self._x_array_from_dataset(obj, dataset, copy=False) - if not setup: - frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) - frame_array.SetNumberOfTuples(len(timesteps)) - setup = True + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_array.SetNumberOfTuples(1) + frame_array.SetTuple(0, idx, array) - frame_array.SetTuple(i, idx, array) if frame_array.GetNumberOfComponents() > 1: frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index baa5f3d8c2..3cb17d8ffc 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -174,41 +174,49 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): return # check if we have timesteps (required!) - abort = True + timesteps = [] info = obj.Source.getOutputAlgorithm().GetOutputInformation(0) if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - if len(timesteps) > 1: - abort = False - if abort: - FreeCAD.Console.PrintWarning("Not sufficient frames available in data, cannot extract data") - obj.Table = table - return algo = obj.Source.getOutputAlgorithm() frame_x_array = vtkDoubleArray() - frame_x_array.SetNumberOfTuples(len(timesteps)) - frame_x_array.SetNumberOfComponents(1) - - frame_y_array = vtkDoubleArray() idx = obj.Index - setup = False - for i, timestep in enumerate(timesteps): - frame_x_array.SetTuple1(i, timestep) + if timesteps: + setup = False + frame_x_array.SetNumberOfTuples(len(timesteps)) + frame_x_array.SetNumberOfComponents(1) + for i, timestep in enumerate(timesteps): - algo.UpdateTimeStep(timestep) + frame_x_array.SetTuple1(i, timestep) + + algo.UpdateTimeStep(timestep) + dataset = algo.GetOutputDataObject(0) + array = self._y_array_from_dataset(obj, dataset, copy=False) + if not setup: + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(len(timesteps)) + setup = True + + frame_y_array.SetTuple(i, idx, array) + + else: + frame_x_array.SetNumberOfTuples(1) + frame_x_array.SetNumberOfComponents(1) + frame_x_array.SetTuple1(0,0) + + algo.Update() dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) - if not setup: - frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) - frame_y_array.SetNumberOfTuples(len(timesteps)) - setup = True - frame_y_array.SetTuple(i, idx, array) + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) + frame_y_array.SetNumberOfTuples(1) + frame_y_array.SetTuple(0, idx, array) + frame_x_array.SetName("Frames") if frame_y_array.GetNumberOfComponents() > 1: diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 8d4b725128..e3483b0bf5 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -1,4 +1,4 @@ -# *************************************************************************** +2# *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * # * This file is part of the FreeCAD CAx development system. * diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index f2b03743d1..0a61fd771d 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -516,6 +516,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): xdata = VTKArray(table.GetColumn(i)) ydata = VTKArray(table.GetColumn(i+1)) + # ensure points are visible if it is a single datapoint + if len(xdata) == 1 and tmp_args["marker"] == "None": + tmp_args["marker"] = "o" + # legend labels if child.ViewObject.Legend: if not legend_multiframe: From f88e9b281a513ab6c80a0359cc4a25fa05f66b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 10:37:53 +0200 Subject: [PATCH 026/126] FEM: Prevent invalid index for data extraction --- src/Mod/Fem/femobjects/post_extract1D.py | 8 ++++++++ src/Mod/Fem/femobjects/post_extract2D.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 1ffb946acd..f04d90bfad 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -168,6 +168,10 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): dataset = algo.GetOutputDataObject(0) array = self._x_array_from_dataset(obj, dataset, copy=False) + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if not setup: frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_array.SetNumberOfTuples(len(timesteps)) @@ -179,6 +183,10 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): dataset = algo.GetOutputDataObject(0) array = self._x_array_from_dataset(obj, dataset, copy=False) + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_array.SetNumberOfTuples(1) frame_array.SetTuple(0, idx, array) diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 3cb17d8ffc..ed7d0faf47 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -197,6 +197,11 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): algo.UpdateTimeStep(timestep) dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) + + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if not setup: frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_y_array.SetNumberOfTuples(len(timesteps)) @@ -213,6 +218,10 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) + # safeguard for invalid access + if idx < 0 or array.GetNumberOfTuples()-1 < idx: + raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_y_array.SetNumberOfTuples(1) frame_y_array.SetTuple(0, idx, array) From bd643036868dbff41e364707c9f77ec91d316a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 10:56:40 +0200 Subject: [PATCH 027/126] FEM: Include code quality improvements from review --- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 20 ++++++++++--------- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 16 ++++++--------- src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 8 ++++---- src/Mod/Fem/femcommands/manager.py | 2 +- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 804cde7f14..88cf26f8f9 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -407,16 +407,18 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) void TaskDlgPost::processCollapsedWidgets() { for (auto& widget : Content) { - if(auto task_box = dynamic_cast(widget)) { - // get the task widget and check if it is a post widget - auto widget = task_box->groupLayout()->itemAt(0)->widget(); - if(auto post_widget = dynamic_cast(widget)) { - if(post_widget->initiallyCollapsed()) { - post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); - task_box->hideGroupBox(); - } - } + auto* task_box = dynamic_cast(widget); + if (!task_box) { + continue; } + // get the task widget and check if it is a post widget + auto* taskwidget = task_box->groupLayout()->itemAt(0)->widget(); + auto* post_widget = dynamic_cast(taskwidget); + if (!post_widget || !post_widget->initiallyCollapsed()) { + continue; + } + post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); + task_box->hideGroupBox(); } } diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index abce2a30cf..4de2401c7c 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -82,17 +82,13 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* 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! + if (auto* widget = qobject_cast(wrap.toQObject(pywidget))) { + // finally we have the usable QWidget. Add to us! - auto layout = new QVBoxLayout(); - layout->addWidget(widget); - setLayout(layout); - return; - } + auto layout = new QVBoxLayout(); + layout->addWidget(widget); + setLayout(layout); + return; } } } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index b3dcbf4c7b..9205e2d708 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -1023,12 +1023,12 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { assert(dlg->getView() == this); - auto disp_panel = new TaskPostDisplay(this); - dlg->addTaskBox(disp_panel->windowIcon().pixmap(32), disp_panel); + auto dispPanel = new TaskPostDisplay(this); + dlg->addTaskBox(dispPanel->windowIcon().pixmap(32), dispPanel); #ifdef FC_USE_VTK_PYTHON - auto extr_panel = new TaskPostExtraction(this); - dlg->addTaskBox(extr_panel->windowIcon().pixmap(32), extr_panel); + auto extrPanel = new TaskPostExtraction(this); + dlg->addTaskBox(extrPanel->windowIcon().pixmap(32), extrPanel); #endif } diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index 50fbae0548..f653d053a0 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -379,7 +379,7 @@ class CommandManager: # like add_obj_on_gui_selobj_noset_edit but the selection is kept # and the selobj is expanded in the tree to see the added obj - # check if we should use python fitler + # check if we should use python filter from femguiutils.vtk_module_handling import vtk_compatibility_abort if vtk_compatibility_abort(True): return From 0e7f7e7813142b1a4da8bca90a68e7c5bed76dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Sun, 15 Jun 2025 13:03:26 +0200 Subject: [PATCH 028/126] FEM: Extraction code CodeQL updated and typo fix --- src/Mod/AddonManager | 2 +- src/Mod/Fem/femguiutils/extract_link_view.py | 3 -- src/Mod/Fem/femguiutils/post_visualization.py | 4 +- src/Mod/Fem/femguiutils/vtk_table_view.py | 8 ++++ .../Fem/femobjects/base_fempostextractors.py | 2 +- .../femobjects/base_fempostvisualizations.py | 4 +- src/Mod/Fem/femobjects/post_extract1D.py | 5 +-- src/Mod/Fem/femobjects/post_extract2D.py | 5 +-- src/Mod/Fem/femobjects/post_lineplot.py | 2 +- .../Fem/femtaskpanels/base_fempostpanel.py | 2 - .../Fem/femtaskpanels/task_post_extractor.py | 4 -- .../femtaskpanels/task_post_glyphfilter.py | 43 ------------------- src/Mod/Fem/femtaskpanels/task_post_table.py | 4 -- .../view_base_fempostextractors.py | 1 - 14 files changed, 19 insertions(+), 70 deletions(-) diff --git a/src/Mod/AddonManager b/src/Mod/AddonManager index 69a6e0dc7b..34d433a02c 160000 --- a/src/Mod/AddonManager +++ b/src/Mod/AddonManager @@ -1 +1 @@ -Subproject commit 69a6e0dc7b8f5fe17547f4d1234df1617b78c45e +Subproject commit 34d433a02c7ec5c73bec9c57d0a27ea70b36c90d diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index 37e8e82c62..a6f6067080 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -590,7 +590,6 @@ class ExtractLinkView(QtGui.QWidget): 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) @@ -620,8 +619,6 @@ class ExtractLinkView(QtGui.QWidget): 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") diff --git a/src/Mod/Fem/femguiutils/post_visualization.py b/src/Mod/Fem/femguiutils/post_visualization.py index 9975db127b..6724fda779 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -40,8 +40,8 @@ from PySide import QtCore import FreeCAD -# Registry to handle visulization commands -# ######################################## +# Registry to handle visualization commands +# ######################################### _registry = {} diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index c1263150ac..f126640d24 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -83,6 +83,8 @@ class VtkTableModel(QtCore.QAbstractTableModel): col = self._table.GetColumn(index.column()) return col.GetTuple(index.row())[0] + return None + def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: @@ -98,6 +100,8 @@ class VtkTableModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return section + return None + def getTable(self): return self._table @@ -134,6 +138,8 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): range = col.GetRange() return range[index.column()] + return None + def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: @@ -142,6 +148,8 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return self._table.GetColumnName(section) + return None + def getTable(self): return self._table diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index e42d2adf1b..48bd9c4951 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -123,7 +123,7 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): case _: return ["Not a vector"] - def get_representive_fieldname(self): + def get_representive_fieldname(self, obj): # should return the representive field name, e.g. Position (X) return "" diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index 396694a652..ffaa94ee8f 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -160,8 +160,8 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): array.SetNumberOfComponents(c_array.GetNumberOfComponents()) array.SetNumberOfTuples(rows) array.Fill(0) # so that all non-used entries are set to 0 - for i in range(c_array.GetNumberOfTuples()): - array.SetTuple(i, c_array.GetTuple(i)) + for j in range(c_array.GetNumberOfTuples()): + array.SetTuple(j, c_array.GetTuple(j)) array.SetName(f"{child.Source.Name}: {c_array.GetName()}") table.AddColumn(array) diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index f04d90bfad..8fcec6e2da 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -77,17 +77,16 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): obj.Table = table return - frames = False + timesteps=[] 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: + if not timesteps: # get the dataset and extract the correct array array = self._x_array_from_dataset(obj, dataset) if array.GetNumberOfComponents() > 1: diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index ed7d0faf47..86827d7a7e 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -78,17 +78,16 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): obj.Table = table return - frames = False + timesteps = [] 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: + if not timesteps: # get the dataset and extract the correct array xarray = self._x_array_from_dataset(obj, dataset) if xarray.GetNumberOfComponents() > 1: diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index e3483b0bf5..8d4b725128 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -1,4 +1,4 @@ -2# *************************************************************************** +# *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * # * This file is part of the FreeCAD CAx development system. * diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 08c067de7f..5efa1aa12a 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -32,9 +32,7 @@ __url__ = "https://www.freecad.org" from PySide import QtCore, QtGui import FreeCAD -import FreeCADGui -from femguiutils import selection_widgets from . import base_femtaskpanel translate = FreeCAD.Qt.translate diff --git a/src/Mod/Fem/femtaskpanels/task_post_extractor.py b/src/Mod/Fem/femtaskpanels/task_post_extractor.py index 6d29a305e8..56fdd998b5 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -31,10 +31,6 @@ __url__ = "https://www.freecad.org" from PySide import QtCore, QtGui -import FreeCAD -import FreeCADGui - -from femguiutils import selection_widgets from . import base_fempostpanel diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index 8804951067..a9a13139e9 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -56,49 +56,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.widget, vobj.createDisplayTaskWidget()] - # 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() - - def accept(self): - # self.obj.CharacteristicLength = self.elelen - # self.obj.References = self.selection_widget.references - # self.selection_widget.finish_selection() - return super().accept() - - def reject(self): - # self.selection_widget.finish_selection() - return super().reject() - - # 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) # Setup functions # ############### diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py index b75549eeb9..1eda150016 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_table.py +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -36,7 +36,6 @@ import FreeCADGui from . import base_fempostpanel from femguiutils import extract_link_view as elv -from femguiutils import vtk_table_view translate = FreeCAD.Qt.translate @@ -78,9 +77,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # connect data widget self.data_widget.show_table.clicked.connect(self.showTable) - # set current values to view widget - viewObj = self.obj.ViewObject - @QtCore.Slot() def showTable(self): diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index 143cd8fba5..338aabb905 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -33,7 +33,6 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui -import FemGui from PySide import QtGui import femobjects.base_fempostextractors as fpe From e9ef35c53d44ecb3f6435722d3c45d1b9901677b Mon Sep 17 00:00:00 2001 From: wmayer Date: Mon, 12 May 2025 12:12:10 +0200 Subject: [PATCH 029/126] Base: Do not use short int in Matrix4D As discussed in https://forum.freecad.org/viewtopic.php?t=65959 replace short with int. --- src/Base/Matrix.cpp | 36 ++++++++++++++++++------------------ src/Base/Matrix.h | 32 ++++++++++++++++---------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Base/Matrix.cpp b/src/Base/Matrix.cpp index aa0623b2b7..f6d0936867 100644 --- a/src/Base/Matrix.cpp +++ b/src/Base/Matrix.cpp @@ -277,8 +277,8 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) double fsin {}; // set all entries to "0" - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { + for (int iz = 0; iz < 4; iz++) { + for (int is = 0; is < 4; is++) { clMA.dMtrx4D[iz][is] = 0; clMB.dMtrx4D[iz][is] = 0; clMC.dMtrx4D[iz][is] = 0; @@ -313,8 +313,8 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) clMC.dMtrx4D[2][0] = -fsin * clRotAxis.y; clMC.dMtrx4D[2][1] = fsin * clRotAxis.x; - for (short iz = 0; iz < 3; iz++) { - for (short is = 0; is < 3; is++) { + for (int iz = 0; iz < 3; iz++) { + for (int is = 0; is < 3; is++) { clMRot.dMtrx4D[iz][is] = clMA.dMtrx4D[iz][is] + clMB.dMtrx4D[iz][is] + clMC.dMtrx4D[iz][is]; } @@ -522,14 +522,14 @@ void Matrix4D::inverse() /**** Herausnehmen und Inversion der TranslationsMatrix aus der TransformationMatrix ****/ - for (short iz = 0; iz < 3; iz++) { + for (int iz = 0; iz < 3; iz++) { clInvTrlMat.dMtrx4D[iz][3] = -dMtrx4D[iz][3]; } /**** Herausnehmen und Inversion der RotationsMatrix aus der TransformationMatrix ****/ - for (short iz = 0; iz < 3; iz++) { - for (short is = 0; is < 3; is++) { + for (int iz = 0; iz < 3; iz++) { + for (int is = 0; is < 3; is++) { clInvRotMat.dMtrx4D[iz][is] = dMtrx4D[is][iz]; } } @@ -651,8 +651,8 @@ void Matrix4D::inverseGauss() void Matrix4D::getMatrix(double dMtrx[16]) const { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { + for (int iz = 0; iz < 4; iz++) { + for (int is = 0; is < 4; is++) { dMtrx[4 * iz + is] = dMtrx4D[iz][is]; } } @@ -660,8 +660,8 @@ void Matrix4D::getMatrix(double dMtrx[16]) const void Matrix4D::setMatrix(const double dMtrx[16]) { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { + for (int iz = 0; iz < 4; iz++) { + for (int is = 0; is < 4; is++) { dMtrx4D[iz][is] = dMtrx[4 * iz + is]; } } @@ -669,8 +669,8 @@ void Matrix4D::setMatrix(const double dMtrx[16]) void Matrix4D::getGLMatrix(double dMtrx[16]) const { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { + for (int iz = 0; iz < 4; iz++) { + for (int is = 0; is < 4; is++) { dMtrx[iz + 4 * is] = dMtrx4D[iz][is]; } } @@ -678,8 +678,8 @@ void Matrix4D::getGLMatrix(double dMtrx[16]) const void Matrix4D::setGLMatrix(const double dMtrx[16]) { - for (short iz = 0; iz < 4; iz++) { - for (short is = 0; is < 4; is++) { + for (int iz = 0; iz < 4; iz++) { + for (int is = 0; is < 4; is++) { dMtrx4D[iz][is] = dMtrx[iz + 4 * is]; } } @@ -693,7 +693,7 @@ unsigned long Matrix4D::getMemSpace() void Matrix4D::Print() const { // NOLINTBEGIN - for (short i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { printf("%9.3f %9.3f %9.3f %9.3f\n", dMtrx4D[i][0], dMtrx4D[i][1], @@ -788,8 +788,8 @@ std::string Matrix4D::analyse() const trp.transpose(); trp = trp * sub; bool ortho = true; - for (unsigned short i = 0; i < 4 && ortho; i++) { - for (unsigned short j = 0; j < 4 && ortho; j++) { + for (unsigned int i = 0; i < 4 && ortho; i++) { + for (unsigned int j = 0; j < 4 && ortho; j++) { if (i != j) { if (fabs(trp[i][j]) > eps) { ortho = false; diff --git a/src/Base/Matrix.h b/src/Base/Matrix.h index b177abb0fe..043b9de62e 100644 --- a/src/Base/Matrix.h +++ b/src/Base/Matrix.h @@ -106,13 +106,13 @@ public: /// Comparison inline bool operator==(const Matrix4D& mat) const; /// Index operator - inline double* operator[](unsigned short usNdx); + inline double* operator[](unsigned int usNdx); /// Index operator - inline const double* operator[](unsigned short usNdx) const; + inline const double* operator[](unsigned int usNdx) const; /// Get vector of row - inline Vector3d getRow(unsigned short usNdx) const; + inline Vector3d getRow(unsigned int usNdx) const; /// Get vector of column - inline Vector3d getCol(unsigned short usNdx) const; + inline Vector3d getCol(unsigned int usNdx) const; /// Get vector of diagonal inline Vector3d diagonal() const; /// Get trace of the 3x3 matrix @@ -120,9 +120,9 @@ public: /// Get trace of the 4x4 matrix inline double trace() const; /// Set row to vector - inline void setRow(unsigned short usNdx, const Vector3d& vec); + inline void setRow(unsigned int usNdx, const Vector3d& vec); /// Set column to vector - inline void setCol(unsigned short usNdx, const Vector3d& vec); + inline void setCol(unsigned int usNdx, const Vector3d& vec); /// Set diagonal to vector inline void setDiagonal(const Vector3d& vec); /// Compute the determinant of the matrix @@ -380,8 +380,8 @@ inline void Matrix4D::multVec(const Vector3f& src, Vector3f& dst) const inline Matrix4D Matrix4D::operator*(double scalar) const { Matrix4D matrix; - for (unsigned short i = 0; i < 4; i++) { - for (unsigned short j = 0; j < 4; j++) { + for (unsigned int i = 0; i < 4; i++) { + for (unsigned int j = 0; j < 4; j++) { matrix.dMtrx4D[i][j] = dMtrx4D[i][j] * scalar; } } @@ -392,8 +392,8 @@ inline Matrix4D Matrix4D::operator*(double scalar) const inline Matrix4D& Matrix4D::operator*=(double scalar) { // NOLINTBEGIN - for (unsigned short i = 0; i < 4; i++) { - for (unsigned short j = 0; j < 4; j++) { + for (unsigned int i = 0; i < 4; i++) { + for (unsigned int j = 0; j < 4; j++) { dMtrx4D[i][j] *= scalar; } } @@ -425,22 +425,22 @@ inline Vector3f& operator*=(Vector3f& vec, const Matrix4D& mat) return vec; } -inline double* Matrix4D::operator[](unsigned short usNdx) +inline double* Matrix4D::operator[](unsigned int usNdx) { return dMtrx4D[usNdx]; } -inline const double* Matrix4D::operator[](unsigned short usNdx) const +inline const double* Matrix4D::operator[](unsigned int usNdx) const { return dMtrx4D[usNdx]; } -inline Vector3d Matrix4D::getRow(unsigned short usNdx) const +inline Vector3d Matrix4D::getRow(unsigned int usNdx) const { return Vector3d(dMtrx4D[usNdx][0], dMtrx4D[usNdx][1], dMtrx4D[usNdx][2]); } -inline Vector3d Matrix4D::getCol(unsigned short usNdx) const +inline Vector3d Matrix4D::getCol(unsigned int usNdx) const { return Vector3d(dMtrx4D[0][usNdx], dMtrx4D[1][usNdx], dMtrx4D[2][usNdx]); } @@ -460,14 +460,14 @@ inline double Matrix4D::trace() const return dMtrx4D[0][0] + dMtrx4D[1][1] + dMtrx4D[2][2] + dMtrx4D[3][3]; } -inline void Matrix4D::setRow(unsigned short usNdx, const Vector3d& vec) +inline void Matrix4D::setRow(unsigned int usNdx, const Vector3d& vec) { dMtrx4D[usNdx][0] = vec.x; dMtrx4D[usNdx][1] = vec.y; dMtrx4D[usNdx][2] = vec.z; } -inline void Matrix4D::setCol(unsigned short usNdx, const Vector3d& vec) +inline void Matrix4D::setCol(unsigned int usNdx, const Vector3d& vec) { dMtrx4D[0][usNdx] = vec.x; dMtrx4D[1][usNdx] = vec.y; From 7998f57048bfbb0b37e39181ffab005b968969cb Mon Sep 17 00:00:00 2001 From: wmayer Date: Mon, 12 May 2025 19:20:55 +0200 Subject: [PATCH 030/126] Base: Use i,j consistently for iterations Matrix4D As discussed in https://forum.freecad.org/viewtopic.php?t=65959 use consistently i,j to iterate over rows and columns --- src/Base/Matrix.cpp | 51 ++++++++++++++++++++--------------------- src/Base/Matrix.h | 56 ++++++++++++++++++++++----------------------- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/Base/Matrix.cpp b/src/Base/Matrix.cpp index f6d0936867..a619fc7354 100644 --- a/src/Base/Matrix.cpp +++ b/src/Base/Matrix.cpp @@ -277,11 +277,11 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) double fsin {}; // set all entries to "0" - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMA.dMtrx4D[iz][is] = 0; - clMB.dMtrx4D[iz][is] = 0; - clMC.dMtrx4D[iz][is] = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + clMA.dMtrx4D[i][j] = 0; + clMB.dMtrx4D[i][j] = 0; + clMC.dMtrx4D[i][j] = 0; } } @@ -313,10 +313,9 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) clMC.dMtrx4D[2][0] = -fsin * clRotAxis.y; clMC.dMtrx4D[2][1] = fsin * clRotAxis.x; - for (int iz = 0; iz < 3; iz++) { - for (int is = 0; is < 3; is++) { - clMRot.dMtrx4D[iz][is] = - clMA.dMtrx4D[iz][is] + clMB.dMtrx4D[iz][is] + clMC.dMtrx4D[iz][is]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + clMRot.dMtrx4D[i][j] = clMA.dMtrx4D[i][j] + clMB.dMtrx4D[i][j] + clMC.dMtrx4D[i][j]; } } @@ -522,15 +521,15 @@ void Matrix4D::inverse() /**** Herausnehmen und Inversion der TranslationsMatrix aus der TransformationMatrix ****/ - for (int iz = 0; iz < 3; iz++) { - clInvTrlMat.dMtrx4D[iz][3] = -dMtrx4D[iz][3]; + for (int i = 0; i < 3; i++) { + clInvTrlMat.dMtrx4D[i][3] = -dMtrx4D[i][3]; } /**** Herausnehmen und Inversion der RotationsMatrix aus der TransformationMatrix ****/ - for (int iz = 0; iz < 3; iz++) { - for (int is = 0; is < 3; is++) { - clInvRotMat.dMtrx4D[iz][is] = dMtrx4D[is][iz]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + clInvRotMat.dMtrx4D[i][j] = dMtrx4D[j][i]; } } @@ -651,36 +650,36 @@ void Matrix4D::inverseGauss() void Matrix4D::getMatrix(double dMtrx[16]) const { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx[4 * iz + is] = dMtrx4D[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx[4 * i + j] = dMtrx4D[i][j]; } } } void Matrix4D::setMatrix(const double dMtrx[16]) { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] = dMtrx[4 * iz + is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] = dMtrx[4 * i + j]; } } } void Matrix4D::getGLMatrix(double dMtrx[16]) const { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx[iz + 4 * is] = dMtrx4D[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx[i + 4 * j] = dMtrx4D[i][j]; } } } void Matrix4D::setGLMatrix(const double dMtrx[16]) { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] = dMtrx[iz + 4 * is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] = dMtrx[i + 4 * j]; } } } diff --git a/src/Base/Matrix.h b/src/Base/Matrix.h index 043b9de62e..41a9d52df2 100644 --- a/src/Base/Matrix.h +++ b/src/Base/Matrix.h @@ -241,9 +241,9 @@ inline Matrix4D Matrix4D::operator+(const Matrix4D& mat) const { Matrix4D clMat; - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = dMtrx4D[iz][is] + mat[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + clMat.dMtrx4D[i][j] = dMtrx4D[i][j] + mat[i][j]; } } @@ -252,9 +252,9 @@ inline Matrix4D Matrix4D::operator+(const Matrix4D& mat) const inline Matrix4D& Matrix4D::operator+=(const Matrix4D& mat) { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] += mat[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] += mat[i][j]; } } @@ -265,9 +265,9 @@ inline Matrix4D Matrix4D::operator-(const Matrix4D& mat) const { Matrix4D clMat; - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = dMtrx4D[iz][is] - mat[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + clMat.dMtrx4D[i][j] = dMtrx4D[i][j] - mat[i][j]; } } @@ -276,9 +276,9 @@ inline Matrix4D Matrix4D::operator-(const Matrix4D& mat) const inline Matrix4D& Matrix4D::operator-=(const Matrix4D& mat) { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] -= mat[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] -= mat[i][j]; } } @@ -289,11 +289,11 @@ inline Matrix4D& Matrix4D::operator*=(const Matrix4D& mat) { Matrix4D clMat; - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = 0; - for (int ie = 0; ie < 4; ie++) { - clMat.dMtrx4D[iz][is] += dMtrx4D[iz][ie] * mat.dMtrx4D[ie][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + clMat.dMtrx4D[i][j] = 0; + for (int e = 0; e < 4; e++) { + clMat.dMtrx4D[i][j] += dMtrx4D[i][e] * mat.dMtrx4D[e][j]; } } } @@ -307,11 +307,11 @@ inline Matrix4D Matrix4D::operator*(const Matrix4D& mat) const { Matrix4D clMat; - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - clMat.dMtrx4D[iz][is] = 0; - for (int ie = 0; ie < 4; ie++) { - clMat.dMtrx4D[iz][is] += dMtrx4D[iz][ie] * mat.dMtrx4D[ie][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + clMat.dMtrx4D[i][j] = 0; + for (int e = 0; e < 4; e++) { + clMat.dMtrx4D[i][j] += dMtrx4D[i][e] * mat.dMtrx4D[e][j]; } } } @@ -325,9 +325,9 @@ inline Matrix4D& Matrix4D::operator=(const Matrix4D& mat) return *this; } - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - dMtrx4D[iz][is] = mat.dMtrx4D[iz][is]; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + dMtrx4D[i][j] = mat.dMtrx4D[i][j]; } } @@ -403,9 +403,9 @@ inline Matrix4D& Matrix4D::operator*=(double scalar) inline bool Matrix4D::operator==(const Matrix4D& mat) const { - for (int iz = 0; iz < 4; iz++) { - for (int is = 0; is < 4; is++) { - if (fabs(dMtrx4D[iz][is] - mat.dMtrx4D[iz][is]) > traits_type::epsilon()) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (fabs(dMtrx4D[i][j] - mat.dMtrx4D[i][j]) > traits_type::epsilon()) { return false; } } From 13232cbc7b14d0ec044869ca21124387c54470c0 Mon Sep 17 00:00:00 2001 From: wmayer Date: Tue, 13 May 2025 13:07:44 +0200 Subject: [PATCH 031/126] Base: Simplify Base::Matrix4D As discussed in https://forum.freecad.org/viewtopic.php?t=65959 reduce code duplications --- src/Base/Matrix.cpp | 12 +-- src/Base/Matrix.h | 67 ++++----------- tests/src/Base/Matrix.cpp | 172 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 61 deletions(-) diff --git a/src/Base/Matrix.cpp b/src/Base/Matrix.cpp index a619fc7354..8bd35ec1ab 100644 --- a/src/Base/Matrix.cpp +++ b/src/Base/Matrix.cpp @@ -277,13 +277,9 @@ void Matrix4D::rotLine(const Vector3d& vec, double fAngle) double fsin {}; // set all entries to "0" - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - clMA.dMtrx4D[i][j] = 0; - clMB.dMtrx4D[i][j] = 0; - clMC.dMtrx4D[i][j] = 0; - } - } + clMA.nullify(); + clMB.nullify(); + clMC.nullify(); // ** normalize the rotation axis clRotAxis.Normalize(); @@ -623,7 +619,7 @@ void Matrix4D::inverseOrthogonal() { Base::Vector3d vec(dMtrx4D[0][3], dMtrx4D[1][3], dMtrx4D[2][3]); transpose(); - vec = this->operator*(vec); + multVec(vec, vec); dMtrx4D[0][3] = -vec.x; dMtrx4D[3][0] = 0; dMtrx4D[1][3] = -vec.y; diff --git a/src/Base/Matrix.h b/src/Base/Matrix.h index 41a9d52df2..665c69376a 100644 --- a/src/Base/Matrix.h +++ b/src/Base/Matrix.h @@ -239,15 +239,8 @@ private: inline Matrix4D Matrix4D::operator+(const Matrix4D& mat) const { - Matrix4D clMat; - - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - clMat.dMtrx4D[i][j] = dMtrx4D[i][j] + mat[i][j]; - } - } - - return clMat; + Matrix4D newMat(*this); + return newMat += mat; } inline Matrix4D& Matrix4D::operator+=(const Matrix4D& mat) @@ -263,15 +256,8 @@ inline Matrix4D& Matrix4D::operator+=(const Matrix4D& mat) inline Matrix4D Matrix4D::operator-(const Matrix4D& mat) const { - Matrix4D clMat; - - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - clMat.dMtrx4D[i][j] = dMtrx4D[i][j] - mat[i][j]; - } - } - - return clMat; + Matrix4D newMat(*this); + return newMat -= mat; } inline Matrix4D& Matrix4D::operator-=(const Matrix4D& mat) @@ -287,19 +273,7 @@ inline Matrix4D& Matrix4D::operator-=(const Matrix4D& mat) inline Matrix4D& Matrix4D::operator*=(const Matrix4D& mat) { - Matrix4D clMat; - - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - clMat.dMtrx4D[i][j] = 0; - for (int e = 0; e < 4; e++) { - clMat.dMtrx4D[i][j] += dMtrx4D[i][e] * mat.dMtrx4D[e][j]; - } - } - } - - (*this) = clMat; - + (*this) = (*this) * mat; return *this; } @@ -336,23 +310,16 @@ inline Matrix4D& Matrix4D::operator=(const Matrix4D& mat) inline Vector3f Matrix4D::operator*(const Vector3f& vec) const { - // clang-format off - double sx = static_cast(vec.x); - double sy = static_cast(vec.y); - double sz = static_cast(vec.z); - return Vector3f(static_cast(dMtrx4D[0][0] * sx + dMtrx4D[0][1] * sy + dMtrx4D[0][2] * sz + dMtrx4D[0][3]), - static_cast(dMtrx4D[1][0] * sx + dMtrx4D[1][1] * sy + dMtrx4D[1][2] * sz + dMtrx4D[1][3]), - static_cast(dMtrx4D[2][0] * sx + dMtrx4D[2][1] * sy + dMtrx4D[2][2] * sz + dMtrx4D[2][3])); - // clang-format on + Vector3f dst; + multVec(vec, dst); + return dst; } inline Vector3d Matrix4D::operator*(const Vector3d& vec) const { - // clang-format off - return Vector3d((dMtrx4D[0][0] * vec.x + dMtrx4D[0][1] * vec.y + dMtrx4D[0][2] * vec.z + dMtrx4D[0][3]), - (dMtrx4D[1][0] * vec.x + dMtrx4D[1][1] * vec.y + dMtrx4D[1][2] * vec.z + dMtrx4D[1][3]), - (dMtrx4D[2][0] * vec.x + dMtrx4D[2][1] * vec.y + dMtrx4D[2][2] * vec.z + dMtrx4D[2][3])); - // clang-format on + Vector3d dst; + multVec(vec, dst); + return dst; } inline void Matrix4D::multVec(const Vector3d& src, Vector3d& dst) const @@ -379,14 +346,8 @@ inline void Matrix4D::multVec(const Vector3f& src, Vector3f& dst) const inline Matrix4D Matrix4D::operator*(double scalar) const { - Matrix4D matrix; - for (unsigned int i = 0; i < 4; i++) { - for (unsigned int j = 0; j < 4; j++) { - matrix.dMtrx4D[i][j] = dMtrx4D[i][j] * scalar; - } - } - - return matrix; + Matrix4D newMat(*this); + return newMat *= scalar; } inline Matrix4D& Matrix4D::operator*=(double scalar) @@ -421,7 +382,7 @@ inline bool Matrix4D::operator!=(const Matrix4D& mat) const inline Vector3f& operator*=(Vector3f& vec, const Matrix4D& mat) { - vec = mat * vec; + mat.multVec(vec, vec); return vec; } diff --git a/tests/src/Base/Matrix.cpp b/tests/src/Base/Matrix.cpp index 63b3bb0a01..6d1e280627 100644 --- a/tests/src/Base/Matrix.cpp +++ b/tests/src/Base/Matrix.cpp @@ -1,6 +1,7 @@ #include #include #include +#include // NOLINTBEGIN(cppcoreguidelines-*,readability-magic-numbers) // clang-format off @@ -155,6 +156,18 @@ TEST(Matrix, TestMultVec) Base::Vector3d vec2 {1, 1, 1}; mat.multVec(vec2, vec2); EXPECT_EQ(vec2, Base::Vector3d(6.0, 7.0, 8.0)); + + Base::Vector3f vec3{1.0F,1.0F,3.0F}; + vec3 = mat * vec3; + EXPECT_EQ(vec3, Base::Vector3f(12.0F, 9.0F, 12.0F)); + + Base::Vector3f vec4 {1.0F, 1.0F, 1.0F}; + mat.multVec(vec4, vec4); + EXPECT_EQ(vec4, Base::Vector3f(6.0F, 7.0F, 8.0F)); + + Base::Vector3f vec5 {1.0F, 1.0F, 1.0F}; + vec5 *= mat; + EXPECT_EQ(vec5, Base::Vector3f(6.0F, 7.0F, 8.0F)); } TEST(Matrix, TestMult) @@ -173,6 +186,7 @@ TEST(Matrix, TestMult) 10.0, 13.0, 13.0, 7.0, 0.0, 0.0, 0.0, 1.0}; EXPECT_EQ(mat3, mat4); + EXPECT_NE(mat3, Base::Matrix4D()); } TEST(Matrix, TestMultAssign) @@ -280,6 +294,21 @@ TEST(Matrix, TestHatOperator) EXPECT_EQ(mat1, mat2); } +TEST(Matrix, TestHatOperatorFloat) +{ + Base::Vector3f vec{1.0, 2.0, 3.0}; + + Base::Matrix4D mat1; + mat1.Hat(vec); + + Base::Matrix4D mat2{0.0F, -vec.z, vec.y, 0.0F, + vec.z, 0.0F, -vec.x, 0.0F, + -vec.y, vec.x, 0.0F, 0.0F, + 0.0F, 0.0F, 0.0F, 1.0F}; + + EXPECT_EQ(mat1, mat2); +} + TEST(Matrix, TestDyadic) { Base::Vector3d vec{1.0, 2.0, 3.0}; @@ -295,6 +324,21 @@ TEST(Matrix, TestDyadic) EXPECT_EQ(mat1, mat2); } +TEST(Matrix, TestDyadicFloat) +{ + Base::Vector3f vec{1.0F, 2.0F, 3.0F}; + + Base::Matrix4D mat1; + mat1.Outer(vec, vec); + + Base::Matrix4D mat2{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + EXPECT_EQ(mat1, mat2); +} + TEST(Matrix, TestDecomposeScale) { Base::Matrix4D mat; @@ -333,6 +377,18 @@ TEST(Matrix, TestDecomposeMove) EXPECT_EQ(res[3], mat); } +TEST(Matrix, TestDecomposeMoveFloat) +{ + Base::Matrix4D mat; + mat.move(Base::Vector3f(1.0F, 2.0F, 3.0F)); + auto res = mat.decompose(); + + EXPECT_TRUE(res[0].isUnity()); + EXPECT_TRUE(res[1].isUnity()); + EXPECT_TRUE(res[2].isUnity()); + EXPECT_EQ(res[3], mat); +} + TEST(Matrix, TestDecompose) { Base::Matrix4D mat; @@ -401,5 +457,121 @@ TEST(Matrix, TestRotAxisFormula) //NOLINT EXPECT_DOUBLE_EQ(mat1[2][1], mat2[2][1]); EXPECT_DOUBLE_EQ(mat1[2][2], mat2[2][2]); } + +TEST(Matrix, TestTransform) +{ + Base::Matrix4D mat; + mat.rotZ(Base::toRadians(90.0)); + + Base::Matrix4D unity; + unity.transform(Base::Vector3d(10.0, 0.0, 0.0), mat); + + Base::Matrix4D mov{0.0, -1.0, 0.0, 10.0, + 1.0, 0.0, 0.0, -10.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + EXPECT_EQ(unity, mov); +} + +TEST(Matrix, TestTransformFloat) +{ + Base::Matrix4D mat; + mat.rotZ(Base::toRadians(90.0)); + + Base::Matrix4D mat2; + mat2.transform(Base::Vector3f(10.0F, 0.0F, 0.0F), mat); + + Base::Matrix4D mov{0.0, -1.0, 0.0, 10.0, + 1.0, 0.0, 0.0, -10.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + EXPECT_EQ(mat2, mov); +} + +TEST(Matrix, TestInverseOrthogonal) +{ + Base::Matrix4D mat; + mat.rotZ(Base::toRadians(90.0)); + + Base::Matrix4D mat2; + mat2.transform(Base::Vector3d(10.0, 0.0, 0.0), mat); + mat2.inverseOrthogonal(); + + Base::Matrix4D mov{0.0, 1.0, 0.0, 10.0, + -1.0, 0.0, 0.0, 10.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + EXPECT_EQ(mat2, mov); +} + +TEST(Matrix, TestTranspose) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, 7.0}; + + Base::Matrix4D trp{1.0, 5.0, 9.0, 4.0, + 2.0, 6.0, 1.0, 5.0, + 3.0, 7.0, 2.0, 6.0, + 4.0, 8.0, 3.0, 7.0}; + + mat.transpose(); + EXPECT_EQ(mat, trp); +} + +TEST(Matrix, TestTrace) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, 7.0}; + EXPECT_DOUBLE_EQ(mat.trace(), 16.0); + EXPECT_DOUBLE_EQ(mat.trace3(), 9.0); +} + +TEST(Matrix, TestSetAndGetMatrix) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + std::array values; + mat.getMatrix(values.data()); + Base::Matrix4D inp; + inp.setMatrix(values.data()); + + EXPECT_EQ(mat, inp); +} + +TEST(Matrix, TestSetAndGetGLMatrix) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + std::array values; + mat.getGLMatrix(values.data()); + Base::Matrix4D inp; + inp.setGLMatrix(values.data()); + + EXPECT_EQ(mat, inp); +} + +TEST(Matrix, TestToAndFromString) +{ + Base::Matrix4D mat{1.0, 2.0, 3.0, 0.0, + 2.0, 4.0, 6.0, 0.0, + 3.0, 6.0, 9.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + std::string str = mat.toString(); + Base::Matrix4D inp; + inp.fromString(str); + + EXPECT_EQ(mat, inp); +} // clang-format on // NOLINTEND(cppcoreguidelines-*,readability-magic-numbers) From 11a05b71b77334ac353ba19515e4d982c248fe07 Mon Sep 17 00:00:00 2001 From: wmayer Date: Tue, 13 May 2025 15:56:20 +0200 Subject: [PATCH 032/126] Base: Use nested std::array for Matrix4D class --- src/Base/Matrix.cpp | 39 ++++++++++++++++++--------------------- src/Base/Matrix.h | 11 ++++++----- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Base/Matrix.cpp b/src/Base/Matrix.cpp index 8bd35ec1ab..8a9f5019c0 100644 --- a/src/Base/Matrix.cpp +++ b/src/Base/Matrix.cpp @@ -35,30 +35,30 @@ using namespace Base; // clang-format off Matrix4D::Matrix4D() - : dMtrx4D {{1., 0., 0., 0.}, - {0., 1., 0., 0.}, - {0., 0., 1., 0.}, - {0., 0., 0., 1.}} + : dMtrx4D {{{1., 0., 0., 0.}, + {0., 1., 0., 0.}, + {0., 0., 1., 0.}, + {0., 0., 0., 1.}}} {} Matrix4D::Matrix4D(float a11, float a12, float a13, float a14, float a21, float a22, float a23, float a24, float a31, float a32, float a33, float a34, float a41, float a42, float a43, float a44) - : dMtrx4D {{a11, a12, a13, a14}, - {a21, a22, a23, a24}, - {a31, a32, a33, a34}, - {a41, a42, a43, a44}} + : dMtrx4D {{{a11, a12, a13, a14}, + {a21, a22, a23, a24}, + {a31, a32, a33, a34}, + {a41, a42, a43, a44}}} {} Matrix4D::Matrix4D(double a11, double a12, double a13, double a14, double a21, double a22, double a23, double a24, double a31, double a32, double a33, double a34, double a41, double a42, double a43, double a44) - : dMtrx4D {{a11, a12, a13, a14}, - {a21, a22, a23, a24}, - {a31, a32, a33, a34}, - {a41, a42, a43, a44}} + : dMtrx4D {{{a11, a12, a13, a14}, + {a21, a22, a23, a24}, + {a31, a32, a33, a34}, + {a41, a42, a43, a44}}} {} // clang-format on @@ -700,15 +700,12 @@ void Matrix4D::Print() const void Matrix4D::transpose() { - double dNew[4][4]; - - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - dNew[j][i] = dMtrx4D[i][j]; - } - } - - memcpy(dMtrx4D, dNew, sizeof(dMtrx4D)); + std::swap(dMtrx4D[0][1], dMtrx4D[1][0]); + std::swap(dMtrx4D[0][2], dMtrx4D[2][0]); + std::swap(dMtrx4D[0][3], dMtrx4D[3][0]); + std::swap(dMtrx4D[1][2], dMtrx4D[2][1]); + std::swap(dMtrx4D[1][3], dMtrx4D[3][1]); + std::swap(dMtrx4D[2][3], dMtrx4D[3][2]); } diff --git a/src/Base/Matrix.h b/src/Base/Matrix.h index 665c69376a..3e0ba98794 100644 --- a/src/Base/Matrix.h +++ b/src/Base/Matrix.h @@ -106,9 +106,9 @@ public: /// Comparison inline bool operator==(const Matrix4D& mat) const; /// Index operator - inline double* operator[](unsigned int usNdx); + inline std::array& operator[](unsigned int usNdx); /// Index operator - inline const double* operator[](unsigned int usNdx) const; + inline const std::array& operator[](unsigned int usNdx) const; /// Get vector of row inline Vector3d getRow(unsigned int usNdx) const; /// Get vector of column @@ -234,7 +234,8 @@ public: void fromString(const std::string& str); private: - double dMtrx4D[4][4]; + using Array2d = std::array, 4>; + Array2d dMtrx4D; }; inline Matrix4D Matrix4D::operator+(const Matrix4D& mat) const @@ -386,12 +387,12 @@ inline Vector3f& operator*=(Vector3f& vec, const Matrix4D& mat) return vec; } -inline double* Matrix4D::operator[](unsigned int usNdx) +inline std::array& Matrix4D::operator[](unsigned int usNdx) { return dMtrx4D[usNdx]; } -inline const double* Matrix4D::operator[](unsigned int usNdx) const +inline const std::array& Matrix4D::operator[](unsigned int usNdx) const { return dMtrx4D[usNdx]; } From ae410afbe1b51f87abdc47e39ec36e02acf6b045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tr=C3=B6ger?= Date: Mon, 16 Jun 2025 20:23:18 +0200 Subject: [PATCH 033/126] FEM: Data extraction lint updates --- .../femviewprovider/view_base_fempostextractors.py | 1 - .../femviewprovider/view_base_fempostvisualization.py | 6 ------ src/Mod/Fem/femviewprovider/view_post_histogram.py | 4 ++-- src/Mod/Fem/femviewprovider/view_post_lineplot.py | 5 ++--- src/Mod/Fem/femviewprovider/view_post_table.py | 11 ++--------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index 338aabb905..bb4aca0934 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -35,7 +35,6 @@ import FreeCADGui from PySide import QtGui -import femobjects.base_fempostextractors as fpe from femtaskpanels import task_post_extractor class VPPostExtractor: diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index b3d244b1ef..9b357a4d0c 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -29,15 +29,9 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief view provider for post visualization object -from PySide import QtGui, QtCore - -import Plot import FreeCAD import FreeCADGui -from . import view_base_femobject -_GuiPropHelper = view_base_femobject._GuiPropHelper - class VPPostVisualization: """ A View Provider for visualization objects diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 1079808fcd..337ad62f86 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -33,7 +33,6 @@ import FreeCAD import FreeCADGui import Plot -import FemGui from PySide import QtGui, QtCore from PySide.QtCore import QT_TRANSLATE_NOOP @@ -48,7 +47,8 @@ from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_histogram -_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper class EditViewWidget(QtGui.QWidget): diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 0a61fd771d..3066e009b7 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -33,7 +33,6 @@ import FreeCAD import FreeCADGui import Plot -import FemGui from PySide import QtGui, QtCore from PySide.QtCore import QT_TRANSLATE_NOOP @@ -47,9 +46,9 @@ from vtkmodules.numpy_interface.dataset_adapter import VTKArray from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_lineplot -from femguiutils import post_visualization as pv -_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper class EditViewWidget(QtGui.QWidget): diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 75458ef9d7..536a3665e2 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -32,23 +32,16 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui -import Plot -import FemGui from PySide import QtGui, QtCore from PySide.QtCore import QT_TRANSLATE_NOOP -import io -import numpy as np -import matplotlib as mpl - -from vtkmodules.numpy_interface.dataset_adapter import VTKArray - from . import view_base_fempostextractors from . import view_base_fempostvisualization from femtaskpanels import task_post_table from femguiutils import vtk_table_view as vtv -_GuiPropHelper = view_base_fempostvisualization._GuiPropHelper +from . import view_base_femobject +_GuiPropHelper = view_base_femobject._GuiPropHelper class EditViewWidget(QtGui.QWidget): From 0b2dae296da9b3b98c2cdd158898346f0707ee09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:26:48 +0000 Subject: [PATCH 034/126] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/Fem/App/FemPostFilterPyImp.cpp | 2 +- src/Mod/Fem/App/FemPostObjectPyImp.cpp | 8 +- src/Mod/Fem/App/FemPostPipelinePyImp.cpp | 6 +- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 14 ++- src/Mod/Fem/Gui/TaskPostBoxes.h | 3 +- src/Mod/Fem/Gui/TaskPostExtraction.cpp | 17 ++- src/Mod/Fem/Gui/Workbench.cpp | 4 +- src/Mod/Fem/InitGui.py | 1 + src/Mod/Fem/ObjectsFem.py | 9 ++ src/Mod/Fem/femcommands/commands.py | 2 + src/Mod/Fem/femcommands/manager.py | 1 + src/Mod/Fem/femguiutils/data_extraction.py | 14 +-- src/Mod/Fem/femguiutils/extract_link_view.py | 113 +++++++++--------- src/Mod/Fem/femguiutils/post_visualization.py | 45 ++++--- src/Mod/Fem/femguiutils/vtk_table_view.py | 37 +++--- .../Fem/femobjects/base_fempostextractors.py | 24 ++-- .../femobjects/base_fempostvisualizations.py | 20 ++-- src/Mod/Fem/femobjects/post_extract1D.py | 35 ++++-- src/Mod/Fem/femobjects/post_extract2D.py | 36 ++++-- src/Mod/Fem/femobjects/post_glyphfilter.py | 1 + src/Mod/Fem/femobjects/post_histogram.py | 53 ++++---- src/Mod/Fem/femobjects/post_lineplot.py | 48 ++++---- src/Mod/Fem/femobjects/post_table.py | 46 +++---- .../Fem/femtaskpanels/base_fempostpanel.py | 12 +- .../Fem/femtaskpanels/task_post_extractor.py | 3 - .../femtaskpanels/task_post_glyphfilter.py | 1 - .../Fem/femtaskpanels/task_post_histogram.py | 19 ++- .../Fem/femtaskpanels/task_post_lineplot.py | 17 ++- src/Mod/Fem/femtaskpanels/task_post_table.py | 4 +- .../femviewprovider/view_base_femobject.py | 1 + .../view_base_fempostextractors.py | 11 +- .../view_base_fempostvisualization.py | 8 +- .../femviewprovider/view_post_histogram.py | 68 ++++++----- .../Fem/femviewprovider/view_post_lineplot.py | 93 ++++++++------ .../Fem/femviewprovider/view_post_table.py | 19 +-- 35 files changed, 447 insertions(+), 348 deletions(-) diff --git a/src/Mod/Fem/App/FemPostFilterPyImp.cpp b/src/Mod/Fem/App/FemPostFilterPyImp.cpp index 2e09c9e3f0..69a5d8f23e 100644 --- a/src/Mod/Fem/App/FemPostFilterPyImp.cpp +++ b/src/Mod/Fem/App/FemPostFilterPyImp.cpp @@ -199,7 +199,7 @@ PyObject* FemPostFilterPy::getOutputAlgorithm(PyObject* args) auto algorithm = getFemPostFilterPtr()->getFilterOutput(); PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); - return Py::new_reference_to(py_algorithm); + return Py::new_reference_to(py_algorithm); #else PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); Py_Return; diff --git a/src/Mod/Fem/App/FemPostObjectPyImp.cpp b/src/Mod/Fem/App/FemPostObjectPyImp.cpp index a35e0b569a..8b242abcf7 100644 --- a/src/Mod/Fem/App/FemPostObjectPyImp.cpp +++ b/src/Mod/Fem/App/FemPostObjectPyImp.cpp @@ -30,9 +30,9 @@ #include "FemPostObjectPy.cpp" #ifdef FC_USE_VTK_PYTHON - #include - #include -#endif //BUILD_FEM_VTK +#include +#include +#endif // BUILD_FEM_VTK using namespace Fem; @@ -71,7 +71,7 @@ PyObject* FemPostObjectPy::getDataSet(PyObject* args) auto dataset = getFemPostObjectPtr()->getDataSet(); if (dataset) { PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(dataset); - return Py::new_reference_to(py_algorithm); + return Py::new_reference_to(py_algorithm); } return Py_None; #else diff --git a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp index 388aed35de..2c493074e6 100644 --- a/src/Mod/Fem/App/FemPostPipelinePyImp.cpp +++ b/src/Mod/Fem/App/FemPostPipelinePyImp.cpp @@ -35,8 +35,8 @@ // clang-format on #ifdef FC_USE_VTK_PYTHON - #include -#endif //BUILD_FEM_VTK +#include +#endif // BUILD_FEM_VTK using namespace Fem; @@ -329,7 +329,7 @@ PyObject* FemPostPipelinePy::getOutputAlgorithm(PyObject* args) auto algorithm = getFemPostPipelinePtr()->getOutputAlgorithm(); PyObject* py_algorithm = vtkPythonUtil::GetObjectFromPointer(algorithm); - return Py::new_reference_to(py_algorithm); + return Py::new_reference_to(py_algorithm); #else PyErr_SetString(PyExc_NotImplementedError, "VTK python wrapper not available"); Py_Return; diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 88cf26f8f9..e50bb0bf98 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -213,7 +213,11 @@ TaskPostWidget::TaskPostWidget(Gui::ViewProviderDocumentObject* view, setWindowIcon(icon); m_icon = icon; - m_connection = m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, this, boost::placeholders::_1, boost::placeholders::_2)); + m_connection = + m_object->signalChanged.connect(boost::bind(&TaskPostWidget::handlePropertyChange, + this, + boost::placeholders::_1, + boost::placeholders::_2)); } TaskPostWidget::~TaskPostWidget() @@ -404,7 +408,8 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) } } -void TaskDlgPost::processCollapsedWidgets() { +void TaskDlgPost::processCollapsedWidgets() +{ for (auto& widget : Content) { auto* task_box = dynamic_cast(widget); @@ -417,7 +422,7 @@ void TaskDlgPost::processCollapsedWidgets() { if (!post_widget || !post_widget->initiallyCollapsed()) { continue; } - post_widget->setGeometry(QRect(QPoint(0,0), post_widget->sizeHint())); + post_widget->setGeometry(QRect(QPoint(0, 0), post_widget->sizeHint())); task_box->hideGroupBox(); } } @@ -584,7 +589,8 @@ void TaskPostFrames::applyPythonCode() // we apply the views widgets python code } -bool TaskPostFrames::initiallyCollapsed() { +bool TaskPostFrames::initiallyCollapsed() +{ return (ui->FrameTable->rowCount() == 0); } diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index 3c60fe8ddf..816dafb080 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -157,7 +157,8 @@ public: virtual void apply() {}; // returns if the widget shall be collapsed when opening the task dialog - virtual bool initiallyCollapsed() { + virtual bool initiallyCollapsed() + { return false; }; diff --git a/src/Mod/Fem/Gui/TaskPostExtraction.cpp b/src/Mod/Fem/Gui/TaskPostExtraction.cpp index 4de2401c7c..e61033957c 100644 --- a/src/Mod/Fem/Gui/TaskPostExtraction.cpp +++ b/src/Mod/Fem/Gui/TaskPostExtraction.cpp @@ -48,9 +48,7 @@ using namespace Gui; // box to handle data extractions TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* parent) - : TaskPostWidget(view, - Gui::BitmapFactory().pixmap("FEM_PostHistogram"), QString(), - 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 @@ -73,7 +71,7 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* m_panel = Py::Object(method.apply(args)); } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } @@ -97,7 +95,8 @@ TaskPostExtraction::TaskPostExtraction(ViewProviderFemPostObject* view, QWidget* Base::Console().error("Unable to import data extraction widget\n"); }; -TaskPostExtraction::~TaskPostExtraction() { +TaskPostExtraction::~TaskPostExtraction() +{ Base::PyGILStateLocker lock; try { @@ -123,7 +122,7 @@ void TaskPostExtraction::onPostDataChanged(Fem::FemPostObject* obj) } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } }; @@ -139,7 +138,7 @@ bool TaskPostExtraction::isGuiTaskOnly() } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } @@ -156,7 +155,7 @@ void TaskPostExtraction::apply() } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } } @@ -172,7 +171,7 @@ bool TaskPostExtraction::initiallyCollapsed() } } catch (Py::Exception&) { - Base::PyException e; // extract the Python error text + Base::PyException e; // extract the Python error text e.reportException(); } diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 44c1186c6f..31f396fbe9 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -218,7 +218,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const #ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif - ; + ; #endif Gui::ToolBarItem* utils = new Gui::ToolBarItem(root); @@ -374,7 +374,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const #ifdef FC_USE_VTK_PYTHON << "FEM_PostVisualization" #endif - ; + ; #endif Gui::MenuItem* utils = new Gui::MenuItem; diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 35c835f81e..e7d0a2ada7 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -83,6 +83,7 @@ class FemWorkbench(Workbench): # check vtk version to potentially find missmatchs if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() def GetClassName(self): diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 1ca11e2566..2bd6e74056 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -696,6 +696,7 @@ def makePostLineplot(doc, name="Lineplot"): post_lineplot.PostLineplot(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplot(obj.ViewObject) return obj @@ -710,6 +711,7 @@ def makePostLineplotFieldData(doc, name="FieldData2D"): post_lineplot.PostLineplotFieldData(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotFieldData(obj.ViewObject) return obj @@ -724,6 +726,7 @@ def makePostLineplotIndexOverFrames(doc, name="IndexOverFrames2D"): post_lineplot.PostLineplotIndexOverFrames(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_lineplot + view_post_lineplot.VPPostLineplotIndexOverFrames(obj.ViewObject) return obj @@ -738,6 +741,7 @@ def makePostHistogram(doc, name="Histogram"): post_histogram.PostHistogram(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogram(obj.ViewObject) return obj @@ -752,6 +756,7 @@ def makePostHistogramFieldData(doc, name="FieldData1D"): post_histogram.PostHistogramFieldData(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramFieldData(obj.ViewObject) return obj @@ -766,6 +771,7 @@ def makePostHistogramIndexOverFrames(doc, name="IndexOverFrames1D"): post_histogram.PostHistogramIndexOverFrames(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_histogram + view_post_histogram.VPPostHistogramIndexOverFrames(obj.ViewObject) return obj @@ -780,6 +786,7 @@ def makePostTable(doc, name="Table"): post_table.PostTable(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_table + view_post_table.VPPostTable(obj.ViewObject) return obj @@ -794,6 +801,7 @@ def makePostTableFieldData(doc, name="FieldData1D"): post_table.PostTableFieldData(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_table + view_post_table.VPPostTableFieldData(obj.ViewObject) return obj @@ -808,6 +816,7 @@ def makePostTableIndexOverFrames(doc, name="IndexOverFrames1D"): post_table.PostTableIndexOverFrames(obj) if FreeCAD.GuiUp: from femviewprovider import view_post_table + view_post_table.VPPostTableIndexOverFrames(obj.ViewObject) return obj diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 8c85fc7270..5d662074be 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1231,6 +1231,7 @@ 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()) @@ -1295,4 +1296,5 @@ if "BUILD_FEM_VTK_PYTHON" in FreeCAD.__cmake__: import femobjects.post_table from femguiutils import post_visualization + post_visualization.setup_commands("FEM_PostVisualization") diff --git a/src/Mod/Fem/femcommands/manager.py b/src/Mod/Fem/femcommands/manager.py index f653d053a0..bb2edc3e05 100644 --- a/src/Mod/Fem/femcommands/manager.py +++ b/src/Mod/Fem/femcommands/manager.py @@ -381,6 +381,7 @@ class CommandManager: # check if we should use python filter from femguiutils.vtk_module_handling import vtk_compatibility_abort + if vtk_compatibility_abort(True): return diff --git a/src/Mod/Fem/femguiutils/data_extraction.py b/src/Mod/Fem/femguiutils/data_extraction.py index cb55295703..dfe0cea7f8 100644 --- a/src/Mod/Fem/femguiutils/data_extraction.py +++ b/src/Mod/Fem/femguiutils/data_extraction.py @@ -37,8 +37,7 @@ from vtkmodules.vtkCommonCore import vtkVersion from vtkmodules.vtkCommonDataModel import vtkTable from vtkmodules.vtkFiltersGeneral import vtkSplitColumnComponents -if vtkVersion.GetVTKMajorVersion() > 9 and \ - vtkVersion.GetVTKMinorVersion() > 3: +if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3: from vtkmodules.vtkFiltersCore import vtkAttributeDataToTableFilter else: from vtkmodules.vtkInfovisCore import vtkDataObjectToTable @@ -51,6 +50,7 @@ import femobjects.base_fempostextractors as extr from femtaskpanels.base_fempostpanel import _BasePostTaskPanel from . import extract_link_view + ExtractLinkView = extract_link_view.ExtractLinkView @@ -83,11 +83,10 @@ class DataExtraction(_BasePostTaskPanel): # 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().addSpacing(self.widget.Data.size().height() / 3) self.widget.layout().addWidget(self._extraction_view) self._extraction_view.repopulate() - @QtCore.Slot() def showData(self): @@ -96,7 +95,7 @@ class DataExtraction(_BasePostTaskPanel): widget = vtk_table_view.VtkTableView(self.data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(1500, 900) @@ -110,7 +109,7 @@ class DataExtraction(_BasePostTaskPanel): widget = vtk_table_view.VtkTableView(self.summary_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(600, 900) @@ -126,8 +125,7 @@ class DataExtraction(_BasePostTaskPanel): if not algo: self.data_model.setTable(vtkTable()) - if vtkVersion.GetVTKMajorVersion() > 9 and \ - vtkVersion.GetVTKMinorVersion() > 3: + if vtkVersion.GetVTKMajorVersion() > 9 and vtkVersion.GetVTKMinorVersion() > 3: filter = vtkAttributeDataToTableFilter() else: filter = vtkDataObjectToTable() diff --git a/src/Mod/Fem/femguiutils/extract_link_view.py b/src/Mod/Fem/femguiutils/extract_link_view.py index a6f6067080..eec8ba6927 100644 --- a/src/Mod/Fem/femguiutils/extract_link_view.py +++ b/src/Mod/Fem/femguiutils/extract_link_view.py @@ -44,6 +44,7 @@ translate = FreeCAD.Qt.translate # a model showing available visualizations and possible extractions # ################################################################# + def build_new_visualization_tree_model(): # model that shows all options to create new visualizations @@ -67,6 +68,7 @@ def build_new_visualization_tree_model(): return model + def build_add_to_visualization_tree_model(): # model that shows all possible visualization objects to add data to @@ -92,7 +94,9 @@ def build_add_to_visualization_tree_model(): for ext in visualizations[vis_type].extractions: icon = FreeCADGui.getIcon(ext.icon) name = ext.name.removeprefix(vis_type) - ext_item = QtGui.QStandardItem(icon, translate("FEM", "Add {}").format(name)) + ext_item = QtGui.QStandardItem( + icon, translate("FEM", "Add {}").format(name) + ) ext_item.setFlags(QtGui.Qt.ItemIsEnabled) ext_item.setData(ext) vis_item.appendRow(ext_item) @@ -102,10 +106,13 @@ def build_add_to_visualization_tree_model(): 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, translate("FEM", "From {}").format(post_object.Label)) + post_item = QtGui.QStandardItem( + post_object.ViewObject.Icon, translate("FEM", "From {}").format(post_object.Label) + ) post_item.setFlags(QtGui.Qt.ItemIsEnabled) post_item.setData(post_object) @@ -150,9 +157,11 @@ def build_add_from_data_tree_model(vis_type): return model + # implementation of GUI and its functionality # ########################################### + class _ElideToolButton(QtGui.QToolButton): # tool button that elides its text, and left align icon and text @@ -174,7 +183,7 @@ class _ElideToolButton(QtGui.QToolButton): button_size = super().sizeHint() icn_size = self.iconSize() min_margin = max((button_size - icn_size).height(), 6) - return QtCore.QSize(self.iconSize().width()+10, icn_size.height() + min_margin) + return QtCore.QSize(self.iconSize().width() + 10, icn_size.height() + min_margin) def paintEvent(self, event): @@ -190,11 +199,10 @@ class _ElideToolButton(QtGui.QToolButton): margin = (self.height() - self.iconSize().height()) / 2 icn_width = self.iconSize().width() if self._icon.isNull(): - icn_width = 0; - + icn_width = 0 fm = self.fontMetrics() - txt_size = self.width() - icn_width - 2*margin + txt_size = self.width() - icn_width - 2 * margin if not self._icon.isNull(): # we add the margin between icon and text txt_size -= margin @@ -205,7 +213,7 @@ class _ElideToolButton(QtGui.QToolButton): xpos = margin if not self._icon.isNull() and txt_size < txt_min: # center icon - xpos = self.width()/2 - self.iconSize().width()/2 + xpos = self.width() / 2 - self.iconSize().width() / 2 if not self._icon.isNull(): match type(self._icon): @@ -213,10 +221,12 @@ class _ElideToolButton(QtGui.QToolButton): painter.drawPixmap(xpos, margin, self._icon.scaled(self.iconSize())) xpos += self.iconSize().width() case QtGui.QIcon: - self._icon.paint(painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize())) + self._icon.paint( + painter, QtCore.QRect(QtCore.QPoint(margin, margin), self.iconSize()) + ) xpos += self.iconSize().width() - xpos += margin # the margin to the text + xpos += margin # the margin to the text if txt_size >= txt_min: text = fm.elidedText(self._text, QtGui.Qt.ElideMiddle, txt_size) @@ -227,7 +237,7 @@ class _ElideToolButton(QtGui.QToolButton): class _TreeChoiceButton(QtGui.QToolButton): - selection = QtCore.Signal(object,object) + selection = QtCore.Signal(object, object) def __init__(self, model): super().__init__() @@ -254,9 +264,10 @@ class _TreeChoiceButton(QtGui.QToolButton): self.popup = QtGui.QWidgetAction(self) self.popup.setDefaultWidget(self.tree_view) self.setPopupMode(QtGui.QToolButton.InstantPopup) - self.addAction(self.popup); + self.addAction(self.popup) QtCore.Slot(QtCore.QModelIndex) + def selectIndex(self, index): item = self.model.itemFromIndex(index) @@ -274,6 +285,7 @@ class _TreeChoiceButton(QtGui.QToolButton): # check if we should be disabled self.setEnabled(bool(model.rowCount())) + class _SettingsPopup(QtGui.QMenu): close = QtCore.Signal() @@ -297,7 +309,7 @@ class _SettingsPopup(QtGui.QMenu): widget.setLayout(vbox) vbox2 = QtGui.QVBoxLayout() - vbox2.setContentsMargins(0,0,0,0) + vbox2.setContentsMargins(0, 0, 0, 0) vbox2.addWidget(widget) self.setLayout(vbox2) @@ -321,7 +333,7 @@ class _SettingsPopup(QtGui.QMenu): class _SummaryWidget(QtGui.QWidget): - delete = QtCore.Signal(object, object) # to delete: document object, summary widget + delete = QtCore.Signal(object, object) # to delete: document object, summary widget def __init__(self, st_object, extractor, post_dialog): super().__init__() @@ -335,17 +347,16 @@ class _SummaryWidget(QtGui.QWidget): # build the UI hbox = QtGui.QHBoxLayout() - hbox.setContentsMargins(6,0,6,0) + hbox.setContentsMargins(6, 0, 6, 0) hbox.setSpacing(2) self.extrButton = self._button(extractor.ViewObject.Icon, extr_label) self.viewButton = self._button(extr_repr[0], extr_repr[1], 1) size = self.viewButton.iconSize() - size.setWidth(size.width()*2) + size.setWidth(size.width() * 2) self.viewButton.setIconSize(size) - if st_object: self.stButton = self._button(st_object.ViewObject.Icon, st_object.Label) hbox.addWidget(self.stButton) @@ -357,7 +368,9 @@ class _SummaryWidget(QtGui.QWidget): self.viewButton.hide() self.warning = QtGui.QLabel(self) - self.warning.full_text = translate("FEM", "{}: Data source not available").format(extractor.Label) + self.warning.full_text = translate("FEM", "{}: Data source not available").format( + extractor.Label + ) hbox.addWidget(self.warning) self.rmButton = QtGui.QToolButton(self) @@ -371,16 +384,16 @@ class _SummaryWidget(QtGui.QWidget): # add the separation line vbox = QtGui.QVBoxLayout() - vbox.setContentsMargins(0,0,0,0) + vbox.setContentsMargins(0, 0, 0, 0) vbox.setSpacing(5) vbox.addItem(hbox) self.frame = QtGui.QFrame(self) - self.frame.setFrameShape(QtGui.QFrame.HLine); + self.frame.setFrameShape(QtGui.QFrame.HLine) vbox.addWidget(self.frame) policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.setSizePolicy(policy) - #self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) + # self.setMinimumSize(self.extrButton.sizeHint()+self.frame.sizeHint()*3) self.setLayout(vbox) # connect actions. We add functions to widget, as well as the data we need, @@ -393,8 +406,7 @@ class _SummaryWidget(QtGui.QWidget): self.rmButton.clicked.connect(self.deleteTriggered) # make sure initial drawing happened - #self._redraw() - + # self._redraw() def _button(self, icon, text, stretch=2): @@ -408,7 +420,6 @@ class _SummaryWidget(QtGui.QWidget): btn.setSizePolicy(policy) return btn - @QtCore.Slot() def showVisualization(self): if vis.is_visualization_object(self._st_object): @@ -426,14 +437,18 @@ class _SummaryWidget(QtGui.QWidget): # very weird values. Hence we build the coords of the widget # ourself - summary = dialog.parent() # == self + summary = dialog.parent() # == self base_widget = summary.parent() viewport = summary.parent() scroll = viewport.parent() - top_left = summary.geometry().topLeft() + base_widget.geometry().topLeft() + viewport.geometry().topLeft() - delta = (summary.width() - dialog.size().width())/2 - local_point = QtCore.QPoint(top_left.x()+delta, top_left.y()+summary.height()) + top_left = ( + summary.geometry().topLeft() + + base_widget.geometry().topLeft() + + viewport.geometry().topLeft() + ) + delta = (summary.width() - dialog.size().width()) / 2 + local_point = QtCore.QPoint(top_left.x() + delta, top_left.y() + summary.height()) global_point = scroll.mapToGlobal(local_point) dialog.setGeometry(QtCore.QRect(global_point, dialog.sizeHint())) @@ -485,7 +500,6 @@ class _SummaryWidget(QtGui.QWidget): self.extrButton.setToolTip(extr_label) - class ExtractLinkView(QtGui.QWidget): def __init__(self, obj, is_source, post_dialog): @@ -506,7 +520,7 @@ class ExtractLinkView(QtGui.QWidget): self._scroll_view.setWidgetResizable(True) self._scroll_widget = QtGui.QWidget(self._scroll_view) vbox = QtGui.QVBoxLayout() - vbox.setContentsMargins(0,6,0,0) + vbox.setContentsMargins(0, 6, 0, 0) vbox.addStretch() self._scroll_widget.setLayout(vbox) self._scroll_view.setWidget(self._scroll_widget) @@ -541,7 +555,7 @@ class ExtractLinkView(QtGui.QWidget): hbox.addWidget(self._add) vbox = QtGui.QVBoxLayout() - vbox.setContentsMargins(0,0,0,0) + vbox.setContentsMargins(0, 0, 0, 0) vbox.addItem(hbox) vbox.addWidget(self._scroll_view) @@ -616,7 +630,8 @@ class ExtractLinkView(QtGui.QWidget): return None - QtCore.Slot(object, object) # visualization data, extraction data + QtCore.Slot(object, object) # visualization data, extraction data + def newVisualization(self, vis_data, ext_data): FreeCADGui.addModule(vis_data.module) @@ -630,17 +645,13 @@ class ExtractLinkView(QtGui.QWidget): analysis = self._find_parent_analysis(self._object) if analysis: - FreeCADGui.doCommand( - f"FreeCAD.ActiveDocument.{analysis.Name}.addObject(visualization)" - ) + 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"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}") # default values: color color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() if color_prop: @@ -648,15 +659,13 @@ class ExtractLinkView(QtGui.QWidget): f"extraction.ViewObject.{color_prop} = visualization.ViewObject.Proxy.get_next_default_color()" ) - FreeCADGui.doCommand( - f"visualization.addObject(extraction)" - ) - + FreeCADGui.doCommand(f"visualization.addObject(extraction)") self._post_dialog._recompute() self.repopulate() - QtCore.Slot(object, object) # visualization object, extraction data + QtCore.Slot(object, object) # visualization object, extraction data + def addExtractionToVisualization(self, vis_obj, ext_data): FreeCADGui.addModule(ext_data.module) @@ -666,9 +675,7 @@ class ExtractLinkView(QtGui.QWidget): 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"extraction.Source = FreeCAD.ActiveDocument.{self._object.Name}") # default values: color color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() @@ -677,14 +684,13 @@ class ExtractLinkView(QtGui.QWidget): f"extraction.ViewObject.{color_prop} = (Gui.ActiveDocument.{vis_obj.Name}.Proxy.get_next_default_color())" ) - FreeCADGui.doCommand( - f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)" - ) + FreeCADGui.doCommand(f"App.ActiveDocument.{vis_obj.Name}.addObject(extraction)") self._post_dialog._recompute() self.repopulate() - QtCore.Slot(object, object) # post object, extraction data + QtCore.Slot(object, object) # post object, extraction data + def addExtractionToPostObject(self, post_obj, ext_data): FreeCADGui.addModule(ext_data.module) @@ -694,9 +700,7 @@ class ExtractLinkView(QtGui.QWidget): 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"extraction.Source = FreeCAD.ActiveDocument.{post_obj.Name}") # default values for color color_prop = FreeCADGui.ActiveDocument.ActiveObject.Proxy.get_default_color_property() @@ -705,10 +709,7 @@ class ExtractLinkView(QtGui.QWidget): f"extraction.ViewObject.{color_prop} = Gui.ActiveDocument.{self._object.Name}.Proxy.get_next_default_color()" ) - FreeCADGui.doCommand( - f"App.ActiveDocument.{self._object.Name}.addObject(extraction)" - ) + 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 index 6724fda779..557c177cd3 100644 --- a/src/Mod/Fem/femguiutils/post_visualization.py +++ b/src/Mod/Fem/femguiutils/post_visualization.py @@ -45,6 +45,7 @@ import FreeCAD _registry = {} + @dataclass class _Extraction: @@ -55,6 +56,7 @@ class _Extraction: module: str factory: str + @dataclass class _Visualization: @@ -64,6 +66,7 @@ class _Visualization: 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: @@ -71,7 +74,10 @@ def register_visualization(visualization_type, icon, module, factory): _registry[visualization_type] = _Visualization(visualization_type, icon, module, factory, []) -def register_extractor(visualization_type, extraction_type, icon, dimension, etype, 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") @@ -79,6 +85,7 @@ def register_extractor(visualization_type, extraction_type, icon, dimension, ety extraction = _Extraction(extraction_type, icon, dimension, etype, module, factory) _registry[visualization_type].extractions.append(extraction) + def get_registered_visualizations(): return copy.deepcopy(_registry) @@ -86,6 +93,7 @@ def get_registered_visualizations(): def _to_command_name(name): return "FEM_PostVisualization" + name + class _VisualizationGroupCommand: def GetCommands(self): @@ -97,14 +105,19 @@ class _VisualizationGroupCommand: return 0 def GetResources(self): - return { 'MenuText': QtCore.QT_TRANSLATE_NOOP("FEM", 'Data Visualizations'), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("FEM", 'Different visualizations to show post processing data in')} + return { + "MenuText": QtCore.QT_TRANSLATE_NOOP("FEM", "Data Visualizations"), + "ToolTip": QtCore.QT_TRANSLATE_NOOP( + "FEM", "Different visualizations to show post processing data in" + ), + } def IsActive(self): if not FreeCAD.ActiveDocument: return False import FemGui + return bool(FemGui.getActiveAnalysis()) @@ -120,12 +133,12 @@ class _VisualizationCommand: tooltip = f"Create a {self._visualization_type} post processing data visualization" return { - "Pixmap": vis.icon, - "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), - "Accel": "", - "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), - "CmdType": "AlterDoc" - } + "Pixmap": vis.icon, + "MenuText": QtCore.QT_TRANSLATE_NOOP(cmd, "Create {}".format(self._visualization_type)), + "Accel": "", + "ToolTip": QtCore.QT_TRANSLATE_NOOP(cmd, tooltip), + "CmdType": "AlterDoc", + } def IsActive(self): # active analysis available @@ -133,6 +146,7 @@ class _VisualizationCommand: return False import FemGui + return bool(FemGui.getActiveAnalysis()) def Activated(self): @@ -144,17 +158,12 @@ class _VisualizationCommand: 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.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)" - ) + FreeCADGui.doCommand("FreeCADGui.ActiveDocument.setEdit(obj)") + def setup_commands(toplevel_name): # creates all visualization commands and registers them. The diff --git a/src/Mod/Fem/femguiutils/vtk_table_view.py b/src/Mod/Fem/femguiutils/vtk_table_view.py index f126640d24..b6e8c939b3 100644 --- a/src/Mod/Fem/femguiutils/vtk_table_view.py +++ b/src/Mod/Fem/femguiutils/vtk_table_view.py @@ -39,13 +39,14 @@ from vtkmodules.vtkIOCore import vtkDelimitedTextWriter translate = FreeCAD.Qt.translate + class VtkTableModel(QtCore.QAbstractTableModel): # Simple table model. Only supports single component columns # One can supply a header_names dict to replace the table column names # in the header. It is a dict "column_idx (int)" to "new name"" or # "orig_name (str)" to "new name" - def __init__(self, header_names = None): + def __init__(self, header_names=None): super().__init__() self._table = None if header_names: @@ -53,7 +54,7 @@ class VtkTableModel(QtCore.QAbstractTableModel): else: self._header = {} - def setTable(self, table, header_names = None): + def setTable(self, table, header_names=None): self.beginResetModel() self._table = table if header_names: @@ -105,6 +106,7 @@ class VtkTableModel(QtCore.QAbstractTableModel): def getTable(self): return self._table + class VtkTableSummaryModel(QtCore.QAbstractTableModel): # Simple model showing a summary of the table. # Only supports single component columns @@ -126,7 +128,7 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): return self._table.GetNumberOfColumns() def columnCount(self, index): - return 2 # min, max + return 2 # min, max def data(self, index, role): @@ -143,7 +145,7 @@ class VtkTableSummaryModel(QtCore.QAbstractTableModel): def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: - return ["Min","Max"][section] + return ["Min", "Max"][section] if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole: return self._table.GetColumnName(section) @@ -162,7 +164,7 @@ class VtkTableView(QtGui.QWidget): self.model = model layout = QtGui.QVBoxLayout() - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # start with the toolbar @@ -177,7 +179,9 @@ class VtkTableView(QtGui.QWidget): copy_action.triggered.connect(self.copyToClipboard) copy_action.setIcon(FreeCADGui.getIcon("edit-copy")) shortcut = QtGui.QKeySequence(QtGui.QKeySequence.Copy) - copy_action.setToolTip(translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString()))) + copy_action.setToolTip( + translate("FEM", "Copy selection to clipboard ({})".format(shortcut.toString())) + ) copy_action.setShortcut(shortcut) self.toolbar.addAction(copy_action) @@ -205,15 +209,19 @@ class VtkTableView(QtGui.QWidget): @QtCore.Slot(bool) def exportCsv(self, state): - file_path, filter = QtGui.QFileDialog.getSaveFileName(None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)") + file_path, filter = QtGui.QFileDialog.getSaveFileName( + None, translate("FEM", "Save as csv file"), "", "CSV (*.csv)" + ) if not file_path: - FreeCAD.Console.PrintMessage(translate("FEM", "CSV file export aborted: no filename selected")) + FreeCAD.Console.PrintMessage( + translate("FEM", "CSV file export aborted: no filename selected") + ) return writer = vtkDelimitedTextWriter() writer.SetFileName(file_path) - writer.SetInputData(self.model.getTable()); - writer.Write(); + writer.SetInputData(self.model.getTable()) + writer.Write() @QtCore.Slot() def copyToClipboard(self): @@ -228,19 +236,18 @@ class VtkTableView(QtGui.QWidget): previous = selection.pop(0) for current in selection: - data = self.model.data(previous, QtCore.Qt.DisplayRole); + data = self.model.data(previous, QtCore.Qt.DisplayRole) copy_table += str(data) if current.row() != previous.row(): - copy_table += '\n' + copy_table += "\n" else: - copy_table += '\t' + copy_table += "\t" previous = current copy_table += str(self.model.data(selection[-1], QtCore.Qt.DisplayRole)) - copy_table += '\n' + copy_table += "\n" clipboard = QtGui.QApplication.instance().clipboard() clipboard.setText(copy_table) - diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 48bd9c4951..9e2ad7104d 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -36,22 +36,26 @@ from vtkmodules.vtkCommonDataModel import vtkTable from PySide.QtCore import QT_TRANSLATE_NOOP 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 @@ -104,7 +108,6 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): 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() @@ -135,7 +138,6 @@ class Extractor1D(Extractor): def __init__(self, obj): super().__init__(obj) - def _get_properties(self): prop = [ _PropHelper( @@ -149,14 +151,15 @@ class Extractor1D(Extractor): type="App::PropertyEnumeration", name="XComponent", group="X Data", - doc=QT_TRANSLATE_NOOP("FEM", "Which part of the X field vector to use for the X axis"), + doc=QT_TRANSLATE_NOOP( + "FEM", "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) @@ -213,7 +216,7 @@ class Extractor1D(Extractor): if array.GetNumberOfComponents() == 1: table.AddColumn(array) else: - component_array = vtkDoubleArray(); + component_array = vtkDoubleArray() component_array.SetNumberOfComponents(1) component_array.SetNumberOfTuples(array.GetNumberOfTuples()) c_idx = obj.getEnumerationsOfProperty("XComponent").index(obj.XComponent) @@ -233,7 +236,7 @@ class Extractor1D(Extractor): array.SetNumberOfTuples(num) array.SetNumberOfComponents(1) for i in range(num): - array.SetValue(i,i) + array.SetValue(i, i) case "Position": @@ -266,6 +269,7 @@ class Extractor1D(Extractor): return label + class Extractor2D(Extractor1D): ExtractionDimension = "2D" @@ -273,7 +277,6 @@ class Extractor2D(Extractor1D): def __init__(self, obj): super().__init__(obj) - def _get_properties(self): prop = [ _PropHelper( @@ -287,14 +290,15 @@ class Extractor2D(Extractor1D): type="App::PropertyEnumeration", name="YComponent", group="Y Data", - doc=QT_TRANSLATE_NOOP("FEM", "Which part of the Y field vector to use for the Y axis"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Which part of the Y field vector to use for the Y axis" + ), value=[], ), ] return super()._get_properties() + prop - def onChanged(self, obj, prop): super().onChanged(obj, prop) @@ -348,7 +352,7 @@ class Extractor2D(Extractor1D): if array.GetNumberOfComponents() == 1: table.AddColumn(array) else: - component_array = vtkDoubleArray(); + component_array = vtkDoubleArray() component_array.SetNumberOfComponents(1) component_array.SetNumberOfTuples(array.GetNumberOfTuples()) c_idx = obj.getEnumerationsOfProperty("YComponent").index(obj.YComponent) diff --git a/src/Mod/Fem/femobjects/base_fempostvisualizations.py b/src/Mod/Fem/femobjects/base_fempostvisualizations.py index ffaa94ee8f..5c7465d5bc 100644 --- a/src/Mod/Fem/femobjects/base_fempostvisualizations.py +++ b/src/Mod/Fem/femobjects/base_fempostvisualizations.py @@ -38,6 +38,7 @@ from . import base_fempostextractors # helper functions # ################ + def is_visualization_object(obj): if not obj: return False @@ -71,27 +72,23 @@ def is_visualization_extractor_type(obj, vistype): return True - # Base class for all visualizations # It collects all data from its extraction objects into a table. # Note: Never use directly, always subclass! This class does not create a # Visualization variable, hence will not work correctly. class PostVisualization(base_fempythonobject.BaseFemPythonObject): - def __init__(self, obj): super().__init__(obj) obj.addExtension("App::GroupExtensionPython") self._setup_properties(obj) - def _setup_properties(self, obj): pl = obj.PropertiesList for prop in self._get_properties(): if not prop.name in pl: prop.add_to_object(obj) - def _get_properties(self): # override if subclass wants to add additional properties @@ -106,14 +103,12 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): ] return prop - def onDocumentRestored(self, obj): # if a new property was added we handle it by setup # Override if subclass needs to handle changed property type self._setup_properties(obj) - def onChanged(self, obj, prop): # Ensure only correct child object types are in the group @@ -123,13 +118,14 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): children = obj.Group for child in obj.Group: if not is_visualization_extractor_type(child, self.VisualizationType): - FreeCAD.Console.PrintWarning(f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added") + FreeCAD.Console.PrintWarning( + f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added" + ) children.remove(child) if len(obj.Group) != len(children): obj.Group = children - def execute(self, obj): # Collect all extractor child data into our table # Note: Each childs table can have different number of rows. We need @@ -144,7 +140,9 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): # to none without recompute, and the visualization was manually # recomputed afterwards if not child.Source and (child.Table.GetNumberOfColumns() > 0): - FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored") + FreeCAD.Console.PrintWarning( + f"{child.Label} has data, but no Source object. Will be ignored" + ) continue c_table = child.Table @@ -159,18 +157,16 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject): else: array.SetNumberOfComponents(c_array.GetNumberOfComponents()) array.SetNumberOfTuples(rows) - array.Fill(0) # so that all non-used entries are set to 0 + array.Fill(0) # so that all non-used entries are set to 0 for j in range(c_array.GetNumberOfTuples()): array.SetTuple(j, c_array.GetTuple(j)) array.SetName(f"{child.Source.Name}: {c_array.GetName()}") table.AddColumn(array) - obj.Table = table return False - def getLongestColumnLength(self, obj): # iterate all extractor children and get the column lengths diff --git a/src/Mod/Fem/femobjects/post_extract1D.py b/src/Mod/Fem/femobjects/post_extract1D.py index 8fcec6e2da..f70c6c65c9 100644 --- a/src/Mod/Fem/femobjects/post_extract1D.py +++ b/src/Mod/Fem/femobjects/post_extract1D.py @@ -33,6 +33,7 @@ import FreeCAD from . import base_fempostextractors from . import base_fempythonobject + _PropHelper = base_fempythonobject._PropHelper from vtkmodules.vtkCommonCore import vtkDoubleArray @@ -53,11 +54,14 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): super().__init__(obj) def _get_properties(self): - prop =[ _PropHelper( + prop = [ + _PropHelper( type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify if the field shall be extracted for every available frame" + ), value=False, ), ] @@ -77,14 +81,16 @@ class PostFieldData1D(base_fempostextractors.Extractor1D): obj.Table = table return - timesteps=[] + timesteps = [] 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()) else: - FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + FreeCAD.Console.PrintWarning( + 'No frames available in data, ignoring "ExtractFrames" property' + ) if not timesteps: # get the dataset and extract the correct array @@ -124,11 +130,14 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): super().__init__(obj) def _get_properties(self): - prop =[_PropHelper( + prop = [ + _PropHelper( type="App::PropertyInteger", name="Index", group="X Data", - doc=QT_TRANSLATE_NOOP("FEM", "Specify for which index the data should be extracted"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify for which index the data should be extracted" + ), value=0, ), ] @@ -154,7 +163,6 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - algo = obj.Source.getOutputAlgorithm() frame_array = vtkDoubleArray() idx = obj.Index @@ -168,8 +176,10 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): array = self._x_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) if not setup: frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) @@ -183,14 +193,15 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D): array = self._x_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) frame_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_array.SetNumberOfTuples(1) frame_array.SetTuple(0, idx, array) - if frame_array.GetNumberOfComponents() > 1: frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}") else: diff --git a/src/Mod/Fem/femobjects/post_extract2D.py b/src/Mod/Fem/femobjects/post_extract2D.py index 86827d7a7e..64cba2d5c7 100644 --- a/src/Mod/Fem/femobjects/post_extract2D.py +++ b/src/Mod/Fem/femobjects/post_extract2D.py @@ -33,6 +33,7 @@ import FreeCAD from . import base_fempostextractors from . import base_fempythonobject + _PropHelper = base_fempythonobject._PropHelper from vtkmodules.vtkCommonCore import vtkDoubleArray @@ -53,17 +54,19 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): super().__init__(obj) def _get_properties(self): - prop =[ _PropHelper( + prop = [ + _PropHelper( type="App::PropertyBool", name="ExtractFrames", group="Multiframe", - doc=QT_TRANSLATE_NOOP("FEM", "Specify if the field shall be extracted for every available frame"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify if the field shall be extracted for every available frame" + ), value=False, ), ] return super()._get_properties() + prop - def execute(self, obj): # on execution we populate the vtk table @@ -85,7 +88,9 @@ class PostFieldData2D(base_fempostextractors.Extractor2D): if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) else: - FreeCAD.Console.PrintWarning("No frames available in data, ignoring \"ExtractFrames\" property") + FreeCAD.Console.PrintWarning( + 'No frames available in data, ignoring "ExtractFrames" property' + ) if not timesteps: # get the dataset and extract the correct array @@ -140,11 +145,14 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): super().__init__(obj) def _get_properties(self): - prop =[_PropHelper( + prop = [ + _PropHelper( type="App::PropertyInteger", name="Index", group="Data", - doc=QT_TRANSLATE_NOOP("FEM", "Specify for which point index the data should be extracted"), + doc=QT_TRANSLATE_NOOP( + "FEM", "Specify for which point index the data should be extracted" + ), value=0, ), ] @@ -178,7 +186,6 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): if info.Has(vtkStreamingDemandDrivenPipeline.TIME_STEPS()): timesteps = info.Get(vtkStreamingDemandDrivenPipeline.TIME_STEPS()) - algo = obj.Source.getOutputAlgorithm() frame_x_array = vtkDoubleArray() @@ -198,8 +205,10 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): array = self._y_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) if not setup: frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) @@ -211,21 +220,22 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D): else: frame_x_array.SetNumberOfTuples(1) frame_x_array.SetNumberOfComponents(1) - frame_x_array.SetTuple1(0,0) + frame_x_array.SetTuple1(0, 0) algo.Update() dataset = algo.GetOutputDataObject(0) array = self._y_array_from_dataset(obj, dataset, copy=False) # safeguard for invalid access - if idx < 0 or array.GetNumberOfTuples()-1 < idx: - raise Exception(f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}") + if idx < 0 or array.GetNumberOfTuples() - 1 < idx: + raise Exception( + f"Invalid index: {idx} is not in range 0 - {array.GetNumberOfTuples()-1}" + ) frame_y_array.SetNumberOfComponents(array.GetNumberOfComponents()) frame_y_array.SetNumberOfTuples(1) frame_y_array.SetTuple(0, idx, array) - frame_x_array.SetName("Frames") if frame_y_array.GetNumberOfComponents() > 1: frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}") diff --git a/src/Mod/Fem/femobjects/post_glyphfilter.py b/src/Mod/Fem/femobjects/post_glyphfilter.py index 51c7d480c6..a783835656 100644 --- a/src/Mod/Fem/femobjects/post_glyphfilter.py +++ b/src/Mod/Fem/femobjects/post_glyphfilter.py @@ -33,6 +33,7 @@ import FreeCAD # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() # IMPORTANT: Never import vtk directly. Often vtk is compiled with different QT diff --git a/src/Mod/Fem/femobjects/post_histogram.py b/src/Mod/Fem/femobjects/post_histogram.py index fcbb1ce2e7..fb0b1343cc 100644 --- a/src/Mod/Fem/femobjects/post_histogram.py +++ b/src/Mod/Fem/femobjects/post_histogram.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecad.org" # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() from . import base_fempostextractors @@ -40,31 +41,35 @@ from . import post_extract1D from femguiutils import post_visualization # register visualization and extractors -post_visualization.register_visualization("Histogram", - ":/icons/FEM_PostHistogram.svg", - "ObjectsFem", - "makePostHistogram") +post_visualization.register_visualization( + "Histogram", ":/icons/FEM_PostHistogram.svg", "ObjectsFem", "makePostHistogram" +) -post_visualization.register_extractor("Histogram", - "HistogramFieldData", - ":/icons/FEM_PostField.svg", - "1D", - "Field", - "ObjectsFem", - "makePostHistogramFieldData") +post_visualization.register_extractor( + "Histogram", + "HistogramFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostHistogramFieldData", +) -post_visualization.register_extractor("Histogram", - "HistogramIndexOverFrames", - ":/icons/FEM_PostIndex.svg", - "1D", - "Index", - "ObjectsFem", - "makePostHistogramIndexOverFrames") +post_visualization.register_extractor( + "Histogram", + "HistogramIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostHistogramIndexOverFrames", +) # Implementation # ############## + def is_histogram_extractor(obj): if not base_fempostextractors.is_extractor_object(obj): @@ -80,6 +85,7 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D): """ A 1D Field extraction for histograms. """ + VisualizationType = "Histogram" @@ -87,6 +93,7 @@ class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D): """ A 1D index extraction for histogram. """ + VisualizationType = "Histogram" @@ -94,13 +101,5 @@ class PostHistogram(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as histograms """ + VisualizationType = "Histogram" - - - - - - - - - diff --git a/src/Mod/Fem/femobjects/post_lineplot.py b/src/Mod/Fem/femobjects/post_lineplot.py index 8d4b725128..3216400415 100644 --- a/src/Mod/Fem/femobjects/post_lineplot.py +++ b/src/Mod/Fem/femobjects/post_lineplot.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecad.org" # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() from . import base_fempostextractors @@ -40,31 +41,35 @@ from . import post_extract2D from femguiutils import post_visualization # register visualization and extractors -post_visualization.register_visualization("Lineplot", - ":/icons/FEM_PostLineplot.svg", - "ObjectsFem", - "makePostLineplot") +post_visualization.register_visualization( + "Lineplot", ":/icons/FEM_PostLineplot.svg", "ObjectsFem", "makePostLineplot" +) -post_visualization.register_extractor("Lineplot", - "LineplotFieldData", - ":/icons/FEM_PostField.svg", - "2D", - "Field", - "ObjectsFem", - "makePostLineplotFieldData") +post_visualization.register_extractor( + "Lineplot", + "LineplotFieldData", + ":/icons/FEM_PostField.svg", + "2D", + "Field", + "ObjectsFem", + "makePostLineplotFieldData", +) -post_visualization.register_extractor("Lineplot", - "LineplotIndexOverFrames", - ":/icons/FEM_PostIndex.svg", - "2D", - "Index", - "ObjectsFem", - "makePostLineplotIndexOverFrames") +post_visualization.register_extractor( + "Lineplot", + "LineplotIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "2D", + "Index", + "ObjectsFem", + "makePostLineplotIndexOverFrames", +) # Implementation # ############## + def is_lineplot_extractor(obj): if not base_fempostextractors.is_extractor_object(obj): @@ -80,6 +85,7 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D): """ A 2D Field extraction for lineplot. """ + VisualizationType = "Lineplot" @@ -87,15 +93,13 @@ class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D): """ A 2D index extraction for lineplot. """ - VisualizationType = "Lineplot" + VisualizationType = "Lineplot" class PostLineplot(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as line plots """ + VisualizationType = "Lineplot" - - - diff --git a/src/Mod/Fem/femobjects/post_table.py b/src/Mod/Fem/femobjects/post_table.py index 0a64dc733d..a12398ab9e 100644 --- a/src/Mod/Fem/femobjects/post_table.py +++ b/src/Mod/Fem/femobjects/post_table.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecad.org" # check vtk version to potentially find missmatchs from femguiutils.vtk_module_handling import vtk_module_handling + vtk_module_handling() from . import base_fempostextractors @@ -40,31 +41,35 @@ from . import post_extract1D from femguiutils import post_visualization # register visualization and extractors -post_visualization.register_visualization("Table", - ":/icons/FEM_PostSpreadsheet.svg", - "ObjectsFem", - "makePostTable") +post_visualization.register_visualization( + "Table", ":/icons/FEM_PostSpreadsheet.svg", "ObjectsFem", "makePostTable" +) -post_visualization.register_extractor("Table", - "TableFieldData", - ":/icons/FEM_PostField.svg", - "1D", - "Field", - "ObjectsFem", - "makePostTableFieldData") +post_visualization.register_extractor( + "Table", + "TableFieldData", + ":/icons/FEM_PostField.svg", + "1D", + "Field", + "ObjectsFem", + "makePostTableFieldData", +) -post_visualization.register_extractor("Table", - "TableIndexOverFrames", - ":/icons/FEM_PostIndex.svg", - "1D", - "Index", - "ObjectsFem", - "makePostTableIndexOverFrames") +post_visualization.register_extractor( + "Table", + "TableIndexOverFrames", + ":/icons/FEM_PostIndex.svg", + "1D", + "Index", + "ObjectsFem", + "makePostTableIndexOverFrames", +) # Implementation # ############## + def is_table_extractor(obj): if not base_fempostextractors.is_extractor_object(obj): @@ -80,6 +85,7 @@ class PostTableFieldData(post_extract1D.PostFieldData1D): """ A 1D Field extraction for tables. """ + VisualizationType = "Table" @@ -87,6 +93,7 @@ class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D): """ A 1D index extraction for table. """ + VisualizationType = "Table" @@ -94,6 +101,5 @@ class PostTable(base_fempostvisualizations.PostVisualization): """ A post processing plot for showing extracted data as tables """ + VisualizationType = "Table" - - diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 5efa1aa12a..81fa9107eb 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -53,7 +53,9 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): # ########################## def getStandardButtons(self): - return QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + return ( + QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel + ) def clicked(self, button): # apply button hit? @@ -63,8 +65,9 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): def open(self): # open a new transaction if non is open if not FreeCAD.getActiveTransaction(): - FreeCAD.ActiveDocument.openTransaction(translate("FEM", "Edit {}").format(self.obj.Label)) - + FreeCAD.ActiveDocument.openTransaction( + translate("FEM", "Edit {}").format(self.obj.Label) + ) # Helper functions # ################ @@ -83,6 +86,3 @@ class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): 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 index 56fdd998b5..9c54352f10 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_extractor.py +++ b/src/Mod/Fem/femtaskpanels/task_post_extractor.py @@ -52,6 +52,3 @@ class _ExtractorTaskPanel(base_fempostpanel._BasePostTaskPanel): view.setWindowIcon(obj.ViewObject.Icon) self.form = [app, view] - - - diff --git a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py index a9a13139e9..570ca63b66 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py +++ b/src/Mod/Fem/femtaskpanels/task_post_glyphfilter.py @@ -56,7 +56,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.widget, vobj.createDisplayTaskWidget()] - # Setup functions # ############### diff --git a/src/Mod/Fem/femtaskpanels/task_post_histogram.py b/src/Mod/Fem/femtaskpanels/task_post_histogram.py index 496fc1792a..df70e2f18d 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_histogram.py +++ b/src/Mod/Fem/femtaskpanels/task_post_histogram.py @@ -40,6 +40,7 @@ from femguiutils import vtk_table_view translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -68,7 +69,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setWindowTitle(translate("FEM", "Histogram data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostHistogram.svg")) - # histogram parameter widget self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostHistogram.ui" @@ -81,7 +81,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.data_widget, self.view_widget] - # Setup functions # ############### @@ -121,12 +120,13 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): 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 @@ -137,49 +137,58 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): widget = vtk_table_view.VtkTableView(data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + 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/femtaskpanels/task_post_lineplot.py b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py index 474d84b80b..f5598e1874 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_lineplot.py +++ b/src/Mod/Fem/femtaskpanels/task_post_lineplot.py @@ -40,6 +40,7 @@ from femguiutils import vtk_table_view translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -68,7 +69,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.data_widget.setWindowTitle(translate("FEM", "Lineplot data")) self.data_widget.setWindowIcon(FreeCADGui.getIcon(":/icons/FEM_PostLineplot.svg")) - # lineplot parameter widget self.view_widget = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/TaskPostLineplot.ui" @@ -81,7 +81,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.data_widget, self.view_widget] - # Setup functions # ############### @@ -104,7 +103,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget.LegendShow.setChecked(viewObj.Legend) self._enumPropertyToCombobox(viewObj, "LegendLocation", self.view_widget.LegendPos) - # connect callbacks self.view_widget.Scale.activated.connect(self.scaleChanged) self.view_widget.Grid.toggled.connect(self.gridChanged) @@ -116,12 +114,13 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): self.view_widget.LegendShow.toggled.connect(self.legendShowChanged) self.view_widget.LegendPos.activated.connect(self.legendPosChanged) - QtCore.Slot() + def showPlot(self): self.obj.ViewObject.Proxy.show_visualization() QtCore.Slot() + def showTable(self): # TODO: make data model update when object is recomputed @@ -132,37 +131,43 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): widget = vtk_table_view.VtkTableView(data_model) layout = QtGui.QVBoxLayout() layout.addWidget(widget) - layout.setContentsMargins(0,0,0,0) + layout.setContentsMargins(0, 0, 0, 0) dialog.setLayout(layout) dialog.resize(1500, 900) dialog.show() - QtCore.Slot(int) + def scaleChanged(self, idx): self.obj.ViewObject.Scale = idx QtCore.Slot(bool) + def gridChanged(self, state): self.obj.ViewObject.Grid = state QtCore.Slot() + def titleChanged(self): self.obj.ViewObject.Title = self.view_widget.Title.text() QtCore.Slot() + def xLabelChanged(self): self.obj.ViewObject.XLabel = self.view_widget.XLabel.text() QtCore.Slot() + def yLabelChanged(self): self.obj.ViewObject.YLabel = self.view_widget.YLabel.text() QtCore.Slot(int) + def legendPosChanged(self, idx): self.obj.ViewObject.LegendLocation = idx QtCore.Slot(bool) + def legendShowChanged(self, state): self.obj.ViewObject.Legend = state diff --git a/src/Mod/Fem/femtaskpanels/task_post_table.py b/src/Mod/Fem/femtaskpanels/task_post_table.py index 1eda150016..98fd1686d6 100644 --- a/src/Mod/Fem/femtaskpanels/task_post_table.py +++ b/src/Mod/Fem/femtaskpanels/task_post_table.py @@ -39,6 +39,7 @@ from femguiutils import extract_link_view as elv translate = FreeCAD.Qt.translate + class _TaskPanel(base_fempostpanel._BasePostTaskPanel): """ The TaskPanel for editing properties of glyph filter @@ -68,7 +69,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # form made from param and selection widget self.form = [self.data_widget] - # Setup functions # ############### @@ -77,8 +77,6 @@ class _TaskPanel(base_fempostpanel._BasePostTaskPanel): # connect data widget self.data_widget.show_table.clicked.connect(self.showTable) - @QtCore.Slot() def showTable(self): self.obj.ViewObject.Proxy.show_visualization() - diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index a86c1288a2..10ba8e2fd0 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -40,6 +40,7 @@ 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. diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index bb4aca0934..b2df81ef0d 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -1,4 +1,3 @@ - # *************************************************************************** # * Copyright (c) 2025 Stefan Tröger * # * * @@ -37,6 +36,7 @@ from PySide import QtGui from femtaskpanels import task_post_extractor + class VPPostExtractor: """ A View Provider for extraction of data @@ -74,18 +74,18 @@ class VPPostExtractor: if not group: return - if (hasattr(group.ViewObject, "Proxy") and - hasattr(group.ViewObject.Proxy, "childViewPropertyChanged")): + 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 + # show it FreeCADGui.Control.showDialog(taskd) return True @@ -112,7 +112,6 @@ class VPPostExtractor: def loads(self, state): return None - # To be implemented by subclasses: # ################################ diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 9b357a4d0c..3abf56b29a 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -32,6 +32,7 @@ __url__ = "https://www.freecad.org" import FreeCAD import FreeCADGui + class VPPostVisualization: """ A View Provider for visualization objects @@ -42,23 +43,19 @@ class VPPostVisualization: self._setup_properties(vobj) vobj.addExtension("Gui::ViewProviderGroupExtensionPython") - 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): # Mark ourself as visible in the tree return True @@ -66,7 +63,7 @@ class VPPostVisualization: def getDisplayModes(self, obj): return ["Dialog"] - def doubleClicked(self,vobj): + def doubleClicked(self, vobj): guidoc = FreeCADGui.getDocument(vobj.Object.Document) @@ -107,7 +104,6 @@ class VPPostVisualization: def loads(self, state): return None - # To be implemented by subclasses: # ################################ diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index 337ad62f86..f6323cafc6 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -48,6 +48,7 @@ from . import view_base_fempostvisualization from femtaskpanels import task_post_histogram from . import view_base_femobject + _GuiPropHelper = view_base_femobject._GuiPropHelper @@ -92,12 +93,11 @@ class EditViewWidget(QtGui.QWidget): self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height()) self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) - def _setup_color_button(self, button, fcColor, callback): - barColor = QtGui.QColor(*[v*255 for v in fcColor]) + barColor = QtGui.QColor(*[v * 255 for v in fcColor]) icon_size = button.iconSize() - icon_size.setWidth(icon_size.width()*2) + icon_size.setWidth(icon_size.width() * 2) button.setIconSize(icon_size) pixmap = QtGui.QPixmap(icon_size) pixmap.fill(barColor) @@ -114,7 +114,6 @@ class EditViewWidget(QtGui.QWidget): button.addAction(action) button.setPopupMode(QtGui.QToolButton.InstantPopup) - @QtCore.Slot(QtGui.QColor) def lineColorChanged(self, color): @@ -199,6 +198,7 @@ class EditFieldAppWidget(QtGui.QWidget): self._object.ExtractFrames = extract self._post_dialog._recompute() + class EditIndexAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -279,7 +279,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="Hatch", group="HistogramBar", doc=QT_TRANSLATE_NOOP("FEM", "The hatch pattern drawn in the bar"), - value=['None', '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'], + value=["None", "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"], ), _GuiPropHelper( type="App::PropertyIntegerConstraint", @@ -293,13 +293,15 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="LineColor", group="HistogramLine", doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"), - value=(0, 0, 0, 1), # black + value=(0, 0, 0, 1), # black ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="LineWidth", group="HistogramLine", - doc=QT_TRANSLATE_NOOP("FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"), + doc=QT_TRANSLATE_NOOP( + "FEM", "The width of the bar, between 0 and 1 (1 being without gaps)" + ), value=(1, 0, 99, 0.1), ), _GuiPropHelper( @@ -307,7 +309,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): name="LineStyle", group="HistogramLine", doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), - value=['None', '-', '--', '-.', ':'], + value=["None", "-", "--", "-.", ":"], ), ] return super()._get_properties() + prop @@ -327,13 +329,13 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): def get_preview(self): - fig = mpl.pyplot.figure(figsize=(0.4,0.2), dpi=500) - ax = mpl.pyplot.Axes(fig, [0., 0., 2, 1]) + fig = mpl.pyplot.figure(figsize=(0.4, 0.2), dpi=500) + ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 2, 1]) ax.set_axis_off() fig.add_axes(ax) kwargs = self.get_kw_args() - patch = mpl.patches.Rectangle(xy=(0,0), width=2, height=1, **kwargs) + patch = mpl.patches.Rectangle(xy=(0, 0), width=2, height=1, **kwargs) ax.add_patch(patch) data = io.BytesIO() @@ -355,7 +357,7 @@ class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): kwargs["linestyle"] = self.ViewObject.LineStyle kwargs["linewidth"] = self.ViewObject.LineWidth if self.ViewObject.Hatch != "None": - kwargs["hatch"] = self.ViewObject.Hatch*self.ViewObject.HatchDensity + kwargs["hatch"] = self.ViewObject.Hatch * self.ViewObject.HatchDensity return kwargs @@ -386,7 +388,6 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - def _get_properties(self): prop = [ @@ -394,7 +395,9 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Cumulative", group="Histogram", - doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), + doc=QT_TRANSLATE_NOOP( + "FEM", "If be the bars shoud show the cumulative sum left to rigth" + ), value=False, ), _GuiPropHelper( @@ -402,13 +405,15 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): name="Type", group="Histogram", doc=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"), - value=["bar","barstacked", "step", "stepfilled"], + value=["bar", "barstacked", "step", "stepfilled"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", name="BarWidth", group="Histogram", - doc=QT_TRANSLATE_NOOP("FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"), + doc=QT_TRANSLATE_NOOP( + "FEM", "The width of the bar, between 0 and 1 (1 being without gaps)" + ), value=(0.9, 0, 1, 0.05), ), _GuiPropHelper( @@ -458,29 +463,36 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): name="LegendLocation", group="Plot", doc=QT_TRANSLATE_NOOP("FEM", "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'], + 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 setEdit(self, vobj, mode): # build up the task panel taskd = task_post_histogram._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True - def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -489,12 +501,11 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) - self._plot.resize(main.size().height()/2, main.size().height()/3) # keep it square + self._plot.resize(main.size().height() / 2, main.size().height() / 3) # keep it square self.update_visualization() self._plot.show() - def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): @@ -503,7 +514,6 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -523,7 +533,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): kwargs = self.get_kw_args(child) # iterate over the table and plot all - color_factor = np.linspace(1,0.5,table.GetNumberOfColumns()) + color_factor = np.linspace(1, 0.5, table.GetNumberOfColumns()) legend_multiframe = table.GetNumberOfColumns() > 1 for i in range(table.GetNumberOfColumns()): @@ -532,7 +542,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): for key in kwargs: if "color" in key: - value = np.array(kwargs[key])*color_factor[i] + value = np.array(kwargs[key]) * color_factor[i] args[key] = mpl.colors.to_hex(value) full_args.append(args) @@ -581,7 +591,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): self._plot.axes.set_ylabel(self.ViewObject.YLabel) if self.ViewObject.Legend and labels: - self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + self._plot.axes.legend(loc=self.ViewObject.LegendLocation) self._plot.update() diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index 3066e009b7..f73d0aad7b 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -48,8 +48,10 @@ from . import view_base_fempostvisualization from femtaskpanels import task_post_lineplot from . import view_base_femobject + _GuiPropHelper = view_base_femobject._GuiPropHelper + class EditViewWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -91,9 +93,9 @@ class EditViewWidget(QtGui.QWidget): def _setup_color_button(self, button, fcColor, callback): - barColor = QtGui.QColor(*[v*255 for v in fcColor]) + barColor = QtGui.QColor(*[v * 255 for v in fcColor]) icon_size = button.iconSize() - icon_size.setWidth(icon_size.width()*2) + icon_size.setWidth(icon_size.width() * 2) button.setIconSize(icon_size) pixmap = QtGui.QPixmap(icon_size) pixmap.fill(barColor) @@ -110,7 +112,6 @@ class EditViewWidget(QtGui.QWidget): button.addAction(action) button.setPopupMode(QtGui.QToolButton.InstantPopup) - @QtCore.Slot(QtGui.QColor) def colorChanged(self, color): @@ -163,9 +164,13 @@ class EditFieldAppWidget(QtGui.QWidget): # set the other properties self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.XField) - self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "XComponent", self.widget.XComponent + ) self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self.widget.Extract.setChecked(self._object.ExtractFrames) self.widget.XField.activated.connect(self.xFieldChanged) @@ -177,7 +182,9 @@ class EditFieldAppWidget(QtGui.QWidget): @QtCore.Slot(int) def xFieldChanged(self, index): self._object.XField = index - self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.XComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "XComponent", self.widget.XComponent + ) self._post_dialog._recompute() @QtCore.Slot(int) @@ -188,7 +195,9 @@ class EditFieldAppWidget(QtGui.QWidget): @QtCore.Slot(int) def yFieldChanged(self, index): self._object.YField = index - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self._post_dialog._recompute() @QtCore.Slot(int) @@ -225,7 +234,9 @@ class EditIndexAppWidget(QtGui.QWidget): self.widget.Index.setValue(self._object.Index) self._post_dialog._enumPropertyToCombobox(self._object, "YField", self.widget.YField) - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self.widget.Index.valueChanged.connect(self.indexChanged) self.widget.YField.activated.connect(self.yFieldChanged) @@ -242,7 +253,9 @@ class EditIndexAppWidget(QtGui.QWidget): @QtCore.Slot(int) def yFieldChanged(self, index): self._object.YField = index - self._post_dialog._enumPropertyToCombobox(self._object, "YComponent", self.widget.YComponent) + self._post_dialog._enumPropertyToCombobox( + self._object, "YComponent", self.widget.YComponent + ) self._post_dialog._recompute() @QtCore.Slot(int) @@ -282,7 +295,7 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): name="LineStyle", group="Lineplot", doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"), - value=['-', '--', '-.', ':', 'None'], + value=["-", "--", "-.", ":", "None"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", @@ -296,7 +309,7 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): name="MarkerStyle", group="Lineplot", doc=QT_TRANSLATE_NOOP("FEM", "The style the data markers are drawn with"), - value=['None', '*', '+', 's', '.', 'o', 'x'], + value=["None", "*", "+", "s", ".", "o", "x"], ), _GuiPropHelper( type="App::PropertyFloatConstraint", @@ -325,13 +338,13 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): # Returns the preview tuple of icon and label: (QPixmap, str) # Note: QPixmap in ratio 2:1 - fig = mpl.pyplot.figure(figsize=(0.2,0.1), dpi=1000) - ax = mpl.pyplot.Axes(fig, [0., 0., 1., 1.]) + fig = mpl.pyplot.figure(figsize=(0.2, 0.1), dpi=1000) + ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 1.0, 1.0]) ax.set_axis_off() fig.add_axes(ax) kwargs = self.get_kw_args() kwargs["markevery"] = [1] - ax.plot([0,0.5,1],[0.5,0.5,0.5], **kwargs) + ax.plot([0, 0.5, 1], [0.5, 0.5, 0.5], **kwargs) data = io.BytesIO() mpl.pyplot.savefig(data, bbox_inches=0, transparent=True) mpl.pyplot.close() @@ -341,7 +354,6 @@ class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): return (pixmap, self.ViewObject.Legend) - def get_kw_args(self): # builds kw args from the properties kwargs = {} @@ -383,7 +395,6 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - def _get_properties(self): prop = [ @@ -391,7 +402,9 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): type="App::PropertyBool", name="Grid", group="Lineplot", - doc=QT_TRANSLATE_NOOP("FEM", "If be the bars shoud show the cumulative sum left to rigth"), + doc=QT_TRANSLATE_NOOP( + "FEM", "If be the bars shoud show the cumulative sum left to rigth" + ), value=True, ), _GuiPropHelper( @@ -399,9 +412,9 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="Scale", group="Lineplot", doc=QT_TRANSLATE_NOOP("FEM", "The scale the axis are drawn in"), - value=["linear","semi-log x", "semi-log y", "log"], + value=["linear", "semi-log x", "semi-log y", "log"], ), - _GuiPropHelper( + _GuiPropHelper( type="App::PropertyString", name="Title", group="Plot", @@ -434,29 +447,36 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="LegendLocation", group="Plot", doc=QT_TRANSLATE_NOOP("FEM", "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'], + value=[ + "best", + "upper right", + "upper left", + "lower left", + "lower right", + "right", + "center left", + "center right", + "lower center", + "upper center", + "center", + ], ), - ] return prop - def getIcon(self): return ":/icons/FEM_PostLineplot.svg" - def setEdit(self, vobj, mode): # build up the task panel taskd = task_post_lineplot._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True - def show_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -465,12 +485,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): self._plot.setWindowTitle(self.Object.Label) self._plot.setParent(main) self._plot.setWindowFlags(QtGui.Qt.Dialog) - self._plot.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self._plot.resize( + main.size().height() / 2, main.size().height() / 3 + ) # keep the aspect ratio self.update_visualization() self._plot.show() - def get_kw_args(self, obj): view = obj.ViewObject if not view or not hasattr(view, "Proxy"): @@ -479,7 +500,6 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): return {} return view.Proxy.get_kw_args() - def update_visualization(self): if not hasattr(self, "_plot") or not self._plot: @@ -496,10 +516,10 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): kwargs = self.get_kw_args(child) # iterate over the table and plot all (note: column 0 is always X!) - color_factor = np.linspace(1,0.5,int(table.GetNumberOfColumns()/2)) + color_factor = np.linspace(1, 0.5, int(table.GetNumberOfColumns() / 2)) legend_multiframe = table.GetNumberOfColumns() > 2 - for i in range(0,table.GetNumberOfColumns(),2): + for i in range(0, table.GetNumberOfColumns(), 2): plotted = True @@ -507,13 +527,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): tmp_args = {} for key in kwargs: if "color" in key: - value = np.array(kwargs[key])*color_factor[int(i/2)] + value = np.array(kwargs[key]) * color_factor[int(i / 2)] tmp_args[key] = mpl.colors.to_hex(value) else: tmp_args[key] = kwargs[key] xdata = VTKArray(table.GetColumn(i)) - ydata = VTKArray(table.GetColumn(i+1)) + ydata = VTKArray(table.GetColumn(i + 1)) # ensure points are visible if it is a single datapoint if len(xdata) == 1 and tmp_args["marker"] == "None": @@ -524,13 +544,13 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): if not legend_multiframe: label = child.ViewObject.Legend else: - postfix = table.GetColumnName(i+1).split("-")[-1] + postfix = table.GetColumnName(i + 1).split("-")[-1] label = child.ViewObject.Legend + " - " + postfix else: legend_prefix = "" if len(self.Object.Group) > 1: legend_prefix = child.Source.Label + ": " - label = legend_prefix + table.GetColumnName(i+1) + label = legend_prefix + table.GetColumnName(i + 1) match self.ViewObject.Scale: case "log": @@ -550,7 +570,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): self._plot.axes.set_ylabel(self.ViewObject.YLabel) if self.ViewObject.Legend and plotted: - self._plot.axes.legend(loc = self.ViewObject.LegendLocation) + self._plot.axes.legend(loc=self.ViewObject.LegendLocation) self._plot.axes.grid(self.ViewObject.Grid) self._plot.update() @@ -561,4 +581,3 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): i = len(self.Object.Group) cmap = mpl.pyplot.get_cmap("tab10") return cmap(i) - diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 536a3665e2..3c22d8b999 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -41,8 +41,10 @@ from femtaskpanels import task_post_table from femguiutils import vtk_table_view as vtv from . import view_base_femobject + _GuiPropHelper = view_base_femobject._GuiPropHelper + class EditViewWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -116,6 +118,7 @@ class EditFieldAppWidget(QtGui.QWidget): self._object.ExtractFrames = extract self._post_dialog._recompute() + class EditIndexAppWidget(QtGui.QWidget): def __init__(self, obj, post_dialog): @@ -180,7 +183,9 @@ class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): type="App::PropertyString", name="Name", group="Table", - doc=QT_TRANSLATE_NOOP("FEM", "The name used in the table header. Default name is used if empty"), + doc=QT_TRANSLATE_NOOP( + "FEM", "The name used in the table header. Default name is used if empty" + ), value="", ), ] @@ -232,22 +237,19 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): def __init__(self, vobj): super().__init__(vobj) - def getIcon(self): return ":/icons/FEM_PostSpreadsheet.svg" - def setEdit(self, vobj, mode): # build up the task panel taskd = task_post_table._TaskPanel(vobj) - #show it + # show it FreeCADGui.Control.showDialog(taskd) return True - def show_visualization(self): if not hasattr(self, "_tableview") or not self._tableview: @@ -257,13 +259,14 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): self._tableview.setWindowTitle(self.Object.Label) self._tableview.setParent(main) self._tableview.setWindowFlags(QtGui.Qt.Dialog) - self._tableview.resize(main.size().height()/2, main.size().height()/3) # keep the aspect ratio + self._tableview.resize( + main.size().height() / 2, main.size().height() / 3 + ) # keep the aspect ratio self.update_visualization() self._tableview.show() - def update_visualization(self): if not hasattr(self, "_tableModel") or not self._tableModel: @@ -286,5 +289,3 @@ class VPPostTable(view_base_fempostvisualization.VPPostVisualization): header[table.GetColumnName(i)] = new_name self._tableModel.setTable(self.Object.Table, header) - - From 9c2df53f5b0ac8314c48eca56c03c59864e97662 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 13 Jun 2025 01:13:41 +0200 Subject: [PATCH 035/126] Sketcher: Prioritize rendering geometry lines rendering over constraints As the title says - since there is pending PR for better SoDatumLabel constraints interactivity, this patch now prioritizes geometry lines over constraints, so constraints will be rendered below lines. This patch changes rendering order of constraint lines to be below geometry lines, so now selection and rendering will be prioritized for geometry lines instead of constraints. This is done by changing depth buffer values and removal of SoAnnotation node which was disabling depth buffer checks on constraints at all. --- src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h | 2 +- src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h index c3549846e2..1b87ca8d4e 100644 --- a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h +++ b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h @@ -75,7 +75,7 @@ struct DrawingParameters const float zMidLines = 0.006f; // Height used for in-the-middle rendered lines const float zHighLines = 0.007f; // Height used for on top rendered lines const float zHighLine = 0.008f; // Height for highlighted lines (selected/preselected) - const float zConstr = 0.009f; // Height for rendering constraints + const float zConstr = 0.004f; // Height for rendering constraints const float zRootPoint = 0.010f; // Height used for rendering the root point const float zLowPoints = 0.011f; // Height used for bottom rendered points const float zMidPoints = 0.012f; // Height used for mid rendered points diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index 92d71ab502..bed80ef06f 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -121,7 +121,6 @@ void EditModeConstraintCoinManager::processConstraints(const GeoListFacade& geol auto zConstrH = ViewProviderSketchCoinAttorney::getViewOrientationFactor(viewProvider) * drawingParameters.zConstr; - // After an undo/redo it can happen that we have an empty geometry list but a non-empty // constraint list In this case just ignore the constraints. (See bug #0000421) if (geolistfacade.geomlist.size() <= 2 && !constrlist.empty()) { @@ -1976,12 +1975,13 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes( text->size.setValue(drawingParameters.labelFontSize); text->lineWidth = 2 * drawingParameters.pixelScalingFactor; text->useAntialiasing = false; - SoAnnotation* anno = new SoAnnotation(); - anno->renderCaching = SoSeparator::OFF; - anno->addChild(text); + // Remove SoAnnotation wrapper to allow proper depth testing + // SoAnnotation* anno = new SoAnnotation(); + // anno->renderCaching = SoSeparator::OFF; + // anno->addChild(text); // #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0 sep->addChild(text); - editModeScenegraphNodes.constrGroup->addChild(anno); + editModeScenegraphNodes.constrGroup->addChild(sep); vConstrType.push_back((*it)->Type); // nodes not needed sep->unref(); From c13d6c8d47eee19d453fb86aa3aeec1bab687933 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 16 Jun 2025 23:57:17 +0200 Subject: [PATCH 036/126] Sketcher: Remove redundant comment regarding old SoAnnotation node --- src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index bed80ef06f..8e25ea1bc8 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -1975,11 +1975,6 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes( text->size.setValue(drawingParameters.labelFontSize); text->lineWidth = 2 * drawingParameters.pixelScalingFactor; text->useAntialiasing = false; - // Remove SoAnnotation wrapper to allow proper depth testing - // SoAnnotation* anno = new SoAnnotation(); - // anno->renderCaching = SoSeparator::OFF; - // anno->addChild(text); - // #define CONSTRAINT_SEPARATOR_INDEX_MATERIAL_OR_DATUMLABEL 0 sep->addChild(text); editModeScenegraphNodes.constrGroup->addChild(sep); vConstrType.push_back((*it)->Type); From f5e26f2fda4bb5a9e019284f9437558b01385cc6 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 17 Jun 2025 00:25:16 +0200 Subject: [PATCH 037/126] Gui: Emit signal to EditableDatumLabel only if there's no digits Small regression of mine, basically this signal to remove set/locked state of EditableDatumLabel should be only sent out if current text in the label is empty or it doesn't contain digits. Previously it was emitted every intermediate wrong state, so stuff like "71." was also being matched, and it resulted in resetting the locked state of the label, which in turn resulted in keeping user from entering float values. --- src/Gui/QuantitySpinBox.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Gui/QuantitySpinBox.cpp b/src/Gui/QuantitySpinBox.cpp index 333f89d9b8..8f4a1e49e3 100644 --- a/src/Gui/QuantitySpinBox.cpp +++ b/src/Gui/QuantitySpinBox.cpp @@ -572,10 +572,16 @@ void QuantitySpinBox::userInput(const QString & text) else { d->validInput = false; - // we have to emit here signal explicitly as validator will not pass - // this value further but we want to check it to disable isSet flag if - // it has been set previously - Q_EMIT valueChanged(d->quantity.getValue()); + // only emit signal to reset EditableDatumLabel if the input is truly empty or has + // no meaningful number don't emit for partially typed numbers like "71." which are + // temporarily invalid + QString trimmedText = text.trimmed(); + if (trimmedText.isEmpty() || !trimmedText.contains(QRegularExpression(QStringLiteral("[0-9]")))) { + // we have to emit here signal explicitly as validator will not pass + // this value further but we want to check it to disable isSet flag if + // it has been set previously + Q_EMIT valueChanged(d->quantity.getValue()); + } return; } From ea006aba10aa2b1a0c69125023c88fd96c942785 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 17 Jun 2025 01:30:02 +0200 Subject: [PATCH 038/126] BIM: Fix IFC type assignment not being saved to file As the title says, this is simple fix - basically right now anytime user changed Link property to point to proper IFC type, we weren't writing to the IFC file buffer to finally write it to the file if user would save it. So this patch makes sure we write to this buffer by calling appropriate function, and making ifc object have proper pointer to IFC type. --- src/Mod/BIM/nativeifc/ifc_objects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/BIM/nativeifc/ifc_objects.py b/src/Mod/BIM/nativeifc/ifc_objects.py index 245d1e213f..3557c72ed7 100644 --- a/src/Mod/BIM/nativeifc/ifc_objects.py +++ b/src/Mod/BIM/nativeifc/ifc_objects.py @@ -65,6 +65,7 @@ class ifc_object: elif prop == "Schema": self.edit_schema(obj, obj.Schema) elif prop == "Type": + self.edit_type(obj) self.assign_classification(obj) elif prop == "Classification": self.edit_classification(obj) From 24851dbae9f38fd5369bdb2e286f384c9f60cc54 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 17 Jun 2025 01:00:35 +0200 Subject: [PATCH 039/126] BIM: Allow user to skip dialog during type conversion As the title says - this adds a new option to the dialog to `never ask again` as well user can customize both settings through preferences. --- src/Mod/BIM/Resources/ui/dialogConvertType.ui | 7 +++ .../BIM/Resources/ui/preferencesNativeIFC.ui | 44 +++++++++++++++++++ src/Mod/BIM/nativeifc/ifc_types.py | 26 ++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Mod/BIM/Resources/ui/dialogConvertType.ui b/src/Mod/BIM/Resources/ui/dialogConvertType.ui index 4466ceacff..d846f5f083 100644 --- a/src/Mod/BIM/Resources/ui/dialogConvertType.ui +++ b/src/Mod/BIM/Resources/ui/dialogConvertType.ui @@ -37,6 +37,13 @@ + + + + Do not ask again and use this setting + + + diff --git a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui index 45c21b4e4c..136249f741 100644 --- a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui +++ b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui @@ -306,6 +306,50 @@ + + + + New type + + + + + + When enabled, converting objects to IFC types will always keep the original object + + + Always keep original object when converting to type + + + ConvertTypeKeepOriginal + + + Mod/NativeIFC + + + + + + + When enabled, a dialog will be shown each time when converting objects to IFC types + + + Show dialog when converting to type + + + true + + + ConvertTypeAskAgain + + + Mod/NativeIFC + + + + + + diff --git a/src/Mod/BIM/nativeifc/ifc_types.py b/src/Mod/BIM/nativeifc/ifc_types.py index 8d78796ee8..7967c471a9 100644 --- a/src/Mod/BIM/nativeifc/ifc_types.py +++ b/src/Mod/BIM/nativeifc/ifc_types.py @@ -30,6 +30,9 @@ from . import ifc_tools translate = FreeCAD.Qt.translate +# Parameters object for NativeIFC preferences +PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC") + def show_type(obj): """Adds the types of that object as FreeCAD objects""" @@ -73,17 +76,36 @@ def convert_to_type(obj, keep_object=False): return if not getattr(obj, "Shape", None): return - if FreeCAD.GuiUp: + + # Check preferences + always_keep = PARAMS.GetBool("ConvertTypeKeepOriginal", False) + ask_again = PARAMS.GetBool("ConvertTypeAskAgain", True) + + if FreeCAD.GuiUp and ask_again: import FreeCADGui dlg = FreeCADGui.PySideUic.loadUi(":/ui/dialogConvertType.ui") - + original_text = dlg.label.text() dlg.label.setText(original_text.replace("%1", obj.Class+"Type")) + # Set the initial state of the checkbox from the "always keep" preference + dlg.checkKeepObject.setChecked(always_keep) + result = dlg.exec_() if not result: return + keep_object = dlg.checkKeepObject.isChecked() + do_not_ask_again = dlg.checkDoNotAskAgain.isChecked() + + # If "Do not ask again" is checked, disable future dialogs and save the current choice + if do_not_ask_again: + PARAMS.SetBool("ConvertTypeAskAgain", False) + PARAMS.SetBool("ConvertTypeKeepOriginal", keep_object) + else: + # Use the saved preference when GUI is not available or user chose "do not ask again" + keep_object = always_keep + element = ifc_tools.get_ifc_element(obj) ifcfile = ifc_tools.get_ifcfile(obj) project = ifc_tools.get_project(obj) From 38499d4470d40db85f255e843c5637dba93909a6 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:38:38 +0200 Subject: [PATCH 040/126] DXF: rename "Group layers into blocks" setting (#21896) * DXF: rename "Group layers into blocks" setting * DXF: apply suggested improvement children => contents * Import: DXF, change tooltip to reflect the reality of the current code https://github.com/FreeCAD/FreeCAD/pull/21896#issuecomment-2958611607 --- src/Mod/Draft/Resources/ui/preferences-dxf.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf.ui b/src/Mod/Draft/Resources/ui/preferences-dxf.ui index 4736743fd8..45b348d707 100644 --- a/src/Mod/Draft/Resources/ui/preferences-dxf.ui +++ b/src/Mod/Draft/Resources/ui/preferences-dxf.ui @@ -394,11 +394,11 @@ Note that this can take a while! - Objects from the same layers will be joined into Draft Blocks, + Objects from the same layers will be joined into Part Compounds, turning the display faster, but making them less easily editable. - Group layers into blocks + Merge layer contents into blocks groupLayers From 21b607a1105ef988eddfa6300b48c9bc1a6d2a1b Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 17 Jun 2025 02:40:01 -0500 Subject: [PATCH 041/126] Draft: Eliminate redundant assignment to self (#22006) * Draft: Eliminate redundant assignment to self * Removed confusing comment. --------- Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com> --- src/Mod/Draft/draftgeoutils/offsets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Mod/Draft/draftgeoutils/offsets.py b/src/Mod/Draft/draftgeoutils/offsets.py index ec9e64eefe..e09ba1ddec 100644 --- a/src/Mod/Draft/draftgeoutils/offsets.py +++ b/src/Mod/Draft/draftgeoutils/offsets.py @@ -338,8 +338,6 @@ def offsetWire(wire, dvec, bind=False, occ=False, if not isinstance(basewireOffset, list): basewireOffset = [basewireOffset] - else: - basewireOffset = basewireOffset # for backward compatibility for i in range(len(edges)): # make a copy so it do not reverse the self.baseWires edges From 3f7438a6868331b22e4f5c0248fbc627884bd772 Mon Sep 17 00:00:00 2001 From: David Tanana <136705971+davetanana@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:59:00 -0400 Subject: [PATCH 042/126] PartDesign: Added 1 3/16 16 threaded drill hole diameter (#22000) * Adding UNf 1 3/16 16 to this list * Updating type and test params --- src/Mod/PartDesign/App/FeatureHole.cpp | 4 +++- src/Mod/PartDesign/App/FeatureHole.h | 2 +- src/Mod/PartDesign/PartDesignTests/TestHole.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Mod/PartDesign/App/FeatureHole.cpp b/src/Mod/PartDesign/App/FeatureHole.cpp index d259e1145c..eeaac1f634 100644 --- a/src/Mod/PartDesign/App/FeatureHole.cpp +++ b/src/Mod/PartDesign/App/FeatureHole.cpp @@ -376,6 +376,7 @@ const std::vector Hole::threadDescription[] = { "7/8", 22.225, 1.814, 20.40 }, { "1", 25.400, 2.117, 23.25 }, { "1 1/8", 28.575, 2.117, 26.50 }, + { "1 3/16", 30.163, 1.588, 28.58 }, { "1 1/4", 31.750, 2.117, 29.50 }, { "1 3/8", 34.925, 2.117, 32.75 }, { "1 1/2", 38.100, 2.117, 36.00 }, @@ -613,7 +614,7 @@ const double Hole::metricHoleDiameters[51][4] = { 150.0, 155.0, 158.0, 165.0} }; -const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[22] = +const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[23] = { /* UTS clearance hole diameters according to ASME B18.2.8 */ // for information: the norm defines a drill bit number (that is in turn standardized in another ASME norm). @@ -641,6 +642,7 @@ const Hole::UTSClearanceDefinition Hole::UTSHoleDiameters[22] = { "7/8", 23.0, 23.8, 26.2 }, { "1", 26.2, 27.8, 29.4 }, { "1 1/8", 29.4, 31.0, 33.3 }, + { "1 3/16", 31.0, 32.5, 34.9 }, { "1 1/4", 32.5, 34.1, 36.5 }, { "1 3/8", 36.5, 38.1, 40.9 }, { "1 1/2", 39.7, 41.3, 44.0 } diff --git a/src/Mod/PartDesign/App/FeatureHole.h b/src/Mod/PartDesign/App/FeatureHole.h index 96b8a91d34..d1ecf74371 100644 --- a/src/Mod/PartDesign/App/FeatureHole.h +++ b/src/Mod/PartDesign/App/FeatureHole.h @@ -116,7 +116,7 @@ public: double normal; double loose; }; - static const UTSClearanceDefinition UTSHoleDiameters[22]; + static const UTSClearanceDefinition UTSHoleDiameters[23]; void Restore(Base::XMLReader & reader) override; diff --git a/src/Mod/PartDesign/PartDesignTests/TestHole.py b/src/Mod/PartDesign/PartDesignTests/TestHole.py index a7af5d0194..37b0aa443b 100644 --- a/src/Mod/PartDesign/PartDesignTests/TestHole.py +++ b/src/Mod/PartDesign/PartDesignTests/TestHole.py @@ -269,7 +269,7 @@ class TestHole(unittest.TestCase): "#0", "#1", "#2", "#3", "#4", "#5", "#6", "#8", "#10", "#12", "1/4", "5/16", "3/8", "7/16", "1/2", "9/16", - "5/8", "3/4", "7/8", "1", "1 1/8", "1 1/4", + "5/8", "3/4", "7/8", "1", "1 1/8", "1 3/16", "1 1/4", "1 3/8", "1 1/2", ], 'UNEF': [ From 8844319d33093d446f873ed8af89b31ca1bb8219 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:24:21 +0100 Subject: [PATCH 043/126] =?UTF-8?q?[BIM][Draft][CAM]=20preparation=20for?= =?UTF-8?q?=20deprecation=20of=20QCheckBox=E2=80=A6=20(#21939)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [BIM][Draft]{CAM] preparation for deprecation of QCheckBox stateChanged -> checkStateChanged --- src/Mod/BIM/bimcommands/BimClassification.py | 5 +- src/Mod/BIM/bimcommands/BimIfcElements.py | 5 +- src/Mod/BIM/bimcommands/BimIfcProperties.py | 11 +++-- src/Mod/BIM/bimcommands/BimIfcQuantities.py | 5 +- src/Mod/BIM/bimcommands/BimWall.py | 5 +- src/Mod/BIM/bimcommands/BimWindow.py | 5 +- .../CAM/Path/Base/Gui/PreferencesAdvanced.py | 8 +++- src/Mod/CAM/Path/Dressup/Gui/Boundary.py | 5 +- src/Mod/CAM/Path/Op/Gui/Adaptive.py | 14 ++++-- src/Mod/CAM/Path/Op/Gui/Drilling.py | 20 +++++--- src/Mod/CAM/Path/Op/Gui/Profile.py | 22 ++++++--- src/Mod/CAM/Path/Op/Gui/Slot.py | 5 +- src/Mod/CAM/Path/Op/Gui/Surface.py | 14 ++++-- src/Mod/CAM/Path/Op/Gui/ThreadMilling.py | 5 +- src/Mod/CAM/Path/Op/Gui/Waterline.py | 5 +- src/Mod/Draft/DraftGui.py | 48 +++++++++++++------ src/Mod/Draft/draftguitools/gui_fillets.py | 8 +++- .../Draft/draftguitools/gui_selectplane.py | 5 +- .../drafttaskpanels/task_circulararray.py | 8 +++- .../Draft/drafttaskpanels/task_orthoarray.py | 8 +++- .../Draft/drafttaskpanels/task_polararray.py | 8 +++- .../Draft/drafttaskpanels/task_shapestring.py | 5 +- 22 files changed, 166 insertions(+), 58 deletions(-) diff --git a/src/Mod/BIM/bimcommands/BimClassification.py b/src/Mod/BIM/bimcommands/BimClassification.py index a858215d52..5c38462f6f 100644 --- a/src/Mod/BIM/bimcommands/BimClassification.py +++ b/src/Mod/BIM/bimcommands/BimClassification.py @@ -159,7 +159,10 @@ class BIM_Classification: self.form.treeClass.itemDoubleClicked.connect(self.apply) self.form.search.up.connect(self.onUpArrow) self.form.search.down.connect(self.onDownArrow) - self.form.onlyVisible.stateChanged.connect(self.onVisible) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.onVisible) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.onVisible) # center the dialog over FreeCAD window mw = FreeCADGui.getMainWindow() diff --git a/src/Mod/BIM/bimcommands/BimIfcElements.py b/src/Mod/BIM/bimcommands/BimIfcElements.py index ced59f2d63..736f488e82 100644 --- a/src/Mod/BIM/bimcommands/BimIfcElements.py +++ b/src/Mod/BIM/bimcommands/BimIfcElements.py @@ -95,7 +95,10 @@ class BIM_IfcElements: ) self.form.groupMode.currentIndexChanged.connect(self.update) self.form.tree.clicked.connect(self.onClickTree) - self.form.onlyVisible.stateChanged.connect(self.update) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.update) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.update) self.form.buttonBox.accepted.connect(self.accept) self.form.globalMode.currentIndexChanged.connect(self.onObjectTypeChanged) self.form.globalMaterial.currentIndexChanged.connect(self.onMaterialChanged) diff --git a/src/Mod/BIM/bimcommands/BimIfcProperties.py b/src/Mod/BIM/bimcommands/BimIfcProperties.py index fc99e827b8..d72d7cc9d4 100644 --- a/src/Mod/BIM/bimcommands/BimIfcProperties.py +++ b/src/Mod/BIM/bimcommands/BimIfcProperties.py @@ -139,10 +139,15 @@ class BIM_IfcProperties: # connect signals self.form.tree.selectionModel().selectionChanged.connect(self.updateProperties) self.form.groupMode.currentIndexChanged.connect(self.update) - self.form.onlyVisible.stateChanged.connect(self.onVisible) - self.form.onlySelected.stateChanged.connect(self.onSelected) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.update) + self.form.onlySelected.checkStateChanged.connect(self.onSelected) + self.form.onlyMatches.checkStateChanged.connect(self.update) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.update) + self.form.onlySelected.stateChanged.connect(self.onSelected) + self.form.onlyMatches.stateChanged.connect(self.update) self.form.buttonBox.accepted.connect(self.accept) - self.form.onlyMatches.stateChanged.connect(self.update) self.form.searchField.currentIndexChanged.connect(self.update) self.form.searchField.editTextChanged.connect(self.update) self.form.comboProperty.currentIndexChanged.connect(self.addProperty) diff --git a/src/Mod/BIM/bimcommands/BimIfcQuantities.py b/src/Mod/BIM/bimcommands/BimIfcQuantities.py index 7622eeca9b..f85f8b61ca 100644 --- a/src/Mod/BIM/bimcommands/BimIfcQuantities.py +++ b/src/Mod/BIM/bimcommands/BimIfcQuantities.py @@ -122,7 +122,10 @@ class BIM_IfcQuantities: self.qmodel.dataChanged.connect(self.setChecked) self.form.buttonBox.accepted.connect(self.accept) self.form.quantities.clicked.connect(self.onClickTree) - self.form.onlyVisible.stateChanged.connect(self.update) + if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.onlyVisible.checkStateChanged.connect(self.update) + else: # Qt version < 6.7.0 + self.form.onlyVisible.stateChanged.connect(self.update) self.form.buttonRefresh.clicked.connect(self.update) self.form.buttonApply.clicked.connect(self.add_qto) diff --git a/src/Mod/BIM/bimcommands/BimWall.py b/src/Mod/BIM/bimcommands/BimWall.py index e94a1f38a8..daeafd839a 100644 --- a/src/Mod/BIM/bimcommands/BimWall.py +++ b/src/Mod/BIM/bimcommands/BimWall.py @@ -358,7 +358,10 @@ class Arch_Wall: inputHeight.valueChanged.connect(self.setHeight) comboAlignment.currentIndexChanged.connect(self.setAlign) inputOffset.valueChanged.connect(self.setOffset) - checkboxUseSketches.stateChanged.connect(self.setUseSketch) + if hasattr(checkboxUseSketches, "checkStateChanged"): # Qt version >= 6.7.0 + checkboxUseSketches.checkStateChanged.connect(self.setUseSketch) + else: # Qt version < 6.7.0 + checkboxUseSketches.stateChanged.connect(self.setUseSketch) comboWallPresets.currentIndexChanged.connect(self.setMat) # Define the workflow of the input fields: diff --git a/src/Mod/BIM/bimcommands/BimWindow.py b/src/Mod/BIM/bimcommands/BimWindow.py index 05ab3d0e00..7ae2e08769 100644 --- a/src/Mod/BIM/bimcommands/BimWindow.py +++ b/src/Mod/BIM/bimcommands/BimWindow.py @@ -315,7 +315,10 @@ class Arch_Window: include = QtGui.QCheckBox(translate("Arch","Auto include in host object")) include.setChecked(True) grid.addWidget(include,0,0,1,2) - include.stateChanged.connect(self.setInclude) + if hasattr(include, "checkStateChanged"): # Qt version >= 6.7.0 + include.checkStateChanged.connect(self.setInclude) + else: # Qt version < 6.7.0 + include.stateChanged.connect(self.setInclude) # sill height labels = QtGui.QLabel(translate("Arch","Sill height")) diff --git a/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py b/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py index 1cab794e0a..cbeffc76e6 100644 --- a/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py +++ b/src/Mod/CAM/Path/Base/Gui/PreferencesAdvanced.py @@ -33,8 +33,12 @@ else: class AdvancedPreferencesPage: def __init__(self, parent=None): self.form = FreeCADGui.PySideUic.loadUi(":preferences/Advanced.ui") - self.form.WarningSuppressAllSpeeds.stateChanged.connect(self.updateSelection) - self.form.EnableAdvancedOCLFeatures.stateChanged.connect(self.updateSelection) + if hasattr(self.form.WarningSuppressAllSpeeds, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.WarningSuppressAllSpeeds.checkStateChanged.connect(self.updateSelection) + self.form.EnableAdvancedOCLFeatures.checkStateChanged.connect(self.updateSelection) + else: # Qt version < 6.7.0 + self.form.WarningSuppressAllSpeeds.stateChanged.connect(self.updateSelection) + self.form.EnableAdvancedOCLFeatures.stateChanged.connect(self.updateSelection) def saveSettings(self): Path.Preferences.setPreferencesAdvanced( diff --git a/src/Mod/CAM/Path/Dressup/Gui/Boundary.py b/src/Mod/CAM/Path/Dressup/Gui/Boundary.py index 56482c31b3..dc2eac675d 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/Boundary.py +++ b/src/Mod/CAM/Path/Dressup/Gui/Boundary.py @@ -182,7 +182,10 @@ class TaskPanel(object): self.form.stockInside.setChecked(self.obj.Inside) self.form.stock.currentIndexChanged.connect(self.updateStockEditor) - self.form.stockInside.stateChanged.connect(self.setDirty) + if hasattr(self.form.stockInside, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.stockInside.checkStateChanged.connect(self.setDirty) + else: # Qt version < 6.7.0 + self.form.stockInside.stateChanged.connect(self.setDirty) self.form.stockExtXneg.textChanged.connect(self.setDirty) self.form.stockExtXpos.textChanged.connect(self.setDirty) self.form.stockExtYneg.textChanged.connect(self.setDirty) diff --git a/src/Mod/CAM/Path/Op/Gui/Adaptive.py b/src/Mod/CAM/Path/Op/Gui/Adaptive.py index c2438dd21c..d16b6ace33 100644 --- a/src/Mod/CAM/Path/Op/Gui/Adaptive.py +++ b/src/Mod/CAM/Path/Op/Gui/Adaptive.py @@ -68,10 +68,16 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.StockToLeave.valueChanged) signals.append(self.form.ZStockToLeave.valueChanged) signals.append(self.form.coolantController.currentIndexChanged) - signals.append(self.form.ForceInsideOut.stateChanged) - signals.append(self.form.FinishingProfile.stateChanged) - signals.append(self.form.useOutline.stateChanged) - signals.append(self.form.orderCutsByRegion.stateChanged) + if hasattr(self.form.ForceInsideOut, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.ForceInsideOut.checkStateChanged) + signals.append(self.form.FinishingProfile.checkStateChanged) + signals.append(self.form.useOutline.checkStateChanged) + signals.append(self.form.orderCutsByRegion.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.ForceInsideOut.stateChanged) + signals.append(self.form.FinishingProfile.stateChanged) + signals.append(self.form.useOutline.stateChanged) + signals.append(self.form.orderCutsByRegion.stateChanged) signals.append(self.form.StopButton.toggled) return signals diff --git a/src/Mod/CAM/Path/Op/Gui/Drilling.py b/src/Mod/CAM/Path/Op/Gui/Drilling.py index f39afee560..5d14580411 100644 --- a/src/Mod/CAM/Path/Op/Gui/Drilling.py +++ b/src/Mod/CAM/Path/Op/Gui/Drilling.py @@ -188,15 +188,23 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): signals.append(self.form.peckRetractHeight.editingFinished) signals.append(self.form.peckDepth.editingFinished) signals.append(self.form.dwellTime.editingFinished) - signals.append(self.form.dwellEnabled.stateChanged) - signals.append(self.form.peckEnabled.stateChanged) - signals.append(self.form.chipBreakEnabled.stateChanged) + if hasattr(self.form.dwellEnabled, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.dwellEnabled.checkStateChanged) + signals.append(self.form.peckEnabled.checkStateChanged) + signals.append(self.form.chipBreakEnabled.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.dwellEnabled.stateChanged) + signals.append(self.form.peckEnabled.stateChanged) + signals.append(self.form.chipBreakEnabled.stateChanged) signals.append(self.form.toolController.currentIndexChanged) signals.append(self.form.coolantController.currentIndexChanged) signals.append(self.form.ExtraOffset.currentIndexChanged) - signals.append(self.form.KeepToolDownEnabled.stateChanged) - signals.append(self.form.feedRetractEnabled.stateChanged) - + if hasattr(self.form.KeepToolDownEnabled, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.KeepToolDownEnabled.checkStateChanged) + signals.append(self.form.feedRetractEnabled.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.KeepToolDownEnabled.stateChanged) + signals.append(self.form.feedRetractEnabled.stateChanged) return signals def updateData(self, obj, prop): diff --git a/src/Mod/CAM/Path/Op/Gui/Profile.py b/src/Mod/CAM/Path/Op/Gui/Profile.py index f7aa0aff22..03146a4356 100644 --- a/src/Mod/CAM/Path/Op/Gui/Profile.py +++ b/src/Mod/CAM/Path/Op/Gui/Profile.py @@ -125,11 +125,18 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.extraOffset.editingFinished) signals.append(self.form.numPasses.editingFinished) signals.append(self.form.stepover.editingFinished) - signals.append(self.form.useCompensation.stateChanged) - signals.append(self.form.useStartPoint.stateChanged) - signals.append(self.form.processHoles.stateChanged) - signals.append(self.form.processPerimeter.stateChanged) - signals.append(self.form.processCircles.stateChanged) + if hasattr(self.form.useCompensation, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.useCompensation.checkStateChanged) + signals.append(self.form.useStartPoint.checkStateChanged) + signals.append(self.form.processHoles.checkStateChanged) + signals.append(self.form.processPerimeter.checkStateChanged) + signals.append(self.form.processCircles.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.useCompensation.stateChanged) + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.processHoles.stateChanged) + signals.append(self.form.processPerimeter.stateChanged) + signals.append(self.form.processCircles.stateChanged) return signals @@ -159,7 +166,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.stepover.setEnabled(self.obj.NumPasses > 1) def registerSignalHandlers(self, obj): - self.form.useCompensation.stateChanged.connect(self.updateVisibility) + if hasattr(self.form.useCompensation, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.useCompensation.checkStateChanged.connect(self.updateVisibility) + else: # Qt version < 6.7.0 + self.form.useCompensation.stateChanged.connect(self.updateVisibility) self.form.numPasses.editingFinished.connect(self.updateVisibility) diff --git a/src/Mod/CAM/Path/Op/Gui/Slot.py b/src/Mod/CAM/Path/Op/Gui/Slot.py index 0b6ad86cd7..f75d28a25c 100644 --- a/src/Mod/CAM/Path/Op/Gui/Slot.py +++ b/src/Mod/CAM/Path/Op/Gui/Slot.py @@ -142,7 +142,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.geo2Reference.currentIndexChanged) signals.append(self.form.layerMode.currentIndexChanged) signals.append(self.form.pathOrientation.currentIndexChanged) - signals.append(self.form.reverseDirection.stateChanged) + if hasattr(self.form.reverseDirection, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.reverseDirection.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.reverseDirection.stateChanged) return signals def updateVisibility(self, sentObj=None): diff --git a/src/Mod/CAM/Path/Op/Gui/Surface.py b/src/Mod/CAM/Path/Op/Gui/Surface.py index b1103a0a98..68a3ed40cc 100644 --- a/src/Mod/CAM/Path/Op/Gui/Surface.py +++ b/src/Mod/CAM/Path/Op/Gui/Surface.py @@ -217,10 +217,16 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.depthOffset.editingFinished) signals.append(self.form.stepOver.editingFinished) signals.append(self.form.sampleInterval.editingFinished) - signals.append(self.form.useStartPoint.stateChanged) - signals.append(self.form.boundaryEnforcement.stateChanged) - signals.append(self.form.optimizeEnabled.stateChanged) - signals.append(self.form.optimizeStepOverTransitions.stateChanged) + if hasattr(self.form.useStartPoint, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.useStartPoint.checkStateChanged) + signals.append(self.form.boundaryEnforcement.checkStateChanged) + signals.append(self.form.optimizeEnabled.checkStateChanged) + signals.append(self.form.optimizeStepOverTransitions.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.boundaryEnforcement.stateChanged) + signals.append(self.form.optimizeEnabled.stateChanged) + signals.append(self.form.optimizeStepOverTransitions.stateChanged) return signals diff --git a/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py b/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py index 4b6d95c78b..d38c601227 100644 --- a/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py +++ b/src/Mod/CAM/Path/Op/Gui/ThreadMilling.py @@ -229,7 +229,10 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): signals.append(self.form.threadTPI.editingFinished) signals.append(self.form.opDirection.currentIndexChanged) signals.append(self.form.opPasses.editingFinished) - signals.append(self.form.leadInOut.stateChanged) + if hasattr(self.form.leadInOut, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.leadInOut.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.leadInOut.stateChanged) signals.append(self.form.toolController.currentIndexChanged) diff --git a/src/Mod/CAM/Path/Op/Gui/Waterline.py b/src/Mod/CAM/Path/Op/Gui/Waterline.py index 23347f8a4c..dba1cd1533 100644 --- a/src/Mod/CAM/Path/Op/Gui/Waterline.py +++ b/src/Mod/CAM/Path/Op/Gui/Waterline.py @@ -126,7 +126,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.boundaryAdjustment.editingFinished) signals.append(self.form.stepOver.editingFinished) signals.append(self.form.sampleInterval.editingFinished) - signals.append(self.form.optimizeEnabled.stateChanged) + if hasattr(self.form.optimizeEnabled, "checkStateChanged"): # Qt version >= 6.7.0 + signals.append(self.form.optimizeEnabled.checkStateChanged) + else: # Qt version < 6.7.0 + signals.append(self.form.optimizeEnabled.stateChanged) return signals diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py index 96dc3e97c6..01b7f1f95d 100644 --- a/src/Mod/Draft/DraftGui.py +++ b/src/Mod/Draft/DraftGui.py @@ -432,7 +432,10 @@ class DraftToolBar: QtCore.QObject.connect(self.zValue,QtCore.SIGNAL("valueChanged(double)"),self.changeZValue) QtCore.QObject.connect(self.lengthValue,QtCore.SIGNAL("valueChanged(double)"),self.changeLengthValue) QtCore.QObject.connect(self.angleValue,QtCore.SIGNAL("valueChanged(double)"),self.changeAngleValue) - QtCore.QObject.connect(self.angleLock,QtCore.SIGNAL("stateChanged(int)"),self.toggleAngle) + if hasattr(self.angleLock, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.connect(self.angleLock,QtCore.SIGNAL("checkStateChanged(int)"),self.toggleAngle) + else: # Qt version < 6.7.0 + QtCore.QObject.connect(self.angleLock,QtCore.SIGNAL("stateChanged(int)"),self.toggleAngle) QtCore.QObject.connect(self.radiusValue,QtCore.SIGNAL("valueChanged(double)"),self.changeRadiusValue) QtCore.QObject.connect(self.xValue,QtCore.SIGNAL("returnPressed()"),self.checkx) QtCore.QObject.connect(self.yValue,QtCore.SIGNAL("returnPressed()"),self.checky) @@ -457,15 +460,22 @@ class DraftToolBar: QtCore.QObject.connect(self.orientWPButton,QtCore.SIGNAL("pressed()"),self.orientWP) QtCore.QObject.connect(self.undoButton,QtCore.SIGNAL("pressed()"),self.undoSegment) QtCore.QObject.connect(self.selectButton,QtCore.SIGNAL("pressed()"),self.selectEdge) - QtCore.QObject.connect(self.continueCmd,QtCore.SIGNAL("stateChanged(int)"),self.setContinue) - QtCore.QObject.connect(self.chainedModeCmd,QtCore.SIGNAL("stateChanged(int)"),self.setChainedMode) - - QtCore.QObject.connect(self.isCopy,QtCore.SIGNAL("stateChanged(int)"),self.setCopymode) - QtCore.QObject.connect(self.isSubelementMode, QtCore.SIGNAL("stateChanged(int)"), self.setSubelementMode) - - QtCore.QObject.connect(self.isRelative,QtCore.SIGNAL("stateChanged(int)"),self.setRelative) - QtCore.QObject.connect(self.isGlobal,QtCore.SIGNAL("stateChanged(int)"),self.setGlobal) - QtCore.QObject.connect(self.makeFace,QtCore.SIGNAL("stateChanged(int)"),self.setMakeFace) + if hasattr(self.continueCmd, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.connect(self.continueCmd,QtCore.SIGNAL("checkStateChanged(int)"),self.setContinue) + QtCore.QObject.connect(self.chainedModeCmd,QtCore.SIGNAL("checkStateChanged(int)"),self.setChainedMode) + QtCore.QObject.connect(self.isCopy,QtCore.SIGNAL("checkStateChanged(int)"),self.setCopymode) + QtCore.QObject.connect(self.isSubelementMode, QtCore.SIGNAL("checkStateChanged(int)"), self.setSubelementMode) + QtCore.QObject.connect(self.isRelative,QtCore.SIGNAL("checkStateChanged(int)"),self.setRelative) + QtCore.QObject.connect(self.isGlobal,QtCore.SIGNAL("checkStateChanged(int)"),self.setGlobal) + QtCore.QObject.connect(self.makeFace,QtCore.SIGNAL("checkStateChanged(int)"),self.setMakeFace) + else: # Qt version < 6.7.0 + QtCore.QObject.connect(self.continueCmd,QtCore.SIGNAL("stateChanged(int)"),self.setContinue) + QtCore.QObject.connect(self.chainedModeCmd,QtCore.SIGNAL("stateChanged(int)"),self.setChainedMode) + QtCore.QObject.connect(self.isCopy,QtCore.SIGNAL("stateChanged(int)"),self.setCopymode) + QtCore.QObject.connect(self.isSubelementMode, QtCore.SIGNAL("stateChanged(int)"), self.setSubelementMode) + QtCore.QObject.connect(self.isRelative,QtCore.SIGNAL("stateChanged(int)"),self.setRelative) + QtCore.QObject.connect(self.isGlobal,QtCore.SIGNAL("stateChanged(int)"),self.setGlobal) + QtCore.QObject.connect(self.makeFace,QtCore.SIGNAL("stateChanged(int)"),self.setMakeFace) QtCore.QObject.connect(self.baseWidget,QtCore.SIGNAL("resized()"),self.relocate) QtCore.QObject.connect(self.baseWidget,QtCore.SIGNAL("retranslate()"),self.retranslateUi) @@ -962,7 +972,12 @@ class DraftToolBar: # gui_stretch.py def setRelative(self, val=-1): if val < 0: - QtCore.QObject.disconnect(self.isRelative, + if hasattr(self.isRelative, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.disconnect(self.isRelative, + QtCore.SIGNAL("checkStateChanged(int)"), + self.setRelative) + else: # Qt version < 6.7.0 + QtCore.QObject.disconnect(self.isRelative, QtCore.SIGNAL("stateChanged(int)"), self.setRelative) if val == -1: @@ -972,9 +987,14 @@ class DraftToolBar: val = params.get_param("RelativeMode") self.isRelative.setChecked(val) self.relativeMode = val - QtCore.QObject.connect(self.isRelative, - QtCore.SIGNAL("stateChanged(int)"), - self.setRelative) + if hasattr(self.isRelative, "checkStateChanged"): # Qt version >= 6.7.0 + QtCore.QObject.disconnect(self.isRelative, + QtCore.SIGNAL("checkStateChanged(int)"), + self.setRelative) + else: # Qt version < 6.7.0 + QtCore.QObject.disconnect(self.isRelative, + QtCore.SIGNAL("stateChanged(int)"), + self.setRelative) else: params.set_param("RelativeMode", bool(val)) self.relativeMode = bool(val) diff --git a/src/Mod/Draft/draftguitools/gui_fillets.py b/src/Mod/Draft/draftguitools/gui_fillets.py index 187c2e75ac..e36610fe59 100644 --- a/src/Mod/Draft/draftguitools/gui_fillets.py +++ b/src/Mod/Draft/draftguitools/gui_fillets.py @@ -100,8 +100,12 @@ class Fillet(gui_base_original.Creator): "Create chamfer")) self.ui.check_chamfer.show() - self.ui.check_delete.stateChanged.connect(self.set_delete) - self.ui.check_chamfer.stateChanged.connect(self.set_chamfer) + if hasattr(self.ui.check_delete, "checkStateChanged"): # Qt version >= 6.7.0 + self.ui.check_delete.checkStateChanged.connect(self.set_delete) + self.ui.check_chamfer.checkStateChanged.connect(self.set_chamfer) + else: # Qt version < 6.7.0 + self.ui.check_delete.stateChanged.connect(self.set_delete) + self.ui.check_chamfer.stateChanged.connect(self.set_chamfer) # TODO: somehow we need to set up the trackers # to show a preview of the fillet. diff --git a/src/Mod/Draft/draftguitools/gui_selectplane.py b/src/Mod/Draft/draftguitools/gui_selectplane.py index a9cdcf4959..5baae69e38 100644 --- a/src/Mod/Draft/draftguitools/gui_selectplane.py +++ b/src/Mod/Draft/draftguitools/gui_selectplane.py @@ -126,7 +126,10 @@ class Draft_SelectPlane: form.buttonPrevious.clicked.connect(self.on_click_previous) form.buttonNext.clicked.connect(self.on_click_next) form.fieldOffset.textEdited.connect(self.on_set_offset) - form.checkCenter.stateChanged.connect(self.on_set_center) + if hasattr(form.checkCenter, "checkStateChanged"): # Qt version >= 6.7.0 + form.checkCenter.checkStateChanged.connect(self.on_set_center) + else: # Qt version < 6.7.0 + form.checkCenter.stateChanged.connect(self.on_set_center) form.fieldGridSpacing.textEdited.connect(self.on_set_grid_size) form.fieldGridMainLine.valueChanged.connect(self.on_set_main_line) form.fieldGridExtension.valueChanged.connect(self.on_set_extension) diff --git a/src/Mod/Draft/drafttaskpanels/task_circulararray.py b/src/Mod/Draft/drafttaskpanels/task_circulararray.py index 31d5361b35..1c555fc998 100644 --- a/src/Mod/Draft/drafttaskpanels/task_circulararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_circulararray.py @@ -129,8 +129,12 @@ class TaskPanelCircularArray: self.form.button_reset.clicked.connect(self.reset_point) # When the checkbox changes, change the internal value - self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - self.form.checkbox_link.stateChanged.connect(self.set_link) + if hasattr(self.form.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.checkbox_fuse.checkStateChanged.connect(self.set_fuse) + self.form.checkbox_link.checkStateChanged.connect(self.set_link) + else: # Qt version < 6.7.0 + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) def accept(self): diff --git a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py index 5be612a1a1..d68ed094da 100644 --- a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py +++ b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py @@ -154,8 +154,12 @@ class TaskPanelOrthoArray: self.form.button_reset_Z.clicked.connect(lambda: self.reset_v("Z")) # When the checkbox changes, change the internal value - self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - self.form.checkbox_link.stateChanged.connect(self.set_link) + if hasattr(self.form.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.checkbox_fuse.checkStateChanged.connect(self.set_fuse) + self.form.checkbox_link.checkStateChanged.connect(self.set_link) + else: # Qt version < 6.7.0 + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) # Linear mode callbacks - only set up if the UI elements exist self.form.button_linear_mode.clicked.connect(self.toggle_linear_mode) diff --git a/src/Mod/Draft/drafttaskpanels/task_polararray.py b/src/Mod/Draft/drafttaskpanels/task_polararray.py index 267b8193c5..324a0ec0e1 100644 --- a/src/Mod/Draft/drafttaskpanels/task_polararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_polararray.py @@ -122,8 +122,12 @@ class TaskPanelPolarArray: self.form.button_reset.clicked.connect(self.reset_point) # When the checkbox changes, change the internal value - self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - self.form.checkbox_link.stateChanged.connect(self.set_link) + if hasattr(self.form.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.checkbox_fuse.checkStateChanged.connect(self.set_fuse) + self.form.checkbox_link.checkStateChanged.connect(self.set_link) + else: # Qt version < 6.7.0 + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) def accept(self): diff --git a/src/Mod/Draft/drafttaskpanels/task_shapestring.py b/src/Mod/Draft/drafttaskpanels/task_shapestring.py index 13bb81b45e..a1a182cf50 100644 --- a/src/Mod/Draft/drafttaskpanels/task_shapestring.py +++ b/src/Mod/Draft/drafttaskpanels/task_shapestring.py @@ -86,7 +86,10 @@ class ShapeStringTaskPanel: self.form.sbX.valueChanged.connect(self.set_point_x) self.form.sbY.valueChanged.connect(self.set_point_y) self.form.sbZ.valueChanged.connect(self.set_point_z) - self.form.cbGlobalMode.stateChanged.connect(self.set_global_mode) + if hasattr(self.form.cbGlobalMode.checkbox_fuse, "checkStateChanged"): # Qt version >= 6.7.0 + self.form.cbGlobalMode.checkStateChanged.connect(self.set_global_mode) + else: # Qt version < 6.7.0 + self.form.cbGlobalMode.stateChanged.connect(self.set_global_mode) self.form.pbReset.clicked.connect(self.reset_point) self.form.sbHeight.valueChanged.connect(self.set_height) self.form.leText.textEdited.connect(self.set_text) From a90a8f1fac6b6ebf556204fd0a23933aa315eca4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:46:33 +0000 Subject: [PATCH 044/126] Bump github/issue-metrics from 3.20.1 to 3.21.0 Bumps [github/issue-metrics](https://github.com/github/issue-metrics) from 3.20.1 to 3.21.0. - [Release notes](https://github.com/github/issue-metrics/releases) - [Commits](https://github.com/github/issue-metrics/compare/119b5237f41e78241b9b9cae254e544b52a359a0...346541fd0068df64c02607a4c7f55438dc2881e2) --- updated-dependencies: - dependency-name: github/issue-metrics dependency-version: 3.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/issue-metrics.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-metrics.yml b/.github/workflows/issue-metrics.yml index eb6914e8c6..42e7d8d49a 100644 --- a/.github/workflows/issue-metrics.yml +++ b/.github/workflows/issue-metrics.yml @@ -35,7 +35,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@119b5237f41e78241b9b9cae254e544b52a359a0 # v3.20.1 + uses: github/issue-metrics@346541fd0068df64c02607a4c7f55438dc2881e2 # v3.21.0 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:FreeCAD/FreeCAD is:issue created:${{ env.last_month }}' From 0662b8c0bb0a4b68d24b4789e91692520ec8eb6c Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 17 Jun 2025 01:08:10 +0200 Subject: [PATCH 045/126] Gui: Change ordering of names in Link property As the title says, currently it is: ObjName (Label), this patch changes it to Label (ObjName) to be more user friendly. --- src/Gui/Dialogs/DlgPropertyLink.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Gui/Dialogs/DlgPropertyLink.cpp b/src/Gui/Dialogs/DlgPropertyLink.cpp index 17dd3a06ef..87ec251ded 100644 --- a/src/Gui/Dialogs/DlgPropertyLink.cpp +++ b/src/Gui/Dialogs/DlgPropertyLink.cpp @@ -173,8 +173,8 @@ DlgPropertyLink::formatObject(App::Document* ownerDoc, App::DocumentObject* obj, if (obj->Label.getStrValue() == obj->getNameInDocument()) { return QLatin1String(objName); } - return QStringLiteral("%1 (%2)").arg(QLatin1String(objName), - QString::fromUtf8(obj->Label.getValue())); + return QStringLiteral("%1 (%2)").arg(QString::fromUtf8(obj->Label.getValue()), + QLatin1String(objName)); } auto sobj = obj->getSubObject(sub); @@ -182,10 +182,10 @@ DlgPropertyLink::formatObject(App::Document* ownerDoc, App::DocumentObject* obj, return QStringLiteral("%1.%2").arg(QLatin1String(objName), QString::fromUtf8(sub)); } - return QStringLiteral("%1.%2 (%3)") - .arg(QLatin1String(objName), - QString::fromUtf8(sub), - QString::fromUtf8(sobj->Label.getValue())); + return QStringLiteral("%1 (%2.%3)") + .arg(QString::fromUtf8(sobj->Label.getValue()), + QLatin1String(objName), + QString::fromUtf8(sub)); } static inline bool isLinkSub(const QList& links) From f23d4c8e7e7ad0f2ab5ccf3f5007195c5c09dd8b Mon Sep 17 00:00:00 2001 From: Jacob Oursland Date: Mon, 16 Jun 2025 22:39:08 -0700 Subject: [PATCH 046/126] CI: determine modified lines in a clang-tidy compatible way. --- .github/workflows/sub_prepare.yml | 33 +++++- tools/lint/changed_lines.py | 169 ++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 tools/lint/changed_lines.py diff --git a/.github/workflows/sub_prepare.yml b/.github/workflows/sub_prepare.yml index cec57f0c17..f7ea5e68a3 100644 --- a/.github/workflows/sub_prepare.yml +++ b/.github/workflows/sub_prepare.yml @@ -46,10 +46,16 @@ on: value: ${{ jobs.Prepare.outputs.reportFile }} changedFiles: value: ${{ jobs.Prepare.outputs.changedFiles }} + changedLines: + value: ${{ jobs.Prepare.outputs.changedLines }} changedPythonFiles: value: ${{ jobs.Prepare.outputs.changedPythonFiles }} + changedPythonLines: + value: ${{ jobs.Prepare.outputs.changedPythonLines }} changedCppFiles: value: ${{ jobs.Prepare.outputs.changedCppFiles }} + changedCppLines: + value: ${{ jobs.Prepare.outputs.changedCppLines }} jobs: @@ -67,8 +73,11 @@ jobs: outputs: reportFile: ${{ steps.Init.outputs.reportFile }} changedFiles: ${{ steps.Output.outputs.changedFiles }} + changedLines: ${{ steps.Output.outputs.changedLines }} changedPythonFiles: ${{ steps.Output.outputs.changedPythonFiles }} + changedPythonLines: ${{ steps.Output.outputs.changedPythonLines }} changedCppFiles: ${{ steps.Output.outputs.changedCppFiles }} + changedCppLines: ${{ steps.Output.outputs.changedCppLines }} steps: - name: Harden the runner (Audit all outbound calls) @@ -84,6 +93,10 @@ jobs: commitCnt=0 touch ${{ env.logdir }}changedFiles.lst ${{ env.logdir }}changedCppFiles.lst ${{ env.logdir }}changedPythonFiles.lst echo "reportFile=${{ env.reportfilename }}" >> $GITHUB_OUTPUT + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: true - name: Determine base and head SHA in case of PR if: env.isPR == 'true' run: | @@ -134,10 +147,21 @@ jobs: echo "Changeset is composed of $commitCnt commit(s)" | tee -a ${{env.reportdir}}${{ env.reportfilename }} - name: Get files modified in changeset #TODO check what happens with deleted file in the subsequent process if: env.isPR == 'true' || env.isPush == 'true' + env: + API_URL: ${{ github.api_url }} + TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + REF: ${{ github.ref_name }} + PR: ${{ github.event.number }} run: | - jq '.files[] | if .status != "removed" then .filename else empty end' ${{ env.logdir }}compare.json > ${{ env.logdir }}changedFiles.lst - grep -E '\.(py|py3)"' ${{ env.logdir }}changedFiles.lst > ${{ env.logdir }}changedPythonFiles.lst || true - grep -E '\.(c|c\+\+|cc|cpp|cu|cuh|cxx|h|h\+\+|hh|hpp|hxx)"' ${{ env.logdir }}changedFiles.lst > ${{ env.logdir }}changedCppFiles.lst || true + # could reduce this to a single + python3 tools/lint/changed_lines.py --api-url ${API_URL} --token ${TOKEN} --repo ${REPO} --ref=${REF} --pr=${PR} > ${{ env.logdir }}changedLines.lst + cat ${{ env.logdir }}changedLines.lst | jq '.[].name' > ${{ env.logdir }}changedFiles.lst + python3 tools/lint/changed_lines.py --api-url ${API_URL} --token ${TOKEN} --repo ${REPO} --ref=${REF} --pr=${PR} --file-filter '.py, .pyi' > ${{ env.logdir }}changedPythonLines.lst + cat ${{ env.logdir }}changedPythonLines.lst | jq '.[].name' > ${{ env.logdir }}changedPythonFiles.lst + python3 tools/lint/changed_lines.py --api-url ${API_URL} --token ${TOKEN} --repo ${REPO} --ref=${REF} --pr=${PR} --file-filter '.c, .cc, .cu, .cuh, .c++, .cpp, .cxx, .h, .hh, .h++, .hpp, .hxx' > ${{ env.logdir }}changedCppLines.lst + cat ${{ env.logdir }}changedCppLines.lst | jq '.[].name' > ${{ env.logdir }}changedCppFiles.lst + # Write the report echo "::group::Modified files in changeset (removed files are ignored) :" ; cat ${{ env.logdir }}changedFiles.lst ; echo "::endgroup::" echo "
Modified files (removed files are ignored):" >> ${{env.reportdir}}${{ env.reportfilename }} @@ -148,8 +172,11 @@ jobs: id: Output run: | echo "changedFiles=$(cat ${{ env.logdir }}changedFiles.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "changedLines=$(cat ${{ env.logdir }}changedLines.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT echo "changedPythonFiles=$(cat ${{ env.logdir }}changedPythonFiles.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "changedPythonLines=$(cat ${{ env.logdir }}changedPythonLines.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT echo "changedCppFiles=$(cat ${{ env.logdir }}changedCppFiles.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "changedCppLines=$(cat ${{ env.logdir }}changedCppLines.lst | tr '\n' ' ')" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT - name: Upload logs if: always() diff --git a/tools/lint/changed_lines.py b/tools/lint/changed_lines.py new file mode 100644 index 0000000000..f292e0e46b --- /dev/null +++ b/tools/lint/changed_lines.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 + +# Modified to generate output compatible with `clang-tidy`'s `--line-filter` option +# +# Based on https://github.com/hestonhoffman/changed-lines/blob/main/main.py +# +# Original License +# +# The MIT License (MIT) +# +# Copyright (c) 2023 Heston Hoffman +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +Uses GitHub API to grab patch data for a PR and calculate changed lines +""" + +import argparse +import json +import os +import re +import requests + + +class MissingPatchData(Exception): + """Raised when the patch data is missing""" + + +def fetch_patch(args): + """Grabs the patch data from the GitHub API.""" + git_session = requests.Session() + headers = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + if args.token: + headers["Authorization"] = f"Bearer {args.token}" + + git_request = git_session.get( + f"{args.api_url}/repos/{args.repo}/pulls/{args.pr}/files", headers=headers + ) + return git_request.json() + + +def parse_patch_file(entry): + """Parses the individual file changes within a patch""" + line_array = [] + sublist = [] + + patch_array = re.split("\n", entry["patch"]) + # clean patch array + patch_array = [i for i in patch_array if i] + + for item in patch_array: + # Grabs hunk annotation and strips out added lines + if item.startswith("@@ -"): + if sublist: + line_array.append(sublist) + sublist = [re.sub(r"\s@@(.*)", "", item.split("+")[1])] + # We don't need removed lines ('-') + elif not item.startswith("-") and not item == "\\ No newline at end of file": + sublist.append(item) + if sublist: + line_array.append(sublist) + return line_array + + +def parse_patch_data(patch_data): + """Takes the patch data and returns a dictionary of files and the lines""" + + final_dict = {} + for entry in patch_data: + # We don't need removed files + if entry["status"] == "removed": + continue + + # We can only operate on files with additions and a patch key + # Some really big files don't have a patch key because GitHub + # returns a message in the PR that the file is too large to display + if entry["additions"] != 0 and "patch" in entry: + line_array = parse_patch_file(entry) + final_dict[entry["filename"]] = line_array + return final_dict + + +def get_lines(line_dict): + """Takes the dictionary of files and lines and returns a dictionary of files and line numbers""" + final_dict = {} + for file_name, sublist in line_dict.items(): + line_array = [] + for array in sublist: + line_number = 0 + if "," not in array[0]: + line_number = int(array[0]) - 1 + else: + line_number = int(array[0].split(",")[0]) - 1 + + start = -1 + end = -1 + for line in array: + if line.startswith("+"): + if start < 0: + start = line_number + end = line_number + # line_array.append(line_number) + line_number += 1 + line_array.append([start, end]) + + # Remove deleted/renamed files (which appear as empty arrays) + if line_array: + final_dict[file_name] = line_array + return final_dict + + +def main(): + """main()""" + parser = argparse.ArgumentParser( + prog="changed_lines.py", + description="Identifies the changed files and lines in a GitHub PR.", + ) + parser.add_argument("--token") + parser.add_argument("--api-url", default="https://api.github.com") + parser.add_argument("--repo", default="FreeCAD/FreeCAD") + parser.add_argument("--ref", required=True) + parser.add_argument("--pr", required=True) + parser.add_argument("--file-filter", default="") + args = parser.parse_args() + + data = fetch_patch(args) + added_line_data = parse_patch_data(data) + added_lines = get_lines(added_line_data) + + if args.file_filter: + args.file_filter = set(args.file_filter.replace(" ", "").split(",")) + + filename_list = [] + line_filter = [] + for filename, _ in added_lines.items(): + if (not args.file_filter) or ( + os.path.splitext(filename)[1] in args.file_filter + ): + filename_list.append(filename) + lines_modified = {} + lines_modified["name"] = filename + lines_modified["lines"] = added_lines[filename] + line_filter.append(lines_modified) + + print(f"{json.dumps(line_filter)}") + + +if __name__ == "__main__": + main() From 7e8a9238fec94a8d64ae93a6733a059290679bdb Mon Sep 17 00:00:00 2001 From: Jacob Oursland Date: Mon, 16 Jun 2025 22:53:26 -0700 Subject: [PATCH 047/126] CI: limit C++ lint to changed lines. --- .github/workflows/CI_master.yml | 3 +++ .github/workflows/sub_lint.yml | 10 ++++++++++ tools/lint/clang_tidy.py | 11 ++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI_master.yml b/.github/workflows/CI_master.yml index 55b2db64d7..c36304c27b 100644 --- a/.github/workflows/CI_master.yml +++ b/.github/workflows/CI_master.yml @@ -63,8 +63,11 @@ jobs: with: artifactBasename: Lint-${{ github.run_id }} changedFiles: ${{ needs.Prepare.outputs.changedFiles }} + changedLines: ${{ needs.Prepare.outputs.changedLines }} changedCppFiles: ${{ needs.Prepare.outputs.changedCppFiles }} + changedCppLines: ${{ needs.Prepare.outputs.changedCppLines }} changedPythonFiles: ${{ needs.Prepare.outputs.changedPythonFiles }} + changedPythonLines: ${{ needs.Prepare.outputs.changedPythonLines }} WrapUp: needs: [ diff --git a/.github/workflows/sub_lint.yml b/.github/workflows/sub_lint.yml index 9b020394db..ac31ef17e1 100644 --- a/.github/workflows/sub_lint.yml +++ b/.github/workflows/sub_lint.yml @@ -34,12 +34,21 @@ on: changedFiles: type: string required: true + changedLines: + type: string + required: false changedCppFiles: type: string required: true + changedCppLines: + type: string + required: false changedPythonFiles: type: string required: true + changedPythonLines: + type: string + required: false checkLineendings: default: false type: boolean @@ -317,6 +326,7 @@ jobs: run: | python3 tools/lint/clang_tidy.py \ --files "${{ inputs.changedCppFiles }}" \ + --line-filter '${{ inputs.changedCppLines }}' \ --clang-style "${{ inputs.clangStyle }}" \ --log-dir "${{ env.logdir }}" \ --report-file "${{ env.reportdir }}${{ env.reportfilename }}" diff --git a/tools/lint/clang_tidy.py b/tools/lint/clang_tidy.py index ea48be086c..8cb86c4f25 100644 --- a/tools/lint/clang_tidy.py +++ b/tools/lint/clang_tidy.py @@ -76,6 +76,11 @@ def main(): required=True, help="Clang-format style (e.g., 'file' to use .clang-format or a specific style).", ) + parser.add_argument( + "--line-filter", + required=False, + help='Line-filter for clang-tidy (i.e. [{"name":"file1.cpp","lines":[[1,3],[5,7]]},...])', + ) args = parser.parse_args() init_environment(args) @@ -96,7 +101,11 @@ def main(): enabled_checks_log = os.path.join(args.log_dir, "clang-tidy-enabled-checks.log") write_file(enabled_checks_log, enabled_output) - clang_cmd = clang_tidy_base_cmd + args.files.split() + clang_cmd = clang_tidy_base_cmd + if args.line_filter: + clang_cmd = clang_cmd + [f"--line-filter={args.line_filter}"] + clang_cmd = clang_cmd + args.files.split() + print("clang_cmd = ", clang_cmd) clang_stdout, clang_stderr, _ = run_command(clang_cmd) clang_tidy_output = clang_stdout + clang_stderr From 0b6a45179019c1e8b23c496c31e684bb98e842dd Mon Sep 17 00:00:00 2001 From: Kris <37947442+OfficialKris@users.noreply.github.com> Date: Wed, 18 Jun 2025 23:33:36 -0700 Subject: [PATCH 048/126] Gui: Move Submenu Commands in Tool Menu (#20864) * Moved tools submenu commands and title case * Apply suggestions from code review Co-authored-by: Max Wilfinger <6246609+maxwxyz@users.noreply.github.com> --------- Co-authored-by: Kacper Donat Co-authored-by: Max Wilfinger <6246609+maxwxyz@users.noreply.github.com> --- src/Gui/CommandStd.cpp | 8 +-- src/Gui/Workbench.cpp | 25 +++++----- src/Mod/Measure/Gui/AppMeasureGui.cpp | 4 -- src/Mod/Measure/Gui/CMakeLists.txt | 2 - src/Mod/Measure/Gui/WorkbenchManipulator.cpp | 51 -------------------- src/Mod/Measure/Gui/WorkbenchManipulator.h | 41 ---------------- 6 files changed, 17 insertions(+), 114 deletions(-) delete mode 100644 src/Mod/Measure/Gui/WorkbenchManipulator.cpp delete mode 100644 src/Mod/Measure/Gui/WorkbenchManipulator.h diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index e8089ff289..2837ccd190 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -379,9 +379,9 @@ StdCmdDlgParameter::StdCmdDlgParameter() { sGroup = "Tools"; sMenuText = QT_TR_NOOP("E&dit parameters..."); - sToolTipText = QT_TR_NOOP("Opens a Dialog to edit the parameters"); + sToolTipText = QT_TR_NOOP("Opens a dialog to edit the parameters"); sWhatsThis = "Std_DlgParameter"; - sStatusTip = QT_TR_NOOP("Opens a Dialog to edit the parameters"); + sStatusTip = QT_TR_NOOP("Opens a dialog to edit the parameters"); sPixmap = "Std_DlgParameter"; eType = 0; } @@ -404,9 +404,9 @@ StdCmdDlgPreferences::StdCmdDlgPreferences() { sGroup = "Tools"; sMenuText = QT_TR_NOOP("Prefere&nces ..."); - sToolTipText = QT_TR_NOOP("Opens a Dialog to edit the preferences"); + sToolTipText = QT_TR_NOOP("Opens a dialog to edit the preferences"); sWhatsThis = "Std_DlgPreferences"; - sStatusTip = QT_TR_NOOP("Opens a Dialog to edit the preferences"); + sStatusTip = QT_TR_NOOP("Opens a dialog to edit the preferences"); sPixmap = "preferences-system"; eType = 0; sAccel = "Ctrl+,"; diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 6aecbb3b49..5acad2bc8f 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -718,24 +718,25 @@ MenuItem* StdWorkbench::setupMenuBar() const // Tools auto tool = new MenuItem( menuBar ); tool->setCommand("&Tools"); - *tool << "Std_DlgParameter" +#ifdef BUILD_ADDONMGR + *tool << "Std_AddonMgr" + << "Separator"; +#endif + *tool << "Std_Measure" + << "Std_UnitsCalculator" << "Separator" - << "Std_ViewScreenShot" << "Std_ViewLoadImage" + << "Std_ViewScreenShot" + << "Std_TextDocument" + << "Std_DemoMode" + << "Separator" << "Std_SceneInspector" << "Std_DependencyGraph" << "Std_ExportDependencyGraph" + << "Separator" << "Std_ProjectUtil" - << "Separator" - << "Std_TextDocument" - << "Separator" - << "Std_DemoMode" - << "Std_UnitsCalculator" - << "Separator" + << "Std_DlgParameter" << "Std_DlgCustomize"; -#ifdef BUILD_ADDONMGR - *tool << "Std_AddonMgr"; -#endif // Macro auto macro = new MenuItem( menuBar ); @@ -811,7 +812,7 @@ ToolBarItem* StdWorkbench::setupToolBars() const auto view = new ToolBarItem( root ); view->setCommand("View"); *view << "Std_ViewFitAll" << "Std_ViewFitSelection" << "Std_ViewGroup" << "Std_AlignToSelection" - << "Separator" << "Std_DrawStyle" << "Std_TreeViewActions"; + << "Separator" << "Std_DrawStyle" << "Std_TreeViewActions" << "Std_Measure"; // Individual views auto individualViews = new ToolBarItem(root, ToolBarItem::DefaultVisibility::Hidden); diff --git a/src/Mod/Measure/Gui/AppMeasureGui.cpp b/src/Mod/Measure/Gui/AppMeasureGui.cpp index 2897c1591b..4da57110ef 100644 --- a/src/Mod/Measure/Gui/AppMeasureGui.cpp +++ b/src/Mod/Measure/Gui/AppMeasureGui.cpp @@ -37,7 +37,6 @@ #include "ViewProviderMeasureAngle.h" #include "ViewProviderMeasureDistance.h" #include "ViewProviderMeasureBase.h" -#include "WorkbenchManipulator.h" // use a different name to CreateCommand() @@ -87,9 +86,6 @@ PyMOD_INIT_FUNC(MeasureGui) PyObject* mod = MeasureGui::initModule(); Base::Console().log("Loading GUI of Measure module... done\n"); - auto manip = std::make_shared(); - Gui::WorkbenchManipulator::installManipulator(manip); - // instantiating the commands CreateMeasureCommands(); diff --git a/src/Mod/Measure/Gui/CMakeLists.txt b/src/Mod/Measure/Gui/CMakeLists.txt index ef1909afc1..68acddfbbe 100644 --- a/src/Mod/Measure/Gui/CMakeLists.txt +++ b/src/Mod/Measure/Gui/CMakeLists.txt @@ -50,8 +50,6 @@ SET(MeasureGui_SRCS ViewProviderMeasureAngle.h ViewProviderMeasureDistance.cpp ViewProviderMeasureDistance.h - WorkbenchManipulator.cpp - WorkbenchManipulator.h DlgPrefsMeasureAppearanceImp.ui DlgPrefsMeasureAppearanceImp.cpp DlgPrefsMeasureAppearanceImp.h diff --git a/src/Mod/Measure/Gui/WorkbenchManipulator.cpp b/src/Mod/Measure/Gui/WorkbenchManipulator.cpp deleted file mode 100644 index 0041db16d7..0000000000 --- a/src/Mod/Measure/Gui/WorkbenchManipulator.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/*************************************************************************** - * Copyright (c) 2024 David Friedli * - * * - * This file is part of FreeCAD. * - * * - * FreeCAD is free software: you can redistribute it and/or modify it * - * under the terms of the GNU Lesser General Public License as * - * published by the Free Software Foundation, either version 2.1 of the * - * License, or (at your option) any later version. * - * * - * FreeCAD 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 * - * Lesser General Public License for more details. * - * * - * You should have received a copy of the GNU Lesser General Public * - * License along with FreeCAD. If not, see * - * . * - * * - **************************************************************************/ - - -#include "PreCompiled.h" -#include "WorkbenchManipulator.h" -#include -#include - -using namespace MeasureGui; - -void WorkbenchManipulator::modifyMenuBar([[maybe_unused]] Gui::MenuItem* menuBar) -{ - auto menuTools = menuBar->findItem("&Tools"); - if (!menuTools) { - return; - } - auto itemMeasure = new Gui::MenuItem(); - itemMeasure->setCommand("Std_Measure"); - menuTools->appendItem(itemMeasure); -} - -void WorkbenchManipulator::modifyToolBars(Gui::ToolBarItem* toolBar) -{ - auto tbView = toolBar->findItem("View"); - if (!tbView) { - return; - } - - auto itemMeasure = new Gui::ToolBarItem(); - itemMeasure->setCommand("Std_Measure"); - tbView->appendItem(itemMeasure); -} diff --git a/src/Mod/Measure/Gui/WorkbenchManipulator.h b/src/Mod/Measure/Gui/WorkbenchManipulator.h deleted file mode 100644 index 767f1e058c..0000000000 --- a/src/Mod/Measure/Gui/WorkbenchManipulator.h +++ /dev/null @@ -1,41 +0,0 @@ -/*************************************************************************** - * Copyright (c) 2024 David Friedli * - * * - * This file is part of FreeCAD. * - * * - * FreeCAD is free software: you can redistribute it and/or modify it * - * under the terms of the GNU Lesser General Public License as * - * published by the Free Software Foundation, either version 2.1 of the * - * License, or (at your option) any later version. * - * * - * FreeCAD 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 * - * Lesser General Public License for more details. * - * * - * You should have received a copy of the GNU Lesser General Public * - * License along with FreeCAD. If not, see * - * . * - * * - **************************************************************************/ - - -#ifndef MEASUREGUI_WORKBENCHMANIPULATOR_H -#define MEASUREGUI_WORKBENCHMANIPULATOR_H - -#include - -namespace MeasureGui -{ - -class WorkbenchManipulator: public Gui::WorkbenchManipulator -{ -protected: - void modifyMenuBar(Gui::MenuItem* menuBar) override; - void modifyToolBars(Gui::ToolBarItem* toolBar) override; -}; - -} // namespace MeasureGui - - -#endif // MEASUREGUI_WORKBENCHMANIPULATOR_H From acdfdd5d1b91932f68bb1abe35e725d09e3c3447 Mon Sep 17 00:00:00 2001 From: Alfredo Monclus Date: Wed, 18 Jun 2025 08:49:58 -0600 Subject: [PATCH 049/126] Gui: Tasks: fix in place close and ok buttons --- .../overlay/Dark Theme + Dark Background.qss | 2 +- .../overlay/Dark Theme + Light Background.qss | 2 +- .../overlay/Light Theme + Dark Background.qss | 2 +- .../Light Theme + Light Background.qss | 2 +- src/Gui/TaskView/TaskView.cpp | 34 ++++++++++++------- src/Gui/TaskView/TaskView.h | 4 ++- 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss b/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss index bb885553b2..b093be348c 100644 --- a/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss +++ b/src/Gui/Stylesheets/overlay/Dark Theme + Dark Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss b/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss index 6cbdfe9743..de4b60cdee 100644 --- a/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss +++ b/src/Gui/Stylesheets/overlay/Dark Theme + Light Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss b/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss index e73a7334ff..84e5a2af91 100644 --- a/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss +++ b/src/Gui/Stylesheets/overlay/Light Theme + Dark Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss b/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss index a1367a3bcc..32c5503272 100644 --- a/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss +++ b/src/Gui/Stylesheets/overlay/Light Theme + Light Background.qss @@ -1,5 +1,5 @@ Gui--DockWnd--ReportOutput, -Gui--TaskView--TaskView { +Gui--TaskView--TaskView QScrollArea { border: none; } diff --git a/src/Gui/TaskView/TaskView.cpp b/src/Gui/TaskView/TaskView.cpp index 37cee0a18d..8efef9d6c0 100644 --- a/src/Gui/TaskView/TaskView.cpp +++ b/src/Gui/TaskView/TaskView.cpp @@ -32,6 +32,7 @@ # include # include # include +# include #endif #include @@ -268,9 +269,15 @@ QSize TaskPanel::minimumSizeHint() const //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TaskView::TaskView(QWidget *parent) - : QScrollArea(parent),ActiveDialog(nullptr),ActiveCtrl(nullptr) + : QWidget(parent),ActiveDialog(nullptr),ActiveCtrl(nullptr) { - taskPanel = new TaskPanel(this); + mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + this->setLayout(mainLayout); + scrollArea = new QScrollArea(this); + + taskPanel = new TaskPanel(scrollArea); QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); @@ -278,10 +285,11 @@ TaskView::TaskView(QWidget *parent) taskPanel->setSizePolicy(sizePolicy); taskPanel->setScheme(QSint::ActionPanelScheme::defaultScheme()); - this->setWidget(taskPanel); - setWidgetResizable(true); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - this->setMinimumWidth(200); + scrollArea->setWidget(taskPanel); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setMinimumWidth(200); + mainLayout->addWidget(scrollArea, 1); Gui::Selection().Attach(this); @@ -361,7 +369,7 @@ bool TaskView::event(QEvent* event) } } } - return QScrollArea::event(event); + return QWidget::event(event); } void TaskView::keyPressEvent(QKeyEvent* ke) @@ -423,7 +431,7 @@ void TaskView::keyPressEvent(QKeyEvent* ke) } } else { - QScrollArea::keyPressEvent(ke); + QWidget::keyPressEvent(ke); } } @@ -441,7 +449,7 @@ void TaskView::adjustMinimumSizeHint() QSize TaskView::minimumSizeHint() const { - QSize ms = QScrollArea::minimumSizeHint(); + QSize ms = QWidget::minimumSizeHint(); int spacing = 0; if (QLayout* layout = taskPanel->layout()) { spacing = 2 * layout->spacing(); @@ -592,7 +600,8 @@ void TaskView::showDialog(TaskDialog *dlg) dlg->modifyStandardButtons(ActiveCtrl->buttonBox); if (dlg->buttonPosition() == TaskDialog::North) { - taskPanel->addWidget(ActiveCtrl); + // Add button box to the top of the main layout + mainLayout->insertWidget(0, ActiveCtrl); for (const auto & it : cont){ taskPanel->addWidget(it); } @@ -601,7 +610,8 @@ void TaskView::showDialog(TaskDialog *dlg) for (const auto & it : cont){ taskPanel->addWidget(it); } - taskPanel->addWidget(ActiveCtrl); + // Add button box to the bottom of the main layout + mainLayout->addWidget(ActiveCtrl); } taskPanel->setScheme(QSint::ActionPanelScheme::defaultScheme()); @@ -627,7 +637,7 @@ void TaskView::removeDialog() getMainWindow()->updateActions(); if (ActiveCtrl) { - taskPanel->removeWidget(ActiveCtrl); + mainLayout->removeWidget(ActiveCtrl); delete ActiveCtrl; ActiveCtrl = nullptr; } diff --git a/src/Gui/TaskView/TaskView.h b/src/Gui/TaskView/TaskView.h index bf4537f1b7..92fada916f 100644 --- a/src/Gui/TaskView/TaskView.h +++ b/src/Gui/TaskView/TaskView.h @@ -138,7 +138,7 @@ public: * This elements get injected mostly by the ViewProvider classes of the selected * DocumentObjects. */ -class GuiExport TaskView : public QScrollArea, public Gui::SelectionSingleton::ObserverType +class GuiExport TaskView : public QWidget, public Gui::SelectionSingleton::ObserverType { Q_OBJECT @@ -188,6 +188,8 @@ private: void slotUndoDocument(const App::Document&); void slotRedoDocument(const App::Document&); void transactionChangeOnDocument(const App::Document&, bool undo); + QVBoxLayout* mainLayout; + QScrollArea* scrollArea; protected: void keyPressEvent(QKeyEvent* event) override; From c8d2ae494b3eddfd0e15d9d75c8fa88ab311d065 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 20 Jun 2025 00:33:48 +0200 Subject: [PATCH 050/126] Sketcher: Handle additional characters for OVP in regexp Co-authored-by: Benjamin Nauck --- src/Gui/QuantitySpinBox.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Gui/QuantitySpinBox.cpp b/src/Gui/QuantitySpinBox.cpp index 8f4a1e49e3..98749a06b0 100644 --- a/src/Gui/QuantitySpinBox.cpp +++ b/src/Gui/QuantitySpinBox.cpp @@ -575,8 +575,9 @@ void QuantitySpinBox::userInput(const QString & text) // only emit signal to reset EditableDatumLabel if the input is truly empty or has // no meaningful number don't emit for partially typed numbers like "71." which are // temporarily invalid - QString trimmedText = text.trimmed(); - if (trimmedText.isEmpty() || !trimmedText.contains(QRegularExpression(QStringLiteral("[0-9]")))) { + const QString trimmedText = text.trimmed(); + static const QRegularExpression partialNumberRegex(QStringLiteral(R"([+-]?(\d+)?(\.,\d*)?)")); + if (trimmedText.isEmpty() || !trimmedText.contains(partialNumberRegex)) { // we have to emit here signal explicitly as validator will not pass // this value further but we want to check it to disable isSet flag if // it has been set previously From 11eaa8dc604bda4cf051f1f2d29bc1cf916707b7 Mon Sep 17 00:00:00 2001 From: Jacob Oursland Date: Fri, 20 Jun 2025 09:57:11 -0700 Subject: [PATCH 051/126] CI: only lint on PRs. --- .github/workflows/sub_prepare.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sub_prepare.yml b/.github/workflows/sub_prepare.yml index f7ea5e68a3..45c97a16b8 100644 --- a/.github/workflows/sub_prepare.yml +++ b/.github/workflows/sub_prepare.yml @@ -146,7 +146,7 @@ jobs: commitCnt=$(jq -re '.ahead_by' ${{ env.logdir }}compare.json) echo "Changeset is composed of $commitCnt commit(s)" | tee -a ${{env.reportdir}}${{ env.reportfilename }} - name: Get files modified in changeset #TODO check what happens with deleted file in the subsequent process - if: env.isPR == 'true' || env.isPush == 'true' + if: env.isPR == 'true' env: API_URL: ${{ github.api_url }} TOKEN: ${{ github.token }} From 45ee397b436786099ebd86d9f25ae2b7e1a17282 Mon Sep 17 00:00:00 2001 From: Max Wilfinger Date: Thu, 19 Jun 2025 14:21:11 +0200 Subject: [PATCH 052/126] Gui: Add toggle overlay icons --- src/Gui/CommandView.cpp | 12 +- src/Gui/Icons/Std_DockOverlayToggleBottom.svg | 270 +++++++++++ src/Gui/Icons/Std_DockOverlayToggleLeft.svg | 171 +++++++ src/Gui/Icons/Std_DockOverlayToggleRight.svg | 171 +++++++ src/Gui/Icons/Std_DockOverlayToggleTop.svg | 171 +++++++ src/Gui/Icons/resource.qrc | 452 +++++++++--------- 6 files changed, 1017 insertions(+), 230 deletions(-) create mode 100755 src/Gui/Icons/Std_DockOverlayToggleBottom.svg create mode 100755 src/Gui/Icons/Std_DockOverlayToggleLeft.svg create mode 100755 src/Gui/Icons/Std_DockOverlayToggleRight.svg create mode 100755 src/Gui/Icons/Std_DockOverlayToggleTop.svg diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index ccf00cb137..91ee820349 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3710,7 +3710,7 @@ StdCmdDockOverlayToggleLeft::StdCmdDockOverlayToggleLeft() sWhatsThis = "Std_DockOverlayToggleLeft"; sStatusTip = sToolTipText; sAccel = "Ctrl+Left"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleLeft"; eType = 0; } @@ -3735,7 +3735,7 @@ StdCmdDockOverlayToggleRight::StdCmdDockOverlayToggleRight() sWhatsThis = "Std_DockOverlayToggleRight"; sStatusTip = sToolTipText; sAccel = "Ctrl+Right"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleRight"; eType = 0; } @@ -3760,7 +3760,7 @@ StdCmdDockOverlayToggleTop::StdCmdDockOverlayToggleTop() sWhatsThis = "Std_DockOverlayToggleTop"; sStatusTip = sToolTipText; sAccel = "Ctrl+Up"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleTop"; eType = 0; } @@ -3785,7 +3785,7 @@ StdCmdDockOverlayToggleBottom::StdCmdDockOverlayToggleBottom() sWhatsThis = "Std_DockOverlayToggleBottom"; sStatusTip = sToolTipText; sAccel = "Ctrl+Down"; - sPixmap = "qss:overlay/icons/close.svg"; + sPixmap = "Std_DockOverlayToggleBottom"; eType = 0; } @@ -3847,8 +3847,8 @@ public: :GroupCommand("Std_DockOverlay") { sGroup = "View"; - sMenuText = QT_TR_NOOP("Dock window overlay"); - sToolTipText = QT_TR_NOOP("Setting docked window overlay mode"); + sMenuText = QT_TR_NOOP("Dock panel overlay"); + sToolTipText = QT_TR_NOOP("Setting docked panel overlay mode"); sWhatsThis = "Std_DockOverlay"; sStatusTip = sToolTipText; eType = 0; diff --git a/src/Gui/Icons/Std_DockOverlayToggleBottom.svg b/src/Gui/Icons/Std_DockOverlayToggleBottom.svg new file mode 100755 index 0000000000..98038cb107 --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleBottom.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/Std_DockOverlayToggleLeft.svg b/src/Gui/Icons/Std_DockOverlayToggleLeft.svg new file mode 100755 index 0000000000..d19b0c4660 --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleLeft.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/Std_DockOverlayToggleRight.svg b/src/Gui/Icons/Std_DockOverlayToggleRight.svg new file mode 100755 index 0000000000..792c4db80d --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleRight.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/Std_DockOverlayToggleTop.svg b/src/Gui/Icons/Std_DockOverlayToggleTop.svg new file mode 100755 index 0000000000..81ceb9aa22 --- /dev/null +++ b/src/Gui/Icons/Std_DockOverlayToggleTop.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/ + + + FreeCAD LGPL2+ + + + 2024 + + + + + + + + + + + diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc index 1621edbb4a..86f5c4d0f9 100644 --- a/src/Gui/Icons/resource.qrc +++ b/src/Gui/Icons/resource.qrc @@ -1,178 +1,181 @@ - freecadsplash.png - freecadsplash0.png - freecadsplash1.png - freecadsplash2.png - freecadsplash3.png - freecadsplash4.png - freecadsplash5.png - freecadsplash6.png - freecadsplash7.png - freecadsplash8.png - freecadsplash9.png - freecadsplash10.png - freecadsplash11.png - freecadsplash12.png - freecadsplash_2x.png - freecadsplash0_2x.png - freecadsplash1_2x.png - freecadsplash2_2x.png - freecadsplash3_2x.png - freecadsplash4_2x.png - freecadsplash5_2x.png - freecadsplash6_2x.png - freecadsplash7_2x.png - freecadsplash8_2x.png - freecadsplash9_2x.png - freecadsplash10_2x.png - freecadsplash11_2x.png - freecadsplash12_2x.png - freecadabout.png + 3dx_pivot.png + accessories-calculator.svg + accessories-text-editor.svg + AddonManager.svg + align-to-selection.svg + application-exit.svg + applications-accessories.svg + applications-python.svg background.png - mouse_pointer.svg - Document.svg - Feature.svg - delete.svg - list-remove.svg - list-add.svg - freecad.svg - freecad-doc.png - freecad-doc.svg + bound-expression-unset.svg + bound-expression.svg + breakpoint.svg bulb.svg - TextDocument.svg + button_add_all.svg button_down.svg + button_invalid.svg button_left.svg button_right.svg - button_up.svg button_sort.svg - button_add_all.svg + button_up.svg button_valid.svg - button_invalid.svg - media-playback-start.svg - media-playback-start-back.svg - media-playback-step.svg - media-playback-step-back.svg - media-record.svg - media-playback-stop.svg - preferences-display.svg - preferences-python.svg - preferences-general.svg - preferences-import-export.svg - preferences-workbenches.svg - utilities-terminal.svg ClassBrowser/const_member.png - ClassBrowser/member.png - ClassBrowser/method.png - ClassBrowser/property.png - ClassBrowser/type_class.png - ClassBrowser/type_enum.png - ClassBrowser/type_module.png ClassBrowser/const_member.svg + ClassBrowser/member.png ClassBrowser/member.svg + ClassBrowser/method.png ClassBrowser/method.svg + ClassBrowser/property.png ClassBrowser/property.svg + ClassBrowser/type_class.png ClassBrowser/type_class.svg + ClassBrowser/type_enum.png ClassBrowser/type_enum.svg + ClassBrowser/type_module.png ClassBrowser/type_module.svg - style/windows_branch_closed.png - style/windows_branch_open.png - Std_ViewScreenShot.svg - bound-expression.svg - bound-expression-unset.svg - breakpoint.svg + clear-selection.svg + colors.svg + critical-info.svg + cursor-through.svg + dagViewFail.svg + dagViewPass.svg + dagViewPending.svg + dagViewVisible.svg debug-marker.svg debug-start.svg debug-stop.svg + delete.svg document-new.svg document-open.svg - document-save.svg - document-save-as.svg - document-print.svg + document-package.svg document-print-preview.svg + document-print.svg document-properties.svg - application-exit.svg - edit_OK.svg + document-python.svg + document-save-as.svg + document-save.svg + Document.svg + DrawStyleAsIs.svg + DrawStyleFlatLines.svg + DrawStyleHiddenLine.svg + DrawStyleNoShading.svg + DrawStylePoints.svg + DrawStyleShaded.svg + DrawStyleWireFrame.svg + edge-selection.svg edit_Cancel.svg + edit_OK.svg + edit-cleartext.svg edit-copy.svg edit-cut.svg edit-delete.svg - edit-paste.svg - edit-select-all.svg - edit-select-box.svg - edit-select-box-cross.svg - edit-element-select-box.svg - edit-element-select-box-cross.svg - edit-redo.svg - edit-undo.svg edit-edit.svg - edit-cleartext.svg + edit-element-select-box-cross.svg + edit-element-select-box.svg + edit-paste.svg + edit-redo.svg + edit-select-all.svg + edit-select-box-cross.svg + edit-select-box.svg + edit-undo.svg + face-selection.svg + feature_suppressed.svg + Feature.svg + folder.svg + forbidden.svg + freecad-doc.png + freecad-doc.svg + freecad.svg + freecadabout.png + freecadsplash_2x.png + freecadsplash.png + freecadsplash0_2x.png + freecadsplash0.png + freecadsplash1_2x.png + freecadsplash1.png + freecadsplash10_2x.png + freecadsplash10.png + freecadsplash11_2x.png + freecadsplash11.png + freecadsplash12_2x.png + freecadsplash12.png + freecadsplash2_2x.png + freecadsplash2.png + freecadsplash3_2x.png + freecadsplash3.png + freecadsplash4_2x.png + freecadsplash4.png + freecadsplash5_2x.png + freecadsplash5.png + freecadsplash6_2x.png + freecadsplash6.png + freecadsplash7_2x.png + freecadsplash7.png + freecadsplash8_2x.png + freecadsplash8.png + freecadsplash9_2x.png + freecadsplash9.png + Geoassembly.svg + Geofeaturegroup.svg + Group.svg + help-browser.svg + image-open.svg + image-plane.svg + image-scaling.svg info.svg - critical-info.svg - tree-item-drag.svg - tree-goto-sel.svg - tree-rec-sel.svg - tree-pre-sel.svg - tree-sync-sel.svg - tree-sync-view.svg - tree-sync-pla.svg - tree-doc-single.svg - tree-doc-multi.svg - tree-doc-collapse.svg + internet-web-browser.svg + InTray_missed_notifications.svg + InTray.svg + Invisible.svg + Link.svg + LinkArray.svg + LinkArrayOverlay.svg + LinkElement.svg + LinkGroup.svg + LinkImport.svg + LinkImportAll.svg + LinkOverlay.svg + LinkReplace.svg + LinkSelect.svg + LinkSelectAll.svg + LinkSelectFinal.svg + LinkSub.svg + LinkSubElement.svg + LinkSubOverlay.svg + list-add.svg + list-remove.svg + MacroEditor.svg + MacroFolder.svg + media-playback-start-back.svg + media-playback-start.svg + media-playback-step-back.svg + media-playback-step.svg + media-playback-stop.svg + media-record.svg + mouse_pointer.svg + overlay_error.svg + overlay_recompute.svg + Param_Bool.svg + Param_Float.svg + Param_Int.svg + Param_Text.svg + Param_UInt.svg + PolygonPick.svg + preferences-display.svg + preferences-general.svg + preferences-import-export.svg + preferences-python.svg + preferences-system.svg + preferences-workbenches.svg + process-stop.svg + px.svg + safe-mode-restart.svg sel-back.svg + sel-bbox.svg sel-forward.svg sel-instance.svg - sel-bbox.svg - vertex-selection.svg - edge-selection.svg - face-selection.svg - clear-selection.svg - help-browser.svg - preferences-system.svg - process-stop.svg - window-new.svg - applications-accessories.svg - applications-python.svg - accessories-text-editor.svg - accessories-calculator.svg - internet-web-browser.svg - InTray.svg - InTray_missed_notifications.svg - safe-mode-restart.svg - view-select.svg - view-unselectable.svg - view-refresh.svg - view-fullscreen.svg - view-axonometric.svg - view-isometric.svg - view-perspective.svg - view-bottom.svg - view-front.svg - view-left.svg - view-rear.svg - view-right.svg - view-top.svg - zoom-all.svg - zoom-border.svg - zoom-border-cross.svg - zoom-fit-best.svg - zoom-in.svg - zoom-out.svg - zoom-selection.svg - view-rotate-left.svg - view-rotate-right.svg - view-measurement.svg - view-measurement-cross.svg - umf-measurement.svg - Tree_Annotation.svg - Tree_Dimension.svg - Tree_Python.svg - TreeItemVisible.svg - TreeItemInvisible.svg - dagViewVisible.svg - dagViewPass.svg - dagViewFail.svg - dagViewPending.svg spaceball_button.svg SpNav-PanLR.svg SpNav-PanUD.svg @@ -180,52 +183,54 @@ SpNav-Spin.svg SpNav-Tilt.svg SpNav-Zoom.svg - DrawStyleAsIs.svg - DrawStyleFlatLines.svg - DrawStylePoints.svg - DrawStyleShaded.svg - DrawStyleWireFrame.svg - DrawStyleHiddenLine.svg - DrawStyleNoShading.svg - user.svg + Std_Alignment.svg + Std_Axis.svg Std_AxisCross.svg - Std_CoordinateSystem.svg - Std_CoordinateSystem_alt.svg - Std_Placement.svg - MacroEditor.svg - MacroFolder.svg - Param_Bool.svg - Param_Float.svg - Param_Int.svg - Param_Text.svg - Param_UInt.svg - PolygonPick.svg Std_CloseActiveWindow.svg Std_CloseAllWindows.svg + Std_CoordinateSystem_alt.svg + Std_CoordinateSystem.svg + Std_DemoMode.svg + Std_DependencyGraph.svg + Std_DlgParameter.svg + Std_DockOverlayToggleBottom.svg + Std_DockOverlayToggleLeft.svg + Std_DockOverlayToggleRight.svg + Std_DockOverlayToggleTop.svg + Std_DuplicateSelection.svg Std_Export.svg Std_HideObjects.svg Std_HideSelection.svg Std_Import.svg - Std_MergeProjects.svg Std_MarkToRecompute.svg + Std_MergeProjects.svg + Std_Placement.svg + Std_Plane.svg + Std_Point.svg Std_PrintPdf.svg + Std_ProjectUtil.svg Std_RandomColor.svg Std_RecentFiles.svg Std_RecentMacros.svg Std_Revert.svg Std_SaveAll.svg Std_SaveCopy.svg + Std_SceneInspector.svg Std_SelectVisibleObjects.svg Std_SetAppearance.svg Std_ShowObjects.svg Std_ShowSelection.svg Std_TextureMapping.svg Std_ToggleClipPlane.svg + Std_ToggleFreeze.svg Std_ToggleNavigation.svg Std_ToggleObjects.svg - Std_ToggleVisibility.svg Std_ToggleTransparency.svg + Std_ToggleVisibility.svg Std_Tool1.svg + Std_Tool10.svg + Std_Tool11.svg + Std_Tool12.svg Std_Tool2.svg Std_Tool3.svg Std_Tool4.svg @@ -234,10 +239,11 @@ Std_Tool7.svg Std_Tool8.svg Std_Tool9.svg - Std_Tool10.svg - Std_Tool11.svg - Std_Tool12.svg Std_TransformManip.svg + Std_UserEditModeColor.svg + Std_UserEditModeCutting.svg + Std_UserEditModeDefault.svg + Std_UserEditModeTransform.svg Std_ViewDimetric.svg Std_ViewHome.svg Std_ViewIvIssueCamPos.svg @@ -246,73 +252,71 @@ Std_ViewIvStereoOff.svg Std_ViewIvStereoQuadBuff.svg Std_ViewIvStereoRedGreen.svg + Std_ViewScreenShot.svg Std_ViewTrimetric.svg - Std_Windows.svg Std_WindowCascade.svg Std_WindowNext.svg Std_WindowPrev.svg + Std_Windows.svg Std_WindowTileVer.svg - Std_DemoMode.svg - Std_DependencyGraph.svg - Std_DlgParameter.svg - Std_ProjectUtil.svg - Std_SceneInspector.svg - WhatsThis.svg - colors.svg - px.svg - AddonManager.svg - align-to-selection.svg - Group.svg - Geofeaturegroup.svg - Geoassembly.svg - Std_Point.svg - Std_Axis.svg - Std_Plane.svg - Link.svg - LinkArray.svg - LinkElement.svg - LinkGroup.svg - LinkOverlay.svg - LinkArrayOverlay.svg - LinkSubOverlay.svg - LinkSubElement.svg - LinkSub.svg - LinkReplace.svg - LinkImport.svg - LinkImportAll.svg - LinkSelect.svg - LinkSelectFinal.svg - LinkSelectAll.svg + style/windows_branch_closed.png + style/windows_branch_open.png + TextDocument.svg + Tree_Annotation.svg + Tree_Dimension.svg + Tree_Python.svg + tree-doc-collapse.svg + tree-doc-multi.svg + tree-doc-single.svg + tree-goto-sel.svg + tree-item-drag.svg + tree-pre-sel.svg + tree-rec-sel.svg + tree-sync-pla.svg + tree-sync-sel.svg + tree-sync-view.svg + TreeItemInvisible.svg + TreeItemVisible.svg + umf-measurement.svg Unlink.svg - Invisible.svg - folder.svg - document-python.svg - document-package.svg - cursor-through.svg - Std_Alignment.svg - Std_DuplicateSelection.svg - Std_UserEditModeDefault.svg - Std_UserEditModeTransform.svg - Std_UserEditModeCutting.svg - Std_UserEditModeColor.svg - Warning.svg - image-open.svg - image-plane.svg - image-scaling.svg - VarSet.svg - Std_ToggleFreeze.svg - 3dx_pivot.png - overlay_recompute.svg - overlay_error.svg - feature_suppressed.svg - forbidden.svg user-input/mouse-left.svg - user-input/mouse-right.svg - user-input/mouse-move.svg user-input/mouse-middle.svg - user-input/mouse-scroll.svg - user-input/mouse-scroll-up.svg + user-input/mouse-move.svg + user-input/mouse-right.svg user-input/mouse-scroll-down.svg + user-input/mouse-scroll-up.svg + user-input/mouse-scroll.svg + user.svg + utilities-terminal.svg + VarSet.svg + vertex-selection.svg + view-axonometric.svg + view-bottom.svg + view-front.svg + view-fullscreen.svg + view-isometric.svg + view-left.svg + view-measurement-cross.svg + view-measurement.svg + view-perspective.svg + view-rear.svg + view-refresh.svg + view-right.svg + view-rotate-left.svg + view-rotate-right.svg + view-select.svg + view-top.svg + view-unselectable.svg + Warning.svg + WhatsThis.svg + window-new.svg + zoom-all.svg + zoom-border-cross.svg + zoom-border.svg + zoom-fit-best.svg + zoom-in.svg + zoom-out.svg + zoom-selection.svg index.theme From a6485d1ae13ef503179f765a487c4bce497a706c Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Mon, 16 Jun 2025 22:53:19 +0200 Subject: [PATCH 053/126] Gui: Use middle elide for text in model tree --- src/Gui/Tree.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 314bf1a934..919c2ffaa9 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -519,6 +519,7 @@ void TreeWidgetItemDelegate::initStyleOption(QStyleOptionViewItem *option, return; } + option->textElideMode = Qt::ElideMiddle; auto mousePos = option->widget->mapFromGlobal(QCursor::pos()); auto isHovered = option->rect.contains(mousePos); if (!isHovered) { From 965bb6d2c8869205d352d05c3e85ba02088f0f5d Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 19:26:04 -0500 Subject: [PATCH 054/126] Tools: Use a safer hostname detection --- src/Tools/SubWCRev.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Tools/SubWCRev.py b/src/Tools/SubWCRev.py index 22e0cc9a6a..7c73a2d321 100644 --- a/src/Tools/SubWCRev.py +++ b/src/Tools/SubWCRev.py @@ -10,6 +10,7 @@ # 2011/02/05: The script was extended to support also Bazaar import os, sys, re, time, getopt +from urllib.parse import urlparse import xml.sax import xml.sax.handler import xml.sax.xmlreader @@ -275,9 +276,10 @@ class GitControl(VersionControl): match = re.match(r"ssh://\S+?@(\S+)", url) if match is not None: url = "git://%s" % match.group(1) + parsed_url = urlparse(url) entryscore = ( url == "git://github.com/FreeCAD/FreeCAD.git", - "github.com" in url, + parsed_url.netloc == "github.com", branch == self.branch, branch == "main", "@" not in url, From f0b53af32f2222981339a43a452492257912dac9 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 20:30:33 -0500 Subject: [PATCH 055/126] Part: Remove remnants of code from TNP merge --- src/Mod/Part/App/PartFeatures.cpp | 12 ------------ src/Mod/Part/App/PartFeatures.h | 3 --- 2 files changed, 15 deletions(-) diff --git a/src/Mod/Part/App/PartFeatures.cpp b/src/Mod/Part/App/PartFeatures.cpp index 4a8fe0d37a..dab9d9219d 100644 --- a/src/Mod/Part/App/PartFeatures.cpp +++ b/src/Mod/Part/App/PartFeatures.cpp @@ -243,12 +243,6 @@ App::DocumentObjectExecReturn* Loft::execute() } } -void Part::Loft::setupObject() -{ - Feature::setupObject(); -// Linearize.setValue(PartParams::getLinearizeExtrusionDraft()); // TODO: Resolve after PartParams -} - // ---------------------------------------------------------------------------- const char* Part::Sweep::TransitionEnums[] = {"Transformed", @@ -349,12 +343,6 @@ App::DocumentObjectExecReturn* Sweep::execute() } } -void Part::Sweep::setupObject() -{ - Feature::setupObject(); -// Linearize.setValue(PartParams::getLinearizeExtrusionDraft()); // TODO: Resolve after PartParams -} - // ---------------------------------------------------------------------------- const char* Part::Thickness::ModeEnums[] = {"Skin", "Pipe", "RectoVerso", nullptr}; diff --git a/src/Mod/Part/App/PartFeatures.h b/src/Mod/Part/App/PartFeatures.h index dacb2cd5b9..f0b408200c 100644 --- a/src/Mod/Part/App/PartFeatures.h +++ b/src/Mod/Part/App/PartFeatures.h @@ -50,7 +50,6 @@ public: short mustExecute() const override; const char* getViewProviderName() const override { return "PartGui::ViewProviderRuledSurface"; - void setupObject(); } //@} @@ -86,7 +85,6 @@ public: const char* getViewProviderName() const override { return "PartGui::ViewProviderLoft"; } - void setupObject() override; //@} protected: @@ -118,7 +116,6 @@ public: const char* getViewProviderName() const override { return "PartGui::ViewProviderSweep"; } - void setupObject() override; //@} protected: From bb1760546b36564c75551abd3a9ff5280ecec529 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 20:54:57 -0500 Subject: [PATCH 056/126] Sketcher: Remove dead code --- src/Mod/Sketcher/App/SketchObject.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 192aae8a6c..c6002f29c5 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -616,13 +616,6 @@ int SketchObject::solve(bool updateGeoAfterSolving /*=true*/) } } } - else if (err < 0) { - // if solver failed, invalid constraints were likely added before solving - // (see solve in addConstraint), so solver information is definitely invalid. - // - // Update: ViewProviderSketch shall now rely on the signalSolverUpdate below for update - // this->Constraints.touch(); - } signalSolverUpdate(); From ec86e2a440ad28355e32d9487ca7c100476bb427 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 20:59:54 -0500 Subject: [PATCH 057/126] Sketcher: Remove redundant checks Also cleanup missing curly braces. --- src/Mod/Sketcher/App/SketchObject.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index c6002f29c5..1102153654 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -1784,8 +1784,9 @@ int SketchObject::delGeometry(int GeoId, bool deleteinternalgeo) Base::StateLocker lock(managedoperation, true); const std::vector& vals = getInternalGeometry(); - if (GeoId < 0 || GeoId >= int(vals.size())) + if (GeoId >= int(vals.size())) { return -1; + } if (deleteinternalgeo && hasInternalGeometry(getGeometry(GeoId))) { // Only for supported types @@ -7568,8 +7569,9 @@ const Part::Geometry* SketchObject::_getGeometry(int GeoId) const if (GeoId < int(geomlist.size())) return geomlist[GeoId]; } - else if (GeoId < 0 && -GeoId-1 < ExternalGeo.getSize()) + else if (-GeoId-1 < ExternalGeo.getSize()) { return ExternalGeo[-GeoId-1]; + } return nullptr; } From eb69381f89e45ec8ecab2a40cc345471831ddc59 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 20:39:41 -0500 Subject: [PATCH 058/126] CI: Eliminate 3rd party and generated code from analysis --- .github/workflows/codeql_cpp.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql_cpp.yml b/.github/workflows/codeql_cpp.yml index 56c1b614ef..6f9045de64 100644 --- a/.github/workflows/codeql_cpp.yml +++ b/.github/workflows/codeql_cpp.yml @@ -114,10 +114,10 @@ jobs: # tools: https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.20.7/codeql-bundle-linux64.tar.gz # Add exclusions - # config: | - # query-filters: - # - exclude: - # id: py/file-not-closed + config: | + paths-ignore: + - src/3rdParty/** + - '**/ui_*.h' # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above From 7619b638e7733d887fd10755c65ed034399e29bf Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 21:14:26 -0500 Subject: [PATCH 059/126] Measure: Remove redundant check for edges > 0 --- src/Mod/Measure/App/Measurement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Measure/App/Measurement.cpp b/src/Mod/Measure/App/Measurement.cpp index 73ae0344cc..6ab6c72dec 100644 --- a/src/Mod/Measure/App/Measurement.cpp +++ b/src/Mod/Measure/App/Measurement.cpp @@ -239,7 +239,7 @@ MeasureType Measurement::findType() } else if (edges > 0) { if (verts > 0) { - if (verts > 1 && edges > 0) { + if (verts > 1) { mode = MeasureType::Invalid; } else { From cc207edb1acb37a0c6e94d8b9bae77607a0a782c Mon Sep 17 00:00:00 2001 From: tetektoza Date: Wed, 11 Jun 2025 23:59:07 +0200 Subject: [PATCH 060/126] Sketcher: Change enter behavior on OVP to put OVP in lock state only This patch adds/changes a couple of things: * if you press enter on a label now, it moves you to another label and adds the label and lock on the previous label, instead of previous behavior where it was accepting whole dimension * if you press enter and have lock state on both labels then you move to next stage * if you press ctrl+enter it's as is if you'd press enter on both labels (the object becomes constrained with whatever dimensions that were in both labels) * tab still works the same way * you can remove "Lock" state from the label by typing something additional or removing the dimension at all --- src/Gui/EditableDatumLabel.cpp | 114 +++++++++++++++++- src/Gui/EditableDatumLabel.h | 5 + src/Mod/Sketcher/Gui/DrawSketchController.h | 32 +++++ src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h | 4 +- .../Sketcher/Gui/DrawSketchHandlerArcSlot.h | 2 +- .../Sketcher/Gui/DrawSketchHandlerBSpline.h | 2 +- .../Sketcher/Gui/DrawSketchHandlerCircle.h | 2 +- .../Sketcher/Gui/DrawSketchHandlerEllipse.h | 4 +- src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h | 2 +- .../Sketcher/Gui/DrawSketchHandlerPolygon.h | 2 +- .../Sketcher/Gui/DrawSketchHandlerRectangle.h | 4 +- src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h | 2 +- .../Sketcher/Gui/DrawSketchHandlerTranslate.h | 4 +- 14 files changed, 163 insertions(+), 18 deletions(-) diff --git a/src/Gui/EditableDatumLabel.cpp b/src/Gui/EditableDatumLabel.cpp index f863b76ab0..c31562a6a1 100644 --- a/src/Gui/EditableDatumLabel.cpp +++ b/src/Gui/EditableDatumLabel.cpp @@ -33,8 +33,13 @@ #include #include +#include +#include +#include +#include #include +#include #include #include @@ -62,6 +67,7 @@ EditableDatumLabel::EditableDatumLabel(View3DInventorViewer* view, , value(0.0) , viewer(view) , spinBox(nullptr) + , lockIconLabel(nullptr) , cameraSensor(nullptr) , function(Function::Positioning) { @@ -151,6 +157,9 @@ void EditableDatumLabel::startEdit(double val, QObject* eventFilteringObj, bool return; } + // Reset locked state when starting to edit + this->resetLockedState(); + QWidget* mdi = viewer->parentWidget(); label->string = " "; @@ -209,11 +218,30 @@ bool EditableDatumLabel::eventFilter(QObject* watched, QEvent* event) if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { if (auto* spinBox = qobject_cast(watched)) { - this->hasFinishedEditing = true; - Q_EMIT this->valueChanged(this->value); - return false; + // for ctrl + enter we accept values as they are + if (keyEvent->modifiers() & Qt::ControlModifier) { + Q_EMIT this->finishEditingOnAllOVPs(); + return true; + } + else { + // regular enter + this->hasFinishedEditing = true; + Q_EMIT this->spinBox->valueChanged(this->value); + + // only set lock state if it passed validation + // (validation can unset isSet if value didn't pass + // confusion point for example) + if (this->isSet) + this->setLockedAppearance(true); + return true; + } } } + else if (this->hasFinishedEditing && keyEvent->key() != Qt::Key_Tab) + { + this->setLockedAppearance(false); + return false; + } } return QObject::eventFilter(watched, event); @@ -233,6 +261,9 @@ void EditableDatumLabel::stopEdit() spinBox->deleteLater(); spinBox = nullptr; + + // Lock icon will be automatically destroyed as it's a child of spinbox + lockIconLabel = nullptr; } } @@ -311,6 +342,19 @@ void EditableDatumLabel::positionSpinbox() pxCoord.setX(posX); pxCoord.setY(posY); spinBox->move(pxCoord); + + // Update lock icon position inside the spinbox if it exists and is visible + if (lockIconLabel && lockIconLabel->isVisible()) { + int iconSize = 14; + int padding = 4; + QSize spinboxSize = spinBox->size(); + lockIconLabel->setGeometry( + spinboxSize.width() - iconSize - padding, + (spinboxSize.height() - iconSize) / 2, + iconSize, + iconSize + ); + } } SbVec3f EditableDatumLabel::getTextCenterPoint() const @@ -441,6 +485,70 @@ void EditableDatumLabel::setSpinboxVisibleToMouse(bool val) spinBox->setAttribute(Qt::WA_TransparentForMouseEvents, !val); } +void EditableDatumLabel::setLockedAppearance(bool locked) +{ + if (locked) { + if (spinBox) { + QWidget* mdi = viewer->parentWidget(); + + // create lock icon label it it doesn't exist, if it does - show it + if (!lockIconLabel) { + lockIconLabel = new QLabel(spinBox); + lockIconLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); + lockIconLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + } + else + { + lockIconLabel->show(); + } + + // load icon and scale it to fit in spinbox + QPixmap lockIcon = Gui::BitmapFactory().pixmap("Constraint_Lock"); + QPixmap scaledIcon = lockIcon.scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation); + lockIconLabel->setPixmap(scaledIcon); + + // position lock icon inside the spinbox + int iconSize = 14; + int padding = 4; + QSize spinboxSize = spinBox->size(); + lockIconLabel->setGeometry( + spinboxSize.width() - iconSize - padding, + (spinboxSize.height() - iconSize) / 2, + iconSize, + iconSize + ); + lockIconLabel->show(); + + // style spinbox and add padding for lock + QString styleSheet = QString::fromLatin1( + "QSpinBox { " + "padding-right: %1px; " + "}" + ).arg(iconSize + padding + 2); + + spinBox->setStyleSheet(styleSheet); + } + } else { + this->hasFinishedEditing = false; + + // if spinbox exists, reset its appearance + if (spinBox) { + spinBox->setStyleSheet(QString()); + + // hide lock icon if it exists for later reuse + if (lockIconLabel) { + lockIconLabel->hide(); + } + } + } +} + +void EditableDatumLabel::resetLockedState() +{ + hasFinishedEditing = false; + setLockedAppearance(false); +} + EditableDatumLabel::Function EditableDatumLabel::getFunction() { return function; diff --git a/src/Gui/EditableDatumLabel.h b/src/Gui/EditableDatumLabel.h index 380532a681..6f6b1bb05c 100644 --- a/src/Gui/EditableDatumLabel.h +++ b/src/Gui/EditableDatumLabel.h @@ -72,6 +72,7 @@ public: void setPoints(SbVec3f p1, SbVec3f p2); void setPoints(Base::Vector3d p1, Base::Vector3d p2); void setFocusToSpinbox(); + void clearSelection(); ///< Clears text selection in the spinbox void setLabelType(SoDatumLabel::Type type, Function function = Function::Positioning); void setLabelDistance(double val); void setLabelStartAngle(double val); @@ -79,6 +80,8 @@ public: void setLabelRecommendedDistance(); void setLabelAutoDistanceReverse(bool val); void setSpinboxVisibleToMouse(bool val); + void setLockedAppearance(bool locked); ///< Sets visual appearance to indicate locked state (finished editing) + void resetLockedState(); ///< Resets both hasFinishedEditing flag and locked appearance Function getFunction(); @@ -95,6 +98,7 @@ public: Q_SIGNALS: void valueChanged(double val); void parameterUnset(); + void finishEditingOnAllOVPs(); ///< Emitted when Ctrl+Enter is pressed to finish editing on all visible OVPs protected: bool eventFilter(QObject* watched, QEvent* event) override; @@ -109,6 +113,7 @@ private: SoTransform* transform; QPointer viewer; QuantitySpinBox* spinBox; + QLabel* lockIconLabel; ///< Label to display lock icon next to spinbox SoNodeSensor* cameraSensor; SbVec3f midpos; diff --git a/src/Mod/Sketcher/Gui/DrawSketchController.h b/src/Mod/Sketcher/Gui/DrawSketchController.h index cf59ba0efb..bc75dee386 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchController.h +++ b/src/Mod/Sketcher/Gui/DrawSketchController.h @@ -365,8 +365,33 @@ public: } } + void finishEditingOnAllOVPs() + { + // we call this on a current OnViewParameter when pressed CTRL+ENTER to accept + // input on all visible ovps of current mode + + // we check for initial state, since `onViewValueChanged` can process to next mode + // if we set hasFinishedEditing on current mode + auto initialState = handler->state(); + for (size_t i = 0; i < onViewParameters.size(); i++) { + if (isOnViewParameterOfCurrentMode(i) && isOnViewParameterVisible(i) && initialState == getState(static_cast(i))) { + onViewParameters[i]->isSet = true; + onViewParameters[i]->hasFinishedEditing = true; + + double currentValue = onViewParameters[i]->getValue(); + onViewValueChanged(static_cast(i), currentValue); + } + } + } + void tryViewValueChanged(int onviewparameterindex, double value) { + // go only to next label if user has currently pressed enter on current one + int nextindex = onviewparameterindex + 1; + if (onViewParameters[onviewparameterindex]->hasFinishedEditing && isOnViewParameterOfCurrentMode(nextindex)) { + setFocusToOnViewParameter(nextindex); + } + /* That is not supported with on-view parameters. // -> A machine does not forward to a next state when adapting the parameter (though it // may forward to @@ -628,6 +653,13 @@ protected: unsetOnViewParameter(parameter); finishControlsChanged(); }); + + // Connect Ctrl+Enter signal to apply values to all visible OVPs in current stage + QObject::connect(parameter, + &Gui::EditableDatumLabel::finishEditingOnAllOVPs, + [this]() { + finishEditingOnAllOVPs(); + }); } } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h index f33f3106e1..062c864d79 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h @@ -729,7 +729,7 @@ void DSHArcController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; @@ -743,7 +743,7 @@ void DSHArcController::doChangeDrawSketchHandlerMode() else { auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h index b5a1e74391..305ef517a0 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h @@ -792,7 +792,7 @@ void DSHArcSlotController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h index 46903efbe6..0d12a1f76e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h @@ -1096,7 +1096,7 @@ void DSHBSplineController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->canGoToNextMode(); // its not going to next mode unsetOnViewParameter(thirdParam.get()); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h index 5e93a86cf7..055a079b88 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h @@ -630,7 +630,7 @@ void DSHCircleController::doChangeDrawSketchHandlerMode() auto& fifthParam = onViewParameters[OnViewParameter::Fifth]; auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h index 033d2597c2..2a478c64bf 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h @@ -705,7 +705,7 @@ void DSHEllipseController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; @@ -720,7 +720,7 @@ void DSHEllipseController::doChangeDrawSketchHandlerMode() } else { auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h index 96e61a22b9..186a741c2f 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h @@ -571,7 +571,7 @@ void DSHLineController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h index cef45cac34..e28abfb2f8 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPoint.h @@ -234,7 +234,7 @@ void DSHPointController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::End); // handler->finish(); // Called by the change of mode } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h index c20651acd6..4bc170f68a 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h @@ -463,7 +463,7 @@ void DSHPolygonController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h index df16b3d043..23302559d7 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h @@ -2353,7 +2353,7 @@ void DSHRectangleController::doChangeDrawSketchHandlerMode() } break; case SelectMode::SeekSecond: { if (onViewParameters[OnViewParameter::Third]->hasFinishedEditing - || onViewParameters[OnViewParameter::Fourth]->hasFinishedEditing) { + && onViewParameters[OnViewParameter::Fourth]->hasFinishedEditing) { if (handler->roundCorners || handler->makeFrame || handler->constructionMethod() == ConstructionMethod::ThreePoints @@ -2387,7 +2387,7 @@ void DSHRectangleController::doChangeDrawSketchHandlerMode() } else { if (onViewParameters[OnViewParameter::Fifth]->hasFinishedEditing - || onViewParameters[OnViewParameter::Sixth]->hasFinishedEditing) { + && onViewParameters[OnViewParameter::Sixth]->hasFinishedEditing) { if (handler->roundCorners || handler->makeFrame) { handler->setState(SelectMode::SeekFourth); } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h index 67c1b7381f..51ac5ca808 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h @@ -550,7 +550,7 @@ void DSHSlotController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { handler->setState(SelectMode::SeekThird); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h index 9e19d89b57..846097ff57 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h @@ -721,7 +721,7 @@ void DSHTranslateController::doChangeDrawSketchHandlerMode() auto& thirdParam = onViewParameters[OnViewParameter::Third]; auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; - if (thirdParam->hasFinishedEditing || fourthParam->hasFinishedEditing) { + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { if (handler->secondNumberOfCopies == 1) { handler->setState(SelectMode::End); } @@ -734,7 +734,7 @@ void DSHTranslateController::doChangeDrawSketchHandlerMode() auto& fifthParam = onViewParameters[OnViewParameter::Fifth]; auto& sixthParam = onViewParameters[OnViewParameter::Sixth]; - if (fifthParam->hasFinishedEditing || sixthParam->hasFinishedEditing) { + if (fifthParam->hasFinishedEditing && sixthParam->hasFinishedEditing) { handler->setState(SelectMode::End); } } break; From a00980cc25a7fba207243725006491d576036b11 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 16 Jun 2025 23:09:38 +0200 Subject: [PATCH 061/126] Sketcher: Adjust P&D mode to new enter behavior --- src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h | 2 +- src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h index 062c864d79..81c7faa2ea 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h @@ -721,7 +721,7 @@ void DSHArcController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h index 305ef517a0..382cf17c04 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h @@ -784,7 +784,7 @@ void DSHArcSlotController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h index 055a079b88..7ebb975b04 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h @@ -603,7 +603,7 @@ void DSHCircleController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h index 2a478c64bf..90413ffd07 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h @@ -697,7 +697,7 @@ void DSHEllipseController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h index 186a741c2f..a31a3f7e65 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h @@ -563,7 +563,7 @@ void DSHLineController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h index 4bc170f68a..b58a1fb7b6 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h @@ -455,7 +455,7 @@ void DSHPolygonController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h index 23302559d7..a9002a30f0 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h @@ -2346,7 +2346,7 @@ void DSHRectangleController::doChangeDrawSketchHandlerMode() switch (handler->state()) { case SelectMode::SeekFirst: { if (onViewParameters[OnViewParameter::First]->hasFinishedEditing - || onViewParameters[OnViewParameter::Second]->hasFinishedEditing) { + && onViewParameters[OnViewParameter::Second]->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h index 4baf4773b1..cf9f8f9342 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h @@ -661,7 +661,7 @@ void DSHRotateController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h index 51ac5ca808..9ffe972cba 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h @@ -542,7 +542,7 @@ void DSHSlotController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h index 846097ff57..833eccc623 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h @@ -713,7 +713,7 @@ void DSHTranslateController::doChangeDrawSketchHandlerMode() auto& firstParam = onViewParameters[OnViewParameter::First]; auto& secondParam = onViewParameters[OnViewParameter::Second]; - if (firstParam->hasFinishedEditing || secondParam->hasFinishedEditing) { + if (firstParam->hasFinishedEditing && secondParam->hasFinishedEditing) { handler->setState(SelectMode::SeekSecond); } } break; From de966ae5befd4a04b84ae75f35bcbae479b1005d Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 16 Jun 2025 23:14:30 +0200 Subject: [PATCH 062/126] Sketcher: Initialize lock icon only once --- src/Gui/EditableDatumLabel.cpp | 52 +++++++++++++++------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/src/Gui/EditableDatumLabel.cpp b/src/Gui/EditableDatumLabel.cpp index c31562a6a1..f3ead76e29 100644 --- a/src/Gui/EditableDatumLabel.cpp +++ b/src/Gui/EditableDatumLabel.cpp @@ -496,37 +496,31 @@ void EditableDatumLabel::setLockedAppearance(bool locked) lockIconLabel = new QLabel(spinBox); lockIconLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); lockIconLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + // load icon and scale it to fit in spinbox + QPixmap lockIcon = Gui::BitmapFactory().pixmap("Constraint_Lock"); + QPixmap scaledIcon = + lockIcon.scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation); + lockIconLabel->setPixmap(scaledIcon); + + // position lock icon inside the spinbox + int iconSize = 14; + int padding = 4; + QSize spinboxSize = spinBox->size(); + lockIconLabel->setGeometry(spinboxSize.width() - iconSize - padding, + (spinboxSize.height() - iconSize) / 2, + iconSize, + iconSize); + // style spinbox and add padding for lock + QString styleSheet = QString::fromLatin1("QSpinBox { " + "padding-right: %1px; " + "}") + .arg(iconSize + padding + 2); + + spinBox->setStyleSheet(styleSheet); } - else - { - lockIconLabel->show(); - } - - // load icon and scale it to fit in spinbox - QPixmap lockIcon = Gui::BitmapFactory().pixmap("Constraint_Lock"); - QPixmap scaledIcon = lockIcon.scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation); - lockIconLabel->setPixmap(scaledIcon); - - // position lock icon inside the spinbox - int iconSize = 14; - int padding = 4; - QSize spinboxSize = spinBox->size(); - lockIconLabel->setGeometry( - spinboxSize.width() - iconSize - padding, - (spinboxSize.height() - iconSize) / 2, - iconSize, - iconSize - ); + lockIconLabel->show(); - - // style spinbox and add padding for lock - QString styleSheet = QString::fromLatin1( - "QSpinBox { " - "padding-right: %1px; " - "}" - ).arg(iconSize + padding + 2); - - spinBox->setStyleSheet(styleSheet); } } else { this->hasFinishedEditing = false; From 9642ce0170bbe90edce2d4924ef056c71f312d97 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 16 Jun 2025 23:36:31 +0200 Subject: [PATCH 063/126] Sketcher: Force cycling back to first labels on OVP if they are not set --- src/Mod/Sketcher/Gui/DrawSketchController.h | 30 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Mod/Sketcher/Gui/DrawSketchController.h b/src/Mod/Sketcher/Gui/DrawSketchController.h index bc75dee386..ac124199e8 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchController.h +++ b/src/Mod/Sketcher/Gui/DrawSketchController.h @@ -386,10 +386,32 @@ public: void tryViewValueChanged(int onviewparameterindex, double value) { - // go only to next label if user has currently pressed enter on current one - int nextindex = onviewparameterindex + 1; - if (onViewParameters[onviewparameterindex]->hasFinishedEditing && isOnViewParameterOfCurrentMode(nextindex)) { - setFocusToOnViewParameter(nextindex); + // go to next label in circular manner if user has currently pressed enter on current one + if (onViewParameters[onviewparameterindex]->hasFinishedEditing) { + // find the first parameter of the current mode that is not locked to start the cycle + auto findNextUnlockedParameter = [this](size_t startIndex) -> int { + for (size_t i = startIndex; i < onViewParameters.size(); i++) { + if (isOnViewParameterOfCurrentMode(i) + && !onViewParameters[i]->hasFinishedEditing) { + return static_cast(i); + } + } + return -1; + }; + + // find first unlocked parameter (for cycling back) + int firstOfCurrentMode = findNextUnlockedParameter(0); + + // find next unlocked parameter after current one + int nextUnlockedIndex = findNextUnlockedParameter(onviewparameterindex + 1); + + // if no next parameter found, cycle back to first of current mode + if (nextUnlockedIndex != -1) { + setFocusToOnViewParameter(nextUnlockedIndex); + } + else if (firstOfCurrentMode != -1) { + setFocusToOnViewParameter(firstOfCurrentMode); + } } /* That is not supported with on-view parameters. From d553e21e5390e815724ae7a3ce77eb2566e84a4b Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 16 Jun 2025 23:55:19 +0200 Subject: [PATCH 064/126] Sketcher: Add QLabel header for newly added locked icon --- src/Gui/EditableDatumLabel.h | 1 + src/Mod/Sketcher/Gui/DrawSketchController.h | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Gui/EditableDatumLabel.h b/src/Gui/EditableDatumLabel.h index 6f6b1bb05c..6b3aad0b78 100644 --- a/src/Gui/EditableDatumLabel.h +++ b/src/Gui/EditableDatumLabel.h @@ -26,6 +26,7 @@ #include #include +#include #include #include "SoDatumLabel.h" diff --git a/src/Mod/Sketcher/Gui/DrawSketchController.h b/src/Mod/Sketcher/Gui/DrawSketchController.h index ac124199e8..3feed1dd3a 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchController.h +++ b/src/Mod/Sketcher/Gui/DrawSketchController.h @@ -374,7 +374,8 @@ public: // if we set hasFinishedEditing on current mode auto initialState = handler->state(); for (size_t i = 0; i < onViewParameters.size(); i++) { - if (isOnViewParameterOfCurrentMode(i) && isOnViewParameterVisible(i) && initialState == getState(static_cast(i))) { + if (isOnViewParameterOfCurrentMode(i) && isOnViewParameterVisible(i) + && initialState == getState(static_cast(i))) { onViewParameters[i]->isSet = true; onViewParameters[i]->hasFinishedEditing = true; @@ -677,11 +678,9 @@ protected: }); // Connect Ctrl+Enter signal to apply values to all visible OVPs in current stage - QObject::connect(parameter, - &Gui::EditableDatumLabel::finishEditingOnAllOVPs, - [this]() { - finishEditingOnAllOVPs(); - }); + QObject::connect(parameter, &Gui::EditableDatumLabel::finishEditingOnAllOVPs, [this]() { + finishEditingOnAllOVPs(); + }); } } From be542053e181b09258e964b812ebdc8cd148eec6 Mon Sep 17 00:00:00 2001 From: xtemp09 Date: Sat, 21 Jun 2025 17:57:07 +0700 Subject: [PATCH 065/126] [Spreadsheet] Replace override cursor with QGraphicsItem::setCursor (#22097) * [Spreadsheet] Replace the risky use of override cursor with QGraphicsItem::setCursor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/Mod/Spreadsheet/Gui/SheetTableView.cpp | 5 ++--- src/Mod/Spreadsheet/Gui/SheetTableView.h | 1 + src/Mod/Spreadsheet/Gui/ZoomableView.cpp | 12 ++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Mod/Spreadsheet/Gui/SheetTableView.cpp b/src/Mod/Spreadsheet/Gui/SheetTableView.cpp index 3ab62076a1..7d71dd7bd3 100644 --- a/src/Mod/Spreadsheet/Gui/SheetTableView.cpp +++ b/src/Mod/Spreadsheet/Gui/SheetTableView.cpp @@ -60,19 +60,18 @@ using namespace App; void SheetViewHeader::mouseMoveEvent(QMouseEvent* e) { // for some reason QWidget::setCursor() has no effect in QGraphicsView - // therefore we resort to override cursor + // therefore we resort to QGraphicsItem::setCursor const QCursor currentCursor = this->cursor(); QHeaderView::mouseMoveEvent(e); const QCursor newerCursor = this->cursor(); if (newerCursor != currentCursor) { - qApp->setOverrideCursor(newerCursor); + Q_EMIT cursorChanged(newerCursor); } } void SheetViewHeader::mouseReleaseEvent(QMouseEvent* event) { QHeaderView::mouseReleaseEvent(event); - qApp->setOverrideCursor(Qt::ArrowCursor); Q_EMIT resizeFinished(); } diff --git a/src/Mod/Spreadsheet/Gui/SheetTableView.h b/src/Mod/Spreadsheet/Gui/SheetTableView.h index 078b0aa61f..2c5bf81307 100644 --- a/src/Mod/Spreadsheet/Gui/SheetTableView.h +++ b/src/Mod/Spreadsheet/Gui/SheetTableView.h @@ -45,6 +45,7 @@ public: } Q_SIGNALS: void resizeFinished(); + void cursorChanged(QCursor); protected: void mouseMoveEvent(QMouseEvent* e) override; diff --git a/src/Mod/Spreadsheet/Gui/ZoomableView.cpp b/src/Mod/Spreadsheet/Gui/ZoomableView.cpp index 7c6a3b0693..52128dbf0c 100644 --- a/src/Mod/Spreadsheet/Gui/ZoomableView.cpp +++ b/src/Mod/Spreadsheet/Gui/ZoomableView.cpp @@ -122,6 +122,18 @@ ZoomableView::ZoomableView(Ui::Sheet* ui) }); resetZoom(); + + auto connectCursorChangedSignal = [this](QHeaderView* hv) { + auto header = qobject_cast(hv); + connect(header, + &SpreadsheetGui::SheetViewHeader::cursorChanged, + this, + [this](const QCursor& newerCursor) { + qpw->setCursor(newerCursor); + }); + }; + connectCursorChangedSignal(stv->horizontalHeader()); + connectCursorChangedSignal(stv->verticalHeader()); } int ZoomableView::zoomLevel() const From b330a9399fda3216d2827fd497c51cb3384b4715 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 21 Jun 2025 13:48:12 +0200 Subject: [PATCH 066/126] Sketcher: Make TAB lock the label if user has typed previously --- src/Gui/EditableDatumLabel.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Gui/EditableDatumLabel.cpp b/src/Gui/EditableDatumLabel.cpp index f3ead76e29..a0fc8f91bc 100644 --- a/src/Gui/EditableDatumLabel.cpp +++ b/src/Gui/EditableDatumLabel.cpp @@ -215,9 +215,15 @@ bool EditableDatumLabel::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::KeyPress) { auto* keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Tab) { if (auto* spinBox = qobject_cast(watched)) { + // if tab has been pressed and user did not type anything previously, + // then just cycle but don't lock anything, otherwise we lock the label + if (keyEvent->key() == Qt::Key_Tab && !this->isSet) { + return false; + } + // for ctrl + enter we accept values as they are if (keyEvent->modifiers() & Qt::ControlModifier) { Q_EMIT this->finishEditingOnAllOVPs(); From 7b1775bc4ccf74ea6e87c3265bdcb5fbf52d10fd Mon Sep 17 00:00:00 2001 From: jffmichi Date: Sat, 21 Jun 2025 19:49:32 +0200 Subject: [PATCH 067/126] CAM: improve Job toggleVisibility (#21802) Co-authored-by: jffmichi <> --- src/Mod/CAM/Path/Main/Gui/Job.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Mod/CAM/Path/Main/Gui/Job.py b/src/Mod/CAM/Path/Main/Gui/Job.py index 6e1c838af5..1efc2c14cd 100644 --- a/src/Mod/CAM/Path/Main/Gui/Job.py +++ b/src/Mod/CAM/Path/Main/Gui/Job.py @@ -150,6 +150,15 @@ class ViewProvider: def onChanged(self, vobj, prop): if prop == "Visibility": self.showOriginAxis(vobj.Visibility) + + # if we're currently restoring the document we do NOT want to call + # hideXXX as this would mark all currently hidden children as + # explicitly hidden by the user and prevent showing them when + # showing the job + + if self.obj.Document.Restoring: + return + if vobj.Visibility: self.restoreOperationsVisibility() self.restoreModelsVisibility() @@ -170,7 +179,8 @@ class ViewProvider: def restoreOperationsVisibility(self): if hasattr(self, "operationsVisibility"): for op in self.obj.Operations.Group: - op.Visibility = self.operationsVisibility[op.Name] + if self.operationsVisibility.get(op.Name, True): + op.Visibility = True else: for op in self.obj.Operations.Group: op.Visibility = True @@ -183,11 +193,12 @@ class ViewProvider: def restoreModelsVisibility(self): if hasattr(self, "modelsVisibility"): - for base in self.obj.Model.Group: - base.Visibility = self.modelsVisibility[base.Name] + for model in self.obj.Model.Group: + if self.modelsVisibility.get(model.Name, True): + model.Visibility = True else: - for base in self.obj.Model.Group: - base.Visibility = True + for model in self.obj.Model.Group: + model.Visibility = True def hideStock(self): self.stockVisibility = self.obj.Stock.Visibility @@ -195,7 +206,8 @@ class ViewProvider: def restoreStockVisibility(self): if hasattr(self, "stockVisibility"): - self.obj.Stock.Visibility = self.stockVisibility + if self.stockVisibility: + self.obj.Stock.Visibility = True def hideTools(self): self.toolsVisibility = {} @@ -206,7 +218,8 @@ class ViewProvider: def restoreToolsVisibility(self): if hasattr(self, "toolsVisibility"): for tc in self.obj.Tools.Group: - tc.Tool.Visibility = self.toolsVisibility[tc.Tool.Name] + if self.toolsVisibility.get(tc.Tool.Name, True): + tc.Tool.Visibility = True def showOriginAxis(self, yes): sw = coin.SO_SWITCH_ALL if yes else coin.SO_SWITCH_NONE From b74a3b5270779bd64e2e6e514d5e60dcb59e86d1 Mon Sep 17 00:00:00 2001 From: jffmichi Date: Sat, 21 Jun 2025 19:49:49 +0200 Subject: [PATCH 068/126] CAM: simplify Order Output By Tool logic and fix #21969 (#21970) Co-authored-by: jffmichi <> --- src/Mod/CAM/Path/Post/Processor.py | 51 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Mod/CAM/Path/Post/Processor.py b/src/Mod/CAM/Path/Post/Processor.py index 1c153c309a..7cffd34264 100644 --- a/src/Mod/CAM/Path/Post/Processor.py +++ b/src/Mod/CAM/Path/Post/Processor.py @@ -222,6 +222,13 @@ class PostProcessor: curlist = [] # list of ops for tool, will repeat for each fixture sublist = [] # list of ops for output splitting + def commitToPostlist(): + if len(curlist) > 0: + for fixture in fixturelist: + sublist.append(fixture) + sublist.extend(curlist) + postlist.append((toolstring, sublist)) + Path.Log.track(self._job.PostProcessorOutputFile) for idx, obj in enumerate(self._job.Operations.Group): Path.Log.track(obj.Label) @@ -231,40 +238,36 @@ class PostProcessor: Path.Log.track() continue - # Determine the proper string for the Op's TC tc = PathUtil.toolControllerForOp(obj) - if tc is None: - tcstring = "None" - elif "%T" in self._job.PostProcessorOutputFile: - tcstring = f"{tc.ToolNumber}" - else: - tcstring = re.sub(r"[^\w\d-]", "_", tc.Label) - Path.Log.track(toolstring) + + # The operation has no ToolController or uses the same + # ToolController as the previous operations if tc is None or tc.ToolNumber == currTool: + # Queue current operation curlist.append(obj) - elif tc.ToolNumber != currTool and currTool is None: # first TC - sublist.append(tc) - curlist.append(obj) - currTool = tc.ToolNumber - toolstring = tcstring - elif tc.ToolNumber != currTool and currTool is not None: # TC - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) - postlist.append((toolstring, sublist)) + # The operation is the first operation or uses a different + # ToolController as the previous operations + + else: + # Commit previous operations + commitToPostlist() + + # Queue current ToolController and operation sublist = [tc] curlist = [obj] currTool = tc.ToolNumber - toolstring = tcstring - if idx == len(self._job.Operations.Group) - 1: # Last operation. - for fixture in fixturelist: - sublist.append(fixture) - sublist.extend(curlist) + # Determine the proper string for the operation's + # ToolController + if "%T" in self._job.PostProcessorOutputFile: + toolstring = f"{tc.ToolNumber}" + else: + toolstring = re.sub(r"[^\w\d-]", "_", tc.Label) - postlist.append((toolstring, sublist)) + # Commit remaining operations + commitToPostlist() elif orderby == "Operation": Path.Log.debug("Ordering by Operation") From 661d2052b71befa5f0d3646e1c75393675c3666f Mon Sep 17 00:00:00 2001 From: Balazs Nagy Date: Sun, 22 Jun 2025 16:18:07 +0200 Subject: [PATCH 069/126] find job in parent chain (#21742) --- src/Mod/CAM/PathScripts/PathUtilsGui.py | 50 +++++++++++++------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/Mod/CAM/PathScripts/PathUtilsGui.py b/src/Mod/CAM/PathScripts/PathUtilsGui.py index d2e9b3ed77..b73dbe350b 100644 --- a/src/Mod/CAM/PathScripts/PathUtilsGui.py +++ b/src/Mod/CAM/PathScripts/PathUtilsGui.py @@ -63,36 +63,38 @@ class PathUtilsUserInput(object): def chooseJob(self, jobs): job = None selected = FreeCADGui.Selection.getSelection() - if 1 == len(selected) and selected[0] in jobs: - job = selected[0] + if 1 == len(selected): + found = PathUtils.findParentJob(selected[0]) + if found: + return found + + modelSelected = [] + for job in jobs: + if all([o in job.Model.Group for o in selected]): + modelSelected.append(job) + if 1 == len(modelSelected): + job = modelSelected[0] else: - modelSelected = [] + modelObjectSelected = [] for job in jobs: - if all([o in job.Model.Group for o in selected]): - modelSelected.append(job) - if 1 == len(modelSelected): - job = modelSelected[0] + if all([o in job.Proxy.baseObjects(job) for o in selected]): + modelObjectSelected.append(job) + if 1 == len(modelObjectSelected): + job = modelObjectSelected[0] else: - modelObjectSelected = [] - for job in jobs: - if all([o in job.Proxy.baseObjects(job) for o in selected]): - modelObjectSelected.append(job) - if 1 == len(modelObjectSelected): - job = modelObjectSelected[0] + if modelObjectSelected: + mylist = [j.Label for j in modelObjectSelected] else: - if modelObjectSelected: - mylist = [j.Label for j in modelObjectSelected] - else: - mylist = [j.Label for j in jobs] + mylist = [j.Label for j in jobs] - jobname, result = QtGui.QInputDialog.getItem( - None, translate("Path", "Choose a CAM Job"), None, mylist - ) + jobname, result = QtGui.QInputDialog.getItem( + None, translate("Path", "Choose a CAM Job"), None, mylist + ) - if result is False: - return None - else: - job = [j for j in jobs if j.Label == jobname][0] + if result is False: + return None + else: + job = [j for j in jobs if j.Label == jobname][0] return job def createJob(self): From 908941f2d1d9fdfcd5b9ab8806d026ffa5084206 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 17:41:22 +0200 Subject: [PATCH 070/126] Core: Add a possibility to extract active object based on extension --- src/Gui/ActiveObjectList.cpp | 12 ++++++++++++ src/Gui/ActiveObjectList.h | 3 +++ src/Gui/MDIView.h | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/src/Gui/ActiveObjectList.cpp b/src/Gui/ActiveObjectList.cpp index 0c0a5693c6..e76882167a 100644 --- a/src/Gui/ActiveObjectList.cpp +++ b/src/Gui/ActiveObjectList.cpp @@ -197,3 +197,15 @@ void ActiveObjectList::objectDeleted(const ViewProviderDocumentObject &vp) } } } + +App::DocumentObject* ActiveObjectList::getObjectWithExtension(const Base::Type extensionTypeId) const +{ + for (const auto& pair : _ObjectMap) { + App::DocumentObject* obj = getObject(pair.second, true); + if (obj && obj->hasExtension(extensionTypeId)) { + return obj; + } + } + + return nullptr; +} diff --git a/src/Gui/ActiveObjectList.h b/src/Gui/ActiveObjectList.h index 8a75ce87bb..bd058d0607 100644 --- a/src/Gui/ActiveObjectList.h +++ b/src/Gui/ActiveObjectList.h @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -67,6 +68,8 @@ namespace Gui void objectDeleted(const ViewProviderDocumentObject& viewProviderIn); bool hasObject(App::DocumentObject *obj, const char *, const char *subname=nullptr) const; + App::DocumentObject* getObjectWithExtension(Base::Type extensionTypeId) const; + private: struct ObjectInfo; void setHighlight(const ObjectInfo &info, Gui::HighlightMode mode, bool enable); diff --git a/src/Gui/MDIView.h b/src/Gui/MDIView.h index f56a04578c..64c92ee4d8 100644 --- a/src/Gui/MDIView.h +++ b/src/Gui/MDIView.h @@ -147,6 +147,11 @@ public: return ActiveObjects.hasObject(o,n,subname); } + App::DocumentObject* getActiveObjectWithExtension(const Base::Type extensionTypeId) const + { + return ActiveObjects.getObjectWithExtension(extensionTypeId); + } + /*! * \brief containsViewProvider * Checks if the given view provider is part of this view. The default implementation From 5ed384c7f3e2deabfa62e801407d2b0a4261c940 Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Sun, 22 Jun 2025 23:25:52 +0200 Subject: [PATCH 071/126] App: Expose allowObject for groups in python --- src/App/GroupExtension.pyi | 6 ++++++ src/App/GroupExtensionPyImp.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/App/GroupExtension.pyi b/src/App/GroupExtension.pyi index cef2617867..8d50bc416b 100644 --- a/src/App/GroupExtension.pyi +++ b/src/App/GroupExtension.pyi @@ -70,3 +70,9 @@ class GroupExtension(DocumentObjectExtension): @param recursive if true check also if the obj is child of some sub group (default is false). """ ... + + def allowObject(self, obj: Any) -> bool: + """ + Returns true if obj is allowed in the group extension. + """ + ... diff --git a/src/App/GroupExtensionPyImp.cpp b/src/App/GroupExtensionPyImp.cpp index a13fbd7f65..fc281215d8 100644 --- a/src/App/GroupExtensionPyImp.cpp +++ b/src/App/GroupExtensionPyImp.cpp @@ -318,3 +318,30 @@ int GroupExtensionPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj* { return 0; } + +// def allowObject(self, obj: Any) -> bool: +PyObject* GroupExtensionPy::allowObject(PyObject* args) +{ + PyObject* object; + if (!PyArg_ParseTuple(args, "O!", &(DocumentObjectPy::Type), &object)) { + return nullptr; + } + + auto* docObj = static_cast(object); + if (!docObj->getDocumentObjectPtr() + || !docObj->getDocumentObjectPtr()->isAttachedToDocument()) { + PyErr_SetString(Base::PyExc_FC_GeneralError, "Cannot check an invalid object"); + return nullptr; + } + if (docObj->getDocumentObjectPtr()->getDocument() + != getGroupExtensionPtr()->getExtendedObject()->getDocument()) { + PyErr_SetString(Base::PyExc_FC_GeneralError, + "Cannot check an object from another document from this group"); + return nullptr; + } + + GroupExtension* grp = getGroupExtensionPtr(); + + bool allowed = grp->allowObject(docObj->getDocumentObjectPtr()); + return PyBool_FromLong(allowed ? 1 : 0); +} From ecad444131c068677007409eea5c19e25217143e Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 20 Jun 2025 20:20:58 +0200 Subject: [PATCH 072/126] Gui: Allow users to add groups to active objects As the title says, if right now there is Arch type active (like Level, Building, etc. etc.), then it's not possible to assign Group to it automatically (it's being created on root level of the document). So this patch basically takes an active object and tries to insert it. --- src/Gui/CommandStructure.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Gui/CommandStructure.cpp b/src/Gui/CommandStructure.cpp index b465c4055c..1fd797e675 100644 --- a/src/Gui/CommandStructure.cpp +++ b/src/Gui/CommandStructure.cpp @@ -32,6 +32,7 @@ #include "ActiveObjectList.h" #include "Application.h" #include "Document.h" +#include "MDIView.h" #include "ViewProviderDocumentObject.h" #include "Selection.h" @@ -121,9 +122,27 @@ void StdCmdGroup::activated(int iMsg) std::string GroupName; GroupName = getUniqueObjectName("Group"); QString label = QApplication::translate("Std_Group", "Group"); - doCommand(Doc,"App.activeDocument().Tip = App.activeDocument().addObject('App::DocumentObjectGroup','%s')",GroupName.c_str()); - doCommand(Doc,"App.activeDocument().%s.Label = '%s'", GroupName.c_str(), - label.toUtf8().data()); + + // create a group + doCommand(Doc,"group = App.activeDocument().addObject('App::DocumentObjectGroup','%s')",GroupName.c_str()); + doCommand(Doc,"group.Label = '%s'", label.toUtf8().data()); + doCommand(Doc,"App.activeDocument().Tip = group"); + + // try to add the group to any active object that supports grouping (has GroupExtension) + if (auto* activeDoc = Gui::Application::Instance->activeDocument()) { + if (auto* activeView = activeDoc->getActiveView()) { + // find the first active object with GroupExtension + if (auto* activeObj = activeView->getActiveObjectWithExtension( + App::GroupExtension::getExtensionClassTypeId())) { + doCommand(Doc, + "active_obj = App.activeDocument().getObject('%s')\n" + "if active_obj and active_obj.allowObject(group):\n" + " active_obj.Group += [group]", + activeObj->getNameInDocument()); + } + } + } // if we have no active object, group will be added to root doc + commitCommand(); Gui::Document* gui = Application::Instance->activeDocument(); From 2412d999660ed69e8a8f485ed11ca4ee2f41794c Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 14 Jun 2025 12:40:09 +0200 Subject: [PATCH 073/126] Core: Introduce searching in Preferences This PR introduces search box in preferences. Features: *supports left click on the result, taking user to the result *clicking anywhere cancels searching and closes popup box, same with ESC key *double click on the result closes the popup too (same behavior as enter) *supports enter (although if you are on the position you are already on it so enter just closes the popup basically) *escape closes it *you can navigate through the list with mouse *support fuzzy search so stuff like "OVP" is being matched to "On-View-Parameters" *there is hierarchical display (tab/page/setting) *some of the results are prioritized but fuzzy search prioritizing is the most important *highlights found item *goes to tab/page of found item *if the pop-up box won't fit next to the right side of the screen, it is added underneath the search box --- src/Gui/Dialogs/DlgPreferences.ui | 29 + src/Gui/Dialogs/DlgPreferencesImp.cpp | 846 +++++++++++++++++++++++++- src/Gui/Dialogs/DlgPreferencesImp.h | 62 ++ 3 files changed, 936 insertions(+), 1 deletion(-) diff --git a/src/Gui/Dialogs/DlgPreferences.ui b/src/Gui/Dialogs/DlgPreferences.ui index 1f169e871c..14bbef221a 100644 --- a/src/Gui/Dialogs/DlgPreferences.ui +++ b/src/Gui/Dialogs/DlgPreferences.ui @@ -210,6 +210,35 @@ + + + + 4 + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + Search preferences... + + + true + + + + + diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index d5b539994e..0f07d0251a 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -28,18 +28,35 @@ # include # include # include +# include # include # include +# include +# include +# include +# include +# include +# include # include # include +# include # include # include # include +# include # include +# include # include # include # include # include +# include +# include +# include +# include +# include +# include +# include #endif #include @@ -56,6 +73,123 @@ using namespace Gui::Dialog; +// Simple delegate to render first line bold, second line normal +// used by search box +class MixedFontDelegate : public QStyledItemDelegate +{ +public: + explicit MixedFontDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + if (!index.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + QString text = index.data(Qt::DisplayRole).toString(); + QStringList lines = text.split(QLatin1Char('\n')); + + if (lines.isEmpty()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + painter->save(); + + // draw selection background if selected + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight()); + } + + // Set text color based on selection + QColor textColor = (option.state & QStyle::State_Selected) + ? option.palette.highlightedText().color() + : option.palette.text().color(); + painter->setPen(textColor); + + // Set up fonts + QFont boldFont = option.font; + boldFont.setBold(true); + QFont normalFont = option.font; + normalFont.setPointSize(normalFont.pointSize() + 2); // make lower text 2 pixels bigger + + QFontMetrics boldFm(boldFont); + QFontMetrics normalFm(normalFont); + + int y = option.rect.top() + 4; // start 4px from top + int x = option.rect.left() + 12; // +12 horizontal padding + int availableWidth = option.rect.width() - 24; // account for left and right padding + + // draw first line in bold (Tab/Page) with wrapping + painter->setFont(boldFont); + QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.first()); + QRect boldRect(x, y, availableWidth, boldBoundingRect.height()); + painter->drawText(boldRect, Qt::TextWordWrap | Qt::AlignTop, lines.first()); + + // move y position after the bold text + y += boldBoundingRect.height(); + + // draw remaining lines in normal font with wrapping + if (lines.size() > 1) { + painter->setFont(normalFont); + + for (int i = 1; i < lines.size(); ++i) { + QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.at(i)); + QRect normalRect(x, y, availableWidth, normalBoundingRect.height()); + painter->drawText(normalRect, Qt::TextWordWrap | Qt::AlignTop, lines.at(i)); + y += normalBoundingRect.height(); + } + } + + painter->restore(); + } + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + if (!index.isValid()) { + return QStyledItemDelegate::sizeHint(option, index); + } + + QString text = index.data(Qt::DisplayRole).toString(); + QStringList lines = text.split(QLatin1Char('\n')); + + if (lines.isEmpty()) { + return QStyledItemDelegate::sizeHint(option, index); + } + + QFont boldFont = option.font; + boldFont.setBold(true); + QFont normalFont = option.font; + normalFont.setPointSize(normalFont.pointSize() + 2); // Make lower text 2 pixels bigger to match paint method + + QFontMetrics boldFm(boldFont); + QFontMetrics normalFm(normalFont); + + int availableWidth = option.rect.width() - 24; // Account for left and right padding + if (availableWidth <= 0) { + availableWidth = 300 - 24; // Fallback to popup width minus padding + } + + int width = 0; + int height = 8; // Start with 8 vertical padding (4 top + 4 bottom) + + // Calculate height for first line (bold) with wrapping + QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.first()); + height += boldBoundingRect.height(); + width = qMax(width, boldBoundingRect.width() + 24); // +24 horizontal padding + + // Calculate height for remaining lines (normal font) with wrapping + for (int i = 1; i < lines.size(); ++i) { + QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.at(i)); + height += normalBoundingRect.height(); + width = qMax(width, normalBoundingRect.width() + 24); + } + + return QSize(width, height); + } +}; + bool isParentOf(const QModelIndex& parent, const QModelIndex& child) { for (auto it = child; it.isValid(); it = it.parent()) { @@ -126,13 +260,33 @@ DlgPreferencesImp* DlgPreferencesImp::_activeDialog = nullptr; */ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) : QDialog(parent, fl), ui(new Ui_DlgPreferences), - invalidParameter(false), canEmbedScrollArea(true), restartRequired(false) + invalidParameter(false), canEmbedScrollArea(true), restartRequired(false), + searchResultsList(nullptr) { ui->setupUi(this); // remove unused help button setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + // Create the search results popup list + searchResultsList = new QListWidget(this); + searchResultsList->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + searchResultsList->setVisible(false); + searchResultsList->setMinimumWidth(300); + searchResultsList->setMaximumHeight(400); // Increased max height + searchResultsList->setFrameStyle(QFrame::Box | QFrame::Raised); + searchResultsList->setLineWidth(1); + searchResultsList->setFocusPolicy(Qt::NoFocus); // Don't steal focus from search box + searchResultsList->setAttribute(Qt::WA_ShowWithoutActivating); // Show without activating/stealing focus + searchResultsList->setWordWrap(true); // Enable word wrapping + searchResultsList->setTextElideMode(Qt::ElideNone); // Don't elide text, let it wrap instead + searchResultsList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Disable horizontal scrollbar + searchResultsList->setSpacing(0); // Remove spacing between items + searchResultsList->setContentsMargins(0, 0, 0, 0); // Remove margins + + // Set custom delegate for mixed font rendering (bold first line, normal second line) + searchResultsList->setItemDelegate(new MixedFontDelegate(searchResultsList)); + setupConnections(); ui->groupsTreeView->setModel(&_model); @@ -150,6 +304,9 @@ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) */ DlgPreferencesImp::~DlgPreferencesImp() { + // Remove global event filter + qApp->removeEventFilter(this); + if (DlgPreferencesImp::_activeDialog == this) { DlgPreferencesImp::_activeDialog = nullptr; } @@ -185,6 +342,35 @@ void DlgPreferencesImp::setupConnections() &QStackedWidget::currentChanged, this, &DlgPreferencesImp::onStackWidgetChange); + connect(ui->searchBox, + &QLineEdit::textChanged, + this, + &DlgPreferencesImp::onSearchTextChanged); + + // Install event filter on search box for arrow key navigation + ui->searchBox->installEventFilter(this); + + // Install global event filter to handle clicks outside popup + qApp->installEventFilter(this); + + // Connect search results list + connect(searchResultsList, + &QListWidget::itemSelectionChanged, + this, + &DlgPreferencesImp::onSearchResultSelected); + connect(searchResultsList, + &QListWidget::itemDoubleClicked, + this, + &DlgPreferencesImp::onSearchResultDoubleClicked); + connect(searchResultsList, + &QListWidget::itemClicked, + this, + &DlgPreferencesImp::onSearchResultClicked); + + // Install event filter for keyboard navigation in search results + searchResultsList->installEventFilter(this); + + } void DlgPreferencesImp::setupPages() @@ -1007,4 +1193,662 @@ PreferencesPageItem* DlgPreferencesImp::getCurrentPage() const return pageWidget->property(PreferencesPageItem::PropertyName).value(); } +void DlgPreferencesImp::onSearchTextChanged(const QString& text) +{ + if (text.isEmpty()) { + clearSearchHighlights(); + searchResults.clear(); + lastSearchText.clear(); + hideSearchResultsList(); + return; + } + + // Only perform new search if text changed + if (text != lastSearchText) { + performSearch(text); + lastSearchText = text; + } +} + +void DlgPreferencesImp::performSearch(const QString& searchText) +{ + clearSearchHighlights(); + searchResults.clear(); + + if (searchText.length() < 2) { + hideSearchResultsList(); + return; + } + + // Search through all groups and pages to collect ALL results + auto root = _model.invisibleRootItem(); + for (int i = 0; i < root->rowCount(); i++) { + auto groupItem = static_cast(root->child(i)); + auto groupName = groupItem->data(GroupNameRole).toString(); + auto groupStack = qobject_cast(groupItem->getWidget()); + + if (!groupStack) continue; + + // Search in each page of the group + for (int j = 0; j < groupItem->rowCount(); j++) { + auto pageItem = static_cast(groupItem->child(j)); + auto pageName = pageItem->data(PageNameRole).toString(); + auto pageWidget = qobject_cast(pageItem->getWidget()); + + if (!pageWidget) continue; + + // Collect all matching widgets in this page + collectSearchResults(pageWidget, searchText, groupName, pageName, pageItem->text(), groupItem->text()); + } + } + + // Sort results by score (highest first) + std::sort(searchResults.begin(), searchResults.end(), + [](const SearchResult& a, const SearchResult& b) { + return a.score > b.score; + }); + + // Update UI with search results + if (!searchResults.isEmpty()) { + populateSearchResultsList(); + showSearchResultsList(); + } else { + hideSearchResultsList(); + } +} + + + +void DlgPreferencesImp::clearSearchHighlights() +{ + // Restore original styles for all highlighted widgets + for (int i = 0; i < highlightedWidgets.size(); ++i) { + QWidget* widget = highlightedWidgets.at(i); + if (widget && originalStyles.contains(widget)) { + widget->setStyleSheet(originalStyles[widget]); + } + } + highlightedWidgets.clear(); + originalStyles.clear(); +} + + + +void DlgPreferencesImp::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) +{ + if (!widget) return; + + const QString lowerSearchText = searchText.toLower(); + + // First, check if the page display name itself matches (highest priority) + int pageScore = 0; + if (fuzzyMatch(searchText, pageDisplayName, pageScore)) { + SearchResult result; + result.groupName = groupName; + result.pageName = pageName; + result.widget = widget; // Use the page widget itself + result.matchText = pageDisplayName; // Use display name, not internal name + result.groupBoxName = QString(); // No groupbox for page-level match + result.tabName = tabName; + result.pageDisplayName = pageDisplayName; + result.isPageLevelMatch = true; // Mark as page-level match + result.score = pageScore + 2000; // Boost page-level matches + result.displayText = formatSearchResultText(result); + searchResults.append(result); + // Continue searching for individual items even if page matches + } + + // Search different widget types using the template method + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); + searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); +} + +void DlgPreferencesImp::navigateToSearchResult(const QString& groupName, const QString& pageName) +{ + // Find the group and page items + auto root = _model.invisibleRootItem(); + for (int i = 0; i < root->rowCount(); i++) { + auto groupItem = static_cast(root->child(i)); + if (groupItem->data(GroupNameRole).toString() == groupName) { + + // Find the specific page + for (int j = 0; j < groupItem->rowCount(); j++) { + auto pageItem = static_cast(groupItem->child(j)); + if (pageItem->data(PageNameRole).toString() == pageName) { + + // Expand the group if needed + ui->groupsTreeView->expand(groupItem->index()); + + // Select the page + ui->groupsTreeView->selectionModel()->select(pageItem->index(), QItemSelectionModel::ClearAndSelect); + + // Navigate to the page + onPageSelected(pageItem->index()); + + return; + } + } + + // If no specific page found, just navigate to the group + ui->groupsTreeView->selectionModel()->select(groupItem->index(), QItemSelectionModel::ClearAndSelect); + onPageSelected(groupItem->index()); + return; + } + } +} + +bool DlgPreferencesImp::eventFilter(QObject* obj, QEvent* event) +{ + // Handle search box key presses + if (obj == ui->searchBox && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + return handleSearchBoxKeyPress(keyEvent); + } + + // Handle popup key presses + if (obj == searchResultsList && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + return handlePopupKeyPress(keyEvent); + } + + // Prevent popup from stealing focus + if (obj == searchResultsList && event->type() == QEvent::FocusIn) { + ensureSearchBoxFocus(); + return true; + } + + // Handle search box focus loss + if (obj == ui->searchBox && event->type() == QEvent::FocusOut) { + QFocusEvent* focusEvent = static_cast(event); + if (focusEvent->reason() != Qt::PopupFocusReason && + focusEvent->reason() != Qt::MouseFocusReason) { + // Only hide if focus is going somewhere else, not due to popup interaction + QTimer::singleShot(100, this, [this]() { + if (!ui->searchBox->hasFocus() && !searchResultsList->underMouse()) { + hideSearchResultsList(); + } + }); + } + } + + // Handle clicks outside popup + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseEvent = static_cast(event); + QWidget* widget = qobject_cast(obj); + + // Check if click is outside search area + if (searchResultsList->isVisible() && + obj != searchResultsList && + obj != ui->searchBox && + widget && // Only check if obj is actually a QWidget + !searchResultsList->isAncestorOf(widget) && + !ui->searchBox->isAncestorOf(widget)) { + + if (isClickOutsidePopup(mouseEvent)) { + hideSearchResultsList(); + } + } + } + + return QDialog::eventFilter(obj, event); +} + +void DlgPreferencesImp::onSearchResultSelected() +{ + // This method is called when a search result is selected (arrow keys or single click) + // Navigate immediately but keep popup open + if (searchResultsList && searchResultsList->currentItem()) { + navigateToCurrentSearchResult(false); // false = don't close popup + } + + ensureSearchBoxFocus(); +} + +void DlgPreferencesImp::onSearchResultClicked() +{ + // Handle single click - navigate immediately but keep popup open + if (searchResultsList && searchResultsList->currentItem()) { + navigateToCurrentSearchResult(false); // false = don't close popup + } + + ensureSearchBoxFocus(); +} + +void DlgPreferencesImp::onSearchResultDoubleClicked() +{ + // Handle double click - navigate and close popup + if (searchResultsList && searchResultsList->currentItem()) { + navigateToCurrentSearchResult(true); // true = close popup + } +} + +void DlgPreferencesImp::navigateToCurrentSearchResult(bool closePopup) +{ + QListWidgetItem* currentItem = searchResultsList->currentItem(); + + // Skip if it's a separator (non-selectable item) or no item selected + if (!currentItem || !(currentItem->flags() & Qt::ItemIsSelectable)) { + return; + } + + // Get the result index directly from the item data + bool ok; + int resultIndex = currentItem->data(Qt::UserRole).toInt(&ok); + + if (ok && resultIndex >= 0 && resultIndex < searchResults.size()) { + const SearchResult& result = searchResults.at(resultIndex); + + // Navigate to the result + navigateToSearchResult(result.groupName, result.pageName); + + // Clear any existing highlights + clearSearchHighlights(); + + // Only highlight specific widgets for non-page-level matches + if (!result.isPageLevelMatch && !result.widget.isNull()) { + applyHighlightToWidget(result.widget); + } + // For page-level matches, we just navigate without highlighting anything + + // Close popup only if requested (double-click or Enter) + if (closePopup) { + hideSearchResultsList(); + } + } +} + +void DlgPreferencesImp::populateSearchResultsList() +{ + searchResultsList->clear(); + + for (int i = 0; i < searchResults.size(); ++i) { + const SearchResult& result = searchResults.at(i); + QListWidgetItem* item = new QListWidgetItem(result.displayText); + item->setData(Qt::UserRole, i); // Store the index instead of pointer + searchResultsList->addItem(item); + } + + // Select first actual item (not separator) + if (!searchResults.isEmpty()) { + searchResultsList->setCurrentRow(0); + } +} + +void DlgPreferencesImp::hideSearchResultsList() +{ + searchResultsList->setVisible(false); +} + +void DlgPreferencesImp::showSearchResultsList() +{ + // Configure popup size and position + configurePopupSize(); + + // Show the popup + searchResultsList->setVisible(true); + searchResultsList->raise(); + + // Use QTimer to ensure focus returns to search box after Qt finishes processing the popup show event + QTimer::singleShot(0, this, [this]() { + if (ui->searchBox) { + ui->searchBox->setFocus(); + ui->searchBox->activateWindow(); + } + }); +} + +QString DlgPreferencesImp::findGroupBoxForWidget(QWidget* widget) +{ + if (!widget) return QString(); + + // Walk up the parent hierarchy to find a QGroupBox + QWidget* parent = widget->parentWidget(); + while (parent) { + QGroupBox* groupBox = qobject_cast(parent); + if (groupBox) { + return groupBox->title(); + } + parent = parent->parentWidget(); + } + + return QString(); +} + +QString DlgPreferencesImp::formatSearchResultText(const SearchResult& result) +{ + // Format for MixedFontDelegate: First line will be bold, subsequent lines normal + QString text = result.tabName + QStringLiteral("/") + result.pageDisplayName; + + if (!result.isPageLevelMatch) { + // Add the actual finding on the second line + text += QStringLiteral("\n") + result.matchText; + } + + return text; +} + +void DlgPreferencesImp::createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName) +{ + SearchResult result; + result.groupName = groupName; + result.pageName = pageName; + result.widget = widget; + result.matchText = matchText; + result.groupBoxName = findGroupBoxForWidget(widget); + result.tabName = tabName; + result.pageDisplayName = pageDisplayName; + result.isPageLevelMatch = false; + result.score = 0; // Will be set by the caller + result.displayText = formatSearchResultText(result); + searchResults.append(result); +} + +template +void DlgPreferencesImp::searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName) +{ + const QList widgets = parentWidget->findChildren(); + + for (WidgetType* widget : widgets) { + QString widgetText; + + // Get text based on widget type + if constexpr (std::is_same_v) { + widgetText = widget->text(); + } else if constexpr (std::is_same_v) { + widgetText = widget->text(); + } else if constexpr (std::is_same_v) { + widgetText = widget->text(); + } else if constexpr (std::is_same_v) { + widgetText = widget->text(); + } + + // Use fuzzy matching instead of simple contains + int score = 0; + if (fuzzyMatch(searchText, widgetText, score)) { + createSearchResult(widget, widgetText, groupName, pageName, pageDisplayName, tabName); + // Update the score of the last added result + if (!searchResults.isEmpty()) { + searchResults.last().score = score; + } + } + } +} + +int DlgPreferencesImp::calculatePopupHeight(int popupWidth) +{ + int totalHeight = 0; + int itemCount = searchResultsList->count(); + int visibleItemCount = 0; + const int maxVisibleItems = 4; + + for (int i = 0; i < itemCount && visibleItemCount < maxVisibleItems; ++i) { + QListWidgetItem* item = searchResultsList->item(i); + if (!item) continue; + + // For separator items, use their widget height + if (searchResultsList->itemWidget(item)) { + totalHeight += searchResultsList->itemWidget(item)->sizeHint().height(); + } else { + // For text items, use the delegate's size hint instead of calculating manually + QStyleOptionViewItem option; + option.rect = QRect(0, 0, popupWidth, 100); // Temporary rect for calculation + option.font = searchResultsList->font(); + + QSize delegateSize = searchResultsList->itemDelegate()->sizeHint(option, searchResultsList->model()->index(i, 0)); + totalHeight += delegateSize.height(); + + visibleItemCount++; // Only count actual items, not separators + } + } + + return qMax(50, totalHeight); // Minimum 50px height +} + +void DlgPreferencesImp::configurePopupSize() +{ + if (searchResults.isEmpty()) { + hideSearchResultsList(); + return; + } + + // Set a fixed width to prevent flashing when content changes + int popupWidth = 300; // Fixed width for consistent appearance + searchResultsList->setFixedWidth(popupWidth); + + // Calculate and set the height + int finalHeight = calculatePopupHeight(popupWidth); + searchResultsList->setFixedHeight(finalHeight); + + // Position the popup's upper-left corner at the upper-right corner of the search box + QPoint globalPos = ui->searchBox->mapToGlobal(QPoint(ui->searchBox->width(), 0)); + + // Check if popup would go off-screen to the right + QScreen* screen = QApplication::screenAt(globalPos); + if (!screen) { + screen = QApplication::primaryScreen(); + } + QRect screenGeometry = screen->availableGeometry(); + + // If popup would extend beyond right edge of screen, position it below the search box instead + if (globalPos.x() + popupWidth > screenGeometry.right()) { + globalPos = ui->searchBox->mapToGlobal(QPoint(0, ui->searchBox->height())); + } + + searchResultsList->move(globalPos); +} + +// Fuzzy search implementation + +bool DlgPreferencesImp::isExactMatch(const QString& searchText, const QString& targetText) +{ + return targetText.toLower().contains(searchText.toLower()); +} + +bool DlgPreferencesImp::fuzzyMatch(const QString& searchText, const QString& targetText, int& score) +{ + if (searchText.isEmpty()) { + score = 0; + return true; + } + + const QString lowerSearch = searchText.toLower(); + const QString lowerTarget = targetText.toLower(); + + // First check for exact substring match (highest score) + if (lowerTarget.contains(lowerSearch)) { + // Score based on how early the match appears and how much of the string it covers + int matchIndex = lowerTarget.indexOf(lowerSearch); + int coverage = (lowerSearch.length() * 100) / lowerTarget.length(); // Percentage coverage + score = 1000 - matchIndex + coverage; // Higher score for earlier matches and better coverage + return true; + } + + // For fuzzy matching, require minimum search length to avoid too many false positives + if (lowerSearch.length() < 3) { + score = 0; + return false; + } + + // Fuzzy matching: check if all characters appear in order + int searchIndex = 0; + int targetIndex = 0; + int consecutiveMatches = 0; + int maxConsecutive = 0; + int totalMatches = 0; + int firstMatchIndex = -1; + int lastMatchIndex = -1; + + while (searchIndex < lowerSearch.length() && targetIndex < lowerTarget.length()) { + if (lowerSearch[searchIndex] == lowerTarget[targetIndex]) { + if (firstMatchIndex == -1) { + firstMatchIndex = targetIndex; + } + lastMatchIndex = targetIndex; + searchIndex++; + totalMatches++; + consecutiveMatches++; + maxConsecutive = qMax(maxConsecutive, consecutiveMatches); + } else { + consecutiveMatches = 0; + } + targetIndex++; + } + + // Check if all search characters were found + if (searchIndex == lowerSearch.length()) { + // Calculate match density - how spread out are the matches? + int matchSpan = lastMatchIndex - firstMatchIndex + 1; + int density = (lowerSearch.length() * 100) / matchSpan; // Characters per span + + // Require minimum density - matches shouldn't be too spread out + if (density < 20) { // Less than 20% density is too sparse + score = 0; + return false; + } + + // Require minimum coverage of search term + int coverage = (lowerSearch.length() * 100) / lowerTarget.length(); + if (coverage < 15 && lowerTarget.length() > 20) { // For long strings, require better coverage + score = 0; + return false; + } + + // Calculate score based on: + // - Match density (how compact the matches are) + // - Consecutive matches bonus + // - Coverage (how much of target string is the search term) + // - Position bonus (earlier matches are better) + int densityScore = qMin(density, 100); // Cap at 100 + int consecutiveBonus = (maxConsecutive * 30) / lowerSearch.length(); + int coverageScore = qMin(coverage * 2, 100); // Coverage is important + int positionBonus = qMax(0, 50 - firstMatchIndex); // Earlier is better + + score = densityScore + consecutiveBonus + coverageScore + positionBonus; + + // Minimum score threshold for fuzzy matches + if (score < 80) { + score = 0; + return false; + } + + return true; + } + + score = 0; + return false; +} + +void DlgPreferencesImp::ensureSearchBoxFocus() +{ + if (ui->searchBox && !ui->searchBox->hasFocus()) { + ui->searchBox->setFocus(); + } +} + +QString DlgPreferencesImp::getHighlightStyleForWidget(QWidget* widget) +{ + const QString baseStyle = QStringLiteral("background-color: #E3F2FD; color: #1565C0; border: 2px solid #2196F3; border-radius: 3px;"); + + if (qobject_cast(widget)) { + return QStringLiteral("QLabel { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QCheckBox { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QRadioButton { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QGroupBox::title { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } else if (qobject_cast(widget)) { + return QStringLiteral("QPushButton { ") + baseStyle + QStringLiteral(" }"); + } else { + return QStringLiteral("QWidget { ") + baseStyle + QStringLiteral(" padding: 2px; }"); + } +} + +void DlgPreferencesImp::applyHighlightToWidget(QWidget* widget) +{ + if (!widget) return; + + originalStyles[widget] = widget->styleSheet(); + widget->setStyleSheet(getHighlightStyleForWidget(widget)); + highlightedWidgets.append(widget); +} + + + +bool DlgPreferencesImp::handleSearchBoxKeyPress(QKeyEvent* keyEvent) +{ + if (!searchResultsList->isVisible() || searchResults.isEmpty()) { + return false; + } + + switch (keyEvent->key()) { + case Qt::Key_Down: { + // Move selection down in popup, skipping separators + int currentRow = searchResultsList->currentRow(); + int totalItems = searchResultsList->count(); + for (int i = 1; i < totalItems; ++i) { + int nextRow = (currentRow + i) % totalItems; + QListWidgetItem* item = searchResultsList->item(nextRow); + if (item && (item->flags() & Qt::ItemIsSelectable)) { + searchResultsList->setCurrentRow(nextRow); + break; + } + } + return true; + } + case Qt::Key_Up: { + // Move selection up in popup, skipping separators + int currentRow = searchResultsList->currentRow(); + int totalItems = searchResultsList->count(); + for (int i = 1; i < totalItems; ++i) { + int prevRow = (currentRow - i + totalItems) % totalItems; + QListWidgetItem* item = searchResultsList->item(prevRow); + if (item && (item->flags() & Qt::ItemIsSelectable)) { + searchResultsList->setCurrentRow(prevRow); + break; + } + } + return true; + } + case Qt::Key_Return: + case Qt::Key_Enter: + navigateToCurrentSearchResult(true); // true = close popup + return true; + case Qt::Key_Escape: + hideSearchResultsList(); + return true; + default: + return false; + } +} + +bool DlgPreferencesImp::handlePopupKeyPress(QKeyEvent* keyEvent) +{ + switch (keyEvent->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + navigateToCurrentSearchResult(true); // true = close popup + return true; + case Qt::Key_Escape: + hideSearchResultsList(); + ensureSearchBoxFocus(); + return true; + default: + return false; + } +} + +bool DlgPreferencesImp::isClickOutsidePopup(QMouseEvent* mouseEvent) +{ + QPoint globalPos = mouseEvent->globalPos(); + QRect searchBoxRect = QRect(ui->searchBox->mapToGlobal(QPoint(0, 0)), ui->searchBox->size()); + QRect popupRect = QRect(searchResultsList->mapToGlobal(QPoint(0, 0)), searchResultsList->size()); + + return !searchBoxRect.contains(globalPos) && !popupRect.contains(globalPos); +} + #include "moc_DlgPreferencesImp.cpp" diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index 9d40d1d308..a24723ed09 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include @@ -134,6 +136,20 @@ class GuiExport DlgPreferencesImp : public QDialog static constexpr int minVerticalEmptySpace = 100; // px of vertical space to leave public: + // Search results navigation + struct SearchResult { + QString groupName; + QString pageName; + QPointer widget; + QString matchText; + QString groupBoxName; + QString tabName; // The tab name (like "Display") + QString pageDisplayName; // The page display name (like "3D View") + QString displayText; + bool isPageLevelMatch = false; // True if this is a page title match + int score = 0; // Fuzzy search score for sorting + }; + static void addPage(const std::string& className, const std::string& group); static void removePage(const std::string& className, const std::string& group); static void setGroupData(const std::string& group, const std::string& icon, const QString& tip); @@ -155,6 +171,7 @@ public: protected: void changeEvent(QEvent *e) override; void showEvent(QShowEvent*) override; + bool eventFilter(QObject* obj, QEvent* event) override; protected Q_SLOTS: void onButtonBoxClicked(QAbstractButton*); @@ -163,6 +180,11 @@ protected Q_SLOTS: void onGroupExpanded(const QModelIndex &index); void onGroupCollapsed(const QModelIndex &index); + + void onSearchTextChanged(const QString& text); + void onSearchResultSelected(); + void onSearchResultClicked(); + void onSearchResultDoubleClicked(); private: /** @name for internal use only */ @@ -190,6 +212,38 @@ private: int minimumPageWidth() const; int minimumDialogWidth(int) const; void expandToMinimumDialogWidth(); + + // Search functionality + void performSearch(const QString& searchText); + void clearSearchHighlights(); + void navigateToSearchResult(const QString& groupName, const QString& pageName); + void collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName); + + void populateSearchResultsList(); + void hideSearchResultsList(); + void showSearchResultsList(); + void navigateToCurrentSearchResult(bool closePopup); + QString findGroupBoxForWidget(QWidget* widget); + QString formatSearchResultText(const SearchResult& result); + + void createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + template + void searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + int calculatePopupHeight(int popupWidth); + void configurePopupSize(); + + // Fuzzy search helpers (for search box inside preferences)) + bool fuzzyMatch(const QString& searchText, const QString& targetText, int& score); + bool isExactMatch(const QString& searchText, const QString& targetText); + + void ensureSearchBoxFocus(); + void applyHighlightToWidget(QWidget* widget); + QString getHighlightStyleForWidget(QWidget* widget); + bool handleSearchBoxKeyPress(QKeyEvent* keyEvent); + bool handlePopupKeyPress(QKeyEvent* keyEvent); + bool isClickOutsidePopup(QMouseEvent* mouseEvent); //@} private: @@ -210,6 +264,14 @@ private: bool invalidParameter; bool canEmbedScrollArea; bool restartRequired; + + // Search state + QList highlightedWidgets; + QMap originalStyles; + + QList searchResults; + QString lastSearchText; + QListWidget* searchResultsList; /**< A name for our Qt::UserRole, used when storing user data in a list item */ static const int GroupNameRole; From b3f37d262add7263361fc581ec9458f8dfde3cfc Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 21 Jun 2025 14:28:31 +0200 Subject: [PATCH 074/126] Core: Formatting changes for search in preferences --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index 0f07d0251a..7b7a393044 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -369,8 +369,6 @@ void DlgPreferencesImp::setupConnections() // Install event filter for keyboard navigation in search results searchResultsList->installEventFilter(this); - - } void DlgPreferencesImp::setupPages() @@ -1257,8 +1255,6 @@ void DlgPreferencesImp::performSearch(const QString& searchText) } } - - void DlgPreferencesImp::clearSearchHighlights() { // Restore original styles for all highlighted widgets @@ -1272,8 +1268,6 @@ void DlgPreferencesImp::clearSearchHighlights() originalStyles.clear(); } - - void DlgPreferencesImp::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) { if (!widget) return; @@ -1501,7 +1495,9 @@ void DlgPreferencesImp::showSearchResultsList() QString DlgPreferencesImp::findGroupBoxForWidget(QWidget* widget) { - if (!widget) return QString(); + if (!widget) { + return QString(); + } // Walk up the parent hierarchy to find a QGroupBox QWidget* parent = widget->parentWidget(); @@ -1844,11 +1840,11 @@ bool DlgPreferencesImp::handlePopupKeyPress(QKeyEvent* keyEvent) bool DlgPreferencesImp::isClickOutsidePopup(QMouseEvent* mouseEvent) { - QPoint globalPos = mouseEvent->globalPos(); + QPointF globalPos = mouseEvent->globalPosition(); QRect searchBoxRect = QRect(ui->searchBox->mapToGlobal(QPoint(0, 0)), ui->searchBox->size()); QRect popupRect = QRect(searchResultsList->mapToGlobal(QPoint(0, 0)), searchResultsList->size()); - return !searchBoxRect.contains(globalPos) && !popupRect.contains(globalPos); + return !searchBoxRect.contains(globalPos.x(), globalPos.y()) && !popupRect.contains(globalPos.x(), globalPos.y()); } #include "moc_DlgPreferencesImp.cpp" From 94559a3092fedd0c77be7d2858fa03cf8f0e3c64 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 21 Jun 2025 18:30:21 +0200 Subject: [PATCH 075/126] Core: Extract preferences search bar to it's own class --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 530 +++++++++++++++----------- src/Gui/Dialogs/DlgPreferencesImp.h | 169 +++++--- 2 files changed, 417 insertions(+), 282 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index 7b7a393044..ae338a809a 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -260,36 +260,24 @@ DlgPreferencesImp* DlgPreferencesImp::_activeDialog = nullptr; */ DlgPreferencesImp::DlgPreferencesImp(QWidget* parent, Qt::WindowFlags fl) : QDialog(parent, fl), ui(new Ui_DlgPreferences), - invalidParameter(false), canEmbedScrollArea(true), restartRequired(false), - searchResultsList(nullptr) + invalidParameter(false), canEmbedScrollArea(true), restartRequired(false) { ui->setupUi(this); // remove unused help button setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - // Create the search results popup list - searchResultsList = new QListWidget(this); - searchResultsList->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - searchResultsList->setVisible(false); - searchResultsList->setMinimumWidth(300); - searchResultsList->setMaximumHeight(400); // Increased max height - searchResultsList->setFrameStyle(QFrame::Box | QFrame::Raised); - searchResultsList->setLineWidth(1); - searchResultsList->setFocusPolicy(Qt::NoFocus); // Don't steal focus from search box - searchResultsList->setAttribute(Qt::WA_ShowWithoutActivating); // Show without activating/stealing focus - searchResultsList->setWordWrap(true); // Enable word wrapping - searchResultsList->setTextElideMode(Qt::ElideNone); // Don't elide text, let it wrap instead - searchResultsList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Disable horizontal scrollbar - searchResultsList->setSpacing(0); // Remove spacing between items - searchResultsList->setContentsMargins(0, 0, 0, 0); // Remove margins + // Initialize search controller + m_searchController = std::make_unique(this, this); - // Set custom delegate for mixed font rendering (bold first line, normal second line) - searchResultsList->setItemDelegate(new MixedFontDelegate(searchResultsList)); - setupConnections(); ui->groupsTreeView->setModel(&_model); + + // Configure search controller after UI setup + m_searchController->setPreferencesModel(&_model); + m_searchController->setGroupNameRole(GroupNameRole); + m_searchController->setPageNameRole(PageNameRole); setupPages(); @@ -342,33 +330,23 @@ void DlgPreferencesImp::setupConnections() &QStackedWidget::currentChanged, this, &DlgPreferencesImp::onStackWidgetChange); + // Connect search functionality to controller connect(ui->searchBox, &QLineEdit::textChanged, + m_searchController.get(), + &PreferencesSearchController::onSearchTextChanged); + + // Connect navigation signal from controller to dialog + connect(m_searchController.get(), + &PreferencesSearchController::navigationRequested, this, - &DlgPreferencesImp::onSearchTextChanged); + &DlgPreferencesImp::onNavigationRequested); // Install event filter on search box for arrow key navigation ui->searchBox->installEventFilter(this); // Install global event filter to handle clicks outside popup qApp->installEventFilter(this); - - // Connect search results list - connect(searchResultsList, - &QListWidget::itemSelectionChanged, - this, - &DlgPreferencesImp::onSearchResultSelected); - connect(searchResultsList, - &QListWidget::itemDoubleClicked, - this, - &DlgPreferencesImp::onSearchResultDoubleClicked); - connect(searchResultsList, - &QListWidget::itemClicked, - this, - &DlgPreferencesImp::onSearchResultClicked); - - // Install event filter for keyboard navigation in search results - searchResultsList->installEventFilter(this); } void DlgPreferencesImp::setupPages() @@ -1087,6 +1065,45 @@ void DlgPreferencesImp::onStackWidgetChange(int index) ui->groupsTreeView->selectionModel()->select(currentIndex, QItemSelectionModel::ClearAndSelect); } +void DlgPreferencesImp::onNavigationRequested(const QString& groupName, const QString& pageName) +{ + navigateToSearchResult(groupName, pageName); +} + +void DlgPreferencesImp::navigateToSearchResult(const QString& groupName, const QString& pageName) +{ + // Find the group and page items + auto root = _model.invisibleRootItem(); + for (int i = 0; i < root->rowCount(); i++) { + auto groupItem = static_cast(root->child(i)); + if (groupItem->data(GroupNameRole).toString() == groupName) { + + // Find the specific page + for (int j = 0; j < groupItem->rowCount(); j++) { + auto pageItem = static_cast(groupItem->child(j)); + if (pageItem->data(PageNameRole).toString() == pageName) { + + // Expand the group if needed + ui->groupsTreeView->expand(groupItem->index()); + + // Select the page + ui->groupsTreeView->selectionModel()->select(pageItem->index(), QItemSelectionModel::ClearAndSelect); + + // Navigate to the page + onPageSelected(pageItem->index()); + + return; + } + } + + // If no specific page found, just navigate to the group + ui->groupsTreeView->selectionModel()->select(groupItem->index(), QItemSelectionModel::ClearAndSelect); + onPageSelected(groupItem->index()); + return; + } + } +} + void DlgPreferencesImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { @@ -1191,27 +1208,117 @@ PreferencesPageItem* DlgPreferencesImp::getCurrentPage() const return pageWidget->property(PreferencesPageItem::PropertyName).value(); } -void DlgPreferencesImp::onSearchTextChanged(const QString& text) +// PreferencesSearchController implementation +PreferencesSearchController::PreferencesSearchController(DlgPreferencesImp* parentDialog, QObject* parent) + : QObject(parent) + , m_parentDialog(parentDialog) + , m_preferencesModel(nullptr) + , m_groupNameRole(0) + , m_pageNameRole(0) + , m_searchBox(nullptr) + , m_searchResultsList(nullptr) +{ + // Get reference to search box from parent dialog's UI + m_searchBox = m_parentDialog->ui->searchBox; + + // Create the search results popup list + m_searchResultsList = new QListWidget(m_parentDialog); + m_searchResultsList->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + m_searchResultsList->setVisible(false); + m_searchResultsList->setMinimumWidth(300); + m_searchResultsList->setMaximumHeight(400); // Increased max height + m_searchResultsList->setFrameStyle(QFrame::Box | QFrame::Raised); + m_searchResultsList->setLineWidth(1); + m_searchResultsList->setFocusPolicy(Qt::NoFocus); // Don't steal focus from search box + m_searchResultsList->setAttribute(Qt::WA_ShowWithoutActivating); // Show without activating/stealing focus + m_searchResultsList->setWordWrap(true); // Enable word wrapping + m_searchResultsList->setTextElideMode(Qt::ElideNone); // Don't elide text, let it wrap instead + m_searchResultsList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Disable horizontal scrollbar + m_searchResultsList->setSpacing(0); // Remove spacing between items + m_searchResultsList->setContentsMargins(0, 0, 0, 0); // Remove margins + + // Set custom delegate for mixed font rendering (bold first line, normal second line) + m_searchResultsList->setItemDelegate(new MixedFontDelegate(m_searchResultsList)); + + // Connect search results list signals + connect(m_searchResultsList, + &QListWidget::itemSelectionChanged, + this, + &PreferencesSearchController::onSearchResultSelected); + connect(m_searchResultsList, + &QListWidget::itemDoubleClicked, + this, + &PreferencesSearchController::onSearchResultDoubleClicked); + connect(m_searchResultsList, + &QListWidget::itemClicked, + this, + &PreferencesSearchController::onSearchResultClicked); + + // Install event filter for keyboard navigation in search results + m_searchResultsList->installEventFilter(m_parentDialog); +} + +PreferencesSearchController::~PreferencesSearchController() +{ + // Destructor - cleanup handled by Qt's object system +} + +void PreferencesSearchController::setPreferencesModel(QStandardItemModel* model) +{ + m_preferencesModel = model; +} + +void PreferencesSearchController::setGroupNameRole(int role) +{ + m_groupNameRole = role; +} + +void PreferencesSearchController::setPageNameRole(int role) +{ + m_pageNameRole = role; +} + +QListWidget* PreferencesSearchController::getSearchResultsList() const +{ + return m_searchResultsList; +} + +bool PreferencesSearchController::isPopupVisible() const +{ + return m_searchResultsList && m_searchResultsList->isVisible(); +} + +bool PreferencesSearchController::isPopupUnderMouse() const +{ + return m_searchResultsList && m_searchResultsList->underMouse(); +} + +bool PreferencesSearchController::isPopupAncestorOf(QWidget* widget) const +{ + return m_searchResultsList && m_searchResultsList->isAncestorOf(widget); +} + +void PreferencesSearchController::onSearchTextChanged(const QString& text) { if (text.isEmpty()) { - clearSearchHighlights(); - searchResults.clear(); - lastSearchText.clear(); + clearHighlights(); + m_searchResults.clear(); + m_lastSearchText.clear(); hideSearchResultsList(); return; } // Only perform new search if text changed - if (text != lastSearchText) { + if (text != m_lastSearchText) { performSearch(text); - lastSearchText = text; + m_lastSearchText = text; } } -void DlgPreferencesImp::performSearch(const QString& searchText) +void PreferencesSearchController::performSearch(const QString& searchText) { - clearSearchHighlights(); - searchResults.clear(); + clearHighlights(); + m_searchResults.clear(); if (searchText.length() < 2) { hideSearchResultsList(); @@ -1219,21 +1326,25 @@ void DlgPreferencesImp::performSearch(const QString& searchText) } // Search through all groups and pages to collect ALL results - auto root = _model.invisibleRootItem(); + auto root = m_preferencesModel->invisibleRootItem(); for (int i = 0; i < root->rowCount(); i++) { auto groupItem = static_cast(root->child(i)); - auto groupName = groupItem->data(GroupNameRole).toString(); + auto groupName = groupItem->data(m_groupNameRole).toString(); auto groupStack = qobject_cast(groupItem->getWidget()); - if (!groupStack) continue; + if (!groupStack) { + continue; + } // Search in each page of the group for (int j = 0; j < groupItem->rowCount(); j++) { auto pageItem = static_cast(groupItem->child(j)); - auto pageName = pageItem->data(PageNameRole).toString(); + auto pageName = pageItem->data(m_pageNameRole).toString(); auto pageWidget = qobject_cast(pageItem->getWidget()); - if (!pageWidget) continue; + if (!pageWidget) { + continue; + } // Collect all matching widgets in this page collectSearchResults(pageWidget, searchText, groupName, pageName, pageItem->text(), groupItem->text()); @@ -1241,13 +1352,13 @@ void DlgPreferencesImp::performSearch(const QString& searchText) } // Sort results by score (highest first) - std::sort(searchResults.begin(), searchResults.end(), + std::sort(m_searchResults.begin(), m_searchResults.end(), [](const SearchResult& a, const SearchResult& b) { return a.score > b.score; }); // Update UI with search results - if (!searchResults.isEmpty()) { + if (!m_searchResults.isEmpty()) { populateSearchResultsList(); showSearchResultsList(); } else { @@ -1255,20 +1366,20 @@ void DlgPreferencesImp::performSearch(const QString& searchText) } } -void DlgPreferencesImp::clearSearchHighlights() +void PreferencesSearchController::clearHighlights() { // Restore original styles for all highlighted widgets - for (int i = 0; i < highlightedWidgets.size(); ++i) { - QWidget* widget = highlightedWidgets.at(i); - if (widget && originalStyles.contains(widget)) { - widget->setStyleSheet(originalStyles[widget]); + for (int i = 0; i < m_highlightedWidgets.size(); ++i) { + QWidget* widget = m_highlightedWidgets.at(i); + if (widget && m_originalStyles.contains(widget)) { + widget->setStyleSheet(m_originalStyles[widget]); } } - highlightedWidgets.clear(); - originalStyles.clear(); + m_highlightedWidgets.clear(); + m_originalStyles.clear(); } -void DlgPreferencesImp::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) +void PreferencesSearchController::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) { if (!widget) return; @@ -1288,7 +1399,7 @@ void DlgPreferencesImp::collectSearchResults(QWidget* widget, const QString& sea result.isPageLevelMatch = true; // Mark as page-level match result.score = pageScore + 2000; // Boost page-level matches result.displayText = formatSearchResultText(result); - searchResults.append(result); + m_searchResults.append(result); // Continue searching for individual items even if page matches } @@ -1299,128 +1410,38 @@ void DlgPreferencesImp::collectSearchResults(QWidget* widget, const QString& sea searchWidgetType(widget, searchText, groupName, pageName, pageDisplayName, tabName); } -void DlgPreferencesImp::navigateToSearchResult(const QString& groupName, const QString& pageName) -{ - // Find the group and page items - auto root = _model.invisibleRootItem(); - for (int i = 0; i < root->rowCount(); i++) { - auto groupItem = static_cast(root->child(i)); - if (groupItem->data(GroupNameRole).toString() == groupName) { - - // Find the specific page - for (int j = 0; j < groupItem->rowCount(); j++) { - auto pageItem = static_cast(groupItem->child(j)); - if (pageItem->data(PageNameRole).toString() == pageName) { - - // Expand the group if needed - ui->groupsTreeView->expand(groupItem->index()); - - // Select the page - ui->groupsTreeView->selectionModel()->select(pageItem->index(), QItemSelectionModel::ClearAndSelect); - - // Navigate to the page - onPageSelected(pageItem->index()); - - return; - } - } - - // If no specific page found, just navigate to the group - ui->groupsTreeView->selectionModel()->select(groupItem->index(), QItemSelectionModel::ClearAndSelect); - onPageSelected(groupItem->index()); - return; - } - } -} - -bool DlgPreferencesImp::eventFilter(QObject* obj, QEvent* event) -{ - // Handle search box key presses - if (obj == ui->searchBox && event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(event); - return handleSearchBoxKeyPress(keyEvent); - } - - // Handle popup key presses - if (obj == searchResultsList && event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(event); - return handlePopupKeyPress(keyEvent); - } - - // Prevent popup from stealing focus - if (obj == searchResultsList && event->type() == QEvent::FocusIn) { - ensureSearchBoxFocus(); - return true; - } - - // Handle search box focus loss - if (obj == ui->searchBox && event->type() == QEvent::FocusOut) { - QFocusEvent* focusEvent = static_cast(event); - if (focusEvent->reason() != Qt::PopupFocusReason && - focusEvent->reason() != Qt::MouseFocusReason) { - // Only hide if focus is going somewhere else, not due to popup interaction - QTimer::singleShot(100, this, [this]() { - if (!ui->searchBox->hasFocus() && !searchResultsList->underMouse()) { - hideSearchResultsList(); - } - }); - } - } - - // Handle clicks outside popup - if (event->type() == QEvent::MouseButtonPress) { - QMouseEvent* mouseEvent = static_cast(event); - QWidget* widget = qobject_cast(obj); - - // Check if click is outside search area - if (searchResultsList->isVisible() && - obj != searchResultsList && - obj != ui->searchBox && - widget && // Only check if obj is actually a QWidget - !searchResultsList->isAncestorOf(widget) && - !ui->searchBox->isAncestorOf(widget)) { - - if (isClickOutsidePopup(mouseEvent)) { - hideSearchResultsList(); - } - } - } - - return QDialog::eventFilter(obj, event); -} - -void DlgPreferencesImp::onSearchResultSelected() +void PreferencesSearchController::onSearchResultSelected() { // This method is called when a search result is selected (arrow keys or single click) // Navigate immediately but keep popup open - if (searchResultsList && searchResultsList->currentItem()) { + if (m_searchResultsList && m_searchResultsList->currentItem()) { navigateToCurrentSearchResult(false); // false = don't close popup } ensureSearchBoxFocus(); } -void DlgPreferencesImp::onSearchResultClicked() +void PreferencesSearchController::onSearchResultClicked() { // Handle single click - navigate immediately but keep popup open - if (searchResultsList && searchResultsList->currentItem()) { + if (m_searchResultsList && m_searchResultsList->currentItem()) { navigateToCurrentSearchResult(false); // false = don't close popup } ensureSearchBoxFocus(); } -void DlgPreferencesImp::onSearchResultDoubleClicked() +void PreferencesSearchController::onSearchResultDoubleClicked() { // Handle double click - navigate and close popup - if (searchResultsList && searchResultsList->currentItem()) { + if (m_searchResultsList && m_searchResultsList->currentItem()) { navigateToCurrentSearchResult(true); // true = close popup } } -void DlgPreferencesImp::navigateToCurrentSearchResult(bool closePopup) +void PreferencesSearchController::navigateToCurrentSearchResult(bool closePopup) { - QListWidgetItem* currentItem = searchResultsList->currentItem(); + QListWidgetItem* currentItem = m_searchResultsList->currentItem(); // Skip if it's a separator (non-selectable item) or no item selected if (!currentItem || !(currentItem->flags() & Qt::ItemIsSelectable)) { @@ -1431,14 +1452,14 @@ void DlgPreferencesImp::navigateToCurrentSearchResult(bool closePopup) bool ok; int resultIndex = currentItem->data(Qt::UserRole).toInt(&ok); - if (ok && resultIndex >= 0 && resultIndex < searchResults.size()) { - const SearchResult& result = searchResults.at(resultIndex); + if (ok && resultIndex >= 0 && resultIndex < m_searchResults.size()) { + const SearchResult& result = m_searchResults.at(resultIndex); - // Navigate to the result - navigateToSearchResult(result.groupName, result.pageName); + // Emit signal to request navigation + Q_EMIT navigationRequested(result.groupName, result.pageName); // Clear any existing highlights - clearSearchHighlights(); + clearHighlights(); // Only highlight specific widgets for non-page-level matches if (!result.isPageLevelMatch && !result.widget.isNull()) { @@ -1453,47 +1474,47 @@ void DlgPreferencesImp::navigateToCurrentSearchResult(bool closePopup) } } -void DlgPreferencesImp::populateSearchResultsList() +void PreferencesSearchController::populateSearchResultsList() { - searchResultsList->clear(); + m_searchResultsList->clear(); - for (int i = 0; i < searchResults.size(); ++i) { - const SearchResult& result = searchResults.at(i); + for (int i = 0; i < m_searchResults.size(); ++i) { + const SearchResult& result = m_searchResults.at(i); QListWidgetItem* item = new QListWidgetItem(result.displayText); item->setData(Qt::UserRole, i); // Store the index instead of pointer - searchResultsList->addItem(item); + m_searchResultsList->addItem(item); } // Select first actual item (not separator) - if (!searchResults.isEmpty()) { - searchResultsList->setCurrentRow(0); + if (!m_searchResults.isEmpty()) { + m_searchResultsList->setCurrentRow(0); } } -void DlgPreferencesImp::hideSearchResultsList() +void PreferencesSearchController::hideSearchResultsList() { - searchResultsList->setVisible(false); + m_searchResultsList->setVisible(false); } -void DlgPreferencesImp::showSearchResultsList() +void PreferencesSearchController::showSearchResultsList() { // Configure popup size and position configurePopupSize(); // Show the popup - searchResultsList->setVisible(true); - searchResultsList->raise(); + m_searchResultsList->setVisible(true); + m_searchResultsList->raise(); // Use QTimer to ensure focus returns to search box after Qt finishes processing the popup show event QTimer::singleShot(0, this, [this]() { - if (ui->searchBox) { - ui->searchBox->setFocus(); - ui->searchBox->activateWindow(); + if (m_searchBox) { + m_searchBox->setFocus(); + m_searchBox->activateWindow(); } }); } -QString DlgPreferencesImp::findGroupBoxForWidget(QWidget* widget) +QString PreferencesSearchController::findGroupBoxForWidget(QWidget* widget) { if (!widget) { return QString(); @@ -1512,7 +1533,7 @@ QString DlgPreferencesImp::findGroupBoxForWidget(QWidget* widget) return QString(); } -QString DlgPreferencesImp::formatSearchResultText(const SearchResult& result) +QString PreferencesSearchController::formatSearchResultText(const SearchResult& result) { // Format for MixedFontDelegate: First line will be bold, subsequent lines normal QString text = result.tabName + QStringLiteral("/") + result.pageDisplayName; @@ -1525,7 +1546,7 @@ QString DlgPreferencesImp::formatSearchResultText(const SearchResult& result) return text; } -void DlgPreferencesImp::createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, +void PreferencesSearchController::createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) { SearchResult result; @@ -1539,11 +1560,11 @@ void DlgPreferencesImp::createSearchResult(QWidget* widget, const QString& match result.isPageLevelMatch = false; result.score = 0; // Will be set by the caller result.displayText = formatSearchResultText(result); - searchResults.append(result); + m_searchResults.append(result); } template -void DlgPreferencesImp::searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, +void PreferencesSearchController::searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) { const QList widgets = parentWidget->findChildren(); @@ -1567,34 +1588,34 @@ void DlgPreferencesImp::searchWidgetType(QWidget* parentWidget, const QString& s if (fuzzyMatch(searchText, widgetText, score)) { createSearchResult(widget, widgetText, groupName, pageName, pageDisplayName, tabName); // Update the score of the last added result - if (!searchResults.isEmpty()) { - searchResults.last().score = score; + if (!m_searchResults.isEmpty()) { + m_searchResults.last().score = score; } } } } -int DlgPreferencesImp::calculatePopupHeight(int popupWidth) +int PreferencesSearchController::calculatePopupHeight(int popupWidth) { int totalHeight = 0; - int itemCount = searchResultsList->count(); + int itemCount = m_searchResultsList->count(); int visibleItemCount = 0; const int maxVisibleItems = 4; for (int i = 0; i < itemCount && visibleItemCount < maxVisibleItems; ++i) { - QListWidgetItem* item = searchResultsList->item(i); + QListWidgetItem* item = m_searchResultsList->item(i); if (!item) continue; // For separator items, use their widget height - if (searchResultsList->itemWidget(item)) { - totalHeight += searchResultsList->itemWidget(item)->sizeHint().height(); + if (m_searchResultsList->itemWidget(item)) { + totalHeight += m_searchResultsList->itemWidget(item)->sizeHint().height(); } else { // For text items, use the delegate's size hint instead of calculating manually QStyleOptionViewItem option; option.rect = QRect(0, 0, popupWidth, 100); // Temporary rect for calculation - option.font = searchResultsList->font(); + option.font = m_searchResultsList->font(); - QSize delegateSize = searchResultsList->itemDelegate()->sizeHint(option, searchResultsList->model()->index(i, 0)); + QSize delegateSize = m_searchResultsList->itemDelegate()->sizeHint(option, m_searchResultsList->model()->index(i, 0)); totalHeight += delegateSize.height(); visibleItemCount++; // Only count actual items, not separators @@ -1604,23 +1625,23 @@ int DlgPreferencesImp::calculatePopupHeight(int popupWidth) return qMax(50, totalHeight); // Minimum 50px height } -void DlgPreferencesImp::configurePopupSize() +void PreferencesSearchController::configurePopupSize() { - if (searchResults.isEmpty()) { + if (m_searchResults.isEmpty()) { hideSearchResultsList(); return; } // Set a fixed width to prevent flashing when content changes int popupWidth = 300; // Fixed width for consistent appearance - searchResultsList->setFixedWidth(popupWidth); + m_searchResultsList->setFixedWidth(popupWidth); // Calculate and set the height int finalHeight = calculatePopupHeight(popupWidth); - searchResultsList->setFixedHeight(finalHeight); + m_searchResultsList->setFixedHeight(finalHeight); // Position the popup's upper-left corner at the upper-right corner of the search box - QPoint globalPos = ui->searchBox->mapToGlobal(QPoint(ui->searchBox->width(), 0)); + QPoint globalPos = m_searchBox->mapToGlobal(QPoint(m_searchBox->width(), 0)); // Check if popup would go off-screen to the right QScreen* screen = QApplication::screenAt(globalPos); @@ -1631,20 +1652,20 @@ void DlgPreferencesImp::configurePopupSize() // If popup would extend beyond right edge of screen, position it below the search box instead if (globalPos.x() + popupWidth > screenGeometry.right()) { - globalPos = ui->searchBox->mapToGlobal(QPoint(0, ui->searchBox->height())); + globalPos = m_searchBox->mapToGlobal(QPoint(0, m_searchBox->height())); } - searchResultsList->move(globalPos); + m_searchResultsList->move(globalPos); } // Fuzzy search implementation -bool DlgPreferencesImp::isExactMatch(const QString& searchText, const QString& targetText) +bool PreferencesSearchController::isExactMatch(const QString& searchText, const QString& targetText) { return targetText.toLower().contains(searchText.toLower()); } -bool DlgPreferencesImp::fuzzyMatch(const QString& searchText, const QString& targetText, int& score) +bool PreferencesSearchController::fuzzyMatch(const QString& searchText, const QString& targetText, int& score) { if (searchText.isEmpty()) { score = 0; @@ -1738,14 +1759,14 @@ bool DlgPreferencesImp::fuzzyMatch(const QString& searchText, const QString& tar return false; } -void DlgPreferencesImp::ensureSearchBoxFocus() +void PreferencesSearchController::ensureSearchBoxFocus() { - if (ui->searchBox && !ui->searchBox->hasFocus()) { - ui->searchBox->setFocus(); + if (m_searchBox && !m_searchBox->hasFocus()) { + m_searchBox->setFocus(); } } -QString DlgPreferencesImp::getHighlightStyleForWidget(QWidget* widget) +QString PreferencesSearchController::getHighlightStyleForWidget(QWidget* widget) { const QString baseStyle = QStringLiteral("background-color: #E3F2FD; color: #1565C0; border: 2px solid #2196F3; border-radius: 3px;"); @@ -1764,33 +1785,31 @@ QString DlgPreferencesImp::getHighlightStyleForWidget(QWidget* widget) } } -void DlgPreferencesImp::applyHighlightToWidget(QWidget* widget) +void PreferencesSearchController::applyHighlightToWidget(QWidget* widget) { if (!widget) return; - originalStyles[widget] = widget->styleSheet(); + m_originalStyles[widget] = widget->styleSheet(); widget->setStyleSheet(getHighlightStyleForWidget(widget)); - highlightedWidgets.append(widget); + m_highlightedWidgets.append(widget); } - - -bool DlgPreferencesImp::handleSearchBoxKeyPress(QKeyEvent* keyEvent) +bool PreferencesSearchController::handleSearchBoxKeyPress(QKeyEvent* keyEvent) { - if (!searchResultsList->isVisible() || searchResults.isEmpty()) { + if (!m_searchResultsList->isVisible() || m_searchResults.isEmpty()) { return false; } switch (keyEvent->key()) { case Qt::Key_Down: { // Move selection down in popup, skipping separators - int currentRow = searchResultsList->currentRow(); - int totalItems = searchResultsList->count(); + int currentRow = m_searchResultsList->currentRow(); + int totalItems = m_searchResultsList->count(); for (int i = 1; i < totalItems; ++i) { int nextRow = (currentRow + i) % totalItems; - QListWidgetItem* item = searchResultsList->item(nextRow); + QListWidgetItem* item = m_searchResultsList->item(nextRow); if (item && (item->flags() & Qt::ItemIsSelectable)) { - searchResultsList->setCurrentRow(nextRow); + m_searchResultsList->setCurrentRow(nextRow); break; } } @@ -1798,13 +1817,13 @@ bool DlgPreferencesImp::handleSearchBoxKeyPress(QKeyEvent* keyEvent) } case Qt::Key_Up: { // Move selection up in popup, skipping separators - int currentRow = searchResultsList->currentRow(); - int totalItems = searchResultsList->count(); + int currentRow = m_searchResultsList->currentRow(); + int totalItems = m_searchResultsList->count(); for (int i = 1; i < totalItems; ++i) { int prevRow = (currentRow - i + totalItems) % totalItems; - QListWidgetItem* item = searchResultsList->item(prevRow); + QListWidgetItem* item = m_searchResultsList->item(prevRow); if (item && (item->flags() & Qt::ItemIsSelectable)) { - searchResultsList->setCurrentRow(prevRow); + m_searchResultsList->setCurrentRow(prevRow); break; } } @@ -1822,7 +1841,7 @@ bool DlgPreferencesImp::handleSearchBoxKeyPress(QKeyEvent* keyEvent) } } -bool DlgPreferencesImp::handlePopupKeyPress(QKeyEvent* keyEvent) +bool PreferencesSearchController::handlePopupKeyPress(QKeyEvent* keyEvent) { switch (keyEvent->key()) { case Qt::Key_Return: @@ -1838,13 +1857,70 @@ bool DlgPreferencesImp::handlePopupKeyPress(QKeyEvent* keyEvent) } } -bool DlgPreferencesImp::isClickOutsidePopup(QMouseEvent* mouseEvent) +bool PreferencesSearchController::isClickOutsidePopup(QMouseEvent* mouseEvent) { QPointF globalPos = mouseEvent->globalPosition(); - QRect searchBoxRect = QRect(ui->searchBox->mapToGlobal(QPoint(0, 0)), ui->searchBox->size()); - QRect popupRect = QRect(searchResultsList->mapToGlobal(QPoint(0, 0)), searchResultsList->size()); + QRect searchBoxRect = QRect(m_searchBox->mapToGlobal(QPoint(0, 0)), m_searchBox->size()); + QRect popupRect = QRect(m_searchResultsList->mapToGlobal(QPoint(0, 0)), m_searchResultsList->size()); return !searchBoxRect.contains(globalPos.x(), globalPos.y()) && !popupRect.contains(globalPos.x(), globalPos.y()); } +bool DlgPreferencesImp::eventFilter(QObject* obj, QEvent* event) +{ + // Handle search box key presses + if (obj == ui->searchBox && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + return m_searchController->handleSearchBoxKeyPress(keyEvent); + } + + // Handle popup key presses + if (obj == m_searchController->getSearchResultsList() && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + return m_searchController->handlePopupKeyPress(keyEvent); + } + + // Prevent popup from stealing focus + if (obj == m_searchController->getSearchResultsList() && event->type() == QEvent::FocusIn) { + m_searchController->ensureSearchBoxFocus(); + return true; + } + + // Handle search box focus loss + if (obj == ui->searchBox && event->type() == QEvent::FocusOut) { + QFocusEvent* focusEvent = static_cast(event); + if (focusEvent->reason() != Qt::PopupFocusReason && + focusEvent->reason() != Qt::MouseFocusReason) { + // Only hide if focus is going somewhere else, not due to popup interaction + QTimer::singleShot(100, this, [this]() { + if (!ui->searchBox->hasFocus() && !m_searchController->isPopupUnderMouse()) { + m_searchController->hideSearchResultsList(); + } + }); + } + } + + // Handle clicks outside popup + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseEvent = static_cast(event); + QWidget* widget = qobject_cast(obj); + + // Check if click is outside search area + if (m_searchController->isPopupVisible() && + obj != m_searchController->getSearchResultsList() && + obj != ui->searchBox && + widget && // Only check if obj is actually a QWidget + !m_searchController->isPopupAncestorOf(widget) && + !ui->searchBox->isAncestorOf(widget)) { + + if (m_searchController->isClickOutsidePopup(mouseEvent)) { + m_searchController->hideSearchResultsList(); + m_searchController->clearHighlights(); + } + } + } + + return QDialog::eventFilter(obj, event); +} + #include "moc_DlgPreferencesImp.cpp" diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index a24723ed09..d67f577fb5 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -31,16 +31,123 @@ #include #include #include +#include #include #include class QAbstractButton; class QListWidgetItem; class QTabWidget; +class QKeyEvent; +class QMouseEvent; namespace Gui::Dialog { class PreferencePage; class Ui_DlgPreferences; +class DlgPreferencesImp; + +class GuiExport PreferencesSearchController : public QObject +{ + Q_OBJECT +public: + // Search results structure + struct SearchResult { + QString groupName; + QString pageName; + QPointer widget; + QString matchText; + QString groupBoxName; + QString tabName; // The tab name (like "Display") + QString pageDisplayName; // The page display name (like "3D View") + QString displayText; + bool isPageLevelMatch = false; // True if this is a page title match + int score = 0; // Fuzzy search score for sorting + }; + + explicit PreferencesSearchController(DlgPreferencesImp* parentDialog, QObject* parent = nullptr); + ~PreferencesSearchController(); + + // Setup methods + void setPreferencesModel(QStandardItemModel* model); + void setGroupNameRole(int role); + void setPageNameRole(int role); + + // UI access methods + QListWidget* getSearchResultsList() const; + bool isPopupVisible() const; + bool isPopupUnderMouse() const; + bool isPopupAncestorOf(QWidget* widget) const; + + // Event handling + bool handleSearchBoxKeyPress(QKeyEvent* keyEvent); + bool handlePopupKeyPress(QKeyEvent* keyEvent); + bool isClickOutsidePopup(QMouseEvent* mouseEvent); + + // Focus management + void ensureSearchBoxFocus(); + + // Search functionality + void performSearch(const QString& searchText); + void clearHighlights(); + void hideSearchResultsList(); + void showSearchResultsList(); + + // Navigation + void navigateToCurrentSearchResult(bool closePopup); + +Q_SIGNALS: + void navigationRequested(const QString& groupName, const QString& pageName); + +public Q_SLOTS: + void onSearchTextChanged(const QString& text); + void onSearchResultSelected(); + void onSearchResultClicked(); + void onSearchResultDoubleClicked(); + +private: + // Search implementation + void collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + void populateSearchResultsList(); + void createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + + template + void searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName); + + // UI helpers + void configurePopupSize(); + int calculatePopupHeight(int popupWidth); + void applyHighlightToWidget(QWidget* widget); + QString getHighlightStyleForWidget(QWidget* widget); + + // Search result navigation + void selectNextSearchResult(); + void selectPreviousSearchResult(); + + // Utility methods + QString findGroupBoxForWidget(QWidget* widget); + QString formatSearchResultText(const SearchResult& result); + bool fuzzyMatch(const QString& searchText, const QString& targetText, int& score); + bool isExactMatch(const QString& searchText, const QString& targetText); + +private: + DlgPreferencesImp* m_parentDialog; + QStandardItemModel* m_preferencesModel; + int m_groupNameRole; + int m_pageNameRole; + + // UI components + QLineEdit* m_searchBox; + QListWidget* m_searchResultsList; + + // Search state + QList m_searchResults; + QString m_lastSearchText; + QList m_highlightedWidgets; + QMap m_originalStyles; +}; class PreferencesPageItem : public QStandardItem { @@ -136,20 +243,6 @@ class GuiExport DlgPreferencesImp : public QDialog static constexpr int minVerticalEmptySpace = 100; // px of vertical space to leave public: - // Search results navigation - struct SearchResult { - QString groupName; - QString pageName; - QPointer widget; - QString matchText; - QString groupBoxName; - QString tabName; // The tab name (like "Display") - QString pageDisplayName; // The page display name (like "3D View") - QString displayText; - bool isPageLevelMatch = false; // True if this is a page title match - int score = 0; // Fuzzy search score for sorting - }; - static void addPage(const std::string& className, const std::string& group); static void removePage(const std::string& className, const std::string& group); static void setGroupData(const std::string& group, const std::string& icon, const QString& tip); @@ -181,10 +274,7 @@ protected Q_SLOTS: void onGroupExpanded(const QModelIndex &index); void onGroupCollapsed(const QModelIndex &index); - void onSearchTextChanged(const QString& text); - void onSearchResultSelected(); - void onSearchResultClicked(); - void onSearchResultDoubleClicked(); + void onNavigationRequested(const QString& groupName, const QString& pageName); private: /** @name for internal use only */ @@ -213,37 +303,8 @@ private: int minimumDialogWidth(int) const; void expandToMinimumDialogWidth(); - // Search functionality - void performSearch(const QString& searchText); - void clearSearchHighlights(); + // Navigation helper for search controller void navigateToSearchResult(const QString& groupName, const QString& pageName); - void collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName); - - void populateSearchResultsList(); - void hideSearchResultsList(); - void showSearchResultsList(); - void navigateToCurrentSearchResult(bool closePopup); - QString findGroupBoxForWidget(QWidget* widget); - QString formatSearchResultText(const SearchResult& result); - - void createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, - const QString& pageName, const QString& pageDisplayName, const QString& tabName); - template - void searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, - const QString& pageName, const QString& pageDisplayName, const QString& tabName); - int calculatePopupHeight(int popupWidth); - void configurePopupSize(); - - // Fuzzy search helpers (for search box inside preferences)) - bool fuzzyMatch(const QString& searchText, const QString& targetText, int& score); - bool isExactMatch(const QString& searchText, const QString& targetText); - - void ensureSearchBoxFocus(); - void applyHighlightToWidget(QWidget* widget); - QString getHighlightStyleForWidget(QWidget* widget); - bool handleSearchBoxKeyPress(QKeyEvent* keyEvent); - bool handlePopupKeyPress(QKeyEvent* keyEvent); - bool isClickOutsidePopup(QMouseEvent* mouseEvent); //@} private: @@ -265,13 +326,8 @@ private: bool canEmbedScrollArea; bool restartRequired; - // Search state - QList highlightedWidgets; - QMap originalStyles; - - QList searchResults; - QString lastSearchText; - QListWidget* searchResultsList; + // Search controller + std::unique_ptr m_searchController; /**< A name for our Qt::UserRole, used when storing user data in a list item */ static const int GroupNameRole; @@ -281,6 +337,9 @@ private: static constexpr char const* PageNameProperty = "PageName"; static DlgPreferencesImp* _activeDialog; /**< Defaults to the nullptr, points to the current instance if there is one */ + + // Friend class to allow search controller access to UI + friend class PreferencesSearchController; }; } // namespace Gui From 9d12f7050609c4ddc889a41fe6bf2de40990c4d2 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 12:12:33 +0200 Subject: [PATCH 076/126] Core: Use an enum for search bar popup in preferences --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 23 ++++++++++------------- src/Gui/Dialogs/DlgPreferencesImp.h | 11 +++++++++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index ae338a809a..ede3e863b9 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -1258,11 +1258,6 @@ PreferencesSearchController::PreferencesSearchController(DlgPreferencesImp* pare m_searchResultsList->installEventFilter(m_parentDialog); } -PreferencesSearchController::~PreferencesSearchController() -{ - // Destructor - cleanup handled by Qt's object system -} - void PreferencesSearchController::setPreferencesModel(QStandardItemModel* model) { m_preferencesModel = model; @@ -1381,7 +1376,9 @@ void PreferencesSearchController::clearHighlights() void PreferencesSearchController::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) { - if (!widget) return; + if (!widget) { + return; + } const QString lowerSearchText = searchText.toLower(); @@ -1415,7 +1412,7 @@ void PreferencesSearchController::onSearchResultSelected() // This method is called when a search result is selected (arrow keys or single click) // Navigate immediately but keep popup open if (m_searchResultsList && m_searchResultsList->currentItem()) { - navigateToCurrentSearchResult(false); // false = don't close popup + navigateToCurrentSearchResult(PopupAction::KeepOpen); } ensureSearchBoxFocus(); @@ -1425,7 +1422,7 @@ void PreferencesSearchController::onSearchResultClicked() { // Handle single click - navigate immediately but keep popup open if (m_searchResultsList && m_searchResultsList->currentItem()) { - navigateToCurrentSearchResult(false); // false = don't close popup + navigateToCurrentSearchResult(PopupAction::KeepOpen); } ensureSearchBoxFocus(); @@ -1435,11 +1432,11 @@ void PreferencesSearchController::onSearchResultDoubleClicked() { // Handle double click - navigate and close popup if (m_searchResultsList && m_searchResultsList->currentItem()) { - navigateToCurrentSearchResult(true); // true = close popup + navigateToCurrentSearchResult(PopupAction::CloseAfter); } } -void PreferencesSearchController::navigateToCurrentSearchResult(bool closePopup) +void PreferencesSearchController::navigateToCurrentSearchResult(PopupAction action) { QListWidgetItem* currentItem = m_searchResultsList->currentItem(); @@ -1468,7 +1465,7 @@ void PreferencesSearchController::navigateToCurrentSearchResult(bool closePopup) // For page-level matches, we just navigate without highlighting anything // Close popup only if requested (double-click or Enter) - if (closePopup) { + if (action == PopupAction::CloseAfter) { hideSearchResultsList(); } } @@ -1831,7 +1828,7 @@ bool PreferencesSearchController::handleSearchBoxKeyPress(QKeyEvent* keyEvent) } case Qt::Key_Return: case Qt::Key_Enter: - navigateToCurrentSearchResult(true); // true = close popup + navigateToCurrentSearchResult(PopupAction::CloseAfter); return true; case Qt::Key_Escape: hideSearchResultsList(); @@ -1846,7 +1843,7 @@ bool PreferencesSearchController::handlePopupKeyPress(QKeyEvent* keyEvent) switch (keyEvent->key()) { case Qt::Key_Return: case Qt::Key_Enter: - navigateToCurrentSearchResult(true); // true = close popup + navigateToCurrentSearchResult(PopupAction::CloseAfter); return true; case Qt::Key_Escape: hideSearchResultsList(); diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index d67f577fb5..d724879e23 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -49,6 +49,13 @@ class DlgPreferencesImp; class GuiExport PreferencesSearchController : public QObject { Q_OBJECT + +private: + enum class PopupAction { + KeepOpen, // don't close popup (used for keyboard navigation) + CloseAfter // close popup (used for mouse clicks and Enter/Return) + }; + public: // Search results structure struct SearchResult { @@ -65,7 +72,7 @@ public: }; explicit PreferencesSearchController(DlgPreferencesImp* parentDialog, QObject* parent = nullptr); - ~PreferencesSearchController(); + ~PreferencesSearchController() = default; // Setup methods void setPreferencesModel(QStandardItemModel* model); @@ -93,7 +100,7 @@ public: void showSearchResultsList(); // Navigation - void navigateToCurrentSearchResult(bool closePopup); + void navigateToCurrentSearchResult(PopupAction action); Q_SIGNALS: void navigationRequested(const QString& groupName, const QString& pageName); From e9f7c95f0ed02c8b8c49ab447dd6f59b84f7073f Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 13:00:52 +0200 Subject: [PATCH 077/126] Core: Use designated init for SearchResult in preferences --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 60 ++++++++++++--------------- src/Gui/Dialogs/DlgPreferencesImp.h | 2 - 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index ede3e863b9..0b140899d0 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -1385,17 +1385,19 @@ void PreferencesSearchController::collectSearchResults(QWidget* widget, const QS // First, check if the page display name itself matches (highest priority) int pageScore = 0; if (fuzzyMatch(searchText, pageDisplayName, pageScore)) { - SearchResult result; - result.groupName = groupName; - result.pageName = pageName; - result.widget = widget; // Use the page widget itself - result.matchText = pageDisplayName; // Use display name, not internal name - result.groupBoxName = QString(); // No groupbox for page-level match - result.tabName = tabName; - result.pageDisplayName = pageDisplayName; - result.isPageLevelMatch = true; // Mark as page-level match - result.score = pageScore + 2000; // Boost page-level matches - result.displayText = formatSearchResultText(result); + SearchResult result + { + .groupName = groupName, + .pageName = pageName, + .widget = widget, // Use the page widget itself + .matchText = pageDisplayName, // Use display name, not internal name + .groupBoxName = QString(), // No groupbox for page-level match + .tabName = tabName, + .pageDisplayName = pageDisplayName, + .displayText = formatSearchResultText(result), + .isPageLevelMatch = true, // Mark as page-level match + .score = pageScore + 2000 // Boost page-level matches + }; m_searchResults.append(result); // Continue searching for individual items even if page matches } @@ -1543,23 +1545,6 @@ QString PreferencesSearchController::formatSearchResultText(const SearchResult& return text; } -void PreferencesSearchController::createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, - const QString& pageName, const QString& pageDisplayName, const QString& tabName) -{ - SearchResult result; - result.groupName = groupName; - result.pageName = pageName; - result.widget = widget; - result.matchText = matchText; - result.groupBoxName = findGroupBoxForWidget(widget); - result.tabName = tabName; - result.pageDisplayName = pageDisplayName; - result.isPageLevelMatch = false; - result.score = 0; // Will be set by the caller - result.displayText = formatSearchResultText(result); - m_searchResults.append(result); -} - template void PreferencesSearchController::searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) @@ -1583,11 +1568,20 @@ void PreferencesSearchController::searchWidgetType(QWidget* parentWidget, const // Use fuzzy matching instead of simple contains int score = 0; if (fuzzyMatch(searchText, widgetText, score)) { - createSearchResult(widget, widgetText, groupName, pageName, pageDisplayName, tabName); - // Update the score of the last added result - if (!m_searchResults.isEmpty()) { - m_searchResults.last().score = score; - } + SearchResult result + { + .groupName = groupName, + .pageName = pageName, + .widget = widget, + .matchText = widgetText, + .groupBoxName = findGroupBoxForWidget(widget), + .tabName = tabName, + .pageDisplayName = pageDisplayName, + .displayText = formatSearchResultText(result), + .isPageLevelMatch = false, + .score = score + }; + m_searchResults.append(result); } } } diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index d724879e23..56fcd44fa0 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -116,8 +116,6 @@ private: void collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName); void populateSearchResultsList(); - void createSearchResult(QWidget* widget, const QString& matchText, const QString& groupName, - const QString& pageName, const QString& pageDisplayName, const QString& tabName); template void searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, From d2f370aeb61b69dded701936b087b286cdf1430e Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 13:50:41 +0200 Subject: [PATCH 078/126] Core: Use separate roles for found item in font delegate for search --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 82 +++++++++++++++++++-------- src/Gui/Dialogs/DlgPreferencesImp.h | 6 ++ 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index 0b140899d0..ee620ad333 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -87,10 +87,22 @@ public: return; } - QString text = index.data(Qt::DisplayRole).toString(); - QStringList lines = text.split(QLatin1Char('\n')); + // Use separate roles instead of parsing mixed string + QString pathText = index.data(PreferencesSearchController::PathRole).toString(); + QString widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); + + if (pathText.isEmpty()) { + QString text = index.data(Qt::DisplayRole).toString(); + QStringList lines = text.split(QLatin1Char('\n')); + if (!lines.isEmpty()) { + pathText = lines.first(); + if (lines.size() > 1) { + widgetText = lines.at(1); + } + } + } - if (lines.isEmpty()) { + if (pathText.isEmpty()) { QStyledItemDelegate::paint(painter, option, index); return; } @@ -121,25 +133,21 @@ public: int x = option.rect.left() + 12; // +12 horizontal padding int availableWidth = option.rect.width() - 24; // account for left and right padding - // draw first line in bold (Tab/Page) with wrapping + // draw path in bold (Tab/Page) with wrapping painter->setFont(boldFont); - QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.first()); + QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText); QRect boldRect(x, y, availableWidth, boldBoundingRect.height()); - painter->drawText(boldRect, Qt::TextWordWrap | Qt::AlignTop, lines.first()); + painter->drawText(boldRect, Qt::TextWordWrap | Qt::AlignTop, pathText); // move y position after the bold text y += boldBoundingRect.height(); - // draw remaining lines in normal font with wrapping - if (lines.size() > 1) { + // draw widget text in normal font (if present) + if (!widgetText.isEmpty()) { painter->setFont(normalFont); - - for (int i = 1; i < lines.size(); ++i) { - QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.at(i)); - QRect normalRect(x, y, availableWidth, normalBoundingRect.height()); - painter->drawText(normalRect, Qt::TextWordWrap | Qt::AlignTop, lines.at(i)); - y += normalBoundingRect.height(); - } + QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText); + QRect normalRect(x, y, availableWidth, normalBoundingRect.height()); + painter->drawText(normalRect, Qt::TextWordWrap | Qt::AlignTop, widgetText); } painter->restore(); @@ -151,10 +159,23 @@ public: return QStyledItemDelegate::sizeHint(option, index); } - QString text = index.data(Qt::DisplayRole).toString(); - QStringList lines = text.split(QLatin1Char('\n')); + // Use separate roles instead of parsing mixed string + QString pathText = index.data(PreferencesSearchController::PathRole).toString(); + QString widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); - if (lines.isEmpty()) { + // Fallback to old method if roles are empty (for compatibility) + if (pathText.isEmpty()) { + QString text = index.data(Qt::DisplayRole).toString(); + QStringList lines = text.split(QLatin1Char('\n')); + if (!lines.isEmpty()) { + pathText = lines.first(); + if (lines.size() > 1) { + widgetText = lines.at(1); + } + } + } + + if (pathText.isEmpty()) { return QStyledItemDelegate::sizeHint(option, index); } @@ -174,14 +195,14 @@ public: int width = 0; int height = 8; // Start with 8 vertical padding (4 top + 4 bottom) - // Calculate height for first line (bold) with wrapping - QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.first()); + // Calculate height for path text (bold) with wrapping + QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText); height += boldBoundingRect.height(); width = qMax(width, boldBoundingRect.width() + 24); // +24 horizontal padding - // Calculate height for remaining lines (normal font) with wrapping - for (int i = 1; i < lines.size(); ++i) { - QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, lines.at(i)); + // Calculate height for widget text (normal font) with wrapping (if present) + if (!widgetText.isEmpty()) { + QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText); height += normalBoundingRect.height(); width = qMax(width, normalBoundingRect.width() + 24); } @@ -1374,7 +1395,8 @@ void PreferencesSearchController::clearHighlights() m_originalStyles.clear(); } -void PreferencesSearchController::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, const QString& pageName, const QString& pageDisplayName, const QString& tabName) +void PreferencesSearchController::collectSearchResults(QWidget* widget, const QString& searchText, const QString& groupName, + const QString& pageName, const QString& pageDisplayName, const QString& tabName) { if (!widget) { return; @@ -1479,8 +1501,18 @@ void PreferencesSearchController::populateSearchResultsList() for (int i = 0; i < m_searchResults.size(); ++i) { const SearchResult& result = m_searchResults.at(i); + + // Create path string + QString pathText = result.tabName + QStringLiteral("/") + result.pageDisplayName; + + // Create item - keep displayText for fallback compatibility QListWidgetItem* item = new QListWidgetItem(result.displayText); - item->setData(Qt::UserRole, i); // Store the index instead of pointer + + // Store path and widget text in separate roles + item->setData(PathRole, pathText); + item->setData(WidgetTextRole, result.matchText); + item->setData(Qt::UserRole, i); // Keep existing index storage + m_searchResultsList->addItem(item); } diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index 56fcd44fa0..dcedfee0f9 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -57,6 +57,12 @@ private: }; public: + // Custom data roles for separating path and widget text + enum SearchDataRole { + PathRole = Qt::UserRole + 10, // Path to page (e.g., "Display/3D View") + WidgetTextRole = Qt::UserRole + 11 // Text from the widget (e.g., "Enable anti-aliasing") + }; + // Search results structure struct SearchResult { QString groupName; From 5daaa8edea243dc22b48e734f1fe320b2b9d0d25 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 14:15:26 +0200 Subject: [PATCH 079/126] Core: Move reusable parts of MixedFontDelegate to separate functions Co-Authored-By: Kacper Donat --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 138 +++++++++++++++----------- 1 file changed, 80 insertions(+), 58 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index ee620ad333..b952ce9ce0 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -77,6 +77,9 @@ using namespace Gui::Dialog; // used by search box class MixedFontDelegate : public QStyledItemDelegate { + static constexpr int horizontalPadding = 12; + static constexpr int verticalPadding = 4; + public: explicit MixedFontDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} @@ -87,26 +90,19 @@ public: return; } - // Use separate roles instead of parsing mixed string - QString pathText = index.data(PreferencesSearchController::PathRole).toString(); - QString widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); - - if (pathText.isEmpty()) { - QString text = index.data(Qt::DisplayRole).toString(); - QStringList lines = text.split(QLatin1Char('\n')); - if (!lines.isEmpty()) { - pathText = lines.first(); - if (lines.size() > 1) { - widgetText = lines.at(1); - } - } - } + QString pathText, widgetText; + extractTextData(index, pathText, widgetText); if (pathText.isEmpty()) { QStyledItemDelegate::paint(painter, option, index); return; } + QFont boldFont, normalFont; + createFonts(option.font, boldFont, normalFont); + + LayoutInfo layout = calculateLayout(pathText, widgetText, boldFont, normalFont, option.rect.width()); + painter->save(); // draw selection background if selected @@ -120,33 +116,19 @@ public: : option.palette.text().color(); painter->setPen(textColor); - // Set up fonts - QFont boldFont = option.font; - boldFont.setBold(true); - QFont normalFont = option.font; - normalFont.setPointSize(normalFont.pointSize() + 2); // make lower text 2 pixels bigger - - QFontMetrics boldFm(boldFont); - QFontMetrics normalFm(normalFont); - - int y = option.rect.top() + 4; // start 4px from top - int x = option.rect.left() + 12; // +12 horizontal padding - int availableWidth = option.rect.width() - 24; // account for left and right padding - // draw path in bold (Tab/Page) with wrapping painter->setFont(boldFont); - QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText); - QRect boldRect(x, y, availableWidth, boldBoundingRect.height()); + QRect boldRect(option.rect.left() + horizontalPadding, option.rect.top() + verticalPadding, + layout.availableWidth, layout.pathHeight); painter->drawText(boldRect, Qt::TextWordWrap | Qt::AlignTop, pathText); - // move y position after the bold text - y += boldBoundingRect.height(); - // draw widget text in normal font (if present) if (!widgetText.isEmpty()) { painter->setFont(normalFont); - QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText); - QRect normalRect(x, y, availableWidth, normalBoundingRect.height()); + QRect normalRect(option.rect.left() + horizontalPadding, + option.rect.top() + verticalPadding + layout.pathHeight, + layout.availableWidth, + layout.widgetHeight); painter->drawText(normalRect, Qt::TextWordWrap | Qt::AlignTop, widgetText); } @@ -158,11 +140,37 @@ public: if (!index.isValid()) { return QStyledItemDelegate::sizeHint(option, index); } + + QString pathText, widgetText; + extractTextData(index, pathText, widgetText); + if (pathText.isEmpty()) { + return QStyledItemDelegate::sizeHint(option, index); + } + + QFont boldFont, normalFont; + createFonts(option.font, boldFont, normalFont); + + LayoutInfo layout = calculateLayout(pathText, widgetText, boldFont, normalFont, option.rect.width()); + + return {layout.totalWidth, layout.totalHeight}; + } + +private: + struct LayoutInfo { + int availableWidth; + int pathHeight; + int widgetHeight; + int totalWidth; + int totalHeight; + }; + + void extractTextData(const QModelIndex& index, QString& pathText, QString& widgetText) const + { // Use separate roles instead of parsing mixed string - QString pathText = index.data(PreferencesSearchController::PathRole).toString(); - QString widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); - + pathText = index.data(PreferencesSearchController::PathRole).toString(); + widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); + // Fallback to old method if roles are empty (for compatibility) if (pathText.isEmpty()) { QString text = index.data(Qt::DisplayRole).toString(); @@ -174,40 +182,54 @@ public: } } } + } - if (pathText.isEmpty()) { - return QStyledItemDelegate::sizeHint(option, index); - } - - QFont boldFont = option.font; + void createFonts(const QFont& baseFont, QFont& boldFont, QFont& normalFont) const + { + boldFont = baseFont; boldFont.setBold(true); - QFont normalFont = option.font; - normalFont.setPointSize(normalFont.pointSize() + 2); // Make lower text 2 pixels bigger to match paint method + normalFont = baseFont; + normalFont.setPointSize(normalFont.pointSize() + 2); // make lower text 2 pixels bigger + } + + LayoutInfo calculateLayout(const QString& pathText, const QString& widgetText, + const QFont& boldFont, const QFont& normalFont, int containerWidth) const + { + QFontMetrics boldFm(boldFont); QFontMetrics normalFm(normalFont); - int availableWidth = option.rect.width() - 24; // Account for left and right padding + int availableWidth = containerWidth - horizontalPadding * 2; // account for left and right padding if (availableWidth <= 0) { - availableWidth = 300 - 24; // Fallback to popup width minus padding + constexpr int defaultPopupWidth = 300; + availableWidth = defaultPopupWidth - horizontalPadding * 2; // Fallback to popup width minus padding } - int width = 0; - int height = 8; // Start with 8 vertical padding (4 top + 4 bottom) + // Calculate dimensions for path text (bold) + QRect pathBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText); + int pathHeight = pathBoundingRect.height(); + int pathWidth = pathBoundingRect.width(); - // Calculate height for path text (bold) with wrapping - QRect boldBoundingRect = boldFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, pathText); - height += boldBoundingRect.height(); - width = qMax(width, boldBoundingRect.width() + 24); // +24 horizontal padding - - // Calculate height for widget text (normal font) with wrapping (if present) + // Calculate dimensions for widget text (normal font, if present) + int widgetHeight = 0; + int widgetWidth = 0; if (!widgetText.isEmpty()) { - QRect normalBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText); - height += normalBoundingRect.height(); - width = qMax(width, normalBoundingRect.width() + 24); + QRect widgetBoundingRect = normalFm.boundingRect(QRect(0, 0, availableWidth, 0), Qt::TextWordWrap, widgetText); + widgetHeight = widgetBoundingRect.height(); + widgetWidth = widgetBoundingRect.width(); } - return QSize(width, height); + int totalWidth = qMax(pathWidth, widgetWidth) + horizontalPadding * 2; // +24 horizontal padding + int totalHeight = verticalPadding * 2 + pathHeight + widgetHeight; // 8 vertical padding + content heights + + LayoutInfo layout; + layout.availableWidth = availableWidth; + layout.pathHeight = pathHeight; + layout.widgetHeight = widgetHeight; + layout.totalWidth = totalWidth; + layout.totalHeight = totalHeight; + return layout; } }; From f4785d6a8f80c1acbd7bc1c1d9028063cadc29f9 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 15:03:20 +0200 Subject: [PATCH 080/126] Core: Remove displayText field from search box's result Removes displayText from the searchboxes result, as it's being handled differently and there are two other fields that store this previously concatenated information separately. --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 48 ++++++++------------------- src/Gui/Dialogs/DlgPreferencesImp.h | 2 -- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index b952ce9ce0..3214c1aa6a 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -167,21 +167,9 @@ private: void extractTextData(const QModelIndex& index, QString& pathText, QString& widgetText) const { - // Use separate roles instead of parsing mixed string + // Use separate roles - all items should have proper role data pathText = index.data(PreferencesSearchController::PathRole).toString(); widgetText = index.data(PreferencesSearchController::WidgetTextRole).toString(); - - // Fallback to old method if roles are empty (for compatibility) - if (pathText.isEmpty()) { - QString text = index.data(Qt::DisplayRole).toString(); - QStringList lines = text.split(QLatin1Char('\n')); - if (!lines.isEmpty()) { - pathText = lines.first(); - if (lines.size() > 1) { - widgetText = lines.at(1); - } - } - } } void createFonts(const QFont& baseFont, QFont& boldFont, QFont& normalFont) const @@ -1438,7 +1426,6 @@ void PreferencesSearchController::collectSearchResults(QWidget* widget, const QS .groupBoxName = QString(), // No groupbox for page-level match .tabName = tabName, .pageDisplayName = pageDisplayName, - .displayText = formatSearchResultText(result), .isPageLevelMatch = true, // Mark as page-level match .score = pageScore + 2000 // Boost page-level matches }; @@ -1524,15 +1511,20 @@ void PreferencesSearchController::populateSearchResultsList() for (int i = 0; i < m_searchResults.size(); ++i) { const SearchResult& result = m_searchResults.at(i); - // Create path string - QString pathText = result.tabName + QStringLiteral("/") + result.pageDisplayName; - - // Create item - keep displayText for fallback compatibility - QListWidgetItem* item = new QListWidgetItem(result.displayText); + // Create item without setting DisplayRole + QListWidgetItem* item = new QListWidgetItem(); // Store path and widget text in separate roles - item->setData(PathRole, pathText); - item->setData(WidgetTextRole, result.matchText); + if (result.isPageLevelMatch) { + // For page matches: parent group as header, page name as content + item->setData(PathRole, result.tabName); + item->setData(WidgetTextRole, result.pageDisplayName); + } else { + // For widget matches: full path as header, widget text as content + QString pathText = result.tabName + QStringLiteral("/") + result.pageDisplayName; + item->setData(PathRole, pathText); + item->setData(WidgetTextRole, result.matchText); + } item->setData(Qt::UserRole, i); // Keep existing index storage m_searchResultsList->addItem(item); @@ -1586,18 +1578,7 @@ QString PreferencesSearchController::findGroupBoxForWidget(QWidget* widget) return QString(); } -QString PreferencesSearchController::formatSearchResultText(const SearchResult& result) -{ - // Format for MixedFontDelegate: First line will be bold, subsequent lines normal - QString text = result.tabName + QStringLiteral("/") + result.pageDisplayName; - - if (!result.isPageLevelMatch) { - // Add the actual finding on the second line - text += QStringLiteral("\n") + result.matchText; - } - - return text; -} + template void PreferencesSearchController::searchWidgetType(QWidget* parentWidget, const QString& searchText, const QString& groupName, @@ -1631,7 +1612,6 @@ void PreferencesSearchController::searchWidgetType(QWidget* parentWidget, const .groupBoxName = findGroupBoxForWidget(widget), .tabName = tabName, .pageDisplayName = pageDisplayName, - .displayText = formatSearchResultText(result), .isPageLevelMatch = false, .score = score }; diff --git a/src/Gui/Dialogs/DlgPreferencesImp.h b/src/Gui/Dialogs/DlgPreferencesImp.h index dcedfee0f9..57c9d367ed 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.h +++ b/src/Gui/Dialogs/DlgPreferencesImp.h @@ -72,7 +72,6 @@ public: QString groupBoxName; QString tabName; // The tab name (like "Display") QString pageDisplayName; // The page display name (like "3D View") - QString displayText; bool isPageLevelMatch = false; // True if this is a page title match int score = 0; // Fuzzy search score for sorting }; @@ -139,7 +138,6 @@ private: // Utility methods QString findGroupBoxForWidget(QWidget* widget); - QString formatSearchResultText(const SearchResult& result); bool fuzzyMatch(const QString& searchText, const QString& targetText, int& score); bool isExactMatch(const QString& searchText, const QString& targetText); From b5b86d5c517b96d687ad80d956f4520fbe3da3bf Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 15:13:43 +0200 Subject: [PATCH 081/126] Core: Correct font sizes to be smaller in search box in preferences --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index 3214c1aa6a..7e1b6fb84b 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -176,9 +176,9 @@ private: { boldFont = baseFont; boldFont.setBold(true); + boldFont.setPointSize(boldFont.pointSize() - 1); // make header smaller like a subtitle - normalFont = baseFont; - normalFont.setPointSize(normalFont.pointSize() + 2); // make lower text 2 pixels bigger + normalFont = baseFont; // keep widget text at normal size } LayoutInfo calculateLayout(const QString& pathText, const QString& widgetText, @@ -1812,7 +1812,9 @@ QString PreferencesSearchController::getHighlightStyleForWidget(QWidget* widget) void PreferencesSearchController::applyHighlightToWidget(QWidget* widget) { - if (!widget) return; + if (!widget) { + return; + } m_originalStyles[widget] = widget->styleSheet(); widget->setStyleSheet(getHighlightStyleForWidget(widget)); From 581f492660f9fad82d39e2482251164dfb2749cd Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 15:34:52 +0200 Subject: [PATCH 082/126] Core: Handle globalPos for both Qt6 and Qt5 --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index 7e1b6fb84b..08e8b476b1 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -1886,11 +1886,15 @@ bool PreferencesSearchController::handlePopupKeyPress(QKeyEvent* keyEvent) bool PreferencesSearchController::isClickOutsidePopup(QMouseEvent* mouseEvent) { - QPointF globalPos = mouseEvent->globalPosition(); +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) + QPoint globalPos = mouseEvent->globalPos(); +#else + QPoint globalPos = mouseEvent->globalPosition().toPoint(); +#endif QRect searchBoxRect = QRect(m_searchBox->mapToGlobal(QPoint(0, 0)), m_searchBox->size()); QRect popupRect = QRect(m_searchResultsList->mapToGlobal(QPoint(0, 0)), m_searchResultsList->size()); - return !searchBoxRect.contains(globalPos.x(), globalPos.y()) && !popupRect.contains(globalPos.x(), globalPos.y()); + return !searchBoxRect.contains(globalPos) && !popupRect.contains(globalPos); } bool DlgPreferencesImp::eventFilter(QObject* obj, QEvent* event) From 0e21764c42225b424034d6867f15cfceb7b18eff Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sun, 22 Jun 2025 15:52:40 +0200 Subject: [PATCH 083/126] Core: Use bypass WM hint for X11 for search list in preferences --- src/Gui/Dialogs/DlgPreferencesImp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/Dialogs/DlgPreferencesImp.cpp b/src/Gui/Dialogs/DlgPreferencesImp.cpp index 08e8b476b1..75d9b1289d 100644 --- a/src/Gui/Dialogs/DlgPreferencesImp.cpp +++ b/src/Gui/Dialogs/DlgPreferencesImp.cpp @@ -1254,7 +1254,7 @@ PreferencesSearchController::PreferencesSearchController(DlgPreferencesImp* pare // Create the search results popup list m_searchResultsList = new QListWidget(m_parentDialog); - m_searchResultsList->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + m_searchResultsList->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); m_searchResultsList->setVisible(false); m_searchResultsList->setMinimumWidth(300); m_searchResultsList->setMaximumHeight(400); // Increased max height From b9b57f8c02985c7fa3631d5cca0067744461f0ca Mon Sep 17 00:00:00 2001 From: matthiasdanner Date: Mon, 23 Jun 2025 02:54:39 +0200 Subject: [PATCH 084/126] Sketcher: 3 Point Symmetry fixed if root is selected first (and simplify and fix the selection logic) (#21612) --- src/Mod/Sketcher/Gui/CommandConstraints.cpp | 152 ++++++++------------ 1 file changed, 62 insertions(+), 90 deletions(-) diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.cpp b/src/Mod/Sketcher/Gui/CommandConstraints.cpp index 664cf85191..3619111a05 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.cpp +++ b/src/Mod/Sketcher/Gui/CommandConstraints.cpp @@ -892,12 +892,12 @@ enum SelType { SelUnknown = 0, SelVertex = 1, - SelVertexOrRoot = 64, SelRoot = 2, + SelVertexOrRoot = SelVertex | SelRoot, SelEdge = 4, - SelEdgeOrAxis = 128, SelHAxis = 8, SelVAxis = 16, + SelEdgeOrAxis = SelEdge | SelHAxis | SelVAxis, SelExternalEdge = 32 }; @@ -928,11 +928,11 @@ public: return false; } std::string element(sSubName); - if ((allowedSelTypes & (SelRoot | SelVertexOrRoot) && element.substr(0, 9) == "RootPoint") - || (allowedSelTypes & (SelVertex | SelVertexOrRoot) && element.substr(0, 6) == "Vertex") - || (allowedSelTypes & (SelEdge | SelEdgeOrAxis) && element.substr(0, 4) == "Edge") - || (allowedSelTypes & (SelHAxis | SelEdgeOrAxis) && element.substr(0, 6) == "H_Axis") - || (allowedSelTypes & (SelVAxis | SelEdgeOrAxis) && element.substr(0, 6) == "V_Axis") + if ((allowedSelTypes & SelRoot && element.substr(0, 9) == "RootPoint") + || (allowedSelTypes & SelVertex && element.substr(0, 6) == "Vertex") + || (allowedSelTypes & SelEdge && element.substr(0, 4) == "Edge") + || (allowedSelTypes & SelHAxis && element.substr(0, 6) == "H_Axis") + || (allowedSelTypes & SelVAxis && element.substr(0, 6) == "V_Axis") || (allowedSelTypes & SelExternalEdge && element.substr(0, 12) == "ExternalEdge")) { return true; } @@ -982,10 +982,6 @@ protected: * generate sequences to be passed to applyConstraint(). * Whenever any sequence is completed, applyConstraint() is called, so it's * best to keep them prefix-free. - * Be mindful that when SelVertex and SelRoot are given preference over - * SelVertexOrRoot, and similar for edges/axes. Thus if a vertex is selected - * when SelVertex and SelVertexOrRoot are both applicable, only sequences with - * SelVertex will be continue. * * TODO: Introduce structs to allow keeping first selection */ @@ -1032,30 +1028,30 @@ public: int VtId = getPreselectPoint(); int CrvId = getPreselectCurve(); int CrsId = getPreselectCross(); - if (allowedSelTypes & (SelRoot | SelVertexOrRoot) && CrsId == 0) { + if (allowedSelTypes & SelRoot && CrsId == 0) { selIdPair.GeoId = Sketcher::GeoEnum::RtPnt; selIdPair.PosId = Sketcher::PointPos::start; - newSelType = (allowedSelTypes & SelRoot) ? SelRoot : SelVertexOrRoot; + newSelType = SelRoot; ss << "RootPoint"; } - else if (allowedSelTypes & (SelVertex | SelVertexOrRoot) && VtId >= 0) { + else if (allowedSelTypes & SelVertex && VtId >= 0) { sketchgui->getSketchObject()->getGeoVertexIndex(VtId, selIdPair.GeoId, selIdPair.PosId); - newSelType = (allowedSelTypes & SelVertex) ? SelVertex : SelVertexOrRoot; + newSelType = SelVertex; ss << "Vertex" << VtId + 1; } - else if (allowedSelTypes & (SelEdge | SelEdgeOrAxis) && CrvId >= 0) { + else if (allowedSelTypes & SelEdge && CrvId >= 0) { selIdPair.GeoId = CrvId; - newSelType = (allowedSelTypes & SelEdge) ? SelEdge : SelEdgeOrAxis; + newSelType = SelEdge; ss << "Edge" << CrvId + 1; } - else if (allowedSelTypes & (SelHAxis | SelEdgeOrAxis) && CrsId == 1) { + else if (allowedSelTypes & SelHAxis && CrsId == 1) { selIdPair.GeoId = Sketcher::GeoEnum::HAxis; - newSelType = (allowedSelTypes & SelHAxis) ? SelHAxis : SelEdgeOrAxis; + newSelType = SelHAxis; ss << "H_Axis"; } - else if (allowedSelTypes & (SelVAxis | SelEdgeOrAxis) && CrsId == 2) { + else if (allowedSelTypes & SelVAxis && CrsId == 2) { selIdPair.GeoId = Sketcher::GeoEnum::VAxis; - newSelType = (allowedSelTypes & SelVAxis) ? SelVAxis : SelEdgeOrAxis; + newSelType = SelVAxis; ss << "V_Axis"; } else if (allowedSelTypes & SelExternalEdge && CrvId <= Sketcher::GeoEnum::RefExt) { @@ -1085,7 +1081,7 @@ public: for (std::set::iterator token = ongoingSequences.begin(); token != ongoingSequences.end(); ++token) { - if ((cmd->allowedSelSequences).at(*token).at(seqIndex) == newSelType) { + if ((cmd->allowedSelSequences).at(*token).at(seqIndex) & newSelType) { if (seqIndex == (cmd->allowedSelSequences).at(*token).size() - 1) { // One of the sequences is completed. Pass to cmd->applyConstraint cmd->applyConstraint(selSeq, *token);// replace arg 2 by ongoingToken @@ -3154,7 +3150,7 @@ CmdSketcherConstrainHorVer::CmdSketcherConstrainHorVer() sAccel = "A"; eType = ForEdit; - allowedSelSequences = { {SelEdge}, {SelVertex, SelVertexOrRoot}, {SelRoot, SelVertex} }; + allowedSelSequences = { {SelEdge}, {SelVertexOrRoot, SelVertexOrRoot} }; } void CmdSketcherConstrainHorVer::activated(int iMsg) @@ -3200,7 +3196,7 @@ CmdSketcherConstrainHorizontal::CmdSketcherConstrainHorizontal() sAccel = "H"; eType = ForEdit; - allowedSelSequences = {{SelEdge}, {SelVertex, SelVertexOrRoot}, {SelRoot, SelVertex}}; + allowedSelSequences = {{SelEdge}, {SelVertexOrRoot, SelVertexOrRoot}}; } void CmdSketcherConstrainHorizontal::activated(int iMsg) @@ -3245,7 +3241,7 @@ CmdSketcherConstrainVertical::CmdSketcherConstrainVertical() sAccel = "V"; eType = ForEdit; - allowedSelSequences = {{SelEdge}, {SelVertex, SelVertexOrRoot}, {SelRoot, SelVertex}}; + allowedSelSequences = {{SelEdge}, {SelVertexOrRoot, SelVertexOrRoot}}; } void CmdSketcherConstrainVertical::activated(int iMsg) @@ -3783,15 +3779,14 @@ CmdSketcherConstrainCoincidentUnified::CmdSketcherConstrainCoincidentUnified(con eType = ForEdit; - allowedSelSequences = { {SelVertex, SelEdgeOrAxis}, + allowedSelSequences = {{SelVertex, SelEdgeOrAxis}, {SelRoot, SelEdge}, {SelVertex, SelExternalEdge}, {SelEdge, SelVertexOrRoot}, {SelEdgeOrAxis, SelVertex}, {SelExternalEdge, SelVertex}, - {SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + {SelVertexOrRoot, SelVertexOrRoot}, {SelEdge, SelEdge}, {SelEdge, SelExternalEdge}, {SelExternalEdge, SelEdge} }; @@ -4112,11 +4107,10 @@ void CmdSketcherConstrainCoincidentUnified::applyConstraint(std::vectorgetGeometry(GeoId1)) || !isGeoConcentricCompatible(Obj->getGeometry(GeoId2))) { @@ -4297,8 +4290,7 @@ CmdSketcherConstrainCoincident::CmdSketcherConstrainCoincident() sAccel = hGrp->GetBool("UnifiedCoincident", true) ? "C,C" : "C"; eType = ForEdit; - allowedSelSequences = {{SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge, SelEdge}, {SelEdge, SelExternalEdge}, {SelExternalEdge, SelEdge}}; @@ -4399,15 +4391,13 @@ CmdSketcherConstrainDistance::CmdSketcherConstrainDistance() sAccel = "K, D"; eType = ForEdit; - allowedSelSequences = { {SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge}, {SelExternalEdge}, {SelVertex, SelEdgeOrAxis}, {SelRoot, SelEdge}, - {SelVertex, SelExternalEdge}, - {SelRoot, SelExternalEdge}, - {SelEdge, SelEdge} }; + {SelVertexOrRoot, SelExternalEdge}, + {SelEdge, SelEdge}}; } void CmdSketcherConstrainDistance::activated(int iMsg) @@ -4783,8 +4773,7 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe bool arebothpointsorsegmentsfixed = areBothPointsOrSegmentsFixed(Obj, GeoId1, GeoId2); switch (seqIndex) { - case 0:// {SelVertex, SelVertexOrRoot} - case 1:// {SelRoot, SelVertex} + case 0:// {SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -4848,8 +4837,8 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe return; } - case 2:// {SelEdge} - case 3:// {SelExternalEdge} + case 1:// {SelEdge} + case 2:// {SelExternalEdge} { GeoId1 = selSeq.at(0).GeoId; @@ -4891,10 +4880,9 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe return; } - case 4:// {SelVertex, SelEdgeOrAxis} - case 5:// {SelRoot, SelEdge} - case 6:// {SelVertex, SelExternalEdge} - case 7:// {SelRoot, SelExternalEdge} + case 3:// {SelVertex, SelEdgeOrAxis} + case 4:// {SelRoot, SelEdge} + case 5:// {SelVertexOrRoot, SelExternalEdge} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -4934,7 +4922,7 @@ void CmdSketcherConstrainDistance::applyConstraint(std::vector& selSe return; } - case 8:// {SelEdge, SelEdge} + case 6:// {SelEdge, SelEdge} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -5052,8 +5040,7 @@ CmdSketcherConstrainDistanceX::CmdSketcherConstrainDistanceX() eType = ForEdit; // Can't do single vertex because its a prefix for 2 vertices - allowedSelSequences = {{SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge}, {SelExternalEdge}}; } @@ -5237,8 +5224,7 @@ void CmdSketcherConstrainDistanceX::applyConstraint(std::vector& selS Sketcher::PointPos PosId1 = Sketcher::PointPos::none, PosId2 = Sketcher::PointPos::none; switch (seqIndex) { - case 0:// {SelVertex, SelVertexOrRoot} - case 1:// {SelRoot, SelVertex} + case 0:// {SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -5246,8 +5232,8 @@ void CmdSketcherConstrainDistanceX::applyConstraint(std::vector& selS PosId2 = selSeq.at(1).PosId; break; } - case 2:// {SelEdge} - case 3:// {SelExternalEdge} + case 1:// {SelEdge} + case 2:// {SelExternalEdge} { GeoId1 = GeoId2 = selSeq.at(0).GeoId; PosId1 = Sketcher::PointPos::start; @@ -5355,8 +5341,7 @@ CmdSketcherConstrainDistanceY::CmdSketcherConstrainDistanceY() eType = ForEdit; // Can't do single vertex because its a prefix for 2 vertices - allowedSelSequences = {{SelVertex, SelVertexOrRoot}, - {SelRoot, SelVertex}, + allowedSelSequences = {{SelVertexOrRoot, SelVertexOrRoot}, {SelEdge}, {SelExternalEdge}}; } @@ -5536,8 +5521,7 @@ void CmdSketcherConstrainDistanceY::applyConstraint(std::vector& selS Sketcher::PointPos PosId1 = Sketcher::PointPos::none, PosId2 = Sketcher::PointPos::none; switch (seqIndex) { - case 0:// {SelVertex, SelVertexOrRoot} - case 1:// {SelRoot, SelVertex} + case 0:// {SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; @@ -5545,8 +5529,8 @@ void CmdSketcherConstrainDistanceY::applyConstraint(std::vector& selS PosId2 = selSeq.at(1).PosId; break; } - case 2:// {SelEdge} - case 3:// {SelExternalEdge} + case 1:// {SelEdge} + case 2:// {SelExternalEdge} { GeoId1 = GeoId2 = selSeq.at(0).GeoId; PosId1 = Sketcher::PointPos::start; @@ -9364,19 +9348,13 @@ CmdSketcherConstrainSymmetric::CmdSketcherConstrainSymmetric() allowedSelSequences = {{SelEdge, SelVertexOrRoot}, {SelExternalEdge, SelVertex}, - {SelVertex, SelEdge, SelVertexOrRoot}, - {SelRoot, SelEdge, SelVertex}, - {SelVertex, SelExternalEdge, SelVertexOrRoot}, - {SelRoot, SelExternalEdge, SelVertex}, + {SelVertexOrRoot, SelEdge, SelVertexOrRoot}, + {SelVertexOrRoot, SelExternalEdge, SelVertexOrRoot}, {SelVertex, SelEdgeOrAxis, SelVertex}, - {SelVertex, SelVertexOrRoot, SelEdge}, - {SelRoot, SelVertex, SelEdge}, - {SelVertex, SelVertexOrRoot, SelExternalEdge}, - {SelRoot, SelVertex, SelExternalEdge}, + {SelVertexOrRoot, SelVertexOrRoot, SelEdge}, + {SelVertexOrRoot, SelVertexOrRoot, SelExternalEdge}, {SelVertex, SelVertex, SelEdgeOrAxis}, - {SelVertex, SelVertexOrRoot, SelVertex}, - {SelVertex, SelVertex, SelVertexOrRoot}, - {SelVertexOrRoot, SelVertex, SelVertex}}; + {SelVertexOrRoot, SelVertexOrRoot, SelVertexOrRoot}}; } void CmdSketcherConstrainSymmetric::activated(int iMsg) @@ -9590,16 +9568,12 @@ void CmdSketcherConstrainSymmetric::applyConstraint(std::vector& selS } break; } - case 2: // {SelVertex, SelEdge, SelVertexOrRoot} - case 3: // {SelRoot, SelEdge, SelVertex} - case 4: // {SelVertex, SelExternalEdge, SelVertexOrRoot} - case 5: // {SelRoot, SelExternalEdge, SelVertex} - case 6: // {SelVertex, SelEdgeOrAxis, SelVertex} - case 7: // {SelVertex, SelVertexOrRoot,SelEdge} - case 8: // {SelRoot, SelVertex, SelEdge} - case 9: // {SelVertex, SelVertexOrRoot, SelExternalEdge} - case 10:// {SelRoot, SelVertex, SelExternalEdge} - case 11:// {SelVertex, SelVertex, SelEdgeOrAxis} + case 2:// {SelVertexOrRoot, SelEdge, SelVertexOrRoot} + case 3:// {SelVertexOrRoot, SelExternalEdge, SelVertexOrRoot} + case 4:// {SelVertex, SelEdgeOrAxis, SelVertex} + case 5:// {SelVertexOrRoot, SelVertexOrRoot, SelEdge} + case 6:// {SelVertexOrRoot, SelVertexOrRoot, SelExternalEdge} + case 7:// {SelVertex, SelVertex, SelEdgeOrAxis} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(2).GeoId; @@ -9658,9 +9632,7 @@ void CmdSketcherConstrainSymmetric::applyConstraint(std::vector& selS } return; } - case 12:// {SelVertex, SelVertexOrRoot, SelVertex} - case 13:// {SelVertex, SelVertex, SelVertexOrRoot} - case 14:// {SelVertexOrRoot, SelVertex, SelVertex} + case 8:// {SelVertexOrRoot, SelVertexOrRoot, SelVertexOrRoot} { GeoId1 = selSeq.at(0).GeoId; GeoId2 = selSeq.at(1).GeoId; From 0a9a16ffe34ddf725bbfcf25670e13577c26062e Mon Sep 17 00:00:00 2001 From: xtemp09 Date: Mon, 23 Jun 2025 09:24:51 +0700 Subject: [PATCH 085/126] [GUI] Remove dark fringe around letters (#21536) Closes #12394 Co-authored-by: Kacper Donat --- src/Gui/SoDatumLabel.cpp | 6 ++++-- src/Gui/SoDatumLabel.h | 1 + src/Mod/AddonManager | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Gui/SoDatumLabel.cpp b/src/Gui/SoDatumLabel.cpp index d80be8fb71..b946d96b1a 100644 --- a/src/Gui/SoDatumLabel.cpp +++ b/src/Gui/SoDatumLabel.cpp @@ -134,6 +134,7 @@ SoDatumLabel::SoDatumLabel() SO_NODE_ADD_FIELD(name, ("Helvetica")); SO_NODE_ADD_FIELD(size, (10.F)); SO_NODE_ADD_FIELD(lineWidth, (2.F)); + SO_NODE_ADD_FIELD(sampling, (2.F)); SO_NODE_ADD_FIELD(datumtype, (SoDatumLabel::DISTANCE)); @@ -187,7 +188,8 @@ void SoDatumLabel::drawImage() QColor front; front.setRgbF(t[0],t[1], t[2]); - QImage image(w, h,QImage::Format_ARGB32_Premultiplied); + QImage image(w * sampling.getValue(), h * sampling.getValue(), QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(sampling.getValue()); image.fill(0x00000000); QPainter painter(&image); @@ -1165,7 +1167,7 @@ void SoDatumLabel::getDimension(float scale, int& srcw, int& srch) srch = imgsize[1]; float aspectRatio = (float) srcw / (float) srch; - this->imgHeight = scale * (float) (srch); + this->imgHeight = scale * (float) (srch) / sampling.getValue(); this->imgWidth = aspectRatio * (float) this->imgHeight; } diff --git a/src/Gui/SoDatumLabel.h b/src/Gui/SoDatumLabel.h index 70aa8c8b96..70dcc7fa90 100644 --- a/src/Gui/SoDatumLabel.h +++ b/src/Gui/SoDatumLabel.h @@ -85,6 +85,7 @@ public: SoSFVec3f norm; SoSFImage image; SoSFFloat lineWidth; + SoSFFloat sampling; bool useAntialiasing; protected: diff --git a/src/Mod/AddonManager b/src/Mod/AddonManager index 69a6e0dc7b..34d433a02c 160000 --- a/src/Mod/AddonManager +++ b/src/Mod/AddonManager @@ -1 +1 @@ -Subproject commit 69a6e0dc7b8f5fe17547f4d1234df1617b78c45e +Subproject commit 34d433a02c7ec5c73bec9c57d0a27ea70b36c90d From 7a639467629f76e395961bb6ade1e988ce379628 Mon Sep 17 00:00:00 2001 From: Bas Ruigrok Date: Thu, 19 Jun 2025 16:38:09 +0200 Subject: [PATCH 086/126] Part: Align to planar curves normal direction --- src/Mod/Part/App/PartFeature.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Mod/Part/App/PartFeature.cpp b/src/Mod/Part/App/PartFeature.cpp index fbaa479fe7..5ec7a459fa 100644 --- a/src/Mod/Part/App/PartFeature.cpp +++ b/src/Mod/Part/App/PartFeature.cpp @@ -1776,11 +1776,19 @@ bool Feature::getCameraAlignmentDirection(Base::Vector3d& directionZ, Base::Vect // Edge direction const size_t edgeCount = topoShape.countSubShapes(TopAbs_EDGE); - if (edgeCount == 1 && topoShape.isLinearEdge()) { - if (const std::unique_ptr geometry = Geometry::fromShape(topoShape.getSubShape(TopAbs_EDGE, 1), true)) { - const std::unique_ptr geomLine(static_cast(geometry.get())->toLine()); - if (geomLine) { - directionZ = geomLine->getDir().Normalize(); + if (edgeCount == 1) { + if (topoShape.isLinearEdge()) { + if (const std::unique_ptr geometry = Geometry::fromShape(topoShape.getSubShape(TopAbs_EDGE, 1), true)) { + const std::unique_ptr geomLine(static_cast(geometry.get())->toLine()); + if (geomLine) { + directionZ = geomLine->getDir().Normalize(); + return true; + } + } + } else { + // Planar curves + if (gp_Pln plane; topoShape.findPlane(plane)) { + directionZ = Base::Vector3d(plane.Axis().Direction().X(), plane.Axis().Direction().Y(), plane.Axis().Direction().Z()).Normalize(); return true; } } From f71d1cf875b3a48e5a4844fc9b4543422d685f73 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 22 Jun 2025 13:12:00 +0200 Subject: [PATCH 087/126] Gui: Use proper placement property for Link Links require different placement property (LinkPlacement) to be used, otherwise it breaks the transform. Fixes: #20776 --- src/Gui/ViewProviderDragger.cpp | 18 ++++++++++++++++-- src/Gui/ViewProviderDragger.h | 3 +++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Gui/ViewProviderDragger.cpp b/src/Gui/ViewProviderDragger.cpp index ad7ed97490..a78e890598 100644 --- a/src/Gui/ViewProviderDragger.cpp +++ b/src/Gui/ViewProviderDragger.cpp @@ -170,6 +170,20 @@ bool ViewProviderDragger::forwardToLink() return forwardedViewProvider != nullptr; } +App::PropertyPlacement* ViewProviderDragger::getPlacementProperty() const +{ + auto object = getObject(); + + if (auto linkExtension = object->getExtensionByType()) { + if (auto linkPlacementProp = linkExtension->getLinkPlacementProperty()) { + return linkPlacementProp; + } + + return linkExtension->getPlacementProperty(); + } + + return getObject()->getPropertyByName("Placement"); +} bool ViewProviderDragger::setEdit(int ModNum) { @@ -254,7 +268,7 @@ void ViewProviderDragger::dragMotionCallback(void* data, [[maybe_unused]] SoDrag void ViewProviderDragger::updatePlacementFromDragger(DraggerComponents components) { - const auto placement = getObject()->getPropertyByName("Placement"); + const auto placement = getPlacementProperty(); if (!placement) { return; @@ -379,7 +393,7 @@ void ViewProviderDragger::updateTransformFromDragger() Base::Placement ViewProviderDragger::getObjectPlacement() const { - if (auto placement = getObject()->getPropertyByName("Placement")) { + if (auto placement = getPlacementProperty()) { return placement->getValue(); } diff --git a/src/Gui/ViewProviderDragger.h b/src/Gui/ViewProviderDragger.h index ef1d89654c..6937dec79f 100644 --- a/src/Gui/ViewProviderDragger.h +++ b/src/Gui/ViewProviderDragger.h @@ -120,6 +120,9 @@ protected: bool forwardToLink(); + /// Gets placement property of the object + App::PropertyPlacement* getPlacementProperty() const; + /** * Returns a newly create dialog for the part to be placed in the task view * Must be reimplemented in subclasses. From 5f0c93758e196facf4856c48c340ef69f0c430c9 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:59:13 +0100 Subject: [PATCH 088/126] [BIM] Fix Runtime Error when creating Wall (#21862) * [BIM] Fix Runtime Error when creating Wall * [BIM] Fix continueMode functionality for Wall, Panel and Structure --- src/Mod/BIM/ArchStructure.py | 2 +- src/Mod/BIM/bimcommands/BimPanel.py | 10 +++++++++- src/Mod/BIM/bimcommands/BimWall.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Mod/BIM/ArchStructure.py b/src/Mod/BIM/ArchStructure.py index 15ee8f4bb9..f0844d6bf9 100644 --- a/src/Mod/BIM/ArchStructure.py +++ b/src/Mod/BIM/ArchStructure.py @@ -424,7 +424,7 @@ class _CommandStructure: self.doc.recompute() # gui_utils.end_all_events() # Causes a crash on Linux. self.tracker.finalize() - if FreeCADGui.draftToolBar.continueCmd.isChecked(): + if FreeCADGui.draftToolBar.continueMode: self.Activated() def _createItemlist(self, baselist): diff --git a/src/Mod/BIM/bimcommands/BimPanel.py b/src/Mod/BIM/bimcommands/BimPanel.py index 8e907ca5f7..bd9d8dff3b 100644 --- a/src/Mod/BIM/bimcommands/BimPanel.py +++ b/src/Mod/BIM/bimcommands/BimPanel.py @@ -127,7 +127,15 @@ class Arch_Panel: FreeCADGui.doCommand('s.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1.00,0.00,0.00),90.00)') self.doc.commitTransaction() self.doc.recompute() - if FreeCADGui.draftToolBar.continueCmd.isChecked(): + from PySide import QtCore + QtCore.QTimer.singleShot(100, self.check_continueMode) + + + def check_continueMode(self): + + "checks if continueMode is true and restarts Panel" + + if FreeCADGui.draftToolBar.continueMode: self.Activated() def taskbox(self): diff --git a/src/Mod/BIM/bimcommands/BimWall.py b/src/Mod/BIM/bimcommands/BimWall.py index daeafd839a..08b3036dec 100644 --- a/src/Mod/BIM/bimcommands/BimWall.py +++ b/src/Mod/BIM/bimcommands/BimWall.py @@ -192,7 +192,7 @@ class Arch_Wall: self.doc.recompute() # gui_utils.end_all_events() # Causes a crash on Linux. self.tracker.finalize() - if FreeCADGui.draftToolBar.continueCmd.isChecked(): + if FreeCADGui.draftToolBar.continueMode: self.Activated() def addDefault(self): From 32976f5500173bf1ab20e48245f9dcb43c2759cb Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Sun, 15 Jun 2025 10:33:26 +0800 Subject: [PATCH 089/126] [ArchRoof] Improve subVolume generation Fix #21633 : Holes in roof are causing troubles FreeCAD Forum : Sketch based Arch_Roof and wall substraction - https://forum.freecad.org/viewtopic.php?t=84389 Improved algorithm: 1. Extrusion of bottom faces in +Z. 2. The roof itself. 3. Extrusion of the top faces in +Z. TODO: Find better way to test and maybe to split suface point up and down and extrude separately --- src/Mod/BIM/ArchRoof.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Mod/BIM/ArchRoof.py b/src/Mod/BIM/ArchRoof.py index 6fb18227b4..4c323e24fb 100644 --- a/src/Mod/BIM/ArchRoof.py +++ b/src/Mod/BIM/ArchRoof.py @@ -762,17 +762,29 @@ class _Roof(ArchComponent.Component): # a Wall, but all portion of the wall above the roof solid would be # subtracted as well. # - # FC forum discussion : Sketch based Arch_Roof and wall substraction + + # FC forum discussion, 2024.1.15 : + # Sketch based Arch_Roof and wall substraction # - https://forum.freecad.org/viewtopic.php?t=84389 # + # Github issue #21633, 2025.5.29 : + # BIM: Holes in roof are causing troubles + # - https://github.com/FreeCAD/FreeCAD/issues/21633#issuecomment-2969640142 + + faces = [] solids = [] for f in obj.Base.Shape.Faces: # obj.Base.Shape.Solids.Faces p = f.findPlane() # Curve face (surface) seems return no Plane if p: - if p.Axis[2] < -1e-7: # i.e. normal pointing below horizon - faces.append(f) + # See github issue #21633, all planes are added for safety + #if p.Axis[2] < -1e-7: # i.e. normal pointing below horizon + faces.append(f) else: + # TODO 2025.6.15: See github issue #21633: Find better way + # to test and maybe to split suface point up and down + # and extrude separately + # Not sure if it is pointing towards and/or above horizon # (upward or downward), or it is curve surface, just add. faces.append(f) @@ -785,6 +797,11 @@ class _Roof(ArchComponent.Component): solid = f.extrude(Vector(0.0, 0.0, 1000000.0)) if not solid.isNull() and solid.isValid() and solid.Volume > 1e-3: solids.append(solid) + + # See github issue #21633: Solids are added for safety + for s in obj.Base.Shape.Solids: + solids.append(s) + compound = Part.Compound(solids) compound.Placement = obj.Placement return compound From 1295f1066962b34eb934e542e9be06d86e482c37 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 15 Jun 2025 19:16:07 -0500 Subject: [PATCH 090/126] Help: Change URL sanitization to be safer --- src/Mod/Help/Help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Help/Help.py b/src/Mod/Help/Help.py index 344e83b734..291f0aa4e2 100644 --- a/src/Mod/Help/Help.py +++ b/src/Mod/Help/Help.py @@ -169,7 +169,7 @@ def location_url(url_localized: str, url_english: str) -> tuple: req = urllib.request.Request(url_localized) with urllib.request.urlopen(req) as response: html = response.read().decode("utf-8") - if re.search(MD_RAW_URL, url_localized): + if url_localized.startswith(MD_RAW_URL): pagename_match = re.search(r"Name/.*?:\s*(.+)", html) else: # Pages from FreeCAD Wiki fall here From 1a86f56051acac8a80c6ae05e86d99b621f123d6 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Sun, 15 Jun 2025 16:29:44 +0800 Subject: [PATCH 091/126] [ArchCurtainWall] Fix Vert-Horiz Mullion Mix-up & Support Swap Fix #21845 Curtain wall vertical/horizontal mullion mix-up - https://github.com/FreeCAD/FreeCAD/issues/21845 Support/Feature #21866 Swap Horizontal Vertical does not work #21866 https://github.com/FreeCAD/FreeCAD/issues/21866 --- src/Mod/BIM/ArchCurtainWall.py | 56 ++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/Mod/BIM/ArchCurtainWall.py b/src/Mod/BIM/ArchCurtainWall.py index 9246689545..ba9e228a4b 100644 --- a/src/Mod/BIM/ArchCurtainWall.py +++ b/src/Mod/BIM/ArchCurtainWall.py @@ -279,16 +279,52 @@ class CurtainWall(ArchComponent.Component): if not vdir.Length: vdir = FreeCAD.Vector(0,0,1) vdir.normalize() - basevector = face.valueAt(fp[1],fp[3]).sub(face.valueAt(fp[0],fp[2])) - a = basevector.getAngle(vdir) - if (a <= math.pi/2+ANGLETOLERANCE) and (a >= math.pi/2-ANGLETOLERANCE): - facedir = True - vertsec = obj.VerticalSections - horizsec = obj.HorizontalSections - else: - facedir = False - vertsec = obj.HorizontalSections - horizsec = obj.VerticalSections + + # Check if face if vertical in the first place + # Fix issue in 'Curtain wall vertical/horizontal mullion mix-up' + # https://github.com/FreeCAD/FreeCAD/issues/21845 + # + p = face.findPlane() # Curve face (surface) seems return no Plane + if p: + if -0.001 < p.Axis[2] < 0.001: # i.e. face is vertical (normal pointing horizon) + faceVert = True + # Support 'Swap Horizontal Vertical' + # See issue 'Swap Horizontal Vertical does not work' + # https://github.com/FreeCAD/FreeCAD/issues/21866 + if obj.SwapHorizontalVertical: + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections + else: + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections + else: + faceVert = False + + # Guess algorithm if face is not vertical + if not faceVert: + # TODO 2025.6.15 : Need a more robust algorithm below + # See issue 'Curtain wall vertical/horizontal mullion mix-up' + # https://github.com/FreeCAD/FreeCAD/issues/21845 + # Partially improved by checking 'if face is vertical' above + # + basevector = face.valueAt(fp[1],fp[3]).sub(face.valueAt(fp[0],fp[2])) + a = basevector.getAngle(vdir) + if (a <= math.pi/2+ANGLETOLERANCE) and (a >= math.pi/2-ANGLETOLERANCE): + facedir = True + if obj.SwapHorizontalVertical: + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections + else: + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections + else: + facedir = False + if obj.SwapHorizontalVertical: + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections + else: + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections hstep = (fp[1]-fp[0]) if vertsec: From 57c3aab2e8389dd5c9c194a3d5ce95c359da1915 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Tue, 17 Jun 2025 03:58:39 +0800 Subject: [PATCH 092/126] [ArchCurtainWall] Fix Vert-Horiz Mullion Mix-up & Support Swap (Variables name) (Variables name improvement only) --- src/Mod/BIM/ArchCurtainWall.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mod/BIM/ArchCurtainWall.py b/src/Mod/BIM/ArchCurtainWall.py index ba9e228a4b..fa394454b5 100644 --- a/src/Mod/BIM/ArchCurtainWall.py +++ b/src/Mod/BIM/ArchCurtainWall.py @@ -284,9 +284,9 @@ class CurtainWall(ArchComponent.Component): # Fix issue in 'Curtain wall vertical/horizontal mullion mix-up' # https://github.com/FreeCAD/FreeCAD/issues/21845 # - p = face.findPlane() # Curve face (surface) seems return no Plane - if p: - if -0.001 < p.Axis[2] < 0.001: # i.e. face is vertical (normal pointing horizon) + face_plane = face.findPlane() # Curve face (surface) seems return no Plane + if face_plane: + if -0.001 < face_plane.Axis[2] < 0.001: # i.e. face is vertical (normal pointing horizon) faceVert = True # Support 'Swap Horizontal Vertical' # See issue 'Swap Horizontal Vertical does not work' @@ -308,8 +308,8 @@ class CurtainWall(ArchComponent.Component): # Partially improved by checking 'if face is vertical' above # basevector = face.valueAt(fp[1],fp[3]).sub(face.valueAt(fp[0],fp[2])) - a = basevector.getAngle(vdir) - if (a <= math.pi/2+ANGLETOLERANCE) and (a >= math.pi/2-ANGLETOLERANCE): + bv_angle = basevector.getAngle(vdir) + if (bv_angle <= math.pi/2+ANGLETOLERANCE) and (bv_angle >= math.pi/2-ANGLETOLERANCE): facedir = True if obj.SwapHorizontalVertical: vertsec = obj.HorizontalSections From e044ddab6d08a3089873a51861848c5b1a3c6ad7 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:16:22 +0200 Subject: [PATCH 093/126] DXF: do not ignore the setting that controls importing paper layouts --- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 36 ++++++++++++++++++++++++++++ src/Mod/Import/App/dxf/ImpExpDxf.h | 5 ++++ 2 files changed, 41 insertions(+) diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 115f9a1125..6b9279deaf 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -207,6 +207,10 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, const Base::Vector3d& end, bool /*hidden*/) { + if (shouldSkipEntity()) { + return; + } + gp_Pnt p0 = makePoint(start); gp_Pnt p1 = makePoint(end); if (p0.IsEqual(p1, 0.00000001)) { @@ -219,6 +223,10 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start) { + if (shouldSkipEntity()) { + return; + } + Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point"); } @@ -229,6 +237,10 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start, bool dir, bool /*hidden*/) { + if (shouldSkipEntity()) { + return; + } + gp_Pnt p0 = makePoint(start); gp_Pnt p1 = makePoint(end); gp_Dir up(0, 0, 1); @@ -251,6 +263,10 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start, bool dir, bool /*hidden*/) { + if (shouldSkipEntity()) { + return; + } + gp_Pnt p0 = makePoint(start); gp_Dir up(0, 0, 1); if (!dir) { @@ -393,6 +409,10 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center, bool dir) // NOLINTEND(bugprone-easily-swappable-parameters) { + if (shouldSkipEntity()) { + return; + } + gp_Dir up(0, 0, 1); if (!dir) { up = -up; @@ -414,6 +434,10 @@ void ImpExpDxfRead::OnReadText(const Base::Vector3d& point, const std::string& text, const double rotation) { + if (shouldSkipEntity()) { + return; + } + // Note that our parameters do not contain all the information needed to properly orient the // text. As a result the text will always appear on the XY plane if (m_importAnnotations) { @@ -454,6 +478,10 @@ void ImpExpDxfRead::OnReadInsert(const Base::Vector3d& point, const std::string& name, double rotation) { + if (shouldSkipEntity()) { + return; + } + Collector->AddInsert(point, scale, name, rotation); } void ImpExpDxfRead::ExpandInsert(const std::string& name, @@ -534,6 +562,10 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, const Base::Vector3d& point, double /*rotation*/) { + if (shouldSkipEntity()) { + return; + } + if (m_importAnnotations) { auto makeDimension = [this, start, end, point](const Base::Matrix4D& transform) -> App::FeaturePython* { @@ -576,6 +608,10 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, } void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags) { + if (shouldSkipEntity()) { + return; + } + std::map> ShapesToCombine; { // TODO: Currently ExpandPolyline calls OnReadArc etc to generate the pieces, and these diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index c7f86715d5..c486e6eb7d 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -108,6 +108,11 @@ public: void setOptions(); private: + bool shouldSkipEntity() const + { + // This entity is in paper space, and the user setting says to ignore it. + return !m_importPaperSpaceEntities && m_entityAttributes.m_paperSpace; + } static gp_Pnt makePoint(const Base::Vector3d& point3d) { return {point3d.x, point3d.y, point3d.z}; From 1800ccd12c7b1207c6a545b6bb02dfc2123c15b0 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:26:47 +0200 Subject: [PATCH 094/126] BIM: fix display of help menu items after WB reactivation (improved) Fixes #22044 Previous PR (#21874) did not work properly if the BIM WB was the start up WB. A scenario that I forgot to test. --- src/Mod/BIM/InitGui.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Mod/BIM/InitGui.py b/src/Mod/BIM/InitGui.py index 517b939ae0..2a61aaa3f8 100644 --- a/src/Mod/BIM/InitGui.py +++ b/src/Mod/BIM/InitGui.py @@ -572,10 +572,12 @@ class BIMWorkbench(Workbench): {"insert": "BIM_Help", "menuItem": "Std_ReportBug", "after": ""}, {"insert": "BIM_Welcome", "menuItem": "Std_ReportBug", "after": ""}, ] - if not hasattr(Gui, "BIM_WBManipulator"): + reload = hasattr(Gui, "BIM_WBManipulator") # BIM WB has previously been loaded. + if not getattr(Gui, "BIM_WBManipulator", None): Gui.BIM_WBManipulator = BIM_WBManipulator() Gui.addWorkbenchManipulator(Gui.BIM_WBManipulator) - Gui.activeWorkbench().reloadActive() + if reload: + Gui.activeWorkbench().reloadActive() Log("BIM workbench activated\n") @@ -626,7 +628,7 @@ class BIMWorkbench(Workbench): # remove manipulator if hasattr(Gui, "BIM_WBManipulator"): Gui.removeWorkbenchManipulator(Gui.BIM_WBManipulator) - del Gui.BIM_WBManipulator + Gui.BIM_WBManipulator = None Gui.activeWorkbench().reloadActive() Log("BIM workbench deactivated\n") From faf9669327196f2c4a9bd511656dc463a01a18f8 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:21:42 +0200 Subject: [PATCH 095/126] BIM: fix ArchProfile update issues Fixes 21001 Fixes 21187 --- src/Mod/BIM/ArchProfile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Mod/BIM/ArchProfile.py b/src/Mod/BIM/ArchProfile.py index 6a90217d4b..51b976797d 100644 --- a/src/Mod/BIM/ArchProfile.py +++ b/src/Mod/BIM/ArchProfile.py @@ -112,12 +112,12 @@ class _Profile(Draft._DraftObject): '''Remove all Profile properties''' - obj.removeProperty("Width") - obj.removeProperty("Height") - obj.removeProperty("WebThickness") - obj.removeProperty("FlangeThickness") - obj.removeProperty("OutDiameter") - obj.removeProperty("Thickness") + for prop in [ + "Width", "Height", "WebThickness", "FlangeThickness","OutDiameter", "Thickness" + ]: + if hasattr(obj, prop): + obj.setPropertyStatus(prop, "-LockDynamic") + obj.removeProperty(prop) class _ProfileC(_Profile): @@ -384,7 +384,7 @@ class ProfileTaskPanel: layout.addWidget(self.comboCategory) self.comboProfile = QtGui.QComboBox(self.form) layout.addWidget(self.comboProfile) - QtCore.QObject.connect(self.comboCategory, QtCore.SIGNAL("currentIndexChanged(QString)"), self.changeCategory) + QtCore.QObject.connect(self.comboCategory, QtCore.SIGNAL("currentTextChanged(QString)"), self.changeCategory) QtCore.QObject.connect(self.comboProfile, QtCore.SIGNAL("currentIndexChanged(int)"), self.changeProfile) # Read preset profiles and add relevant ones self.categories = [] From 8d46d437f8a4e5499e1c24353a104d1133a9b3ab Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:25:45 +0200 Subject: [PATCH 096/126] BIM: fix index error in ifc_viewproviders.py Fixes #21912 --- src/Mod/BIM/nativeifc/ifc_viewproviders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/BIM/nativeifc/ifc_viewproviders.py b/src/Mod/BIM/nativeifc/ifc_viewproviders.py index ee9d67b4b0..3f50e774fe 100644 --- a/src/Mod/BIM/nativeifc/ifc_viewproviders.py +++ b/src/Mod/BIM/nativeifc/ifc_viewproviders.py @@ -224,7 +224,7 @@ class ifc_vp_object: """Recursively gets the children only used by this object""" children = [] for child in obj.OutList: - if len(child.InList) == 1 and child.InList[1] == obj: + if len(child.InList) == 1 and child.InList[0] == obj: children.append(child) children.extend(self.getOwnChildren(child)) return children From 70dac9834811cf68070c5da6ce58e912a6990297 Mon Sep 17 00:00:00 2001 From: wmayer Date: Sun, 18 May 2025 10:30:55 +0200 Subject: [PATCH 097/126] Tools: Add ExpressionLineEdit to QtDesigner plugin --- src/Tools/plugins/widget/customwidgets.cpp | 62 ++++++++++++++++++++++ src/Tools/plugins/widget/customwidgets.h | 28 ++++++++++ src/Tools/plugins/widget/plugin.cpp | 51 ++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/src/Tools/plugins/widget/customwidgets.cpp b/src/Tools/plugins/widget/customwidgets.cpp index 79836644b2..2514dbc18f 100644 --- a/src/Tools/plugins/widget/customwidgets.cpp +++ b/src/Tools/plugins/widget/customwidgets.cpp @@ -22,11 +22,13 @@ #include +#include #include #include #include #include #include +#include #include #include #include @@ -544,6 +546,66 @@ void InputField::setHistorySize(int i) // -------------------------------------------------------------------- +ExpressionLineEdit::ExpressionLineEdit(QWidget* parent) + : QLineEdit(parent) + , exactMatch {false} +{ + completer = new QCompleter(this); + connect(this, &QLineEdit::textEdited, this, &ExpressionLineEdit::slotTextChanged); +} + +void ExpressionLineEdit::setExactMatch(bool enabled) +{ + exactMatch = enabled; + if (completer) { + completer->setFilterMode(exactMatch ? Qt::MatchStartsWith : Qt::MatchContains); + } +} + +void ExpressionLineEdit::slotTextChanged(const QString& text) +{ + Q_EMIT textChanged2(text, cursorPosition()); +} + +void ExpressionLineEdit::slotCompleteText(const QString& completionPrefix, bool isActivated) +{ + Q_UNUSED(completionPrefix) + Q_UNUSED(isActivated) +} + +void ExpressionLineEdit::slotCompleteTextHighlighted(const QString& completionPrefix) +{ + slotCompleteText(completionPrefix, false); +} + +void ExpressionLineEdit::slotCompleteTextSelected(const QString& completionPrefix) +{ + slotCompleteText(completionPrefix, true); +} + +void ExpressionLineEdit::keyPressEvent(QKeyEvent* e) +{ + QLineEdit::keyPressEvent(e); +} + +void ExpressionLineEdit::contextMenuEvent(QContextMenuEvent* event) +{ + QMenu* menu = createStandardContextMenu(); + + if (completer) { + menu->addSeparator(); + QAction* match = menu->addAction(tr("Exact match")); + match->setCheckable(true); + match->setChecked(completer->filterMode() == Qt::MatchStartsWith); + QObject::connect(match, &QAction::toggled, this, &Gui::ExpressionLineEdit::setExactMatch); + } + menu->setAttribute(Qt::WA_DeleteOnClose); + + menu->popup(event->globalPos()); +} + +// -------------------------------------------------------------------- + namespace Base { diff --git a/src/Tools/plugins/widget/customwidgets.h b/src/Tools/plugins/widget/customwidgets.h index c80966b493..ed91cf2c24 100644 --- a/src/Tools/plugins/widget/customwidgets.h +++ b/src/Tools/plugins/widget/customwidgets.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -386,6 +387,33 @@ private: // ------------------------------------------------------------------------------ +class ExpressionLineEdit: public QLineEdit +{ + Q_OBJECT +public: + ExpressionLineEdit(QWidget* parent = nullptr); + +public Q_SLOTS: + void slotTextChanged(const QString& text); + void slotCompleteText(const QString& completionPrefix, bool isActivated); + void slotCompleteTextHighlighted(const QString& completionPrefix); + void slotCompleteTextSelected(const QString& completionPrefix); + void setExactMatch(bool enabled = true); + +protected: + void keyPressEvent(QKeyEvent* event) override; + void contextMenuEvent(QContextMenuEvent* event) override; + +Q_SIGNALS: + void textChanged2(QString text, int pos); + +private: + QCompleter* completer; + bool exactMatch; +}; + +// ------------------------------------------------------------------------------ + class QuantitySpinBoxPrivate; class QuantitySpinBox: public QAbstractSpinBox { diff --git a/src/Tools/plugins/widget/plugin.cpp b/src/Tools/plugins/widget/plugin.cpp index 62b9672fff..dee5844e13 100644 --- a/src/Tools/plugins/widget/plugin.cpp +++ b/src/Tools/plugins/widget/plugin.cpp @@ -523,6 +523,56 @@ public: } }; +class ExpressionLineEditPlugin: public QDesignerCustomWidgetInterface +{ + Q_INTERFACES(QDesignerCustomWidgetInterface) +public: + ExpressionLineEditPlugin() + {} + QWidget* createWidget(QWidget* parent) + { + return new Gui::ExpressionLineEdit(parent); + } + QString group() const + { + return QLatin1String("Input Widgets"); + } + QIcon icon() const + { + return QIcon(QPixmap(inputfield_pixmap)); + } + QString includeFile() const + { + return QLatin1String("Gui/InputField.h"); + } + QString toolTip() const + { + return QLatin1String("Expression line edit"); + } + QString whatsThis() const + { + return QLatin1String("A widget to work with expressions."); + } + bool isContainer() const + { + return false; + } + QString domXml() const + { + return "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + ""; + } + QString name() const + { + return QLatin1String("Gui::ExpressionLineEdit"); + } +}; + /* XPM */ static const char* quantityspinbox_pixmap[] = {"22 22 6 1", "a c #000000", @@ -1653,6 +1703,7 @@ QList CustomWidgetPlugin::customWidgets() const cw.append(new FileChooserPlugin); cw.append(new AccelLineEditPlugin); cw.append(new ActionSelectorPlugin); + cw.append(new ExpressionLineEditPlugin); cw.append(new InputFieldPlugin); cw.append(new QuantitySpinBoxPlugin); cw.append(new CommandIconViewPlugin); From c4a0c9f37c52db42a2a04e12c91e0bb0b195f85a Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:45:02 +0200 Subject: [PATCH 098/126] Update ArchMaterial.py --- src/Mod/BIM/ArchMaterial.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Mod/BIM/ArchMaterial.py b/src/Mod/BIM/ArchMaterial.py index f6eaf4c053..6f589c7e2d 100644 --- a/src/Mod/BIM/ArchMaterial.py +++ b/src/Mod/BIM/ArchMaterial.py @@ -279,12 +279,18 @@ class _ArchMaterial: def execute(self,obj): if obj.Material: if FreeCAD.GuiUp: + c = None + t = None if "DiffuseColor" in obj.Material: - c = tuple([float(f) for f in obj.Material['DiffuseColor'].strip("()").strip("[]").split(",")]) - for p in obj.InList: - if hasattr(p,"Material") and ( (not hasattr(p.ViewObject,"UseMaterialColor")) or p.ViewObject.UseMaterialColor): - if p.Material.Name == obj.Name: - p.ViewObject.ShapeColor = c + c = tuple([float(f) for f in obj.Material["DiffuseColor"].strip("()").strip("[]").split(",")]) + if "Transparency" in obj.Material: + t = int(obj.Material["Transparency"]) + for p in obj.InList: + if hasattr(p,"Material") \ + and p.Material.Name == obj.Name \ + and getattr(obj.ViewObject,"UseMaterialColor",True): + if c: p.ViewObject.ShapeColor = c + if t: p.ViewObject.Transparency = t return def dumps(self): From c6f89f646cf98b300ef089181bcf46a734d46e13 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:01:11 +0200 Subject: [PATCH 099/126] Update BimProjectManager.py --- src/Mod/BIM/bimcommands/BimProjectManager.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Mod/BIM/bimcommands/BimProjectManager.py b/src/Mod/BIM/bimcommands/BimProjectManager.py index e9d0ab1d21..ebde0fd12b 100644 --- a/src/Mod/BIM/bimcommands/BimProjectManager.py +++ b/src/Mod/BIM/bimcommands/BimProjectManager.py @@ -469,10 +469,6 @@ class BIM_ProjectManager: + "\n" ) s += "groups=" + ";;".join(groups) + "\n" - - s += "levelsWP=" + str(int(self.form.levelsWP.isChecked())) + "\n" - s += "levelsAxis=" + str(int(self.form.levelsAxis.isChecked())) + "\n" - s += ( "addHumanFigure=" + str(int(self.form.addHumanFigure.isChecked())) @@ -563,10 +559,6 @@ class BIM_ProjectManager: groups = s[1].split(";;") self.form.groupsList.clear() self.form.groupsList.addItems(groups) - elif s[0] == "levelsWP": - self.form.levelsWP.setChecked(bool(int(s[1]))) - elif s[0] == "levelsAxis": - self.form.levelsAxis.setChecked(bool(int(s[1]))) elif s[0] == "addHumanFigure": self.form.addHumanFigure.setChecked(bool(int(s[1]))) From 6adb97d79d14086c224607b9eb3b19c62edcc911 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:14:51 +0200 Subject: [PATCH 100/126] BIM: fix filtering out level issue (#22059) * Update ifc_status.py * Update ifc_tools.py --- src/Mod/BIM/nativeifc/ifc_status.py | 4 +--- src/Mod/BIM/nativeifc/ifc_tools.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Mod/BIM/nativeifc/ifc_status.py b/src/Mod/BIM/nativeifc/ifc_status.py index 6c3264fe89..4d4dcbbf91 100644 --- a/src/Mod/BIM/nativeifc/ifc_status.py +++ b/src/Mod/BIM/nativeifc/ifc_status.py @@ -481,14 +481,12 @@ def filter_out(objs): nobjs.append(obj) elif obj.isDerivedFrom("Mesh::Feature"): nobjs.append(obj) - elif obj.isDerivedFrom("App::DocumentObjectGroup"): + elif Draft.is_group(obj): if filter_out(obj.Group): # only append groups that contain exportable objects nobjs.append(obj) else: print("DEBUG: Filtering out",obj.Label) - elif obj.isDerivedFrom("Mesh::Feature"): - nobjs.append(obj) elif obj.isDerivedFrom("App::Feature"): if Draft.get_type(obj) in ("Dimension","LinearDimension","Layer","Text","DraftText"): nobjs.append(obj) diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 675c2a99a1..8bc16126b4 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -1598,7 +1598,7 @@ def get_orphan_elements(ifcfile): products = ifcfile.by_type("IfcProduct") products = [p for p in products if not p.Decomposes] - products = [p for p in products if not p.ContainedInStructure] + products = [p for p in products if not getattr(p, "ContainedInStructure", [])] products = [ p for p in products if not hasattr(p, "VoidsElements") or not p.VoidsElements ] From dc948671bdcf37d6f81a26aa1d4aa223f9444e17 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:10:18 +0200 Subject: [PATCH 101/126] Update ArchAxis.py --- src/Mod/BIM/ArchAxis.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Mod/BIM/ArchAxis.py b/src/Mod/BIM/ArchAxis.py index bdbd89482c..f39afa3a83 100644 --- a/src/Mod/BIM/ArchAxis.py +++ b/src/Mod/BIM/ArchAxis.py @@ -109,25 +109,20 @@ class _Axis: if distances and obj.Length.Value: if angles and len(distances) == len(angles): for i in range(len(distances)): - if hasattr(obj.Length,"Value"): - l = obj.Length.Value - else: - l = obj.Length dist += distances[i] ang = math.radians(angles[i]) - p1 = Vector(dist,0,0) - p2 = Vector(dist+(l/math.cos(ang))*math.sin(ang),l,0) + ln = obj.Length.Value + ln = 100 * ln if abs(math.cos(ang)) < 0.01 else ln / math.cos(ang) + unitvec = Vector(math.sin(ang), math.cos(ang), 0) + p1 = Vector(dist, 0, 0) + p2 = p1 + unitvec * ln if hasattr(obj,"Limit") and obj.Limit.Value: - p3 = p2.sub(p1) - p3.normalize() - p3.multiply(-obj.Limit.Value) - p4 = p1.sub(p2) - p4.normalize() - p4.multiply(-obj.Limit.Value) - geoms.append(Part.LineSegment(p1,p1.add(p4)).toShape()) - geoms.append(Part.LineSegment(p2,p2.add(p3)).toShape()) + p3 = unitvec * obj.Limit.Value + p4 = unitvec * -obj.Limit.Value + geoms.append(Part.LineSegment(p1, p1 + p3).toShape()) + geoms.append(Part.LineSegment(p2, p2 + p4).toShape()) else: - geoms.append(Part.LineSegment(p1,p2).toShape()) + geoms.append(Part.LineSegment(p1, p2).toShape()) if geoms: sh = Part.Compound(geoms) obj.Shape = sh From a62dddde078b67fe240db464a6cdaef7e2408030 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sat, 21 Jun 2025 11:08:37 +0200 Subject: [PATCH 102/126] Update ArchSchedule.py --- src/Mod/BIM/ArchSchedule.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Mod/BIM/ArchSchedule.py b/src/Mod/BIM/ArchSchedule.py index d4d28ff894..305b4c73c6 100644 --- a/src/Mod/BIM/ArchSchedule.py +++ b/src/Mod/BIM/ArchSchedule.py @@ -799,9 +799,6 @@ class ArchScheduleTaskPanel: mw = FreeCADGui.getMainWindow() self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center()) - # maintain above FreeCAD window - self.form.setWindowFlags(self.form.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - self.form.show() def add(self): From bacc9c76165be890c4c75dbddfeffe3105d82c2b Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sat, 21 Jun 2025 11:48:39 +0200 Subject: [PATCH 103/126] Update BimTDPage.py --- src/Mod/BIM/bimcommands/BimTDPage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/BIM/bimcommands/BimTDPage.py b/src/Mod/BIM/bimcommands/BimTDPage.py index 492093b8dd..b756ebe298 100644 --- a/src/Mod/BIM/bimcommands/BimTDPage.py +++ b/src/Mod/BIM/bimcommands/BimTDPage.py @@ -102,6 +102,7 @@ class BIM_TDPage: page.Scale = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/BIM" ).GetFloat("DefaultPageScale", 0.01) + page.ViewObject.show() FreeCAD.ActiveDocument.recompute() From 0e55ce8d9fc234b20bbc67f4be23805a28d83ba5 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sat, 21 Jun 2025 16:10:14 +0200 Subject: [PATCH 104/126] Update ArchComponent.py --- src/Mod/BIM/ArchComponent.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Mod/BIM/ArchComponent.py b/src/Mod/BIM/ArchComponent.py index 62f0ec7355..9c403a3606 100644 --- a/src/Mod/BIM/ArchComponent.py +++ b/src/Mod/BIM/ArchComponent.py @@ -1353,19 +1353,16 @@ class ViewProviderComponent: #print(obj.Name," : updating ",prop) if prop == "Material": - if obj.Material and ( (not hasattr(obj.ViewObject,"UseMaterialColor")) or obj.ViewObject.UseMaterialColor): + if obj.Material and getattr(obj.ViewObject,"UseMaterialColor",True): if hasattr(obj.Material,"Material"): - if 'DiffuseColor' in obj.Material.Material: - if "(" in obj.Material.Material['DiffuseColor']: - c = tuple([float(f) for f in obj.Material.Material['DiffuseColor'].strip("()").split(",")]) - if obj.ViewObject: - if obj.ViewObject.ShapeColor != c: - obj.ViewObject.ShapeColor = c - if 'Transparency' in obj.Material.Material: - t = int(obj.Material.Material['Transparency']) - if obj.ViewObject: - if obj.ViewObject.Transparency != t: - obj.ViewObject.Transparency = t + if "DiffuseColor" in obj.Material.Material: + c = tuple([float(f) for f in obj.Material.Material["DiffuseColor"].strip("()").strip("[]").split(",")]) + if obj.ViewObject.ShapeColor != c: + obj.ViewObject.ShapeColor = c + if "Transparency" in obj.Material.Material: + t = int(obj.Material.Material["Transparency"]) + if obj.ViewObject.Transparency != t: + obj.ViewObject.Transparency = t elif prop == "Shape": if obj.Base: if obj.Base.isDerivedFrom("Part::Compound"): @@ -1375,11 +1372,7 @@ class ViewProviderComponent: obj.ViewObject.update() elif prop == "CloneOf": if obj.CloneOf: - mat = None - if hasattr(obj,"Material"): - if obj.Material: - mat = obj.Material - if (not mat) and hasattr(obj.CloneOf.ViewObject,"DiffuseColor"): + if (not getattr(obj,"Material",None)) and hasattr(obj.CloneOf.ViewObject,"DiffuseColor"): if obj.ViewObject.DiffuseColor != obj.CloneOf.ViewObject.DiffuseColor: if len(obj.CloneOf.ViewObject.DiffuseColor) > 1: obj.ViewObject.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor From 0fd4f2bac6f2ceb387feedc56c37e7ab2dee051e Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Mon, 23 Jun 2025 16:14:35 +0200 Subject: [PATCH 105/126] Spreadsheet: Only use validator when when prefix is not '=' --- src/Gui/ExpressionCompleter.cpp | 4 ++-- src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp index 1499bf2ce6..332cfbc692 100644 --- a/src/Gui/ExpressionCompleter.cpp +++ b/src/Gui/ExpressionCompleter.cpp @@ -921,15 +921,15 @@ ExpressionLineEdit::ExpressionLineEdit(QWidget* parent, , noProperty(noProperty) , exactMatch(false) , checkInList(checkInList) - , checkPrefix(checkPrefix) { - setValidator(new ExpressionValidator(this)); + setPrefix(checkPrefix); connect(this, &QLineEdit::textEdited, this, &ExpressionLineEdit::slotTextChanged); } void ExpressionLineEdit::setPrefix(char prefix) { checkPrefix = prefix; + setValidator(checkPrefix == '=' ? nullptr : new ExpressionValidator(this)); } void ExpressionLineEdit::setDocumentObject(const App::DocumentObject* currentDocObj, diff --git a/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp b/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp index 8dab3b284c..463d3640e4 100644 --- a/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp +++ b/src/Mod/Spreadsheet/Gui/SpreadsheetView.cpp @@ -154,6 +154,8 @@ SheetView::SheetView(Gui::Document* pcDocument, App::DocumentObject* docObj, QWi // Set document object to create auto completer ui->cellContent->setDocumentObject(sheet); ui->cellAlias->setDocumentObject(sheet); + + ui->cellContent->setPrefix('='); } SheetView::~SheetView() From 35cdd31193d3f0736f9a4e6e08a97e0962d2d721 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 23 Jun 2025 09:50:17 -0500 Subject: [PATCH 106/126] CAM: Fix format specifier for size_t (unsigned long) (#22005) --- src/Mod/CAM/App/Area.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mod/CAM/App/Area.cpp b/src/Mod/CAM/App/Area.cpp index de73697705..2165865944 100644 --- a/src/Mod/CAM/App/Area.cpp +++ b/src/Mod/CAM/App/Area.cpp @@ -1920,7 +1920,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR builder.MakeCompound(comp); for (TopExp_Explorer xp(s.shape.Moved(loc), TopAbs_SOLID); xp.More(); xp.Next()) { - showShape(xp.Current(), nullptr, "section_%u_shape", i); + showShape(xp.Current(), nullptr, "section_%ul_shape", i); std::list wires; Part::CrossSection section(a, b, c, xp.Current()); Part::FuzzyHelper::withBooleanFuzzy(.0, [&]() { @@ -1930,7 +1930,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR // here for now to be on the safe side. wires = section.slice(-d); }); - showShapes(wires, nullptr, "section_%u_wire", i); + showShapes(wires, nullptr, "section_%ul_wire", i); if (wires.empty()) { AREA_LOG("Section returns no wires"); continue; @@ -1951,7 +1951,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR AREA_WARN("FaceMakerBullseye return null shape on section"); } else { - showShape(shape, nullptr, "section_%u_face", i); + showShape(shape, nullptr, "section_%ul_face", i); for (auto it = wires.begin(), itNext = it; it != wires.end(); it = itNext) { ++itNext; @@ -1979,7 +1979,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR // Make sure the compound has at least one edge if (TopExp_Explorer(comp, TopAbs_EDGE).More()) { const TopoDS_Shape& shape = comp.Moved(locInverse); - showShape(shape, nullptr, "section_%u_result", i); + showShape(shape, nullptr, "section_%ul_result", i); area->add(shape, s.op); } else if (area->myShapes.empty()) { @@ -1994,7 +1994,7 @@ std::vector> Area::makeSections(PARAM_ARGS(PARAM_FARG, AREA_PAR if (!area->myShapes.empty()) { sections.push_back(area); FC_TIME_LOG(t1, "makeSection " << z); - showShape(area->getShape(), nullptr, "section_%u_final", i); + showShape(area->getShape(), nullptr, "section_%ul_final", i); break; } if (retried) { From 91cc7c37e555119e5d3f94cdc5ad75011a388439 Mon Sep 17 00:00:00 2001 From: tarman3 Date: Mon, 23 Jun 2025 18:05:01 +0300 Subject: [PATCH 107/126] CAM: Custom gcode - Improve error messages (#21509) --- src/Mod/CAM/Path/Op/Custom.py | 53 ++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Custom.py b/src/Mod/CAM/Path/Op/Custom.py index 5b409c5153..f4a217bc76 100644 --- a/src/Mod/CAM/Path/Op/Custom.py +++ b/src/Mod/CAM/Path/Op/Custom.py @@ -163,11 +163,26 @@ class ObjectCustom(PathOp.ObjectOp): def opExecute(self, obj): self.commandlist.append(Path.Command("(Begin Custom)")) + errorNumLines = [] + errorLines = [] + counter = 0 if obj.Source == "Text" and obj.Gcode: for l in obj.Gcode: - newcommand = Path.Command(str(l)) - self.commandlist.append(newcommand) + counter += 1 + try: + newcommand = Path.Command(str(l)) + self.commandlist.append(newcommand) + except ValueError: + errorNumLines.append(counter) + if len(errorLines) < 7: + errorLines.append(f"{counter}: {str(l).strip()}") + if errorLines: + Path.Log.warning( + translate("PathCustom", "Total invalid lines in Custom Text G-code: %s") + % len(errorNumLines) + ) + elif obj.Source == "File" and len(obj.GcodeFile) > 0: gcode_file = self.findGcodeFile(obj.GcodeFile) @@ -176,15 +191,33 @@ class ObjectCustom(PathOp.ObjectOp): Path.Log.error( translate("PathCustom", "Custom file %s could not be found.") % obj.GcodeFile ) + else: + with open(gcode_file) as fd: + for l in fd.readlines(): + counter += 1 + try: + newcommand = Path.Command(str(l)) + self.commandlist.append(newcommand) + except ValueError: + errorNumLines.append(counter) + if len(errorLines) < 7: + errorLines.append(f"{counter}: {str(l).strip()}") + if errorLines: + Path.Log.warning(f'"{gcode_file}"') + Path.Log.warning( + translate("PathCustom", "Total invalid lines in Custom File G-code: %s") + % len(errorNumLines) + ) - with open(gcode_file) as fd: - for l in fd.readlines(): - try: - newcommand = Path.Command(str(l)) - self.commandlist.append(newcommand) - except ValueError: - Path.Log.warning(translate("PathCustom", "Invalid G-code line: %s") % l) - continue + if errorNumLines: + Path.Log.warning( + translate("PathCustom", "Please check lines: %s") + % ", ".join(map(str, errorNumLines)) + ) + + if len(errorLines) > 7: + errorLines.append("...") + Path.Log.warning("\n" + "\n".join(errorLines)) self.commandlist.append(Path.Command("(End Custom)")) From 10d3a47e99e7ca03db36d19284860b74f40aced0 Mon Sep 17 00:00:00 2001 From: Max Wilfinger Date: Thu, 19 Jun 2025 11:09:58 +0200 Subject: [PATCH 108/126] Assembly: Fix conflicting shortcuts --- src/Mod/Assembly/CommandCreateJoint.py | 4 ++-- src/Mod/Assembly/CommandCreateSimulation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index 29428a9c16..e4ba97910c 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -392,7 +392,7 @@ class CommandCreateJointGears: return { "Pixmap": "Assembly_CreateJointGears", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointGears", "Create Gears Joint"), - "Accel": "X", + "Accel": "T", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointGears", @@ -423,7 +423,7 @@ class CommandCreateJointBelt: return { "Pixmap": "Assembly_CreateJointPulleys", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointBelt", "Create Belt Joint"), - "Accel": "P", + "Accel": "L", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointBelt", diff --git a/src/Mod/Assembly/CommandCreateSimulation.py b/src/Mod/Assembly/CommandCreateSimulation.py index 1612069adf..56dc2268e2 100644 --- a/src/Mod/Assembly/CommandCreateSimulation.py +++ b/src/Mod/Assembly/CommandCreateSimulation.py @@ -65,7 +65,7 @@ class CommandCreateSimulation: return { "Pixmap": "Assembly_CreateSimulation", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateSimulation", "Create Simulation"), - "Accel": "S", + "Accel": "V", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateSimulation", From f829a09018649e34ed8e969af4d361d589c88627 Mon Sep 17 00:00:00 2001 From: jffmichi Date: Mon, 23 Jun 2025 17:22:21 +0200 Subject: [PATCH 109/126] CAM: sort tool paths for Engrave and Deburr operation (#21531) Co-authored-by: jffmichi <> --- src/Mod/CAM/Path/Op/EngraveBase.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Mod/CAM/Path/Op/EngraveBase.py b/src/Mod/CAM/Path/Op/EngraveBase.py index 1c4248695a..cbbaa72368 100644 --- a/src/Mod/CAM/Path/Op/EngraveBase.py +++ b/src/Mod/CAM/Path/Op/EngraveBase.py @@ -24,6 +24,7 @@ from lazy_loader.lazy_loader import LazyLoader import Path import Path.Op.Base as PathOp import Path.Op.Util as PathOpUtil +import PathScripts.PathUtils as PathUtils import copy __doc__ = "Base class for all ops in the engrave family." @@ -61,6 +62,15 @@ class ObjectOp(PathOp.ObjectOp): """buildpathocc(obj, wires, zValues, relZ=False) ... internal helper function to generate engraving commands.""" Path.Log.track(obj.Label, len(wires), zValues) + # sort wires, adapted from Area.py + if len(wires) > 1: + locations = [] + for w in wires: + locations.append({"x": w.BoundBox.Center.x, "y": w.BoundBox.Center.y, "wire": w}) + + locations = PathUtils.sort_locations(locations, ["x", "y"]) + wires = [j["wire"] for j in locations] + decomposewires = [] for wire in wires: decomposewires.extend(PathOpUtil.makeWires(wire.Edges)) From 59f67812a78fd4e4208f7fe333e2f1d4f1977f72 Mon Sep 17 00:00:00 2001 From: Florian Foinant-Willig Date: Mon, 23 Jun 2025 17:39:34 +0200 Subject: [PATCH 110/126] Sketcher: refactor planecgs/Constraints (#21988) * Sketcher: refactor planecgs/Constraints Remove code duplication * Clarify param push with `Copy()` --- src/Mod/Sketcher/App/planegcs/Constraints.cpp | 585 ++---------------- src/Mod/Sketcher/App/planegcs/Constraints.h | 410 ++++++------ 2 files changed, 243 insertions(+), 752 deletions(-) diff --git a/src/Mod/Sketcher/App/planegcs/Constraints.cpp b/src/Mod/Sketcher/App/planegcs/Constraints.cpp index 336d20e3d2..d6efa6ec5c 100644 --- a/src/Mod/Sketcher/App/planegcs/Constraints.cpp +++ b/src/Mod/Sketcher/App/planegcs/Constraints.cpp @@ -83,16 +83,6 @@ void Constraint::rescale(double coef) scale = coef * 1.0; } -double Constraint::error() -{ - return 0.0; -} - -double Constraint::grad(double* /*param*/) -{ - return 0.0; -} - double Constraint::maxStep(MAP_pD_D& /*dir*/, double lim) { return lim; @@ -114,8 +104,8 @@ int Constraint::findParamInPvec(double* param) // -------------------------------------------------------- // Equal ConstraintEqual::ConstraintEqual(double* p1, double* p2, double p1p2ratio) + : ratio(p1p2ratio) { - ratio = p1p2ratio; pvec.push_back(p1); pvec.push_back(p2); origpvec = pvec; @@ -127,11 +117,6 @@ ConstraintType ConstraintEqual::getTypeId() return Equal; } -void ConstraintEqual::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintEqual::error() { return scale * (*param1() - ratio * (*param2())); @@ -171,11 +156,6 @@ ConstraintType ConstraintWeightedLinearCombination::getTypeId() return WeightedLinearCombination; } -void ConstraintWeightedLinearCombination::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintWeightedLinearCombination::error() { // Explanation of the math here: @@ -232,9 +212,10 @@ double ConstraintWeightedLinearCombination::grad(double* param) ConstraintCenterOfGravity::ConstraintCenterOfGravity(const std::vector& givenpvec, const std::vector& givenweights) : weights(givenweights) + , numpoints(givenpvec.size() - 1) { pvec = givenpvec; - numpoints = pvec.size() - 1; + assert(pvec.size() > 1); assert(weights.size() == numpoints); origpvec = pvec; @@ -246,11 +227,6 @@ ConstraintType ConstraintCenterOfGravity::getTypeId() return CenterOfGravity; } -void ConstraintCenterOfGravity::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintCenterOfGravity::error() { double sum = 0; @@ -332,7 +308,7 @@ ConstraintSlopeAtBSplineKnot::ConstraintSlopeAtBSplineKnot(BSpline& b, Line& l, } origpvec = pvec; - rescale(); + ConstraintSlopeAtBSplineKnot::rescale(); } ConstraintType ConstraintSlopeAtBSplineKnot::getTypeId() @@ -555,11 +531,6 @@ void ConstraintPointOnBSpline::setStartPole(double u) } } -void ConstraintPointOnBSpline::rescale(double coef) -{ - scale = coef * 1.0; -} - double ConstraintPointOnBSpline::error() { if (*theparam() < bsp.flattenedknots[startpole + bsp.degree] @@ -666,11 +637,6 @@ ConstraintType ConstraintDifference::getTypeId() return Difference; } -void ConstraintDifference::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintDifference::error() { return scale * (*param2() - *param1() - *difference()); @@ -710,11 +676,6 @@ ConstraintType ConstraintP2PDistance::getTypeId() return P2PDistance; } -void ConstraintP2PDistance::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintP2PDistance::error() { double dx = (*p1x() - *p2x()); @@ -811,11 +772,6 @@ ConstraintType ConstraintP2PAngle::getTypeId() return P2PAngle; } -void ConstraintP2PAngle::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintP2PAngle::error() { double dx = (*p2x() - *p1x()); @@ -897,11 +853,6 @@ ConstraintType ConstraintP2LDistance::getTypeId() return P2LDistance; } -void ConstraintP2LDistance::rescale(double coef) -{ - scale = coef; -} - double ConstraintP2LDistance::error() { double x0 = *p0x(), x1 = *p1x(), x2 = *p2x(); @@ -1041,11 +992,6 @@ ConstraintType ConstraintPointOnLine::getTypeId() return PointOnLine; } -void ConstraintPointOnLine::rescale(double coef) -{ - scale = coef; -} - double ConstraintPointOnLine::error() { double x0 = *p0x(), x1 = *p1x(), x2 = *p2x(); @@ -1123,11 +1069,6 @@ ConstraintType ConstraintPointOnPerpBisector::getTypeId() return PointOnPerpBisector; } -void ConstraintPointOnPerpBisector::rescale(double coef) -{ - scale = coef; -} - void ConstraintPointOnPerpBisector::errorgrad(double* err, double* grad, double* param) { DeriVector2 p0(Point(p0x(), p0y()), param); @@ -1152,26 +1093,6 @@ void ConstraintPointOnPerpBisector::errorgrad(double* err, double* grad, double* } } -double ConstraintPointOnPerpBisector::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintPointOnPerpBisector::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // Parallel @@ -1186,7 +1107,7 @@ ConstraintParallel::ConstraintParallel(Line& l1, Line& l2) pvec.push_back(l2.p2.x); pvec.push_back(l2.p2.y); origpvec = pvec; - rescale(); + ConstraintParallel::rescale(); } ConstraintType ConstraintParallel::getTypeId() @@ -1258,7 +1179,7 @@ ConstraintPerpendicular::ConstraintPerpendicular(Line& l1, Line& l2) pvec.push_back(l2.p2.x); pvec.push_back(l2.p2.y); origpvec = pvec; - rescale(); + ConstraintPerpendicular::rescale(); } ConstraintPerpendicular::ConstraintPerpendicular(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2) @@ -1272,7 +1193,7 @@ ConstraintPerpendicular::ConstraintPerpendicular(Point& l1p1, Point& l1p2, Point pvec.push_back(l2p2.x); pvec.push_back(l2p2.y); origpvec = pvec; - rescale(); + ConstraintPerpendicular::rescale(); } ConstraintType ConstraintPerpendicular::getTypeId() @@ -1372,11 +1293,6 @@ ConstraintType ConstraintL2LAngle::getTypeId() return L2LAngle; } -void ConstraintL2LAngle::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintL2LAngle::error() { double dx1 = (*l1p2x() - *l1p1x()); @@ -1497,11 +1413,6 @@ ConstraintType ConstraintMidpointOnLine::getTypeId() return MidpointOnLine; } -void ConstraintMidpointOnLine::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintMidpointOnLine::error() { double x0 = ((*l1p1x()) + (*l1p2x())) / 2; @@ -1565,8 +1476,9 @@ ConstraintTangentCircumf::ConstraintTangentCircumf(Point& p1, double* rad1, double* rad2, bool internal_) + : internal(internal_) { - internal = internal_; + pvec.push_back(p1.x); pvec.push_back(p1.y); pvec.push_back(p2.x); @@ -1582,11 +1494,6 @@ ConstraintType ConstraintTangentCircumf::getTypeId() return TangentCircumf; } -void ConstraintTangentCircumf::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintTangentCircumf::error() { double dx = (*c1x() - *c2x()); @@ -1659,11 +1566,6 @@ ConstraintType ConstraintPointOnEllipse::getTypeId() return PointOnEllipse; } -void ConstraintPointOnEllipse::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintPointOnEllipse::error() { double X_0 = *p1x(); @@ -1737,12 +1639,13 @@ double ConstraintPointOnEllipse::grad(double* param) // -------------------------------------------------------- // ConstraintEllipseTangentLine ConstraintEllipseTangentLine::ConstraintEllipseTangentLine(Line& l, Ellipse& e) + : l(l) + , e(e) { - this->l = l; - this->l.PushOwnParams(pvec); - this->e = e; + this->l.PushOwnParams(pvec); this->e.PushOwnParams(pvec); // DeepSOIC: hopefully, this won't push arc's parameters + origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -1761,11 +1664,6 @@ ConstraintType ConstraintEllipseTangentLine::getTypeId() return TangentEllipseLine; } -void ConstraintEllipseTangentLine::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEllipseTangentLine::errorgrad(double* err, double* grad, double* param) { // DeepSOIC equation @@ -1803,26 +1701,6 @@ void ConstraintEllipseTangentLine::errorgrad(double* err, double* grad, double* } } -double ConstraintEllipseTangentLine::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEllipseTangentLine::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintInternalAlignmentPoint2Ellipse @@ -1830,13 +1708,13 @@ ConstraintInternalAlignmentPoint2Ellipse::ConstraintInternalAlignmentPoint2Ellip Ellipse& e, Point& p1, InternalAlignmentType alignmentType) + : e(e) + , p(p1) + , AlignmentType(alignmentType) { - this->p = p1; pvec.push_back(p.x); pvec.push_back(p.y); - this->e = e; this->e.PushOwnParams(pvec); - this->AlignmentType = alignmentType; origpvec = pvec; rescale(); } @@ -1857,11 +1735,6 @@ ConstraintType ConstraintInternalAlignmentPoint2Ellipse::getTypeId() return InternalAlignmentPoint2Ellipse; } -void ConstraintInternalAlignmentPoint2Ellipse::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintInternalAlignmentPoint2Ellipse::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -1925,26 +1798,6 @@ void ConstraintInternalAlignmentPoint2Ellipse::errorgrad(double* err, double* gr } } -double ConstraintInternalAlignmentPoint2Ellipse::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintInternalAlignmentPoint2Ellipse::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintInternalAlignmentPoint2Hyperbola @@ -1952,13 +1805,13 @@ ConstraintInternalAlignmentPoint2Hyperbola::ConstraintInternalAlignmentPoint2Hyp Hyperbola& e, Point& p1, InternalAlignmentType alignmentType) + : e(e) + , p(p1) + , AlignmentType(alignmentType) { - this->p = p1; pvec.push_back(p.x); pvec.push_back(p.y); - this->e = e; this->e.PushOwnParams(pvec); - this->AlignmentType = alignmentType; origpvec = pvec; rescale(); } @@ -1979,11 +1832,6 @@ ConstraintType ConstraintInternalAlignmentPoint2Hyperbola::getTypeId() return InternalAlignmentPoint2Hyperbola; } -void ConstraintInternalAlignmentPoint2Hyperbola::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintInternalAlignmentPoint2Hyperbola::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2052,35 +1900,15 @@ void ConstraintInternalAlignmentPoint2Hyperbola::errorgrad(double* err, double* } } -double ConstraintInternalAlignmentPoint2Hyperbola::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintInternalAlignmentPoint2Hyperbola::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintEqualMajorAxesEllipse ConstraintEqualMajorAxesConic::ConstraintEqualMajorAxesConic(MajorRadiusConic* a1, MajorRadiusConic* a2) + : e1(a1) + , e2(a2) { - this->e1 = a1; this->e1->PushOwnParams(pvec); - this->e2 = a2; this->e2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; @@ -2100,11 +1928,6 @@ ConstraintType ConstraintEqualMajorAxesConic::getTypeId() return EqualMajorAxesConic; } -void ConstraintEqualMajorAxesConic::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEqualMajorAxesConic::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2122,26 +1945,6 @@ void ConstraintEqualMajorAxesConic::errorgrad(double* err, double* grad, double* } } -double ConstraintEqualMajorAxesConic::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEqualMajorAxesConic::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // ConstraintEqualFocalDistance ConstraintEqualFocalDistance::ConstraintEqualFocalDistance(ArcOfParabola* a1, ArcOfParabola* a2) { @@ -2167,11 +1970,6 @@ ConstraintType ConstraintEqualFocalDistance::getTypeId() return EqualFocalDistance; } -void ConstraintEqualFocalDistance::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEqualFocalDistance::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2204,37 +2002,17 @@ void ConstraintEqualFocalDistance::errorgrad(double* err, double* grad, double* } } -double ConstraintEqualFocalDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEqualFocalDistance::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintCurveValue -ConstraintCurveValue::ConstraintCurveValue(Point& p, double* pcoord, Curve& crv, double* u) +ConstraintCurveValue::ConstraintCurveValue(Point& p, double* pcoord, Curve& c, double* u) + : crv(c.Copy()) { pvec.push_back(p.x); pvec.push_back(p.y); pvec.push_back(pcoord); pvec.push_back(u); - crv.PushOwnParams(pvec); - this->crv = crv.Copy(); + crv->PushOwnParams(pvec); pvecChangedFlag = true; origpvec = pvec; rescale(); @@ -2264,11 +2042,6 @@ ConstraintType ConstraintCurveValue::getTypeId() return CurveValue; } -void ConstraintCurveValue::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintCurveValue::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2307,26 +2080,6 @@ void ConstraintCurveValue::errorgrad(double* err, double* grad, double* param) } } -double ConstraintCurveValue::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintCurveValue::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - double ConstraintCurveValue::maxStep(MAP_pD_D& /*dir*/, double lim) { return lim; @@ -2366,11 +2119,6 @@ ConstraintType ConstraintPointOnHyperbola::getTypeId() return PointOnHyperbola; } -void ConstraintPointOnHyperbola::rescale(double coef) -{ - scale = coef * 1; -} - double ConstraintPointOnHyperbola::error() { double X_0 = *p1x(); @@ -2457,22 +2205,22 @@ double ConstraintPointOnHyperbola::grad(double* param) // -------------------------------------------------------- // ConstraintPointOnParabola ConstraintPointOnParabola::ConstraintPointOnParabola(Point& p, Parabola& e) + : parab(e.Copy()) { pvec.push_back(p.x); pvec.push_back(p.y); - e.PushOwnParams(pvec); - this->parab = e.Copy(); + parab->PushOwnParams(pvec); pvecChangedFlag = true; origpvec = pvec; rescale(); } ConstraintPointOnParabola::ConstraintPointOnParabola(Point& p, ArcOfParabola& e) + : parab(e.Copy()) { pvec.push_back(p.x); pvec.push_back(p.y); - e.PushOwnParams(pvec); - this->parab = e.Copy(); + parab->PushOwnParams(pvec); pvecChangedFlag = true; origpvec = pvec; rescale(); @@ -2500,11 +2248,6 @@ ConstraintType ConstraintPointOnParabola::getTypeId() return PointOnParabola; } -void ConstraintPointOnParabola::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintPointOnParabola::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -2542,38 +2285,18 @@ void ConstraintPointOnParabola::errorgrad(double* err, double* grad, double* par } } -double ConstraintPointOnParabola::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintPointOnParabola::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintAngleViaPoint ConstraintAngleViaPoint::ConstraintAngleViaPoint(Curve& acrv1, Curve& acrv2, Point p, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p.x); pvec.push_back(p.y); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2605,11 +2328,6 @@ ConstraintType ConstraintAngleViaPoint::getTypeId() return AngleViaPoint; } -void ConstraintAngleViaPoint::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaPoint::error() { if (pvecChangedFlag) { @@ -2662,16 +2380,16 @@ ConstraintAngleViaTwoPoints::ConstraintAngleViaTwoPoints(Curve& acrv1, Point p1, Point p2, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p1.x); pvec.push_back(p1.y); pvec.push_back(p2.x); pvec.push_back(p2.y); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2707,11 +2425,6 @@ ConstraintType ConstraintAngleViaTwoPoints::getTypeId() return AngleViaTwoPoints; } -void ConstraintAngleViaTwoPoints::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaTwoPoints::error() { if (pvecChangedFlag) { @@ -2764,15 +2477,15 @@ ConstraintAngleViaPointAndParam::ConstraintAngleViaPointAndParam(Curve& acrv1, Point p, double* cparam, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p.x); pvec.push_back(p.y); pvec.push_back(cparam); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2805,11 +2518,6 @@ ConstraintType ConstraintAngleViaPointAndParam::getTypeId() return AngleViaPointAndParam; } -void ConstraintAngleViaPointAndParam::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaPointAndParam::error() { if (pvecChangedFlag) { @@ -2863,16 +2571,16 @@ ConstraintAngleViaPointAndTwoParams::ConstraintAngleViaPointAndTwoParams(Curve& double* cparam1, double* cparam2, double* angle) + : crv1(acrv1.Copy()) + , crv2(acrv2.Copy()) { pvec.push_back(angle); pvec.push_back(p.x); pvec.push_back(p.y); pvec.push_back(cparam1); pvec.push_back(cparam2); - acrv1.PushOwnParams(pvec); - acrv2.PushOwnParams(pvec); - crv1 = acrv1.Copy(); - crv2 = acrv2.Copy(); + crv1->PushOwnParams(pvec); + crv2->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; rescale(); @@ -2906,11 +2614,6 @@ ConstraintType ConstraintAngleViaPointAndTwoParams::getTypeId() return AngleViaPointAndTwoParams; } -void ConstraintAngleViaPointAndTwoParams::rescale(double coef) -{ - scale = coef * 1.; -} - double ConstraintAngleViaPointAndTwoParams::error() { if (pvecChangedFlag) { @@ -2959,31 +2662,30 @@ double ConstraintAngleViaPointAndTwoParams::grad(double* param) // -------------------------------------------------------- // ConstraintSnell -ConstraintSnell::ConstraintSnell(Curve& ray1, - Curve& ray2, - Curve& boundary, +ConstraintSnell::ConstraintSnell(Curve& r1, + Curve& r2, + Curve& b, Point p, double* n1, double* n2, bool flipn1, bool flipn2) + : ray1(r1.Copy()) + , ray2(r2.Copy()) + , boundary(b.Copy()) + , flipn1(flipn1) + , flipn2(flipn2) { pvec.push_back(n1); pvec.push_back(n2); pvec.push_back(p.x); pvec.push_back(p.y); - ray1.PushOwnParams(pvec); - ray2.PushOwnParams(pvec); - boundary.PushOwnParams(pvec); - this->ray1 = ray1.Copy(); - this->ray2 = ray2.Copy(); - this->boundary = boundary.Copy(); + ray1->PushOwnParams(pvec); + ray2->PushOwnParams(pvec); + boundary->PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; - this->flipn1 = flipn1; - this->flipn2 = flipn2; - rescale(); } @@ -3017,11 +2719,6 @@ ConstraintType ConstraintSnell::getTypeId() return Snell; } -void ConstraintSnell::rescale(double coef) -{ - scale = coef * 1.; -} - // error and gradient combined. Values are returned through pointers. void ConstraintSnell::errorgrad(double* err, double* grad, double* param) { @@ -3053,35 +2750,14 @@ void ConstraintSnell::errorgrad(double* err, double* grad, double* param) } } -double ConstraintSnell::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintSnell::grad(double* param) -{ - // first of all, check that we need to compute anything. - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return scale * deriv; -} - // -------------------------------------------------------- // ConstraintEqualLineLength ConstraintEqualLineLength::ConstraintEqualLineLength(Line& l1, Line& l2) + : l1(l1) + , l2(l2) { - this->l1 = l1; this->l1.PushOwnParams(pvec); - - this->l2 = l2; this->l2.PushOwnParams(pvec); origpvec = pvec; pvecChangedFlag = true; @@ -3101,11 +2777,6 @@ ConstraintType ConstraintEqualLineLength::getTypeId() return EqualLineLength; } -void ConstraintEqualLineLength::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintEqualLineLength::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -3169,37 +2840,14 @@ void ConstraintEqualLineLength::errorgrad(double* err, double* grad, double* par } } -double ConstraintEqualLineLength::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintEqualLineLength::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - - // -------------------------------------------------------- // ConstraintC2CDistance ConstraintC2CDistance::ConstraintC2CDistance(Circle& c1, Circle& c2, double* d) + : c1(c1) + , c2(c2) { - this->d = d; pvec.push_back(d); - - this->c1 = c1; this->c1.PushOwnParams(pvec); - - this->c2 = c2; this->c2.PushOwnParams(pvec); origpvec = pvec; @@ -3221,11 +2869,6 @@ ConstraintType ConstraintC2CDistance::getTypeId() return C2CDistance; } -void ConstraintC2CDistance::rescale(double coef) -{ - scale = coef * 1; -} - void ConstraintC2CDistance::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -3282,36 +2925,14 @@ void ConstraintC2CDistance::errorgrad(double* err, double* grad, double* param) } } -double ConstraintC2CDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintC2CDistance::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintC2LDistance ConstraintC2LDistance::ConstraintC2LDistance(Circle& c, Line& l, double* d) + : circle(c) + , line(l) { - this->d = d; pvec.push_back(d); - - this->circle = c; this->circle.PushOwnParams(pvec); - - this->line = l; this->line.PushOwnParams(pvec); origpvec = pvec; @@ -3324,11 +2945,6 @@ ConstraintType ConstraintC2LDistance::getTypeId() return C2LDistance; } -void ConstraintC2LDistance::rescale(double coef) -{ - scale = coef; -} - void ConstraintC2LDistance::ReconstructGeomPointers() { int i = 0; @@ -3386,36 +3002,14 @@ void ConstraintC2LDistance::errorgrad(double* err, double* grad, double* param) } } -double ConstraintC2LDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintC2LDistance::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintP2CDistance ConstraintP2CDistance::ConstraintP2CDistance(Point& p, Circle& c, double* d) + : circle(c) + , pt(p) { - this->d = d; pvec.push_back(d); - - this->circle = c; this->circle.PushOwnParams(pvec); - - this->pt = p; this->pt.PushOwnParams(pvec); origpvec = pvec; @@ -3428,11 +3022,6 @@ ConstraintType ConstraintP2CDistance::getTypeId() return P2CDistance; } -void ConstraintP2CDistance::rescale(double coef) -{ - scale = coef; -} - void ConstraintP2CDistance::ReconstructGeomPointers() { int i = 0; @@ -3477,33 +3066,12 @@ void ConstraintP2CDistance::errorgrad(double* err, double* grad, double* param) } } -double ConstraintP2CDistance::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintP2CDistance::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - // -------------------------------------------------------- // ConstraintArcLength ConstraintArcLength::ConstraintArcLength(Arc& a, double* d) + : arc(a) { - this->d = d; pvec.push_back(d); - - this->arc = a; this->arc.PushOwnParams(pvec); origpvec = pvec; @@ -3524,11 +3092,6 @@ ConstraintType ConstraintArcLength::getTypeId() return ArcLength; } -void ConstraintArcLength::rescale(double coef) -{ - scale = coef; -} - void ConstraintArcLength::errorgrad(double* err, double* grad, double* param) { if (pvecChangedFlag) { @@ -3562,24 +3125,4 @@ void ConstraintArcLength::errorgrad(double* err, double* grad, double* param) } } -double ConstraintArcLength::error() -{ - double err; - errorgrad(&err, nullptr, nullptr); - return scale * err; -} - -double ConstraintArcLength::grad(double* param) -{ - if (findParamInPvec(param) == -1) { - return 0.0; - } - - double deriv; - errorgrad(nullptr, &deriv, param); - - return deriv * scale; -} - - } // namespace GCS diff --git a/src/Mod/Sketcher/App/planegcs/Constraints.h b/src/Mod/Sketcher/App/planegcs/Constraints.h index 52d8d0ac36..e9512e51e9 100644 --- a/src/Mod/Sketcher/App/planegcs/Constraints.h +++ b/src/Mod/Sketcher/App/planegcs/Constraints.h @@ -133,7 +133,7 @@ public: virtual ~Constraint() {} - inline VEC_pD params() + VEC_pD params() { return pvec; } @@ -167,10 +167,38 @@ public: return internalAlignment; } + virtual ConstraintType getTypeId(); virtual void rescale(double coef = 1.); - virtual double error(); - virtual double grad(double*); + + // error and gradient combined. Values are returned through pointers. + virtual void errorgrad(double* err, double* grad, double* param) + { + (void)param; + if (err) { + *err = 0.; + } + if (grad) { + *grad = 0.; + } + }; + virtual double error() + { + double err; + errorgrad(&err, nullptr, nullptr); + return scale * err; + }; + virtual double grad(double* param) + { + if (findParamInPvec(param) == -1) { + return 0.0; + } + + double deriv; + errorgrad(nullptr, &deriv, param); + + return deriv * scale; + }; // virtual void grad(MAP_pD_D &deriv); --> TODO: vectorized grad version virtual double maxStep(MAP_pD_D& dir, double lim = 1.); // Finds first occurrence of param in pvec. This is useful to test if a constraint depends @@ -185,11 +213,11 @@ class ConstraintEqual: public Constraint { private: double ratio; - inline double* param1() + double* param1() { return pvec[0]; } - inline double* param2() + double* param2() { return pvec[1]; } @@ -197,7 +225,6 @@ private: public: ConstraintEqual(double* p1, double* p2, double p1p2ratio = 1.0); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -205,11 +232,11 @@ public: // Center of Gravity class ConstraintCenterOfGravity: public Constraint { - inline double* thecenter() + double* thecenter() { return pvec[0]; } - inline double* pointat(size_t i) + double* pointat(size_t i) { return pvec[1 + i]; } @@ -222,7 +249,6 @@ public: ConstraintCenterOfGravity(const std::vector& givenpvec, const std::vector& givenweights); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; @@ -234,15 +260,15 @@ private: // Weighted Linear Combination class ConstraintWeightedLinearCombination: public Constraint { - inline double* thepoint() + double* thepoint() { return pvec[0]; } - inline double* poleat(size_t i) + double* poleat(size_t i) { return pvec[1 + i]; } - inline double* weightat(size_t i) + double* weightat(size_t i) { return pvec[1 + numpoles + i]; } @@ -264,7 +290,6 @@ public: const std::vector& givenpvec, const std::vector& givenfactors); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; @@ -277,31 +302,31 @@ private: class ConstraintSlopeAtBSplineKnot: public Constraint { private: - inline double* polexat(size_t i) + double* polexat(size_t i) { return pvec[i]; } - inline double* poleyat(size_t i) + double* poleyat(size_t i) { return pvec[numpoles + i]; } - inline double* weightat(size_t i) + double* weightat(size_t i) { return pvec[2 * numpoles + i]; } - inline double* linep1x() + double* linep1x() { return pvec[3 * numpoles + 0]; } - inline double* linep1y() + double* linep1y() { return pvec[3 * numpoles + 1]; } - inline double* linep2x() + double* linep2x() { return pvec[3 * numpoles + 2]; } - inline double* linep2y() + double* linep2y() { return pvec[3 * numpoles + 3]; } @@ -325,20 +350,20 @@ private: class ConstraintPointOnBSpline: public Constraint { private: - inline double* thepoint() + double* thepoint() { return pvec[0]; } // TODO: better name because param has a different meaning here? - inline double* theparam() + double* theparam() { return pvec[1]; } - inline double* poleat(size_t i) + double* poleat(size_t i) { return pvec[2 + (startpole + i) % bsp.poles.size()]; } - inline double* weightat(size_t i) + double* weightat(size_t i) { return pvec[2 + bsp.poles.size() + (startpole + i) % bsp.weights.size()]; } @@ -349,7 +374,6 @@ public: /// coordidx = 0 if x, 1 if y ConstraintPointOnBSpline(double* point, double* initparam, int coordidx, BSpline& b); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; size_t numpoints; @@ -361,15 +385,15 @@ public: class ConstraintDifference: public Constraint { private: - inline double* param1() + double* param1() { return pvec[0]; } - inline double* param2() + double* param2() { return pvec[1]; } - inline double* difference() + double* difference() { return pvec[2]; } @@ -377,7 +401,6 @@ private: public: ConstraintDifference(double* p1, double* p2, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -386,23 +409,23 @@ public: class ConstraintP2PDistance: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* p2x() + double* p2x() { return pvec[2]; } - inline double* p2y() + double* p2y() { return pvec[3]; } - inline double* distance() + double* distance() { return pvec[4]; } @@ -410,11 +433,10 @@ private: public: ConstraintP2PDistance(Point& p1, Point& p2, double* d); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintP2PDistance() + ConstraintP2PDistance() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -424,23 +446,23 @@ public: class ConstraintP2PAngle: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* p2x() + double* p2x() { return pvec[2]; } - inline double* p2y() + double* p2y() { return pvec[3]; } - inline double* angle() + double* angle() { return pvec[4]; } @@ -449,11 +471,10 @@ private: public: ConstraintP2PAngle(Point& p1, Point& p2, double* a, double da_ = 0.); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintP2PAngle() + ConstraintP2PAngle() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -463,31 +484,31 @@ public: class ConstraintP2LDistance: public Constraint { private: - inline double* p0x() + double* p0x() { return pvec[0]; } - inline double* p0y() + double* p0y() { return pvec[1]; } - inline double* p1x() + double* p1x() { return pvec[2]; } - inline double* p1y() + double* p1y() { return pvec[3]; } - inline double* p2x() + double* p2x() { return pvec[4]; } - inline double* p2y() + double* p2y() { return pvec[5]; } - inline double* distance() + double* distance() { return pvec[6]; } @@ -495,11 +516,10 @@ private: public: ConstraintP2LDistance(Point& p, Line& l, double* d); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintP2LDistance() + ConstraintP2LDistance() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -510,27 +530,27 @@ public: class ConstraintPointOnLine: public Constraint { private: - inline double* p0x() + double* p0x() { return pvec[0]; } - inline double* p0y() + double* p0y() { return pvec[1]; } - inline double* p1x() + double* p1x() { return pvec[2]; } - inline double* p1y() + double* p1y() { return pvec[3]; } - inline double* p2x() + double* p2x() { return pvec[4]; } - inline double* p2y() + double* p2y() { return pvec[5]; } @@ -539,11 +559,10 @@ public: ConstraintPointOnLine(Point& p, Line& l); ConstraintPointOnLine(Point& p, Point& lp1, Point& lp2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnLine() + ConstraintPointOnLine() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -552,78 +571,74 @@ public: class ConstraintPointOnPerpBisector: public Constraint { private: - inline double* p0x() + double* p0x() { return pvec[0]; } - inline double* p0y() + double* p0y() { return pvec[1]; } - inline double* p1x() + double* p1x() { return pvec[2]; } - inline double* p1y() + double* p1y() { return pvec[3]; } - inline double* p2x() + double* p2x() { return pvec[4]; } - inline double* p2y() + double* p2y() { return pvec[5]; } - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintPointOnPerpBisector(Point& p, Line& l); ConstraintPointOnPerpBisector(Point& p, Point& lp1, Point& lp2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnPerpBisector() {}; + ConstraintPointOnPerpBisector() {}; #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - - double error() override; - double grad(double*) override; }; // Parallel class ConstraintParallel: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } @@ -631,7 +646,7 @@ private: public: ConstraintParallel(Line& l1, Line& l2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintParallel() + ConstraintParallel() {} #endif ConstraintType getTypeId() override; @@ -644,35 +659,35 @@ public: class ConstraintPerpendicular: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } @@ -681,7 +696,7 @@ public: ConstraintPerpendicular(Line& l1, Line& l2); ConstraintPerpendicular(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPerpendicular() + ConstraintPerpendicular() {} #endif ConstraintType getTypeId() override; @@ -694,39 +709,39 @@ public: class ConstraintL2LAngle: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } - inline double* angle() + double* angle() { return pvec[8]; } @@ -735,11 +750,10 @@ public: ConstraintL2LAngle(Line& l1, Line& l2, double* a); ConstraintL2LAngle(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2, double* a); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintL2LAngle() + ConstraintL2LAngle() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; @@ -749,35 +763,35 @@ public: class ConstraintMidpointOnLine: public Constraint { private: - inline double* l1p1x() + double* l1p1x() { return pvec[0]; } - inline double* l1p1y() + double* l1p1y() { return pvec[1]; } - inline double* l1p2x() + double* l1p2x() { return pvec[2]; } - inline double* l1p2y() + double* l1p2y() { return pvec[3]; } - inline double* l2p1x() + double* l2p1x() { return pvec[4]; } - inline double* l2p1y() + double* l2p1y() { return pvec[5]; } - inline double* l2p2x() + double* l2p2x() { return pvec[6]; } - inline double* l2p2y() + double* l2p2y() { return pvec[7]; } @@ -786,11 +800,10 @@ public: ConstraintMidpointOnLine(Line& l1, Line& l2); ConstraintMidpointOnLine(Point& l1p1, Point& l1p2, Point& l2p1, Point& l2p2); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintMidpointOnLine() + ConstraintMidpointOnLine() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -799,27 +812,27 @@ public: class ConstraintTangentCircumf: public Constraint { private: - inline double* c1x() + double* c1x() { return pvec[0]; } - inline double* c1y() + double* c1y() { return pvec[1]; } - inline double* c2x() + double* c2x() { return pvec[2]; } - inline double* c2y() + double* c2y() { return pvec[3]; } - inline double* r1() + double* r1() { return pvec[4]; } - inline double* r2() + double* r2() { return pvec[5]; } @@ -832,17 +845,16 @@ public: double* rd2, bool internal_ = false); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintTangentCircumf(bool internal_) + ConstraintTangentCircumf(bool internal_) { internal = internal_; } #endif - inline bool getInternal() + bool getInternal() { return internal; }; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -850,31 +862,31 @@ public: class ConstraintPointOnEllipse: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* cx() + double* cx() { return pvec[2]; } - inline double* cy() + double* cy() { return pvec[3]; } - inline double* f1x() + double* f1x() { return pvec[4]; } - inline double* f1y() + double* f1y() { return pvec[5]; } - inline double* rmin() + double* rmin() { return pvec[6]; } @@ -883,11 +895,10 @@ public: ConstraintPointOnEllipse(Point& p, Ellipse& e); ConstraintPointOnEllipse(Point& p, ArcOfEllipse& a); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnEllipse() + ConstraintPointOnEllipse() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -899,15 +910,11 @@ private: Ellipse e; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEllipseTangentLine(Line& l, Ellipse& e); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintInternalAlignmentPoint2Ellipse: public Constraint @@ -917,13 +924,9 @@ public: Point& p1, InternalAlignmentType alignmentType); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; private: - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Ellipse e; @@ -938,13 +941,9 @@ public: Point& p1, InternalAlignmentType alignmentType); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; private: - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Hyperbola e; @@ -959,15 +958,11 @@ private: MajorRadiusConic* e2; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEqualMajorAxesConic(MajorRadiusConic* a1, MajorRadiusConic* a2); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintEqualFocalDistance: public Constraint @@ -977,31 +972,26 @@ private: ArcOfParabola* e2; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEqualFocalDistance(ArcOfParabola* a1, ArcOfParabola* a2); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintCurveValue: public Constraint { private: // defines, which coordinate of point is being constrained by this constraint - inline double* pcoord() + double* pcoord() { return pvec[2]; } - inline double* u() + double* u() { return pvec[3]; } - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Curve* crv; @@ -1019,9 +1009,6 @@ public: ConstraintCurveValue(Point& p, double* pcoord, Curve& crv, double* u); ~ConstraintCurveValue() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; double maxStep(MAP_pD_D& dir, double lim = 1.) override; }; @@ -1029,31 +1016,31 @@ public: class ConstraintPointOnHyperbola: public Constraint { private: - inline double* p1x() + double* p1x() { return pvec[0]; } - inline double* p1y() + double* p1y() { return pvec[1]; } - inline double* cx() + double* cx() { return pvec[2]; } - inline double* cy() + double* cy() { return pvec[3]; } - inline double* f1x() + double* f1x() { return pvec[4]; } - inline double* f1y() + double* f1y() { return pvec[5]; } - inline double* rmin() + double* rmin() { return pvec[6]; } @@ -1062,11 +1049,10 @@ public: ConstraintPointOnHyperbola(Point& p, Hyperbola& e); ConstraintPointOnHyperbola(Point& p, ArcOfHyperbola& a); #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnHyperbola() + ConstraintPointOnHyperbola() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1075,8 +1061,7 @@ public: class ConstraintPointOnParabola: public Constraint { private: - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); Parabola* parab; @@ -1087,19 +1072,16 @@ public: ConstraintPointOnParabola(Point& p, ArcOfParabola& a); ~ConstraintPointOnParabola() override; #ifdef _GCS_EXTRACT_SOLVER_SUBSYSTEM_ - inline ConstraintPointOnParabola() + ConstraintPointOnParabola() {} #endif ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintAngleViaPoint: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; @@ -1123,7 +1105,6 @@ public: ConstraintAngleViaPoint(Curve& acrv1, Curve& acrv2, Point p, double* angle); ~ConstraintAngleViaPoint() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1131,7 +1112,7 @@ public: class ConstraintAngleViaTwoPoints: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; @@ -1158,7 +1139,6 @@ public: ConstraintAngleViaTwoPoints(Curve& acrv1, Curve& acrv2, Point p1, Point p2, double* angle); ~ConstraintAngleViaTwoPoints() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1167,11 +1147,11 @@ public: class ConstraintSnell: public Constraint { private: - inline double* n1() + double* n1() { return pvec[0]; }; - inline double* n2() + double* n2() { return pvec[1]; }; @@ -1192,8 +1172,7 @@ private: bool flipn1, flipn2; // writes pointers in pvec to the parameters of crv1, crv2 and poa void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: // n1dn2 = n1 divided by n2. from n1 to n2. flipn1 = true instructs to flip ray1's tangent @@ -1207,19 +1186,16 @@ public: bool flipn2); ~ConstraintSnell() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintAngleViaPointAndParam: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; - inline double* cparam() + double* cparam() { return pvec[3]; }; @@ -1247,7 +1223,6 @@ public: double* angle); ~ConstraintAngleViaPointAndParam() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1256,15 +1231,15 @@ public: class ConstraintAngleViaPointAndTwoParams: public Constraint { private: - inline double* angle() + double* angle() { return pvec[0]; }; - inline double* cparam1() + double* cparam1() { return pvec[3]; }; - inline double* cparam2() + double* cparam2() { return pvec[4]; }; @@ -1292,7 +1267,6 @@ public: double* angle); ~ConstraintAngleViaPointAndTwoParams() override; ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; double error() override; double grad(double*) override; }; @@ -1304,15 +1278,11 @@ private: Line l2; // writes pointers in pvec to the parameters of line1, line2 void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintEqualLineLength(Line& l1, Line& l2); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; class ConstraintC2CDistance: public Constraint @@ -1320,22 +1290,17 @@ class ConstraintC2CDistance: public Constraint private: Circle c1; Circle c2; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } // writes pointers in pvec to the parameters of c1, c2 void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintC2CDistance(Circle& c1, Circle& c2, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; // C2LDistance @@ -1344,22 +1309,17 @@ class ConstraintC2LDistance: public Constraint private: Circle circle; Line line; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } // writes pointers in pvec to the parameters of c, l void ReconstructGeomPointers(); - // error and gradient combined. Values are returned through pointers. - void errorgrad(double* err, double* grad, double* param); + void errorgrad(double* err, double* grad, double* param) override; public: ConstraintC2LDistance(Circle& c, Line& l, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; // P2CDistance @@ -1368,22 +1328,16 @@ class ConstraintP2CDistance: public Constraint private: Circle circle; Point pt; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } void ReconstructGeomPointers(); // writes pointers in pvec to the parameters of c - void - errorgrad(double* err, - double* grad, - double* param); // error and gradient combined. Values are returned through pointers. + void errorgrad(double* err, double* grad, double* param) override; + public: ConstraintP2CDistance(Point& p, Circle& c, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; // ArcLength @@ -1391,22 +1345,16 @@ class ConstraintArcLength: public Constraint { private: Arc arc; - double* d; - inline double* distance() + double* distance() { return pvec[0]; } void ReconstructGeomPointers(); // writes pointers in pvec to the parameters of a - void - errorgrad(double* err, - double* grad, - double* param); // error and gradient combined. Values are returned through pointers. + void errorgrad(double* err, double* grad, double* param) override; + public: ConstraintArcLength(Arc& a, double* d); ConstraintType getTypeId() override; - void rescale(double coef = 1.) override; - double error() override; - double grad(double*) override; }; } // namespace GCS From a7ec15f72519625beb9e86b474c767e2927b1d17 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:44:51 +0100 Subject: [PATCH 111/126] [Measure] Fix seg fault in MeasurePosition::execute if subElements is empty (#22016) * [Measure] take into account if subElements is empty * [Measure] Remove unnecessary else block --- src/Mod/Measure/App/MeasurePosition.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mod/Measure/App/MeasurePosition.cpp b/src/Mod/Measure/App/MeasurePosition.cpp index aa3d3160a2..8c871c29fa 100644 --- a/src/Mod/Measure/App/MeasurePosition.cpp +++ b/src/Mod/Measure/App/MeasurePosition.cpp @@ -95,7 +95,9 @@ App::DocumentObjectExecReturn* MeasurePosition::execute() { const App::DocumentObject* object = Element.getValue(); const std::vector& subElements = Element.getSubValues(); - + if (subElements.empty()) { + return {}; + } App::SubObjectT subject {object, subElements.front().c_str()}; auto info = getMeasureInfo(subject); From f6842ebbf8faf04052e871b06dee9b59bc743e79 Mon Sep 17 00:00:00 2001 From: LarryWoestman <68401843+LarryWoestman@users.noreply.github.com> Date: Mon, 23 Jun 2025 08:45:19 -0700 Subject: [PATCH 112/126] CAM: added command line arguments for finish label, (#21881) output machine name, and post operation. With tests. --- .../CAM/CAMTests/TestRefactoredTestPost.py | 232 ++++++++++++++++++ src/Mod/CAM/Path/Post/UtilsArguments.py | 55 +++++ 2 files changed, 287 insertions(+) diff --git a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py index d1a33d5554..e9cd07cb35 100644 --- a/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py +++ b/src/Mod/CAM/CAMTests/TestRefactoredTestPost.py @@ -882,6 +882,52 @@ G54 ############################################################################# + def test00145(self) -> None: + """Test the finish label argument.""" + # test the default finish label + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + + # test a changed finish label + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(End operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(End operation) +(Begin operation) +(End operation) +(Begin postamble) +""", + "--finish_label='End' --comments", + ) + + ############################################################################# + def test00150(self): """Test output with an empty path. @@ -1178,6 +1224,9 @@ G0 Z8.000 --feed-precision FEED_PRECISION Number of digits of precision for feed rate, default is 3 + --finish_label FINISH_LABEL + The characters to use in the 'Finish operation' + comment, default is "Finish" --header Output headers (default) --no-header Suppress header output --line_number_increment LINE_NUMBER_INCREMENT @@ -1198,6 +1247,16 @@ G0 Z8.000 Output all of the available arguments --no-output_all_arguments Don't output all of the available arguments (default) + --output_machine_name + Output the machine name in the pre-operation + information + --no-output_machine_name + Don't output the machine name in the pre-operation + information (default) + --output_path_labels Output Path labels at the beginning of each Path + --no-output_path_labels + Don't output Path labels at the beginning of each Path + (default) --output_visible_arguments Output all of the visible arguments --no-output_visible_arguments @@ -1205,6 +1264,9 @@ G0 Z8.000 --postamble POSTAMBLE Set commands to be issued after the last command, default is "" + --post_operation POST_OPERATION + Set commands to be issued after every operation, + default is "" --preamble PREAMBLE Set commands to be issued before the first command, default is "" --precision PRECISION @@ -1293,6 +1355,146 @@ G0 X2.000 ############################################################################# + def test00205(self) -> None: + """Test output_machine_name argument.""" + # test the default behavior + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + + # test outputting the machine name + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +(Machine: test, mm/min) +G54 +(Finish operation) +(Begin operation) +(Machine: test, mm/min) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Machine: test, mm/min) +(Finish operation) +(Begin postamble) +""", + "--output_machine_name --comments", + ) + + # test not outputting the machine name + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--no-output_machine_name --comments", + ) + + ############################################################################# + + def test00206(self) -> None: + """Test output_path_labels argument.""" + # test the default behavior + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--comments", + ) + + # test outputting the path labels + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +(Path: Fixture) +G54 +(Finish operation) +(Begin operation) +(Path: TC: Default Tool) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Path: Profile) +(Finish operation) +(Begin postamble) +""", + "--output_path_labels --comments", + ) + + # test not outputting the path labels + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +(Begin operation) +(Finish operation) +(Begin postamble) +""", + "--no-output_path_labels --comments", + ) + + ############################################################################# + def test00210(self): """Test Post-amble.""" nl = "\n" @@ -1307,6 +1509,36 @@ G0 X2.000 ############################################################################# + def test00215(self) -> None: + """Test the post_operation argument.""" + self.multi_compare( + [], + """(Begin preamble) +G90 +G21 +(Begin operation) +G54 +(Finish operation) +G90 G80 +G40 G49 +(Begin operation) +(TC: Default Tool) +(Begin toolchange) +(M6 T1) +(Finish operation) +G90 G80 +G40 G49 +(Begin operation) +(Finish operation) +G90 G80 +G40 G49 +(Begin postamble) +""", + "--comments --post_operation='G90 G80\nG40 G49'", + ) + + ############################################################################# + def test00220(self): """Test Pre-amble.""" nl = "\n" diff --git a/src/Mod/CAM/Path/Post/UtilsArguments.py b/src/Mod/CAM/Path/Post/UtilsArguments.py index 3642bbad35..c75cd5b50a 100644 --- a/src/Mod/CAM/Path/Post/UtilsArguments.py +++ b/src/Mod/CAM/Path/Post/UtilsArguments.py @@ -82,6 +82,8 @@ def init_argument_defaults(argument_defaults: Dict[str, bool]) -> None: argument_defaults["metric_inches"] = True argument_defaults["modal"] = False argument_defaults["output_all_arguments"] = False + argument_defaults["output_machine_name"] = False + argument_defaults["output_path_labels"] = False argument_defaults["output_visible_arguments"] = False argument_defaults["show-editor"] = True argument_defaults["tlo"] = True @@ -102,6 +104,7 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None: arguments_visible["enable_machine_specific_commands"] = False arguments_visible["end_of_line_characters"] = False arguments_visible["feed-precision"] = True + arguments_visible["finish_label"] = False arguments_visible["header"] = True arguments_visible["line_number_increment"] = False arguments_visible["line_number_start"] = False @@ -110,8 +113,11 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None: arguments_visible["metric_inches"] = True arguments_visible["modal"] = True arguments_visible["output_all_arguments"] = True + arguments_visible["output_machine_name"] = False + arguments_visible["output_path_labels"] = False arguments_visible["output_visible_arguments"] = True arguments_visible["postamble"] = True + arguments_visible["post_operation"] = False arguments_visible["preamble"] = True arguments_visible["precision"] = True arguments_visible["return-to"] = False @@ -263,6 +269,17 @@ def init_shared_arguments( type=int, help=help_message, ) + if arguments_visible["finish_label"]: + help_message = ( + "The characters to use in the 'Finish operation' comment, " + f'default is "{values["FINISH_LABEL"]}"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--finish_label", + help=help_message, + ) add_flag_type_arguments( shared, argument_defaults["header"], @@ -332,6 +349,24 @@ def init_shared_arguments( "Don't output all of the available arguments", arguments_visible["output_all_arguments"], ) + add_flag_type_arguments( + shared, + argument_defaults["output_machine_name"], + "--output_machine_name", + "--no-output_machine_name", + "Output the machine name in the pre-operation information", + "Don't output the machine name in the pre-operation information", + arguments_visible["output_machine_name"], + ) + add_flag_type_arguments( + shared, + argument_defaults["output_path_labels"], + "--output_path_labels", + "--no-output_path_labels", + "Output Path labels at the beginning of each Path", + "Don't output Path labels at the beginning of each Path", + arguments_visible["output_path_labels"], + ) add_flag_type_arguments( shared, argument_defaults["output_visible_arguments"], @@ -349,6 +384,14 @@ def init_shared_arguments( else: help_message = argparse.SUPPRESS shared.add_argument("--postamble", help=help_message) + if arguments_visible["post_operation"]: + help_message = ( + f"Set commands to be issued after every operation, " + f'default is "{values["POST_OPERATION"]}"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument("--post_operation", help=help_message) if arguments_visible["preamble"]: help_message = ( f"Set commands to be issued before the first command, " @@ -817,6 +860,8 @@ def process_shared_arguments( values["END_OF_LINE_CHARACTERS"] = "\r\n" else: print("invalid end_of_line_characters, ignoring") + if args.finish_label: + values["FINISH_LABEL"] = args.finish_label if args.header: values["OUTPUT_HEADER"] = True if args.no_header: @@ -837,8 +882,18 @@ def process_shared_arguments( values["MODAL"] = True if args.no_modal: values["MODAL"] = False + if args.output_machine_name: + values["OUTPUT_MACHINE_NAME"] = True + if args.no_output_machine_name: + values["OUTPUT_MACHINE_NAME"] = False + if args.output_path_labels: + values["OUTPUT_PATH_LABELS"] = True + if args.no_output_path_labels: + values["OUTPUT_PATH_LABELS"] = False if args.postamble is not None: values["POSTAMBLE"] = args.postamble.replace("\\n", "\n") + if args.post_operation is not None: + values["POST_OPERATION"] = args.post_operation.replace("\\n", "\n") if args.preamble is not None: values["PREAMBLE"] = args.preamble.replace("\\n", "\n") if args.return_to != "": From b749db373d2d325bb469b63b78c170a865fbe19b Mon Sep 17 00:00:00 2001 From: Florian Foinant-Willig Date: Mon, 16 Jun 2025 22:25:13 +0200 Subject: [PATCH 113/126] Sketcher: Fix circle-line negative distance --- src/Mod/Sketcher/App/planegcs/Constraints.cpp | 14 ++++++++++++-- src/Mod/Sketcher/Gui/CommandConstraints.cpp | 8 ++++---- .../Sketcher/SketcherTests/TestSketcherSolver.py | 3 +-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Mod/Sketcher/App/planegcs/Constraints.cpp b/src/Mod/Sketcher/App/planegcs/Constraints.cpp index d6efa6ec5c..b6f564ca2b 100644 --- a/src/Mod/Sketcher/App/planegcs/Constraints.cpp +++ b/src/Mod/Sketcher/App/planegcs/Constraints.cpp @@ -2990,11 +2990,21 @@ void ConstraintC2LDistance::errorgrad(double* err, double* grad, double* param) double dh = (darea - h * dlength) / length; if (err) { - *err = *distance() + *circle.rad - h; + if (h < *circle.rad) { + *err = *circle.rad - *distance() - h; + } + else { + *err = *circle.rad + *distance() - h; + } } else if (grad) { if (param == distance() || param == circle.rad) { - *grad = 1.0; + if (h < *circle.rad) { + *grad = -1.0; + } + else { + *grad = 1.0; + } } else { *grad = -dh; diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.cpp b/src/Mod/Sketcher/Gui/CommandConstraints.cpp index 3619111a05..894cd063c3 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.cpp +++ b/src/Mod/Sketcher/Gui/CommandConstraints.cpp @@ -2259,10 +2259,10 @@ protected: Base::Vector3d pnt1 = lineSeg->getStartPoint(); Base::Vector3d pnt2 = lineSeg->getEndPoint(); Base::Vector3d d = pnt2 - pnt1; - double ActDist = - std::abs(-center1.x * d.y + center1.y * d.x + pnt1.x * pnt2.y - pnt2.x * pnt1.y) - / d.Length() - - radius1; + double ActDist = std::abs( + std::abs(-center1.x * d.y + center1.y * d.x + pnt1.x * pnt2.y - pnt2.x * pnt1.y) + / d.Length() + - radius1); Gui::cmdAppObjectArgs(Obj, "addConstraint(Sketcher.Constraint('Distance',%d,%d,%f))", diff --git a/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py b/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py index 00c9676ac0..e88c6dc6e7 100644 --- a/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py +++ b/src/Mod/Sketcher/SketcherTests/TestSketcherSolver.py @@ -464,8 +464,7 @@ class TestSketcherSolver(unittest.TestCase): sketch.addConstraint( [Sketcher.Constraint("Block", c_idx), Sketcher.Constraint("Block", l_idx)] ) - # use a negative distance to tell "line is within the circle" - expected_distance = -radius / 2 # note that we don't set this in the constraint below! + expected_distance = radius / 2 # note that we don't set this in the constraint below! # TODO: addConstraint(constraint) triggers a solve (for godd reasons) however, this way # one cannot add non-driving constraints. In contrast, addConstraint(list(constraint)) # does not solve automatically, thus we use this "overload". From 8db55284cc8cdc07ff1791711d13eb59b5a3ea36 Mon Sep 17 00:00:00 2001 From: Karliss Date: Tue, 17 Jun 2025 12:29:39 +0300 Subject: [PATCH 114/126] Sketcher: Implement related constraint command for non edges --- src/Mod/Sketcher/Gui/CommandSketcherTools.cpp | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp index 6c007b8761..13ccbea6cd 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp @@ -375,7 +375,7 @@ void CmdSketcherSelectConstraints::activated(int iMsg) // get the needed lists and objects const std::vector& SubNames = selection[0].getSubNames(); Sketcher::SketchObject* Obj = static_cast(selection[0].getObject()); - const std::vector& vals = Obj->Constraints.getValues(); + const std::vector& constraints = Obj->Constraints.getValues(); std::string doc_name = Obj->getDocument()->getName(); std::string obj_name = Obj->getNameInDocument(); @@ -384,23 +384,26 @@ void CmdSketcherSelectConstraints::activated(int iMsg) std::vector constraintSubNames; // go through the selected subelements - for (std::vector::const_iterator it = SubNames.begin(); it != SubNames.end(); - ++it) { - // only handle edges - if (it->size() > 4 && it->substr(0, 4) == "Edge") { - int GeoId = std::atoi(it->substr(4, 4000).c_str()) - 1; - - // push all the constraints - int i = 0; - for (std::vector::const_iterator it = vals.begin(); - it != vals.end(); - ++it, ++i) { - if ((*it)->First == GeoId || (*it)->Second == GeoId || (*it)->Third == GeoId) { - constraintSubNames.push_back( - Sketcher::PropertyConstraintList::getConstraintName(i)); - } + int i = 0; + for (auto const& constraint : constraints) { + auto isRelated = [&] (const std::string& subName){ + int geoId; + PointPos pointPos; + Data::IndexedName name = Obj->checkSubName(subName.c_str()); + if (!Obj->geoIdFromShapeType(name, geoId, pointPos)) { + return false; } + if (pointPos != PointPos::none) { + return constraint->involvesGeoIdAndPosId(geoId, pointPos); + } else { + return constraint->involvesGeoId(geoId); + } + }; + + if (std::ranges::any_of(SubNames, isRelated)) { + constraintSubNames.push_back(PropertyConstraintList::getConstraintName(i)); } + ++i; } if (!constraintSubNames.empty()) From 565190fa9dcbc9508252530b8d30d07ea8a51d3f Mon Sep 17 00:00:00 2001 From: George Peden Date: Mon, 23 Jun 2025 09:09:59 -0700 Subject: [PATCH 115/126] Sketcher: Add contextual input hints to transform tools (InputHints Phase 4) (#21840) * Add hints to symettry tool * Add hint system for transform tools - Design decision: Keep hints simple and focused on primary mouse actions - Avoid redundancy with dialog UI which already shows keyboard shortcuts clearly - Implements progressive hints for multi-state tools (Rotate, Scale, Translate) using declarative hint tables, and focused hints for single-state tools (Symmetry, Offset) using direct return implementations. * Cleanup unused declarative hint decls * Change hint to 'pick axis, edge, or point" per PR feedback --- .../Sketcher/Gui/DrawSketchHandlerOffset.h | 9 ++++ .../Sketcher/Gui/DrawSketchHandlerRotate.h | 41 ++++++++++++++++++ src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h | 40 +++++++++++++++++ .../Sketcher/Gui/DrawSketchHandlerSymmetry.h | 9 ++++ .../Sketcher/Gui/DrawSketchHandlerTranslate.h | 43 +++++++++++++++++++ 5 files changed, 142 insertions(+) diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h index c143c0223e..30ea54eb0e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h @@ -191,6 +191,15 @@ private: generateSourceWires(); } +public: + std::list getToolHints() const override + { + using enum Gui::InputHint::UserInput; + + return {{QObject::tr("%1 set offset direction and distance", "Sketcher Offset: hint"), + {MouseLeft}}}; + } + private: class CoincidencePointPos { diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h index 4baf4773b1..fb7ce2f00b 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h @@ -455,8 +455,49 @@ private: return pointToRotate; } + + struct HintEntry + { + SelectMode state; + std::list hints; + }; + + using HintTable = std::vector; + + static HintTable getRotateHintTable(); + static std::list lookupRotateHints(SelectMode state); + +public: + std::list getToolHints() const override + { + return lookupRotateHints(state()); + } }; +DrawSketchHandlerRotate::HintTable DrawSketchHandlerRotate::getRotateHintTable() +{ + using enum Gui::InputHint::UserInput; + + return { + {.state = SelectMode::SeekFirst, + .hints = {{QObject::tr("%1 pick center point", "Sketcher Rotate: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekSecond, + .hints = {{QObject::tr("%1 set start angle", "Sketcher Rotate: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekThird, + .hints = {{QObject::tr("%1 set rotation angle", "Sketcher Rotate: hint"), {MouseLeft}}}}}; +} + +std::list DrawSketchHandlerRotate::lookupRotateHints(SelectMode state) +{ + const auto rotateHintTable = getRotateHintTable(); + + auto it = std::ranges::find_if(rotateHintTable, [state](const HintEntry& entry) { + return entry.state == state; + }); + + return (it != rotateHintTable.end()) ? it->hints : std::list {}; +} + template<> auto DSHRotateControllerBase::getState(int labelindex) const { diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h index 30292f6e78..28e155ea88 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h @@ -132,6 +132,11 @@ public: } + std::list getToolHints() const override + { + return lookupScaleHints(state()); + } + private: void updateDataAndDrawToPosition(Base::Vector2d onSketchPos) override { @@ -228,6 +233,17 @@ private: bool allowOriginConstraint; // Conserve constraints with origin double refLength, length, scaleFactor; + struct HintEntry + { + SelectMode state; + std::list hints; + }; + + using HintTable = std::vector; + + static HintTable getScaleHintTable(); + static std::list lookupScaleHints(SelectMode state); + void deleteOriginalGeos() { @@ -447,6 +463,30 @@ private: } }; +DrawSketchHandlerScale::HintTable DrawSketchHandlerScale::getScaleHintTable() +{ + using enum Gui::InputHint::UserInput; + + return { + {.state = SelectMode::SeekFirst, + .hints = {{QObject::tr("%1 pick reference point", "Sketcher Scale: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekSecond, + .hints = {{QObject::tr("%1 set reference length", "Sketcher Scale: hint"), {MouseLeft}}}}, + {.state = SelectMode::SeekThird, + .hints = {{QObject::tr("%1 set scale factor", "Sketcher Scale: hint"), {MouseLeft}}}}}; +} + +std::list DrawSketchHandlerScale::lookupScaleHints(SelectMode state) +{ + const auto scaleHintTable = getScaleHintTable(); + + auto it = std::ranges::find_if(scaleHintTable, [state](const HintEntry& entry) { + return entry.state == state; + }); + + return (it != scaleHintTable.end()) ? it->hints : std::list {}; +} + template<> auto DSHScaleControllerBase::getState(int labelindex) const { diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h index bda7afdfbf..bb6988c17e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h @@ -215,6 +215,15 @@ private: Sketcher::PointPos refPosId; bool deleteOriginal, createSymConstraints; +public: + std::list getToolHints() const override + { + using enum Gui::InputHint::UserInput; + + return { + {QObject::tr("%1 pick axis, edge, or point", "Sketcher Symmetry: hint"), {MouseLeft}}}; + } + void deleteOriginalGeos() { std::stringstream stream; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h index 9e19d89b57..0e524a5e76 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h @@ -426,8 +426,51 @@ private: } } } + + struct HintEntry + { + SelectMode state; + std::list hints; + }; + + using HintTable = std::vector; + + static HintTable getTranslateHintTable(); + static std::list lookupTranslateHints(SelectMode state); + +public: + std::list getToolHints() const override + { + return lookupTranslateHints(state()); + } }; +DrawSketchHandlerTranslate::HintTable DrawSketchHandlerTranslate::getTranslateHintTable() +{ + using enum Gui::InputHint::UserInput; + + return {{.state = SelectMode::SeekFirst, + .hints = {{QObject::tr("%1 pick reference point", "Sketcher Translate: hint"), + {MouseLeft}}}}, + {.state = SelectMode::SeekSecond, + .hints = {{QObject::tr("%1 set translation vector", "Sketcher Translate: hint"), + {MouseLeft}}}}, + {.state = SelectMode::SeekThird, + .hints = {{QObject::tr("%1 set second translation vector", "Sketcher Translate: hint"), + {MouseLeft}}}}}; +} + +std::list DrawSketchHandlerTranslate::lookupTranslateHints(SelectMode state) +{ + const auto translateHintTable = getTranslateHintTable(); + + auto it = std::ranges::find_if(translateHintTable, [state](const HintEntry& entry) { + return entry.state == state; + }); + + return (it != translateHintTable.end()) ? it->hints : std::list {}; +} + template<> auto DSHTranslateControllerBase::getState(int labelindex) const { From c25782551b8548cfb420ce0b1bebad73eb262ad9 Mon Sep 17 00:00:00 2001 From: mosfet80 <10235105+mosfet80@users.noreply.github.com> Date: Sat, 21 Jun 2025 08:58:47 +0200 Subject: [PATCH 116/126] fix unused variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove unused parameter ‘method’ --- src/Gui/propertyeditor/PropertyItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/propertyeditor/PropertyItem.cpp b/src/Gui/propertyeditor/PropertyItem.cpp index 4bf7a42bb1..69e3c5b5eb 100644 --- a/src/Gui/propertyeditor/PropertyItem.cpp +++ b/src/Gui/propertyeditor/PropertyItem.cpp @@ -1376,7 +1376,7 @@ void PropertyBoolItem::setValue(const QVariant& value) } QWidget* PropertyBoolItem::createEditor(QWidget* parent, - const std::function& method, + const std::function& /*method*/, FrameOption /*frameOption*/) const { auto checkbox = new QCheckBox(parent); From 51184b99d535d201e27f2a0bea6f36a1fb0fe902 Mon Sep 17 00:00:00 2001 From: WandererFan Date: Mon, 23 Jun 2025 12:13:27 -0400 Subject: [PATCH 117/126] [TechDraw]Detail highlight drag (fix #21828) (#22036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [TD]add preferences for detail highlight snapping * [TD]fix highlight drag issues * Update src/Mod/TechDraw/Gui/TaskDetail.cpp minor format change from benj5378. Co-authored-by: Benjamin Bræstrup Sayoc --------- Co-authored-by: Benjamin Bræstrup Sayoc --- src/Mod/TechDraw/App/DrawViewDetail.cpp | 7 + src/Mod/TechDraw/App/DrawViewPart.cpp | 32 +++++ src/Mod/TechDraw/App/DrawViewPart.h | 3 + src/Mod/TechDraw/App/Preferences.cpp | 12 ++ src/Mod/TechDraw/App/Preferences.h | 3 + .../TechDraw/Gui/DlgPrefsTechDrawGeneral.ui | 103 ++++++++----- .../Gui/DlgPrefsTechDrawGeneralImp.cpp | 5 + src/Mod/TechDraw/Gui/QGIHighlight.cpp | 2 + src/Mod/TechDraw/Gui/QGIViewPart.cpp | 18 ++- src/Mod/TechDraw/Gui/TaskDetail.cpp | 136 +++++++++--------- src/Mod/TechDraw/Gui/TaskDetail.h | 1 + src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp | 32 +++-- src/Mod/TechDraw/Gui/ViewProviderViewPart.h | 1 + 13 files changed, 230 insertions(+), 125 deletions(-) diff --git a/src/Mod/TechDraw/App/DrawViewDetail.cpp b/src/Mod/TechDraw/App/DrawViewDetail.cpp index 4e7f322d0e..3a9d0a67e3 100644 --- a/src/Mod/TechDraw/App/DrawViewDetail.cpp +++ b/src/Mod/TechDraw/App/DrawViewDetail.cpp @@ -400,6 +400,13 @@ void DrawViewDetail::postHlrTasks(void) Scale.purgeTouched(); detailExec(m_saveShape, m_saveDvp, m_saveDvs); } + + auto* baseView = freecad_cast(BaseView.getValue()); + if (!baseView) { + throw Base::RuntimeError("Detail has no base view!"); + } + baseView->requestPaint(); // repaint the highlight on the base view. + overrideKeepUpdated(false); } diff --git a/src/Mod/TechDraw/App/DrawViewPart.cpp b/src/Mod/TechDraw/App/DrawViewPart.cpp index abc3157730..1b866a095b 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.cpp +++ b/src/Mod/TechDraw/App/DrawViewPart.cpp @@ -718,6 +718,38 @@ void DrawViewPart::onFacesFinished() requestPaint(); } + +//! returns the position of the first visible vertex within snap radius of newAnchorPoint. newAnchorPoint +//! should be unscaled in conventional coordinates. if no suitable vertex is found, newAnchorPoint +//! is returned. the result is unscaled and inverted? +Base::Vector3d DrawViewPart::snapHighlightToVertex(Base::Vector3d newAnchorPoint, + double radius) const +{ + if (!Preferences::snapDetailHighlights()) { + return newAnchorPoint; + } + + double snapRadius = radius * Preferences::detailSnapRadius(); + double dvpScale = getScale(); + std::vector vertexPoints; + auto vertsAll = getVertexGeometry(); + double nearDistance{std::numeric_limits::max()}; + Base::Vector3d nearPoint{newAnchorPoint}; + for (auto& vert: vertsAll) { + if (vert->getHlrVisible()) { + Base::Vector3d vertPointUnscaled = DU::invertY(vert->point()) / dvpScale; + double distanceToVertex = (vertPointUnscaled - newAnchorPoint).Length(); + if (distanceToVertex < snapRadius && + distanceToVertex < nearDistance) { + nearDistance = distanceToVertex; + nearPoint = vertPointUnscaled; + } + } + } + return nearPoint; +} + + //retrieve all the face hatches associated with this dvp std::vector DrawViewPart::getHatches() const { diff --git a/src/Mod/TechDraw/App/DrawViewPart.h b/src/Mod/TechDraw/App/DrawViewPart.h index 4b29b9db30..c4a971781d 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.h +++ b/src/Mod/TechDraw/App/DrawViewPart.h @@ -243,6 +243,9 @@ public: bool isCosmeticEdge(const std::string& element); bool isCenterLine(const std::string& element); + Base::Vector3d snapHighlightToVertex(Base::Vector3d newAnchorPoint, double radius) const; + + public Q_SLOTS: void onHlrFinished(void); void onFacesFinished(void); diff --git a/src/Mod/TechDraw/App/Preferences.cpp b/src/Mod/TechDraw/App/Preferences.cpp index f65a583042..c8dcee130b 100644 --- a/src/Mod/TechDraw/App/Preferences.cpp +++ b/src/Mod/TechDraw/App/Preferences.cpp @@ -693,3 +693,15 @@ bool Preferences::showUnits() } +bool Preferences::snapDetailHighlights() +{ + return Preferences::getPreferenceGroup("General")->GetBool("SnapHighlights", true); +} + + +//! distance within which we should snap a highlight to a vertex +double Preferences::detailSnapRadius() +{ + return getPreferenceGroup("General")->GetFloat("DetailSnapRadius", 0.6); +} + diff --git a/src/Mod/TechDraw/App/Preferences.h b/src/Mod/TechDraw/App/Preferences.h index 09689d6994..ea823bbb83 100644 --- a/src/Mod/TechDraw/App/Preferences.h +++ b/src/Mod/TechDraw/App/Preferences.h @@ -163,6 +163,9 @@ public: static bool showUnits(); + static bool snapDetailHighlights(); + static double detailSnapRadius(); + }; diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui index 8df8ba9242..fb821d651e 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui @@ -6,8 +6,8 @@ 0 0 - 578 - 1073 + 676 + 1200 @@ -254,7 +254,7 @@ for ProjectionGroups - + 0 @@ -264,10 +264,7 @@ for ProjectionGroups Label size - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + 8.000000000000000 @@ -449,7 +446,7 @@ for ProjectionGroups - + 1 @@ -468,7 +465,7 @@ for ProjectionGroups - + 1 @@ -487,7 +484,7 @@ for ProjectionGroups - + 1 @@ -518,7 +515,7 @@ for ProjectionGroups - + 1 @@ -537,7 +534,7 @@ for ProjectionGroups - + 1 @@ -547,9 +544,6 @@ for ProjectionGroups Default directory for welding symbols - - Gui::FileChooser::Directory - WeldingDir @@ -559,7 +553,7 @@ for ProjectionGroups - + 1 @@ -569,9 +563,6 @@ for ProjectionGroups Starting directory for menu 'Insert Page using Template' - - Gui::FileChooser::Directory - TemplateDir @@ -588,7 +579,7 @@ for ProjectionGroups - + 1 @@ -598,9 +589,6 @@ for ProjectionGroups Alternate directory to search for SVG symbol files. - - Gui::FileChooser::Directory - DirSymbol @@ -752,14 +740,11 @@ for ProjectionGroups - + Distance between Page grid lines. - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + 10.000000000000000 @@ -899,7 +884,7 @@ for ProjectionGroups - + @@ -919,6 +904,44 @@ for ProjectionGroups + + + + Check this box if you want detail view highlights to snap to the nearest vertex when dragging in TaskDetail. + + + Snap Detail Highlights + + + true + + + SnapHighlights + + + /Mod/TechDraw/General + + + + + + + When dragging a view, if it is within this fraction of view size of the correct alignment, it will snap into alignment. + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0.050000000000000 + + + SnapLimitFactor + + + /Mod/TechDraw/General + + + @@ -939,23 +962,31 @@ for ProjectionGroups - - + + + + Highlight SnappingFactor + + + + + - When dragging a view, if it is within this fraction of view size of the correct alignment, it will snap into alignment. + Controls the snap radius for highlights. Vertex must be within this factor times the highlight size to be a snap target. Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 0.050000000000000 + 0.600000000000000 - SnapLimitFactor + DetailSnapRadius /Mod/TechDraw/General + @@ -1046,8 +1077,6 @@ for ProjectionGroups

Gui/PrefWidgets.h
- - - + diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp index 30f04ee679..6cc3c89269 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp @@ -81,6 +81,8 @@ void DlgPrefsTechDrawGeneralImp::saveSettings() ui->cb_alwaysShowLabel->onSave(); ui->cb_SnapViews->onSave(); ui->psb_SnapFactor->onSave(); + ui->cb_SnapHighlights->onSave(); + ui->psb_HighlightSnapFactor->onSave(); } void DlgPrefsTechDrawGeneralImp::loadSettings() @@ -129,6 +131,9 @@ void DlgPrefsTechDrawGeneralImp::loadSettings() ui->cb_SnapViews->onRestore(); ui->psb_SnapFactor->onRestore(); + + ui->cb_SnapHighlights->onRestore(); + ui->psb_HighlightSnapFactor->onRestore(); } /** diff --git a/src/Mod/TechDraw/Gui/QGIHighlight.cpp b/src/Mod/TechDraw/Gui/QGIHighlight.cpp index a2886b657c..9fb0bc2e32 100644 --- a/src/Mod/TechDraw/Gui/QGIHighlight.cpp +++ b/src/Mod/TechDraw/Gui/QGIHighlight.cpp @@ -64,6 +64,8 @@ QGIHighlight::~QGIHighlight() } + +// QGIHighlight is no longer dragged except through TaskDetail. void QGIHighlight::onDragFinished() { // Base::Console().message("QGIH::onDragFinished - pos: %s\n", diff --git a/src/Mod/TechDraw/Gui/QGIViewPart.cpp b/src/Mod/TechDraw/Gui/QGIViewPart.cpp index 095fa93f14..8f2a7cafc3 100644 --- a/src/Mod/TechDraw/Gui/QGIViewPart.cpp +++ b/src/Mod/TechDraw/Gui/QGIViewPart.cpp @@ -930,7 +930,7 @@ void QGIViewPart::drawAllHighlights() void QGIViewPart::drawHighlight(TechDraw::DrawViewDetail* viewDetail, bool b) { - TechDraw::DrawViewPart* viewPart = static_cast(getViewObject()); + auto* viewPart = static_cast(getViewObject()); if (!viewPart || !viewDetail) { return; } @@ -950,14 +950,16 @@ void QGIViewPart::drawHighlight(TechDraw::DrawViewDetail* viewDetail, bool b) if (b) { double fontSize = Preferences::labelFontSizeMM(); - QGIHighlight* highlight = new QGIHighlight(); + auto* highlight = new QGIHighlight(); + scene()->addItem(highlight); highlight->setReference(viewDetail->Reference.getValue()); Base::Color color = Preferences::getAccessibleColor(vp->HighlightLineColor.getValue()); highlight->setColor(color.asValue()); highlight->setFeatureName(viewDetail->getNameInDocument()); - highlight->setInteractive(true); + + highlight->setInteractive(false); addToGroup(highlight); highlight->setPos(0.0, 0.0);//sb setPos(center.x, center.y)? @@ -986,20 +988,26 @@ void QGIViewPart::drawHighlight(TechDraw::DrawViewDetail* viewDetail, bool b) } } +//! this method is no longer used due to conflicts with TaskDetail dialog highlight drag void QGIViewPart::highlightMoved(QGIHighlight* highlight, QPointF newPos) { std::string highlightName = highlight->getFeatureName(); App::Document* doc = getViewObject()->getDocument(); App::DocumentObject* docObj = doc->getObject(highlightName.c_str()); auto detail = freecad_cast(docObj); - if (detail) { + auto baseView = freecad_cast(getViewObject()); + if (detail && baseView) { auto oldAnchor = detail->AnchorPoint.getValue(); Base::Vector3d delta = Rez::appX(DrawUtil::toVector3d(newPos)) / getViewObject()->getScale(); delta = DrawUtil::invertY(delta); - detail->AnchorPoint.setValue(oldAnchor + delta); + Base::Vector3d newAnchorPoint = oldAnchor + delta; + newAnchorPoint = baseView->snapHighlightToVertex(newAnchorPoint, + detail->Radius.getValue()); + detail->AnchorPoint.setValue(newAnchorPoint); } } + void QGIViewPart::drawMatting() { auto viewPart(dynamic_cast(getViewObject())); diff --git a/src/Mod/TechDraw/Gui/TaskDetail.cpp b/src/Mod/TechDraw/Gui/TaskDetail.cpp index 473674a785..f4a3776a16 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.cpp +++ b/src/Mod/TechDraw/Gui/TaskDetail.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include "ui_TaskDetail.h" #include "TaskDetail.h" @@ -59,7 +60,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): m_ghost(nullptr), m_detailFeat(nullptr), m_baseFeat(baseFeat), - m_basePage(nullptr), + m_basePage(m_baseFeat->findParentPage()), m_qgParent(nullptr), m_inProgressLock(false), m_btnOK(nullptr), @@ -67,9 +68,6 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): m_saveAnchor(Base::Vector3d(0.0, 0.0, 0.0)), m_saveRadius(0.0), m_saved(false), - m_baseName(std::string()), - m_pageName(std::string()), - m_detailName(std::string()), m_doc(nullptr), m_mode(CREATEMODE), m_created(false) @@ -137,9 +135,6 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): m_saveAnchor(Base::Vector3d(0.0, 0.0, 0.0)), m_saveRadius(0.0), m_saved(false), - m_baseName(std::string()), - m_pageName(std::string()), - m_detailName(std::string()), m_doc(nullptr), m_mode(EDITMODE), m_created(false) @@ -160,12 +155,13 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): App::DocumentObject* baseObj = m_detailFeat->BaseView.getValue(); m_baseFeat = dynamic_cast(baseObj); - if (m_baseFeat) { - m_baseName = m_baseFeat->getNameInDocument(); - } else { + if (!m_baseFeat) { Base::Console().error("TaskDetail - no BaseView. Can not proceed.\n"); return; } + m_baseName = m_baseFeat->getNameInDocument(); + // repaint baseObj here to make highlight inactive. + m_baseFeat->requestPaint(); ui->setupUi(this); @@ -219,7 +215,6 @@ void TaskDetail::changeEvent(QEvent *e) //save the start conditions void TaskDetail::saveDetailState() { -// Base::Console().message("TD::saveDetailState()\n"); TechDraw::DrawViewDetail* dvd = getDetailFeat(); m_saveAnchor = dvd->AnchorPoint.getValue(); m_saveRadius = dvd->Radius.getValue(); @@ -228,7 +223,6 @@ void TaskDetail::saveDetailState() void TaskDetail::restoreDetailState() { -// Base::Console().message("TD::restoreDetailState()\n"); TechDraw::DrawViewDetail* dvd = getDetailFeat(); dvd->AnchorPoint.setValue(m_saveAnchor); dvd->Radius.setValue(m_saveRadius); @@ -238,7 +232,6 @@ void TaskDetail::restoreDetailState() void TaskDetail::setUiFromFeat() { -// Base::Console().message("TD::setUIFromFeat()\n"); if (m_baseFeat) { std::string baseName = getBaseFeat()->getNameInDocument(); ui->leBaseView->setText(QString::fromStdString(baseName)); @@ -271,10 +264,12 @@ void TaskDetail::setUiFromFeat() ui->qsbRadius->setValue(radius); ui->qsbScale->setDecimals(decimals); ui->cbScaleType->setCurrentIndex(ScaleType); - if (ui->cbScaleType->currentIndex() == 2) // only if custom scale + if (ui->cbScaleType->currentIndex() == 2) { // only if custom scale ui->qsbScale->setEnabled(true); - else + } + else { ui->qsbScale->setEnabled(false); + } ui->qsbScale->setValue(scale); ui->leReference->setText(ref); } @@ -282,16 +277,23 @@ void TaskDetail::setUiFromFeat() //update ui point fields after tracker finishes void TaskDetail::updateUi(QPointF pos) { + ui->qsbX->blockSignals(true); + ui->qsbY->blockSignals(true); + ui->qsbX->setValue(pos.x()); - ui->qsbY->setValue(- pos.y()); + ui->qsbY->setValue(pos.y()); + + ui->qsbX->blockSignals(false); + ui->qsbY->blockSignals(false); } void TaskDetail::enableInputFields(bool isEnabled) { ui->qsbX->setEnabled(isEnabled); ui->qsbY->setEnabled(isEnabled); - if (ui->cbScaleType->currentIndex() == 2) // only if custom scale + if (ui->cbScaleType->currentIndex() == 2) { // only if custom scale ui->qsbScale->setEnabled(isEnabled); + } ui->qsbRadius->setEnabled(isEnabled); ui->leReference->setEnabled(isEnabled); } @@ -315,10 +317,10 @@ void TaskDetail::onScaleTypeEdit() { TechDraw::DrawViewDetail* detailFeat = getDetailFeat(); - if (ui->cbScaleType->currentIndex() == 0) { + detailFeat->ScaleType.setValue(ui->cbScaleType->currentIndex()); + if (ui->cbScaleType->currentIndex() == 0) { // page scale ui->qsbScale->setEnabled(false); - detailFeat->ScaleType.setValue(0.0); // set the page scale if there is a valid page if (m_basePage) { // set the page scale @@ -331,15 +333,12 @@ void TaskDetail::onScaleTypeEdit() else if (ui->cbScaleType->currentIndex() == 1) { // automatic scale (if view is too large to fit into page, it will be scaled down) ui->qsbScale->setEnabled(false); - detailFeat->ScaleType.setValue(1.0); // updating the feature will trigger the rescaling updateDetail(); } else if (ui->cbScaleType->currentIndex() == 2) { // custom scale ui->qsbScale->setEnabled(true); - detailFeat->ScaleType.setValue(2.0); - // no updateDetail() necessary since nothing visibly was changed } } @@ -359,12 +358,10 @@ void TaskDetail::onDraggerClicked(bool clicked) ui->pbDragger->setEnabled(false); enableInputFields(false); editByHighlight(); - return; } void TaskDetail::editByHighlight() { -// Base::Console().message("TD::editByHighlight()\n"); if (!m_ghost) { Base::Console().error("TaskDetail::editByHighlight - no ghost object\n"); return; @@ -382,34 +379,38 @@ void TaskDetail::editByHighlight() //dragEnd is in scene coords. void TaskDetail::onHighlightMoved(QPointF dragEnd) { -// Base::Console().message("TD::onHighlightMoved(%s) - highlight: %X\n", -// DrawUtil::formatVector(dragEnd).c_str(), m_ghost); ui->pbDragger->setEnabled(true); + double radius = m_detailFeat->Radius.getValue(); double scale = getBaseFeat()->getScale(); double x = Rez::guiX(getBaseFeat()->X.getValue()); double y = Rez::guiX(getBaseFeat()->Y.getValue()); DrawViewPart* dvp = getBaseFeat(); - DrawProjGroupItem* dpgi = freecad_cast(dvp); - if (dpgi) { - DrawProjGroup* dpg = dpgi->getPGroup(); - if (!dpg) { - Base::Console().message("TD::getAnchorScene - projection group is confused\n"); - //TODO::throw something. - return; - } + auto* dpgi = freecad_cast(dvp); + DrawProjGroup* dpg{nullptr}; + if (dpgi && DrawView::isProjGroupItem(dpgi)) { + dpg = dpgi->getPGroup(); + } + + if (dpg) { x += Rez::guiX(dpg->X.getValue()); y += Rez::guiX(dpg->Y.getValue()); } QPointF basePosScene(x, -y); //base position in scene coords QPointF anchorDisplace = dragEnd - basePosScene; - QPointF newAnchorPos = Rez::appX(anchorDisplace / scale); + QPointF newAnchorPosScene = Rez::appX(anchorDisplace / scale); - updateUi(newAnchorPos); + + Base::Vector3d newAnchorPosPage = DrawUtil::toVector3d(newAnchorPosScene); + newAnchorPosPage = DrawUtil::invertY(newAnchorPosPage); + Base::Vector3d snappedPos = dvp->snapHighlightToVertex(newAnchorPosPage, radius); + + updateUi(DrawUtil::toQPointF(snappedPos)); updateDetail(); enableInputFields(true); + m_ghost->setSelected(false); m_ghost->hide(); } @@ -430,7 +431,6 @@ void TaskDetail::enableTaskButtons(bool button) //***** Feature create & edit stuff ******************************************* void TaskDetail::createDetail() { -// Base::Console().message("TD::createDetail()\n"); Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Create Detail View")); const std::string objectName{"Detail"}; @@ -471,14 +471,15 @@ void TaskDetail::createDetail() void TaskDetail::updateDetail() { -// Base::Console().message("TD::updateDetail()\n"); + TechDraw::DrawViewDetail* detailFeat = getDetailFeat(); try { Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Update Detail")); double x = ui->qsbX->rawValue(); double y = ui->qsbY->rawValue(); Base::Vector3d temp(x, y, 0.0); - TechDraw::DrawViewDetail* detailFeat = getDetailFeat(); - detailFeat->AnchorPoint.setValue(temp); + + detailFeat->AnchorPoint.setValue(temp); // point2d + double scale = ui->qsbScale->rawValue(); detailFeat->Scale.setValue(scale); double radius = ui->qsbRadius->rawValue(); @@ -487,8 +488,6 @@ void TaskDetail::updateDetail() std::string ref = qRef.toStdString(); detailFeat->Reference.setValue(ref); - detailFeat->recomputeFeature(); - getBaseFeat()->requestPaint(); Gui::Command::updateActive(); Gui::Command::commitCommand(); } @@ -496,6 +495,8 @@ void TaskDetail::updateDetail() //this is probably due to appl closing while dialog is still open Base::Console().error("Task Detail - detail feature update failed.\n"); } + + detailFeat->recomputeFeature(); } //***** Getters **************************************************************** @@ -504,45 +505,42 @@ void TaskDetail::updateDetail() QPointF TaskDetail::getAnchorScene() { DrawViewPart* dvp = getBaseFeat(); - DrawProjGroupItem* dpgi = freecad_cast(dvp); + auto* dpgi = freecad_cast(dvp); DrawViewDetail* dvd = getDetailFeat(); Base::Vector3d anchorPos = dvd->AnchorPoint.getValue(); anchorPos.y = -anchorPos.y; Base::Vector3d basePos; double scale = 1; - if (!dpgi) { //base is normal view - double x = dvp->X.getValue(); - double y = dvp->Y.getValue(); - basePos = Base::Vector3d (x, -y, 0.0); - scale = dvp->getScale(); - } else { //part of projection group + double x = dvp->X.getValue(); + double y = dvp->Y.getValue(); + scale = dvp->getScale(); - DrawProjGroup* dpg = dpgi->getPGroup(); - if (!dpg) { - Base::Console().message("TD::getAnchorScene - projection group is confused\n"); - //TODO::throw something. - return QPointF(0.0, 0.0); - } - double x = dpg->X.getValue(); + DrawProjGroup* dpg{nullptr}; + if (dpgi && DrawProjGroup::isProjGroupItem(dpgi)) { + dpg = dpgi->getPGroup(); + } + + if (dpg) { + // part of a projection group + x = dpg->X.getValue(); x += dpgi->X.getValue(); - double y = dpg->Y.getValue(); + y = dpg->Y.getValue(); y += dpgi->Y.getValue(); - basePos = Base::Vector3d(x, -y, 0.0); scale = dpgi->getScale(); } + basePos = Base::Vector3d (x, -y, 0.0); + Base::Vector3d xyScene = Rez::guiX(basePos); Base::Vector3d anchorOffsetScene = Rez::guiX(anchorPos) * scale; Base::Vector3d netPos = xyScene + anchorOffsetScene; - return QPointF(netPos.x, netPos.y); + return {netPos.x, netPos.y}; } // protects against stale pointers DrawViewPart* TaskDetail::getBaseFeat() { -// Base::Console().message("TD::getBaseFeat()\n"); - if (m_doc) { App::DocumentObject* baseObj = m_doc->getObject(m_baseName.c_str()); if (baseObj) { @@ -560,8 +558,6 @@ DrawViewPart* TaskDetail::getBaseFeat() // protects against stale pointers DrawViewDetail* TaskDetail::getDetailFeat() { -// Base::Console().message("TD::getDetailFeat()\n"); - if (m_baseFeat) { App::DocumentObject* detailObj = m_baseFeat->getDocument()->getObject(m_detailName.c_str()); if (detailObj) { @@ -572,7 +568,6 @@ DrawViewDetail* TaskDetail::getDetailFeat() std::string msg = "TaskDetail - detail feature " + m_detailName + " not found \n"; -// throw Base::TypeError("TaskDetail - detail feature not found\n"); throw Base::TypeError(msg); return nullptr; } @@ -581,15 +576,14 @@ DrawViewDetail* TaskDetail::getDetailFeat() bool TaskDetail::accept() { -// Base::Console().message("TD::accept()\n"); - Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); - if (!doc) + if (!doc) { return false; + } m_ghost->hide(); - getDetailFeat()->requestPaint(); - getBaseFeat()->requestPaint(); + getDetailFeat()->recomputeFeature(); + Gui::Command::doCommand(Gui::Command::Gui, "Gui.ActiveDocument.resetEdit()"); return true; @@ -597,10 +591,10 @@ bool TaskDetail::accept() bool TaskDetail::reject() { -// Base::Console().message("TD::reject()\n"); Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); - if (!doc) + if (!doc) { return false; + } m_ghost->hide(); if (m_mode == CREATEMODE) { diff --git a/src/Mod/TechDraw/Gui/TaskDetail.h b/src/Mod/TechDraw/Gui/TaskDetail.h index 8a9cbd71b5..5c1ae309fc 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.h +++ b/src/Mod/TechDraw/Gui/TaskDetail.h @@ -28,6 +28,7 @@ #include #include +#include "QGIGhostHighlight.h" namespace TechDraw { diff --git a/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp b/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp index 10e6d080c2..a3696d81ff 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp +++ b/src/Mod/TechDraw/Gui/ViewProviderViewPart.cpp @@ -60,6 +60,7 @@ #include "TaskProjGroup.h" #include "ViewProviderViewPart.h" #include "ViewProviderPage.h" +#include "QGIViewPart.h" #include "QGIViewDimension.h" #include "QGIViewBalloon.h" #include "QGSPage.h" @@ -213,8 +214,8 @@ void ViewProviderViewPart::onChanged(const App::Property* prop) void ViewProviderViewPart::attach(App::DocumentObject *pcFeat) { // Base::Console().message("VPVP::attach(%s)\n", pcFeat->getNameInDocument()); - TechDraw::DrawViewMulti* dvm = dynamic_cast(pcFeat); - TechDraw::DrawViewDetail* dvd = dynamic_cast(pcFeat); + auto* dvm = dynamic_cast(pcFeat); + auto* dvd = dynamic_cast(pcFeat); if (dvm) { sPixmap = "TechDraw_TreeMulti"; } else if (dvd) { @@ -269,7 +270,7 @@ std::vector ViewProviderViewPart::claimChildren() const } return temp; } catch (...) { - return std::vector(); + return {}; } } @@ -287,25 +288,32 @@ bool ViewProviderViewPart::setEdit(int ModNum) Gui::Selection().clearSelection(); TechDraw::DrawViewPart* dvp = getViewObject(); - TechDraw::DrawViewDetail* dvd = dynamic_cast(dvp); + auto* dvd = dynamic_cast(dvp); if (dvd) { if (!dvd->BaseView.getValue()) { Base::Console().error("DrawViewDetail - %s - has no BaseView!\n", dvd->getNameInDocument()); return false; } - Gui::Control().showDialog(new TaskDlgDetail(dvd)); - Gui::Selection().clearSelection(); - Gui::Selection().addSelection(dvd->getDocument()->getName(), - dvd->getNameInDocument()); - } - else { - auto* view = getObject(); - Gui::Control().showDialog(new TaskDlgProjGroup(view, false)); + return setDetailEdit(ModNum, dvd); } + auto* view = getObject(); + Gui::Control().showDialog(new TaskDlgProjGroup(view, false)); return true; } +bool ViewProviderViewPart::setDetailEdit(int ModNum, DrawViewDetail* dvd) +{ + Q_UNUSED(ModNum); + + Gui::Control().showDialog(new TaskDlgDetail(dvd)); + Gui::Selection().clearSelection(); + Gui::Selection().addSelection(dvd->getDocument()->getName(), + dvd->getNameInDocument()); + return true; +} + + bool ViewProviderViewPart::doubleClicked() { setEdit(ViewProvider::Default); diff --git a/src/Mod/TechDraw/Gui/ViewProviderViewPart.h b/src/Mod/TechDraw/Gui/ViewProviderViewPart.h index d19137e349..2d9b89ab0c 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderViewPart.h +++ b/src/Mod/TechDraw/Gui/ViewProviderViewPart.h @@ -71,6 +71,7 @@ public: bool onDelete(const std::vector &) override; bool canDelete(App::DocumentObject* obj) const override; bool setEdit(int ModNum) override; + bool setDetailEdit(int ModNum, TechDraw::DrawViewDetail* dvd); bool doubleClicked(void) override; void onChanged(const App::Property *prop) override; void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property * prop) override; From a3fb529bc8386ff10c49a975e499e24e7a1c9e79 Mon Sep 17 00:00:00 2001 From: Ryan Kembrey Date: Thu, 19 Jun 2025 20:54:28 +1000 Subject: [PATCH 118/126] TechDraw: Remove redundant apply button. (Fix #21792) --- src/Mod/TechDraw/Gui/TaskProjGroup.cpp | 7 ++----- src/Mod/TechDraw/Gui/TaskProjGroup.h | 6 ++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Mod/TechDraw/Gui/TaskProjGroup.cpp b/src/Mod/TechDraw/Gui/TaskProjGroup.cpp index d19a178abf..b7b8543105 100644 --- a/src/Mod/TechDraw/Gui/TaskProjGroup.cpp +++ b/src/Mod/TechDraw/Gui/TaskProjGroup.cpp @@ -777,12 +777,10 @@ QString TaskProjGroup::formatVector(Base::Vector3d vec) } void TaskProjGroup::saveButtons(QPushButton* btnOK, - QPushButton* btnCancel, - QPushButton* btnApply) + QPushButton* btnCancel) { m_btnOK = btnOK; m_btnCancel = btnCancel; - m_btnApply = btnApply; } @@ -887,8 +885,7 @@ void TaskDlgProjGroup::modifyStandardButtons(QDialogButtonBox* box) { QPushButton* btnOK = box->button(QDialogButtonBox::Ok); QPushButton* btnCancel = box->button(QDialogButtonBox::Cancel); - QPushButton* btnApply = box->button(QDialogButtonBox::Apply); - widget->saveButtons(btnOK, btnCancel, btnApply); + widget->saveButtons(btnOK, btnCancel); } //==== calls from the TaskView =============================================================== diff --git a/src/Mod/TechDraw/Gui/TaskProjGroup.h b/src/Mod/TechDraw/Gui/TaskProjGroup.h index 501123e761..2c40f4e952 100644 --- a/src/Mod/TechDraw/Gui/TaskProjGroup.h +++ b/src/Mod/TechDraw/Gui/TaskProjGroup.h @@ -61,8 +61,7 @@ public: virtual bool apply(); void modifyStandardButtons(QDialogButtonBox* box); void saveButtons(QPushButton* btnOK, - QPushButton* btnCancel, - QPushButton* btnApply); + QPushButton* btnCancel); void updateTask(); // Sets the numerator and denominator widgets to match newScale @@ -126,7 +125,6 @@ private: QPushButton* m_btnOK{nullptr}; QPushButton* m_btnCancel{nullptr}; - QPushButton* m_btnApply{nullptr}; std::vector m_saveSource; std::string m_saveProjType; @@ -151,7 +149,7 @@ public: TechDraw::DrawView* getView() const { return view; } QDialogButtonBox::StandardButtons getStandardButtons() const override - { return QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel; } + { return QDialogButtonBox::Ok | QDialogButtonBox::Cancel; } void modifyStandardButtons(QDialogButtonBox* box) override; /// is called the TaskView when the dialog is opened From 9b3220219993cad2dc0b498bf2fbbc1d0ae05cf2 Mon Sep 17 00:00:00 2001 From: Ryan K <114723629+ryankembrey@users.noreply.github.com> Date: Tue, 24 Jun 2025 02:26:40 +1000 Subject: [PATCH 119/126] TechDraw: Add command tool label for vertex group (#22118) Co-authored-by: Ryan Kembrey --- src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py b/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py index b6f113e922..b66aa1fecf 100644 --- a/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py +++ b/src/Mod/TechDraw/TechDrawTools/CommandVertexCreations.py @@ -54,7 +54,13 @@ class CommandVertexCreationGroup: return 0 def GetResources(self): - return {'Pixmap':'TechDraw_ExtensionVertexAtIntersection'} + """Return a dictionary with data that will be used by the button or menu item.""" + return {'Pixmap': 'TechDraw_ExtensionVertexAtIntersection.svg', + 'Accel': "", + 'MenuText': QT_TRANSLATE_NOOP("TechDraw_ExtensionVertexAtIntersection","Add Cosmetic Intersection Vertex(es)"), + 'ToolTip': QT_TRANSLATE_NOOP("TechDraw_ExtensionVertexAtIntersection", "Add cosmetic vertex(es) at the intersection(s) of selected edges:
\ + - Select two edges
\ + - Click this tool")} def IsActive(self): """Return True when the command should be active or False when it should be disabled (greyed).""" From 625458f119d28d034ffea0aabc3f433083ae2531 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 23 Jun 2025 19:24:30 +0200 Subject: [PATCH 120/126] Sketcher: Make TAB clear the field if user hasn't valid input --- src/Gui/EditableDatumLabel.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Gui/EditableDatumLabel.cpp b/src/Gui/EditableDatumLabel.cpp index a0fc8f91bc..d0d36d1182 100644 --- a/src/Gui/EditableDatumLabel.cpp +++ b/src/Gui/EditableDatumLabel.cpp @@ -221,6 +221,10 @@ bool EditableDatumLabel::eventFilter(QObject* watched, QEvent* event) // if tab has been pressed and user did not type anything previously, // then just cycle but don't lock anything, otherwise we lock the label if (keyEvent->key() == Qt::Key_Tab && !this->isSet) { + if (!this->spinBox->hasValidInput()) { + Q_EMIT this->spinBox->valueChanged(this->value); + return true; + } return false; } From ab6e3d18dd6a91fd8a7524a8a1e2664ef88942d5 Mon Sep 17 00:00:00 2001 From: tiagomscardoso Date: Mon, 23 Jun 2025 18:40:16 +0100 Subject: [PATCH 121/126] Gui: prevent hover tooltip from covering menu items (#22019) * fix #21330: prevent hover tooltip from covering menu items Instead of showing the tooltip at the mouse cursor, it is now displayed to the right of the corresponding menu option, avoiding overlap with the menu itself. * Update src/Gui/Action.cpp --------- Co-authored-by: Chris Hennes --- src/Gui/Action.cpp | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index c839412deb..8588bc4d70 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -606,9 +606,37 @@ void ActionGroup::onActivated (QAction* act) } } -void ActionGroup::onHovered (QAction *act) +/** + * Shows tooltip at the right side when hovered. + */ +void ActionGroup::onHovered(QAction *act) { - QToolTip::showText(QCursor::pos(), act->toolTip()); + const auto topLevelWidgets = QApplication::topLevelWidgets(); + QMenu* foundMenu = nullptr; + + for (QWidget* widget : topLevelWidgets) { + QList menus = widget->findChildren(); + + for (QMenu* menu : menus) { + if (menu->isVisible() && menu->actions().contains(act)) { + foundMenu = menu; + break; + } + } + + if (foundMenu) { + break; + } + + } + + if (foundMenu) { + QRect actionRect = foundMenu->actionGeometry(act); + QPoint globalPos = foundMenu->mapToGlobal(actionRect.topRight()); + QToolTip::showText(globalPos, act->toolTip(), foundMenu, actionRect); + } else { + QToolTip::showText(QCursor::pos(), act->toolTip()); + } } From c9532316ff393f0197ab3dd273bb16aadebe879f Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 23 Jun 2025 19:42:03 +0200 Subject: [PATCH 122/126] Gui: Fix wildcard call disconnects warnings Qt6.9 (#22096) * Gui: Fix wildcard call disconnects warnings Qt6.9 * Gui: Fix typo --------- Co-authored-by: Benjamin Nauck --- src/Gui/PythonWrapper.cpp | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Gui/PythonWrapper.cpp b/src/Gui/PythonWrapper.cpp index 1bda2cf2d1..e18d9b1d84 100644 --- a/src/Gui/PythonWrapper.cpp +++ b/src/Gui/PythonWrapper.cpp @@ -392,34 +392,39 @@ public: */ void addQObject(QObject* obj, PyObject* pyobj) { - const auto PyW_unique_name = QString::number(reinterpret_cast (pyobj)); - auto PyW_invalidator = findChild (PyW_unique_name, Qt::FindDirectChildrenOnly); + // static array to contain created connections so they can be safely disconnected later + static std::map connections = {}; + + const auto PyW_uniqueName = QString::number(reinterpret_cast(pyobj)); + auto PyW_invalidator = findChild(PyW_uniqueName, Qt::FindDirectChildrenOnly); if (PyW_invalidator == nullptr) { PyW_invalidator = new QObject(this); - PyW_invalidator->setObjectName(PyW_unique_name); + PyW_invalidator->setObjectName(PyW_uniqueName); Py_INCREF (pyobj); } - else { - PyW_invalidator->disconnect(); + else if (connections.contains(PyW_invalidator)) { + disconnect(connections[PyW_invalidator]); + connections.erase(PyW_invalidator); } - auto destroyedFun = [pyobj](){ + auto destroyedFun = [pyobj]() { Base::PyGILStateLocker lock; - auto sbk_ptr = reinterpret_cast (pyobj); - if (sbk_ptr != nullptr) { - Shiboken::Object::setValidCpp(sbk_ptr, false); + + if (auto sbkPtr = reinterpret_cast(pyobj); sbkPtr != nullptr) { + Shiboken::Object::setValidCpp(sbkPtr, false); } else { Base::Console().developerError("WrapperManager", "A QObject has just been destroyed after its Pythonic wrapper.\n"); } + Py_DECREF (pyobj); }; - QObject::connect(PyW_invalidator, &QObject::destroyed, this, destroyedFun); - QObject::connect(obj, &QObject::destroyed, PyW_invalidator, &QObject::deleteLater); -} + connections[PyW_invalidator] = connect(PyW_invalidator, &QObject::destroyed, this, destroyedFun); + connect(obj, &QObject::destroyed, PyW_invalidator, &QObject::deleteLater); + } private: void wrapQApplication() From 77334c8d4fb902708811eedd91491c726a1e15ba Mon Sep 17 00:00:00 2001 From: Luz Paz Date: Mon, 23 Jun 2025 13:22:20 -0400 Subject: [PATCH 123/126] FEM: fix typos --- src/Mod/Fem/femobjects/base_fempostextractors.py | 8 ++++---- src/Mod/Fem/femtaskpanels/base_fempostpanel.py | 2 +- src/Mod/Fem/femviewprovider/view_base_femobject.py | 2 +- .../Fem/femviewprovider/view_base_fempostextractors.py | 2 +- .../Fem/femviewprovider/view_base_fempostvisualization.py | 2 +- src/Mod/Fem/femviewprovider/view_post_histogram.py | 8 ++++---- src/Mod/Fem/femviewprovider/view_post_lineplot.py | 8 ++++---- src/Mod/Fem/femviewprovider/view_post_table.py | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Mod/Fem/femobjects/base_fempostextractors.py b/src/Mod/Fem/femobjects/base_fempostextractors.py index 9e2ad7104d..41ba270c35 100644 --- a/src/Mod/Fem/femobjects/base_fempostextractors.py +++ b/src/Mod/Fem/femobjects/base_fempostextractors.py @@ -21,7 +21,7 @@ # * * # *************************************************************************** -__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts" +__title__ = "FreeCAD FEM postprocessing data exxtractor base objects" __author__ = "Stefan Tröger" __url__ = "https://www.freecad.org" @@ -127,7 +127,7 @@ class Extractor(base_fempythonobject.BaseFemPythonObject): return ["Not a vector"] def get_representive_fieldname(self, obj): - # should return the representive field name, e.g. Position (X) + # should return the representative field name, e.g. Position (X) return "" @@ -259,7 +259,7 @@ class Extractor1D(Extractor): return array def get_representive_fieldname(self, obj): - # representive field is the x field + # representative field is the x field label = obj.XField if not label: return "" @@ -387,7 +387,7 @@ class Extractor2D(Extractor1D): return array def get_representive_fieldname(self, obj): - # representive field is the y field + # representative field is the y field label = obj.YField if not label: return "" diff --git a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py index 81fa9107eb..c972921dac 100644 --- a/src/Mod/Fem/femtaskpanels/base_fempostpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_fempostpanel.py @@ -40,7 +40,7 @@ translate = FreeCAD.Qt.translate class _BasePostTaskPanel(base_femtaskpanel._BaseTaskPanel): """ - The TaskPanel for post objects, mimicing the c++ functionality + The TaskPanel for post objects, mimicking the c++ functionality """ def __init__(self, obj): diff --git a/src/Mod/Fem/femviewprovider/view_base_femobject.py b/src/Mod/Fem/femviewprovider/view_base_femobject.py index 10ba8e2fd0..d170f2d6f2 100644 --- a/src/Mod/Fem/femviewprovider/view_base_femobject.py +++ b/src/Mod/Fem/femviewprovider/view_base_femobject.py @@ -44,7 +44,7 @@ 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 + Based on the App version, but viewprovider addProperty does not take keyword args, hence we use positional arguments here """ diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py index b2df81ef0d..37ea937910 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostextractors.py @@ -65,7 +65,7 @@ class VPPostExtractor: def onChanged(self, vobj, prop): # one of our view properties was changed. Lets inform our parent visualization - # that this happend, as this is the one that needs to redraw + # that this happened, as this is the one that needs to redraw if prop == "Proxy": return diff --git a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py index 3abf56b29a..d66b8ac738 100644 --- a/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py +++ b/src/Mod/Fem/femviewprovider/view_base_fempostvisualization.py @@ -118,7 +118,7 @@ class VPPostVisualization: def get_next_default_color(self): # Returns the next default color a new object should use - # Returns color in FreeCAD proeprty notation (r,g,b,a) + # Returns color in FreeCAD property notation (r,g,b,a) # If the relevant extractors do not have color properties, this # can stay unimplemented raise FreeCAD.Base.FreeCADError("Not implemented") diff --git a/src/Mod/Fem/femviewprovider/view_post_histogram.py b/src/Mod/Fem/femviewprovider/view_post_histogram.py index f6323cafc6..9f88ee2b57 100644 --- a/src/Mod/Fem/femviewprovider/view_post_histogram.py +++ b/src/Mod/Fem/femviewprovider/view_post_histogram.py @@ -89,7 +89,7 @@ class EditViewWidget(QtGui.QWidget): self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged) self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) - # sometimes wierd sizes occur with spinboxes + # sometimes weird sizes occur with spinboxes self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height()) self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) @@ -228,7 +228,7 @@ class EditIndexAppWidget(QtGui.QWidget): self.widget.Field.activated.connect(self.fieldChanged) self.widget.Component.activated.connect(self.componentChanged) - # sometimes wierd sizes occur with spinboxes + # sometimes weird sizes occur with spinboxes self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) @QtCore.Slot(int) @@ -250,7 +250,7 @@ class EditIndexAppWidget(QtGui.QWidget): class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor): """ - A View Provider for extraction of 1D field data specialy for histograms + A View Provider for extraction of 1D field data specially for histograms """ def __init__(self, vobj): @@ -396,7 +396,7 @@ class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization): name="Cumulative", group="Histogram", doc=QT_TRANSLATE_NOOP( - "FEM", "If be the bars shoud show the cumulative sum left to rigth" + "FEM", "If be the bars should show the cumulative sum left to right" ), value=False, ), diff --git a/src/Mod/Fem/femviewprovider/view_post_lineplot.py b/src/Mod/Fem/femviewprovider/view_post_lineplot.py index f73d0aad7b..dd58066604 100644 --- a/src/Mod/Fem/femviewprovider/view_post_lineplot.py +++ b/src/Mod/Fem/femviewprovider/view_post_lineplot.py @@ -87,7 +87,7 @@ class EditViewWidget(QtGui.QWidget): self.widget.MarkerSize.valueChanged.connect(self.markerSizeChanged) self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged) - # sometimes wierd sizes occur with spinboxes + # sometimes weird sizes occur with spinboxes self.widget.MarkerSize.setMaximumHeight(self.widget.MarkerStyle.sizeHint().height()) self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height()) @@ -242,7 +242,7 @@ class EditIndexAppWidget(QtGui.QWidget): self.widget.YField.activated.connect(self.yFieldChanged) self.widget.YComponent.activated.connect(self.yComponentChanged) - # sometimes wierd sizes occur with spinboxes + # sometimes weird sizes occur with spinboxes self.widget.Index.setMaximumHeight(self.widget.YField.sizeHint().height()) @QtCore.Slot(int) @@ -266,7 +266,7 @@ class EditIndexAppWidget(QtGui.QWidget): class VPPostLineplotFieldData(view_base_fempostextractors.VPPostExtractor): """ - A View Provider for extraction of 2D field data specialy for histograms + A View Provider for extraction of 2D field data specially for histograms """ def __init__(self, vobj): @@ -403,7 +403,7 @@ class VPPostLineplot(view_base_fempostvisualization.VPPostVisualization): name="Grid", group="Lineplot", doc=QT_TRANSLATE_NOOP( - "FEM", "If be the bars shoud show the cumulative sum left to rigth" + "FEM", "If be the bars should show the cumulative sum left to right" ), value=True, ), diff --git a/src/Mod/Fem/femviewprovider/view_post_table.py b/src/Mod/Fem/femviewprovider/view_post_table.py index 3c22d8b999..8d00e9db4b 100644 --- a/src/Mod/Fem/femviewprovider/view_post_table.py +++ b/src/Mod/Fem/femviewprovider/view_post_table.py @@ -148,7 +148,7 @@ class EditIndexAppWidget(QtGui.QWidget): self.widget.Field.activated.connect(self.fieldChanged) self.widget.Component.activated.connect(self.componentChanged) - # sometimes wierd sizes occur with spinboxes + # sometimes weird sizes occur with spinboxes self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height()) @QtCore.Slot(int) @@ -170,7 +170,7 @@ class EditIndexAppWidget(QtGui.QWidget): class VPPostTableFieldData(view_base_fempostextractors.VPPostExtractor): """ - A View Provider for extraction of 1D field data specialy for tables + A View Provider for extraction of 1D field data specially for tables """ def __init__(self, vobj): From 46bff2af0511c76274905ce55aaafaa6ea85b3e7 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 23 Jun 2025 21:11:23 +0200 Subject: [PATCH 124/126] CAM: Load preferences before activating the workbench (#21981) --- src/Mod/CAM/InitGui.py | 54 ++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index d9f92c86b7..259bf310d5 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -22,11 +22,39 @@ # * * # *************************************************************************** import FreeCAD +from PySide.QtCore import QT_TRANSLATE_NOOP +import Path.Dressup.Gui.Preferences as PathPreferencesPathDressup +import Path.Tool.assets.ui.preferences as AssetPreferences +import Path.Main.Gui.PreferencesJob as PathPreferencesPathJob +import Path.Base.Gui.PreferencesAdvanced as PathPreferencesAdvanced +import Path.Op.Base +import Path.Tool FreeCAD.__unit_test__ += ["TestCAMGui"] +if FreeCAD.GuiUp: + import FreeCADGui + + FreeCADGui.addPreferencePage( + PathPreferencesPathJob.JobPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + FreeCADGui.addPreferencePage( + AssetPreferences.AssetPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + FreeCADGui.addPreferencePage( + PathPreferencesPathDressup.DressupPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + FreeCADGui.addPreferencePage( + PathPreferencesAdvanced.AdvancedPreferencesPage, + QT_TRANSLATE_NOOP("QObject", "CAM"), + ) + + class PathCommandGroup: def __init__(self, cmdlist, menu, tooltip=None): self.cmdlist = cmdlist @@ -66,13 +94,10 @@ class CAMWorkbench(Workbench): import Path.Tool.assets.ui.preferences as AssetPreferences import Path.Main.Gui.PreferencesJob as PathPreferencesPathJob - translate = FreeCAD.Qt.translate - # load the builtin modules import Path import PathScripts import PathGui - from PySide import QtCore, QtGui FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addIconPath(":/icons") @@ -89,19 +114,6 @@ class CAMWorkbench(Workbench): import subprocess from packaging.version import Version, parse - FreeCADGui.addPreferencePage( - PathPreferencesPathJob.JobPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) - FreeCADGui.addPreferencePage( - AssetPreferences.AssetPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) - FreeCADGui.addPreferencePage( - PathPreferencesPathDressup.DressupPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) - Path.GuiInit.Startup() # build commands list @@ -291,14 +303,6 @@ class CAMWorkbench(Workbench): if curveAccuracy: Path.Area.setDefaultParams(Accuracy=curveAccuracy) - # keep this one the last entry in the preferences - import Path.Base.Gui.PreferencesAdvanced as PathPreferencesAdvanced - from Path.Preferences import preferences - - FreeCADGui.addPreferencePage( - PathPreferencesAdvanced.AdvancedPreferencesPage, - QT_TRANSLATE_NOOP("QObject", "CAM"), - ) Log("Loading CAM workbench... done\n") def GetClassName(self): @@ -314,8 +318,6 @@ class CAMWorkbench(Workbench): pass def ContextMenu(self, recipient): - import PathScripts - menuAppended = False if len(FreeCADGui.Selection.getSelection()) == 1: obj = FreeCADGui.Selection.getSelection()[0] From 062f40d2b68713bde25b57779eef48ba4f7b215f Mon Sep 17 00:00:00 2001 From: sliptonic Date: Mon, 23 Jun 2025 14:22:33 -0500 Subject: [PATCH 125/126] Refactor slot op. (#21799) * Refactor slot op. Move out of experimenation features * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Incorporate Hyarion's suggestions * Update src/Mod/CAM/Path/Op/Slot.py Co-authored-by: Benjamin Nauck --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Benjamin Nauck --- src/Mod/CAM/InitGui.py | 2 +- src/Mod/CAM/Path/Op/Slot.py | 987 +++++++++++++++--------------------- 2 files changed, 419 insertions(+), 570 deletions(-) diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index 259bf310d5..6f3b6d434f 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -138,6 +138,7 @@ class CAMWorkbench(Workbench): "CAM_MillFace", "CAM_Helix", "CAM_Adaptive", + "CAM_Slot", ] threedopcmdlist = ["CAM_Pocket3D"] engravecmdlist = ["CAM_Engrave", "CAM_Deburr", "CAM_Vcarve"] @@ -189,7 +190,6 @@ class CAMWorkbench(Workbench): prepcmdlist.append("CAM_PathShapeTC") extracmdlist.extend(["CAM_Area", "CAM_Area_Workplane"]) specialcmdlist.append("CAM_ThreadMilling") - twodopcmdlist.append("CAM_Slot") if Path.Preferences.advancedOCLFeaturesEnabled(): try: diff --git a/src/Mod/CAM/Path/Op/Slot.py b/src/Mod/CAM/Path/Op/Slot.py index d4fd14fa21..c67435a221 100644 --- a/src/Mod/CAM/Path/Op/Slot.py +++ b/src/Mod/CAM/Path/Op/Slot.py @@ -484,6 +484,7 @@ class ObjectSlot(PathOp.ObjectOp): """opExecute(obj) ... process surface operation""" Path.Log.track() + # Init operation state self.base = None self.shape1 = None self.shape2 = None @@ -494,53 +495,45 @@ class ObjectSlot(PathOp.ObjectOp): self.dYdX1 = None self.dYdX2 = None self.bottomEdges = None - self.stockZMin = None self.isArc = 0 self.arcCenter = None self.arcMidPnt = None self.arcRadius = 0.0 self.newRadius = 0.0 self.featureDetails = ["", ""] - self.isDebug = False if Path.Log.getLevel(Path.Log.thisModule()) != 4 else True - self.showDebugObjects = False + self.commandlist = [] self.stockZMin = self.job.Stock.Shape.BoundBox.ZMin - CMDS = list() - try: - dotIdx = __name__.index(".") + 1 - except Exception: - dotIdx = 0 - self.module = __name__[dotIdx:] + # Debug settings + self.isDebug = Path.Log.getLevel(Path.Log.thisModule()) == 4 + self.showDebugObjects = self.isDebug and obj.ShowTempObjects - # Setup debugging group for temp objects, when in DEBUG mode - if self.isDebug: - self.showDebugObjects = obj.ShowTempObjects if self.showDebugObjects: - FCAD = FreeCAD.ActiveDocument - for grpNm in ["tmpDebugGrp", "tmpDebugGrp001"]: - if hasattr(FCAD, grpNm): - for go in FCAD.getObject(grpNm).Group: - FCAD.removeObject(go.Name) - FCAD.removeObject(grpNm) - self.tmpGrp = FCAD.addObject("App::DocumentObjectGroup", "tmpDebugGrp") + self._clearDebugGroups() + self.tmpGrp = FreeCAD.ActiveDocument.addObject( + "App::DocumentObjectGroup", "tmpDebugGrp" + ) - # Begin GCode for operation with basic information - # ... and move cutter to clearance height and startpoint + # GCode operation header tool = obj.ToolController.Tool - toolType = tool.ToolType if hasattr(tool, "ToolType") else tool.ShapeName - output = "" - if obj.Comment != "": - self.commandlist.append(Path.Command("N ({})".format(obj.Comment), {})) - self.commandlist.append(Path.Command("N ({})".format(obj.Label), {})) - self.commandlist.append(Path.Command("N (Tool type: {})".format(toolType), {})) + toolType = getattr(tool, "ShapeType", None) + if toolType is None: + Path.Log.warning("Tool does not define ShapeType, using label as fallback.") + toolType = tool.Label + + if obj.Comment: + self.commandlist.append(Path.Command(f"N ({obj.Comment})", {})) + self.commandlist.append(Path.Command(f"N ({obj.Label})", {})) + self.commandlist.append(Path.Command(f"N (Tool type: {toolType})", {})) self.commandlist.append( - Path.Command("N (Compensated Tool Path. Diameter: {})".format(tool.Diameter), {}) + Path.Command(f"N (Compensated Tool Path. Diameter: {tool.Diameter})", {}) ) - self.commandlist.append(Path.Command("N ({})".format(output), {})) + self.commandlist.append(Path.Command("N ()", {})) + self.commandlist.append( Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) ) - if obj.UseStartPoint is True: + if obj.UseStartPoint: self.commandlist.append( Path.Command( "G0", @@ -552,10 +545,8 @@ class ObjectSlot(PathOp.ObjectOp): ) ) - # Impose property limits + # Enforce limits and prep depth steps self.opApplyPropertyLimits(obj) - - # Calculate default depthparams for operation self.depthParams = PathUtils.depth_params( obj.ClearanceHeight.Value, obj.SafeHeight.Value, @@ -565,26 +556,32 @@ class ObjectSlot(PathOp.ObjectOp): obj.FinalDepth.Value, ) - # ###### MAIN COMMANDS FOR OPERATION ###### - + # Main path generation cmds = self._makeOperation(obj) if cmds: - CMDS.extend(cmds) + self.commandlist.extend(cmds) - # Save gcode produced - CMDS.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid})) - self.commandlist.extend(CMDS) + # Final move to clearance height + self.commandlist.append( + Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) + ) - # ###### CLOSING COMMANDS FOR OPERATION ###### - - # Hide the temporary objects - if self.showDebugObjects: - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(self.tmpGrp.Name).Visibility = False + # Hide debug visuals + if self.showDebugObjects and FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(self.tmpGrp.Name).Visibility = False self.tmpGrp.purgeTouched() return True + def _clearDebugGroups(self): + doc = FreeCAD.ActiveDocument + for name in ["tmpDebugGrp", "tmpDebugGrp001"]: + grp = getattr(doc, name, None) + if grp: + for obj in grp.Group: + doc.removeObject(obj.Name) + doc.removeObject(name) + # Control methods for operation def _makeOperation(self, obj): """This method controls the overall slot creation process.""" @@ -951,10 +948,10 @@ class ObjectSlot(PathOp.ObjectOp): def _processSingleHorizFace(self, obj, shape): """Determine slot path endpoints from a single horizontally oriented face.""" Path.Log.debug("_processSingleHorizFace()") - lineTypes = ["Part::GeomLine"] + line_types = ["Part::GeomLine"] - def getRadians(self, E): - vect = self._dXdYdZ(E) + def get_edge_angle_deg(edge): + vect = self._dXdYdZ(edge) norm = self._normalizeVector(vect) rads = self._getVectorAngle(norm) deg = math.degrees(rads) @@ -968,87 +965,95 @@ class ObjectSlot(PathOp.ObjectOp): FreeCAD.Console.PrintError(msg + "\n") return False - # Create tuples as (edge index, length, angle) - eTups = list() - for i in range(0, 4): - eTups.append((i, shape.Edges[i].Length, getRadians(self, shape.Edges[i]))) + # Create tuples as (edge index, edge length, edge angle) + edge_info_list = [] + for edge_index in range(4): + edge = shape.Edges[edge_index] + edge_length = edge.Length + edge_angle = get_edge_angle_deg(edge) + edge_info_list.append((edge_index, edge_length, edge_angle)) - # Sort tuples by edge angle - eTups.sort(key=lambda tup: tup[2]) + # Sort edges by angle ascending + edge_info_list.sort(key=lambda tup: tup[2]) - # Identify parallel edges - parallel_edge_pairs = list() - parallel_edge_flags = list() - flag = 1 - eCnt = len(shape.Edges) - lstE = eCnt - 1 - for i in range(0, eCnt): # populate empty parallel edge flag list - parallel_edge_flags.append(0) - for i in range(0, eCnt): # Cycle through edges to identify parallel pairs - if i < lstE: - ni = i + 1 - A = eTups[i] - B = eTups[ni] - if abs(A[2] - B[2]) < 0.00000001: # test slopes(yaw angles) - debug = False - eA = shape.Edges[A[0]] - eB = shape.Edges[B[0]] - if eA.Curve.TypeId not in lineTypes: - debug = eA.Curve.TypeId - if not debug: - if eB.Curve.TypeId not in lineTypes: - debug = eB.Curve.TypeId - else: - parallel_edge_pairs.append((eA, eB)) - # set parallel flags for this pair of edges - parallel_edge_flags[A[0]] = flag - parallel_edge_flags[B[0]] = flag - flag += 1 - if debug: - msg = "Erroneous Curve.TypeId: {}".format(debug) - Path.Log.debug(msg) + # Identify parallel edge pairs and track flags + parallel_pairs = [] + parallel_flags = [0] * len(shape.Edges) + current_flag = 1 + last_edge_index = len(shape.Edges) - 1 - pairCnt = len(parallel_edge_pairs) - if pairCnt > 1: - parallel_edge_pairs.sort(key=lambda tup: tup[0].Length, reverse=True) + for i in range(len(shape.Edges)): + if i >= last_edge_index: + continue + + next_i = i + 1 + edge_a_info = edge_info_list[i] + edge_b_info = edge_info_list[next_i] + angle_a = edge_a_info[2] + angle_b = edge_b_info[2] + + if abs(angle_a - angle_b) >= 1e-6: # consider improving with normalized angle diff + continue + + edge_a = shape.Edges[edge_a_info[0]] + edge_b = shape.Edges[edge_b_info[0]] + + debug_type_id = None + if edge_a.Curve.TypeId not in line_types: + debug_type_id = edge_a.Curve.TypeId + elif edge_b.Curve.TypeId not in line_types: + debug_type_id = edge_b.Curve.TypeId + + if debug_type_id: + Path.Log.debug(f"Erroneous Curve.TypeId: {debug_type_id}") + else: + parallel_pairs.append((edge_a, edge_b)) + parallel_flags[edge_a_info[0]] = current_flag + parallel_flags[edge_b_info[0]] = current_flag + current_flag += 1 + + pair_count = len(parallel_pairs) + if pair_count > 1: + # Sort pairs by longest edge first + parallel_pairs.sort(key=lambda pair: pair[0].Length, reverse=True) if self.isDebug: - Path.Log.debug(" -pairCnt: {}".format(pairCnt)) - for a, b in parallel_edge_pairs: - Path.Log.debug(" -pair: {}, {}".format(round(a.Length, 4), round(b.Length, 4))) - Path.Log.debug(" -parallel_edge_flags: {}".format(parallel_edge_flags)) + Path.Log.debug(f" - Parallel pair count: {pair_count}") + for edge1, edge2 in parallel_pairs: + Path.Log.debug( + f" - Pair lengths: {round(edge1.Length, 4)}, {round(edge2.Length, 4)}" + ) + Path.Log.debug(f" - Parallel flags: {parallel_flags}") - if pairCnt == 0: + if pair_count == 0: msg = translate("CAM_Slot", "No parallel edges identified.") FreeCAD.Console.PrintError(msg + "\n") return False - elif pairCnt == 1: - # One pair of parallel edges identified - if eCnt == 4: - flag_set = list() - for i in range(0, 4): - e = parallel_edge_flags[i] - if e == 0: - flag_set.append(shape.Edges[i]) - if len(flag_set) == 2: - same = (flag_set[0], flag_set[1]) + + if pair_count == 1: + if len(shape.Edges) == 4: + # Find edges that are NOT in the identified parallel pair + non_parallel_edges = [ + shape.Edges[i] for i, flag in enumerate(parallel_flags) if flag == 0 + ] + if len(non_parallel_edges) == 2: + selected_edges = (non_parallel_edges[0], non_parallel_edges[1]) else: - same = parallel_edge_pairs[0] + selected_edges = parallel_pairs[0] else: - same = parallel_edge_pairs[0] + selected_edges = parallel_pairs[0] else: if obj.Reference1 == "Long Edge": - same = parallel_edge_pairs[1] + selected_edges = parallel_pairs[1] elif obj.Reference1 == "Short Edge": - same = parallel_edge_pairs[0] + selected_edges = parallel_pairs[0] else: - msg = "Reference1 " - msg += translate("CAM_Slot", "value error.") + msg = "Reference1 " + translate("CAM_Slot", "value error.") FreeCAD.Console.PrintError(msg + "\n") return False - (p1, p2) = self._getOppMidPoints(same) - return (p1, p2) + (point1, point2) = self._getOppMidPoints(selected_edges) + return (point1, point2) def _processSingleComplexFace(self, obj, shape): """Determine slot path endpoints from a single complex face.""" @@ -1098,12 +1103,11 @@ class ObjectSlot(PathOp.ObjectOp): def _processSingleEdge(self, obj, edge): """Determine slot path endpoints from a single horizontally oriented edge.""" Path.Log.debug("_processSingleEdge()") - tolrnc = 0.0000001 - lineTypes = ["Part::GeomLine"] - curveTypes = ["Part::GeomCircle"] + tol = 1e-7 + lineTypes = {"Part::GeomLine"} + curveTypes = {"Part::GeomCircle"} def oversizedTool(holeDiam): - # Test if tool larger than opening if self.tool.Diameter > holeDiam: msg = translate("CAM_Slot", "Current tool larger than arc diameter.") FreeCAD.Console.PrintError(msg + "\n") @@ -1111,52 +1115,37 @@ class ObjectSlot(PathOp.ObjectOp): return False def isHorizontal(z1, z2, z3): - # Check that all Z values are equal (isRoughly same) - if abs(z1 - z2) > tolrnc or abs(z1 - z3) > tolrnc: - # abs(z2 - z3) > tolrnc): 3rd test redundant. - return False - return True + return abs(z1 - z2) <= tol and abs(z1 - z3) <= tol def circumCircleFrom3Points(P1, P2, P3): - # Source code for this function copied from (with modifications): - # https://wiki.freecad.org/Macro_Draft_Circle_3_Points_3D - vP2P1 = P2 - P1 - vP3P2 = P3 - P2 - vP1P3 = P1 - P3 - - L = vP2P1.cross(vP3P2).Length - # Circle radius (not used) - # r = vP1P2.Length * vP2P3.Length * vP3P1.Length / 2 / l + v1 = P2 - P1 + v2 = P3 - P2 + v3 = P1 - P3 + L = v1.cross(v2).Length if round(L, 8) == 0.0: - Path.Log.error("The three points are colinear, arc is a straight.") + Path.Log.error("Three points are colinear. Arc is straight.") return False - - # Sphere center. - twolsqr = 2 * L * L - a = -vP3P2.dot(vP3P2) * vP2P1.dot(vP1P3) / twolsqr - b = -vP1P3.dot(vP1P3) * vP3P2.dot(vP2P1) / twolsqr - c = -vP2P1.dot(vP2P1) * vP1P3.dot(vP3P2) / twolsqr + twoL2 = 2 * L * L + a = -v2.dot(v2) * v1.dot(v3) / twoL2 + b = -v3.dot(v3) * v2.dot(v1) / twoL2 + c = -v1.dot(v1) * v3.dot(v2) / twoL2 return P1 * a + P2 * b + P3 * c - V1 = edge.Vertexes[0] + verts = edge.Vertexes + V1 = verts[0] p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0) - if len(edge.Vertexes) == 1: # circle has one virtex - p2 = FreeCAD.Vector(p1) - else: - V2 = edge.Vertexes[1] - p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0) + p2 = p1 if len(verts) == 1 else FreeCAD.Vector(verts[1].X, verts[1].Y, 0.0) - # Process edge based on curve type - if edge.Curve.TypeId in lineTypes: + curveType = edge.Curve.TypeId + if curveType in lineTypes: return (p1, p2) - elif edge.Curve.TypeId in curveTypes: - if len(edge.Vertexes) == 1: - # Circle edge - Path.Log.debug("Arc with single vertex.") + elif curveType in curveTypes: + if len(verts) == 1: + # Full circle + Path.Log.debug("Arc with single vertex (circle).") if oversizedTool(edge.BoundBox.XLength): return False - self.isArc = 1 tp1 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.33)) tp2 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.66)) @@ -1165,37 +1154,37 @@ class ObjectSlot(PathOp.ObjectOp): center = edge.BoundBox.Center self.arcCenter = FreeCAD.Vector(center.x, center.y, 0.0) - midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) - self.arcMidPnt = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0) + mid = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) + self.arcMidPnt = FreeCAD.Vector(mid.x, mid.y, 0.0) self.arcRadius = edge.BoundBox.XLength / 2.0 else: - # Arc edge + # Arc segment Path.Log.debug("Arc with multiple vertices.") - self.isArc = 2 - midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) - if not isHorizontal(V1.Z, V2.Z, midPnt.z): + V2 = verts[1] + mid = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) + if not isHorizontal(V1.Z, V2.Z, mid.z): + return False + mid.z = 0.0 + center = circumCircleFrom3Points(p1, p2, FreeCAD.Vector(mid.x, mid.y, 0.0)) + if not center: return False - midPnt.z = 0.0 - circleCenter = circumCircleFrom3Points(p1, p2, midPnt) - if not circleCenter: - return False - self.arcMidPnt = midPnt - self.arcCenter = circleCenter - self.arcRadius = p1.sub(circleCenter).Length + self.isArc = 2 + self.arcMidPnt = FreeCAD.Vector(mid.x, mid.y, 0.0) + self.arcCenter = center + self.arcRadius = (p1 - center).Length if oversizedTool(self.arcRadius * 2.0): return False return (p1, p2) + else: msg = translate( - "CAM_Slot", - "Failed, slot from edge only accepts lines, arcs and circles.", + "CAM_Slot", "Failed, slot from edge only accepts lines, arcs and circles." ) FreeCAD.Console.PrintError(msg + "\n") - - return False # not line , not circle + return False # Methods for processing double geometry def _processDouble(self, obj, shape_1, sub1, shape_2, sub2): @@ -1253,218 +1242,152 @@ class ObjectSlot(PathOp.ObjectOp): return FreeCAD.Vector(dX, dY, dZ) def _normalizeVector(self, v): - """_normalizeVector(v)... - Returns a copy of the vector received with values rounded to 10 decimal places.""" - posTol = 0.0000000001 # arbitrary, use job Geometry Tolerance ??? - negTol = -1 * posTol - V = FreeCAD.Vector(v.x, v.y, v.z) - V.normalize() - x = V.x - y = V.y - z = V.z + """Return a normalized vector with components rounded to nearest axis-aligned value if close.""" + tol = 1e-10 + V = FreeCAD.Vector(v).normalize() - if V.x != 0 and abs(V.x) < posTol: - x = 0.0 - if V.x != 1 and 1.0 - V.x < posTol: - x = 1.0 - if V.x != -1 and -1.0 - V.x > negTol: - x = -1.0 + def snap(val): + if abs(val) < tol: + return 0.0 + if abs(1.0 - abs(val)) < tol: + return 1.0 if val > 0 else -1.0 + return val - if V.y != 0 and abs(V.y) < posTol: - y = 0.0 - if V.y != 1 and 1.0 - V.y < posTol: - y = 1.0 - if V.y != -1 and -1.0 - V.y > negTol: - y = -1.0 + return FreeCAD.Vector(snap(V.x), snap(V.y), snap(V.z)) - if V.z != 0 and abs(V.z) < posTol: - z = 0.0 - if V.z != 1 and 1.0 - V.z < posTol: - z = 1.0 - if V.z != -1 and -1.0 - V.z > negTol: - z = -1.0 + def _getLowestPoint(self, shape): + """Return the average XY of the vertices with the lowest Z value.""" + vertices = shape.Vertexes + lowest_z = min(v.Z for v in vertices) + lowest_vertices = [v for v in vertices if v.Z == lowest_z] - return FreeCAD.Vector(x, y, z) + avg_x = sum(v.X for v in lowest_vertices) / len(lowest_vertices) + avg_y = sum(v.Y for v in lowest_vertices) / len(lowest_vertices) + return FreeCAD.Vector(avg_x, avg_y, lowest_z) - def _getLowestPoint(self, shape_1): - """_getLowestPoint(shape)... Returns lowest vertex of shape as vector.""" - # find lowest vertex - vMin = shape_1.Vertexes[0] - zmin = vMin.Z - same = [vMin] - for V in shape_1.Vertexes: - if V.Z < zmin: - zmin = V.Z - # vMin = V - elif V.Z == zmin: - same.append(V) - if len(same) > 1: - X = [E.X for E in same] - Y = [E.Y for E in same] - avgX = sum(X) / len(X) - avgY = sum(Y) / len(Y) - return FreeCAD.Vector(avgX, avgY, zmin) - else: - return FreeCAD.Vector(V.X, V.Y, V.Z) + def _getHighestPoint(self, shape): + """Return the average XY of the vertices with the highest Z value.""" + vertices = shape.Vertexes + highest_z = max(v.Z for v in vertices) + highest_vertices = [v for v in vertices if v.Z == highest_z] - def _getHighestPoint(self, shape_1): - """_getHighestPoint(shape)... Returns highest vertex of shape as vector.""" - # find highest vertex - vMax = shape_1.Vertexes[0] - zmax = vMax.Z - same = [vMax] - for V in shape_1.Vertexes: - if V.Z > zmax: - zmax = V.Z - # vMax = V - elif V.Z == zmax: - same.append(V) - if len(same) > 1: - X = [E.X for E in same] - Y = [E.Y for E in same] - avgX = sum(X) / len(X) - avgY = sum(Y) / len(Y) - return FreeCAD.Vector(avgX, avgY, zmax) - else: - return FreeCAD.Vector(V.X, V.Y, V.Z) + avg_x = sum(v.X for v in highest_vertices) / len(highest_vertices) + avg_y = sum(v.Y for v in highest_vertices) / len(highest_vertices) + return FreeCAD.Vector(avg_x, avg_y, highest_z) def _processFeature(self, obj, shape, sub, pNum): - """_processFeature(obj, shape, sub, pNum)... - This function analyzes a shape and returns a three item tuple containing: - working point, - shape orientation/slope, - shape category as face, edge, or vert.""" + """Analyze a shape and return a tuple: (working point, slope, category).""" p = None dYdX = None - cat = sub[:4] - Path.Log.debug("sub-feature is {}".format(cat)) - Ref = getattr(obj, "Reference" + str(pNum)) - if cat == "Face": + + Ref = getattr(obj, f"Reference{pNum}") + + if sub.startswith("Face"): + cat = "Face" BE = self._getBottomEdge(shape) if BE: self.bottomEdges.append(BE) - # calculate slope of face + + # Get slope from first vertex to center of mass V0 = shape.Vertexes[0] v1 = shape.CenterOfMass temp = FreeCAD.Vector(v1.x - V0.X, v1.y - V0.Y, 0.0) - dYdX = self._normalizeVector(temp) + dYdX = self._normalizeVector(temp) if temp.Length != 0 else FreeCAD.Vector(0, 0, 0) - # Determine normal vector for face + # Face normal must be vertical norm = shape.normalAt(0.0, 0.0) - # FreeCAD.Console.PrintMessage('{} normal {}.\n'.format(sub, norm)) if norm.z != 0: msg = translate("CAM_Slot", "The selected face is not oriented vertically:") - FreeCAD.Console.PrintError(msg + " {}.\n".format(sub)) + FreeCAD.Console.PrintError(f"{msg} {sub}.\n") return False + # Choose working point if Ref == "Center of Mass": - comS = shape.CenterOfMass - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + com = shape.CenterOfMass + p = FreeCAD.Vector(com.x, com.y, 0.0) elif Ref == "Center of BoundBox": - comS = shape.BoundBox.Center - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + bbox = shape.BoundBox.Center + p = FreeCAD.Vector(bbox.x, bbox.y, 0.0) elif Ref == "Lowest Point": p = self._getLowestPoint(shape) elif Ref == "Highest Point": p = self._getHighestPoint(shape) - elif cat == "Edge": + elif sub.startswith("Edge"): + cat = "Edge" featDetIdx = pNum - 1 if shape.Curve.TypeId == "Part::GeomCircle": self.featureDetails[featDetIdx] = "arc" - # calculate slope between end vertexes - v0 = shape.Edges[0].Vertexes[0] - v1 = shape.Edges[0].Vertexes[1] + + edge = shape.Edges[0] if hasattr(shape, "Edges") else shape + v0 = edge.Vertexes[0] + v1 = edge.Vertexes[1] temp = FreeCAD.Vector(v1.X - v0.X, v1.Y - v0.Y, 0.0) - dYdX = self._normalizeVector(temp) + dYdX = self._normalizeVector(temp) if temp.Length != 0 else FreeCAD.Vector(0, 0, 0) if Ref == "Center of Mass": - comS = shape.CenterOfMass - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + com = shape.CenterOfMass + p = FreeCAD.Vector(com.x, com.y, 0.0) elif Ref == "Center of BoundBox": - comS = shape.BoundBox.Center - p = FreeCAD.Vector(comS.x, comS.y, 0.0) + bbox = shape.BoundBox.Center + p = FreeCAD.Vector(bbox.x, bbox.y, 0.0) elif Ref == "Lowest Point": p = self._findLowestPointOnEdge(shape) elif Ref == "Highest Point": p = self._findHighestPointOnEdge(shape) - elif cat == "Vert": + elif sub.startswith("Vert"): + cat = "Vert" V = shape.Vertexes[0] p = FreeCAD.Vector(V.X, V.Y, 0.0) + else: + Path.Log.warning(f"Unrecognized subfeature type: {sub}") + return False + if p: return (p, dYdX, cat) return False def _extendArcSlot(self, p1, p2, cent, begExt, endExt): - """_extendArcSlot(p1, p2, cent, begExt, endExt)... - This function extends an arc defined by two end points, p1 and p2, and the center. - The arc is extended along the circumference with begExt and endExt values. - The function returns the new end points as tuple (n1, n2) to replace p1 and p2.""" - cancel = True + """Extend an arc defined by endpoints p1, p2 and center cent. + begExt and endExt are extension lengths along the arc at each end. + Returns new (p1, p2) as (n1, n2).""" if not begExt and not endExt: return (p1, p2) - n1 = p1 - n2 = p2 - - # Create a chord of the right length, on XY plane, starting on x axis - def makeChord(rads): - x = self.newRadius * math.cos(rads) - y = self.newRadius * math.sin(rads) - a = FreeCAD.Vector(self.newRadius, 0.0, 0.0) - b = FreeCAD.Vector(x, y, 0.0) + def makeChord(angle_rad): + x = self.newRadius * math.cos(angle_rad) + y = self.newRadius * math.sin(angle_rad) + a = FreeCAD.Vector(self.newRadius, 0, 0) + b = FreeCAD.Vector(x, y, 0) return Part.makeLine(a, b) - # Convert extension to radians; make a generic chord ( line ) on XY plane from the x axis - # rotate and shift into place so it has same vertices as the required arc extension - # adjust rotation angle to provide +ve or -ve extension as needed - origin = FreeCAD.Vector(0.0, 0.0, 0.0) + origin = FreeCAD.Vector(0, 0, 0) + z_axis = FreeCAD.Vector(0, 0, 1) + + n1, n2 = p1, p2 + if begExt: - ExtRadians = abs(begExt / self.newRadius) - chord = makeChord(ExtRadians) - - beginRadians = self._getVectorAngle(p1.sub(self.arcCenter)) - if begExt < 0: - beginRadians += ( - 0 # negative Ext shortens slot so chord endpoint is slot start point - ) - else: - beginRadians -= ( - 2 * ExtRadians - ) # positive Ext lengthens slot so decrease start point angle - - # Path.Log.debug('begExt angles are: {}, {}'.format(beginRadians, math.degrees(beginRadians))) - - chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(beginRadians)) + ext_rad = abs(begExt / self.newRadius) + angle = self._getVectorAngle(p1.sub(self.arcCenter)) + angle += -2 * ext_rad if begExt > 0 else 0 + chord = makeChord(ext_rad) + chord.rotate(origin, z_axis, math.degrees(angle)) chord.translate(self.arcCenter) self._addDebugObject(chord, "ExtendStart") - - v1 = chord.Vertexes[1] - n1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) + n1 = chord.Vertexes[1].Point if endExt: - ExtRadians = abs(endExt / self.newRadius) - chord = makeChord(ExtRadians) - - endRadians = self._getVectorAngle(p2.sub(self.arcCenter)) - if endExt > 0: - endRadians += 0 # positive Ext lengthens slot so chord endpoint is good - else: - endRadians -= ( - 2 * ExtRadians - ) # negative Ext shortens slot so decrease end point angle - - # Path.Log.debug('endExt angles are: {}, {}'.format(endRadians, math.degrees(endRadians))) - - chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(endRadians)) + ext_rad = abs(endExt / self.newRadius) + angle = self._getVectorAngle(p2.sub(self.arcCenter)) + angle += 0 if endExt > 0 else -2 * ext_rad + chord = makeChord(ext_rad) + chord.rotate(origin, z_axis, math.degrees(angle)) chord.translate(self.arcCenter) self._addDebugObject(chord, "ExtendEnd") - - v1 = chord.Vertexes[1] - n2 = FreeCAD.Vector(v1.X, v1.Y, 0.0) + n2 = chord.Vertexes[1].Point return (n1, n2) @@ -1504,161 +1427,135 @@ class ObjectSlot(PathOp.ObjectOp): def _isParallel(self, dYdX1, dYdX2): """Determine if two orientation vectors are parallel.""" - # if dYdX1.add(dYdX2).Length == 0: - # return True - # if ((dYdX1.x + dYdX2.x) / 2.0 == dYdX1.x and - # (dYdX1.y + dYdX2.y) / 2.0 == dYdX1.y): - # return True - # return False return dYdX1.cross(dYdX2) == FreeCAD.Vector(0, 0, 0) def _makePerpendicular(self, p1, p2, length): - """_makePerpendicular(p1, p2, length)... - Using a line defined by p1 and p2, returns a perpendicular vector centered - at the midpoint of the line, with length value.""" - line = Part.makeLine(p1, p2) - midPnt = line.CenterOfMass + """Using a line defined by p1 and p2, returns a perpendicular vector + centered at the midpoint of the line, with given length.""" + midPnt = (p1.add(p2)).multiply(0.5) halfDist = length / 2.0 - if self.dYdX1: + + if getattr(self, "dYdX1", None): half = FreeCAD.Vector(self.dYdX1.x, self.dYdX1.y, 0.0).multiply(halfDist) n1 = midPnt.add(half) n2 = midPnt.sub(half) return (n1, n2) - elif self.dYdX2: + + elif getattr(self, "dYdX2", None): half = FreeCAD.Vector(self.dYdX2.x, self.dYdX2.y, 0.0).multiply(halfDist) n1 = midPnt.add(half) n2 = midPnt.sub(half) return (n1, n2) + else: toEnd = p2.sub(p1) - perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0) - perp.normalize() - perp.multiply(halfDist) + perp = FreeCAD.Vector(-toEnd.y, toEnd.x, 0.0) + perp = perp.normalize() # normalize() returns the vector normalized + perp = perp.multiply(halfDist) n1 = midPnt.add(perp) n2 = midPnt.sub(perp) return (n1, n2) def _findLowestPointOnEdge(self, E): - tol = 0.0000001 + tol = 1e-7 zMin = E.BoundBox.ZMin - # Test first vertex - v = E.Vertexes[0] - if abs(v.Z - zMin) < tol: - return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test second vertex - v = E.Vertexes[1] - if abs(v.Z - zMin) < tol: - return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test middle point of edge - eMidLen = E.Length / 2.0 - eMidPnt = E.valueAt(E.getParameterByLength(eMidLen)) - if abs(eMidPnt.z - zMin) < tol: - return eMidPnt - if E.BoundBox.ZLength < 0.000000001: # roughly horizontal edge - return eMidPnt + + # Try each vertex + for v in E.Vertexes: + if abs(v.Z - zMin) < tol: + return FreeCAD.Vector(v.X, v.Y, v.Z) + + # Try midpoint + mid = E.valueAt(E.getParameterByLength(E.Length / 2.0)) + if abs(mid.z - zMin) < tol or E.BoundBox.ZLength < 1e-9: + return mid + + # Fallback return self._findLowestEdgePoint(E) def _findLowestEdgePoint(self, E): zMin = E.BoundBox.ZMin - eLen = E.Length - L0 = 0.0 - L1 = eLen - p0 = None - p1 = None + L0, L1 = 0.0, E.Length + tol = 1e-5 + max_iter = 2000 cnt = 0 - while L1 - L0 > 0.00001 and cnt < 2000: - adj = (L1 - L0) * 0.1 - # Get points at L0 and L1 along edge + + while (L1 - L0) > tol and cnt < max_iter: p0 = E.valueAt(E.getParameterByLength(L0)) p1 = E.valueAt(E.getParameterByLength(L1)) - # Adjust points based on proximity to target depth + diff0 = p0.z - zMin diff1 = p1.z - zMin + + adj = (L1 - L0) * 0.1 if diff0 < diff1: L1 -= adj elif diff0 > diff1: L0 += adj else: + # When equal, narrow from both ends L0 += adj L1 -= adj cnt += 1 + midLen = (L0 + L1) / 2.0 return E.valueAt(E.getParameterByLength(midLen)) def _findHighestPointOnEdge(self, E): - tol = 0.0000001 + tol = 1e-7 zMax = E.BoundBox.ZMax - # Test first vertex + + # Check first vertex v = E.Vertexes[0] if abs(zMax - v.Z) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test second vertex + + # Check second vertex v = E.Vertexes[1] if abs(zMax - v.Z) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) - # Test middle point of edge - eMidLen = E.Length / 2.0 - eMidPnt = E.valueAt(E.getParameterByLength(eMidLen)) - if abs(zMax - eMidPnt.z) < tol: - return eMidPnt - if E.BoundBox.ZLength < 0.000000001: # roughly horizontal edge - return eMidPnt + + # Check midpoint on edge + midLen = E.Length / 2.0 + midPnt = E.valueAt(E.getParameterByLength(midLen)) + if abs(zMax - midPnt.z) < tol or E.BoundBox.ZLength < 1e-9: + return midPnt + return self._findHighestEdgePoint(E) def _findHighestEdgePoint(self, E): zMax = E.BoundBox.ZMax eLen = E.Length - L0 = 0 + L0 = 0.0 L1 = eLen - p0 = None - p1 = None cnt = 0 - while L1 - L0 > 0.00001 and cnt < 2000: + while L1 - L0 > 1e-5 and cnt < 2000: adj = (L1 - L0) * 0.1 - # Get points at L0 and L1 along edge p0 = E.valueAt(E.getParameterByLength(L0)) p1 = E.valueAt(E.getParameterByLength(L1)) - # Adjust points based on proximity to target depth + diff0 = zMax - p0.z diff1 = zMax - p1.z - if diff0 < diff1: - L1 -= adj - elif diff0 > diff1: + + # Closer to zMax means smaller diff (diff >= 0) + if diff0 > diff1: + # p1 is closer to zMax, so move L0 up to narrow range toward p1 L0 += adj + elif diff0 < diff1: + # p0 is closer, move L1 down to narrow range toward p0 + L1 -= adj else: L0 += adj L1 -= adj + cnt += 1 + midLen = (L0 + L1) / 2.0 return E.valueAt(E.getParameterByLength(midLen)) def _getVectorAngle(self, v): - # Assumes Z value of vector is zero - halfPi = math.pi / 2 - - if v.y == 1 and v.x == 0: - return halfPi - if v.y == -1 and v.x == 0: - return math.pi + halfPi - if v.y == 0 and v.x == 1: - return 0.0 - if v.y == 0 and v.x == -1: - return math.pi - - x = abs(v.x) - y = abs(v.y) - rads = math.atan(y / x) - if v.x > 0: - if v.y > 0: - return rads - else: - return (2 * math.pi) - rads - if v.x < 0: - if v.y > 0: - return math.pi - rads - else: - return math.pi + rads + return math.atan2(v.y, v.x) % (2 * math.pi) def _getCutSidePoints(self, obj, v0, v1, a1, a2, b1, b2): ea1 = Part.makeLine(v0, a1) @@ -1693,29 +1590,28 @@ class ObjectSlot(PathOp.ObjectOp): return False def _getVertFaceType(self, shape): - wires = list() + bottom_edge = self._getBottomEdge(shape) + if bottom_edge: + return ("Edge", bottom_edge) - bottomEdge = self._getBottomEdge(shape) - if bottomEdge: - return ("Edge", bottomEdge) + # Extrude vertically to create a sliceable solid + z_length = shape.BoundBox.ZLength + extrude_vec = FreeCAD.Vector(0, 0, z_length * 2.2 + 10) + extruded = shape.extrude(extrude_vec) - # Extract cross-section of face - extFwd = (shape.BoundBox.ZLength * 2.2) + 10 - extShp = shape.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - sliceZ = shape.BoundBox.ZMin + (extFwd / 2.0) - slcs = extShp.slice(FreeCAD.Vector(0, 0, 1), sliceZ) - for i in slcs: - wires.append(i) - if len(wires) > 0: - if wires[0].isClosed(): - face = Part.Face(wires[0]) - if face.Area > 0: - face.translate( - FreeCAD.Vector(0.0, 0.0, shape.BoundBox.ZMin - face.BoundBox.ZMin) - ) - return ("Face", face) - return ("Wire", wires[0]) - return False + # Slice halfway up the extrusion + slice_z = shape.BoundBox.ZMin + extrude_vec.z / 2.0 + slices = extruded.slice(FreeCAD.Vector(0, 0, 1), slice_z) + + if not slices: + return False + + if (wire := slices[0]).isClosed() and (face := Part.Face(wire)) > 0: + # Align face Z with original shape + z_offset = shape.BoundBox.ZMin - face.BoundBox.ZMin + face.translate(FreeCAD.Vector(0, 0, z_offset)) + return ("Face", face) + return ("Wire", wire) def _makeReference1Enumerations(self, sub, single=False): """Customize Reference1 enumerations based on feature type.""" @@ -1742,184 +1638,137 @@ class ObjectSlot(PathOp.ObjectOp): return ["Center of Mass", "Center of BoundBox", "Lowest Point", "Highest Point"] def _lineCollisionCheck(self, obj, p1, p2): - """Make simple circle with diameter of tool, at start point. - Extrude it latterally along path. - Extrude it vertically. - Check for collision with model.""" - # Make path travel of tool as 3D solid. - rad = self.tool.Diameter / 2.0 + """Model the swept volume of a linear tool move and check for collision with the model.""" + rad = getattr(self.tool.Diameter, "Value", self.tool.Diameter) / 2.0 + extVect = FreeCAD.Vector(0.0, 0.0, obj.StartDepth.Value - obj.FinalDepth.Value) - def getPerp(p1, p2, dist): + def make_cylinder(point): + circle = Part.makeCircle(rad, point) + face = Part.Face(Part.Wire(circle.Edges)) + face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - face.BoundBox.ZMin)) + return face.extrude(extVect) + + def make_rect_prism(p1, p2): toEnd = p2.sub(p1) - perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0) - if perp.x == 0 and perp.y == 0: - return perp + if toEnd.Length == 0: + return None + perp = FreeCAD.Vector(-toEnd.y, toEnd.x, 0.0) + if perp.Length == 0: + return None perp.normalize() - perp.multiply(dist) - return perp + perp.multiply(rad) - # Make first cylinder - ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges) - C1 = Part.Face(ce1) - zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin - C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - extFwd = obj.StartDepth.Value - obj.FinalDepth.Value - extVect = FreeCAD.Vector(0.0, 0.0, extFwd) - startShp = C1.extrude(extVect) + v1, v2 = p1.add(perp), p1.sub(perp) + v3, v4 = p2.sub(perp), p2.add(perp) + edges = Part.__sortEdges__( + [ + Part.makeLine(v1, v2), + Part.makeLine(v2, v3), + Part.makeLine(v3, v4), + Part.makeLine(v4, v1), + ] + ) + face = Part.Face(Part.Wire(edges)) + face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - face.BoundBox.ZMin)) + return face.extrude(extVect) - if p2.sub(p1).Length > 0: - # Make second cylinder - ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges) - C2 = Part.Face(ce2) - zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin - C2.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - endShp = C2.extrude(extVect) + # Build swept volume + startShp = make_cylinder(p1) + endShp = make_cylinder(p2) if p1 != p2 else None + boxShp = make_rect_prism(p1, p2) - # Make extruded rectangle to connect cylinders - perp = getPerp(p1, p2, rad) - v1 = p1.add(perp) - v2 = p1.sub(perp) - v3 = p2.sub(perp) - v4 = p2.add(perp) - e1 = Part.makeLine(v1, v2) - e2 = Part.makeLine(v2, v3) - e3 = Part.makeLine(v3, v4) - e4 = Part.makeLine(v4, v1) - edges = Part.__sortEdges__([e1, e2, e3, e4]) - rectFace = Part.Face(Part.Wire(edges)) - zTrans = obj.FinalDepth.Value - rectFace.BoundBox.ZMin - rectFace.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - boxShp = rectFace.extrude(extVect) - - # Fuse two cylinders and box together - part1 = startShp.fuse(boxShp) - pathTravel = part1.fuse(endShp) - else: - pathTravel = startShp + pathTravel = startShp + if boxShp: + pathTravel = pathTravel.fuse(boxShp) + if endShp: + pathTravel = pathTravel.fuse(endShp) self._addDebugObject(pathTravel, "PathTravel") - # Check for collision with model try: cmn = self.base.Shape.common(pathTravel) - if cmn.Volume > 0.000001: - return True + return cmn.Volume > 1e-6 except Exception: Path.Log.debug("Failed to complete path collision check.") - - return False + return False def _arcCollisionCheck(self, obj, p1, p2, arcCenter, arcRadius): - """Make simple circle with diameter of tool, at start and end points. - Make arch face between circles. Fuse and extrude it vertically. - Check for collision with model.""" - # Make path travel of tool as 3D solid. - if hasattr(self.tool.Diameter, "Value"): - rad = self.tool.Diameter.Value / 2.0 - else: - rad = self.tool.Diameter / 2.0 - extFwd = obj.StartDepth.Value - obj.FinalDepth.Value - extVect = FreeCAD.Vector(0.0, 0.0, extFwd) + """Check for collision by modeling the swept volume of an arc toolpath.""" - if self.isArc == 1: - # full circular slot - # make outer circle - oCircle = Part.makeCircle(arcRadius + rad, arcCenter) - oWire = Part.Wire(oCircle.Edges[0]) - outer = Part.Face(oWire) - # make inner circle - iRadius = arcRadius - rad - if iRadius > 0: - iCircle = Part.makeCircle(iRadius, arcCenter) - iWire = Part.Wire(iCircle.Edges[0]) - inner = Part.Face(iWire) - # Cut outer with inner - path = outer.cut(inner) - else: - path = outer - zTrans = obj.FinalDepth.Value - path.BoundBox.ZMin - path.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - pathTravel = path.extrude(extVect) - else: - # arc slot - # Make first cylinder - ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges) - C1 = Part.Face(ce1) - zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin - C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - startShp = C1.extrude(extVect) - # self._addDebugObject(startShp, 'StartCyl') + def make_cylinder_at_point(point, radius, height, final_depth): + circle = Part.makeCircle(radius, point) + face = Part.Face(Part.Wire(circle.Edges)) + face.translate(FreeCAD.Vector(0, 0, final_depth - face.BoundBox.ZMin)) + return face.extrude(FreeCAD.Vector(0, 0, height)) - # Make second cylinder - ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges) - C2 = Part.Face(ce2) - zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin - C2.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - endShp = C2.extrude(extVect) - # self._addDebugObject(endShp, 'EndCyl') + def make_arc_face(p1, p2, center, inner_radius, outer_radius): + (pA, pB) = self._makeOffsetArc(p1, p2, center, inner_radius) + arc_inside = Arcs.arcFrom2Pts(pA, pB, center) - # Make wire with inside and outside arcs, and lines on ends. - # Convert wire to face, then extrude + (pC, pD) = self._makeOffsetArc(p1, p2, center, outer_radius) + arc_outside = Arcs.arcFrom2Pts(pC, pD, center) - # verify offset does not force radius < 0 - newRadius = arcRadius - rad - # Path.Log.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius)) - if newRadius <= 0: - msg = translate("CAM_Slot", "Current offset value produces negative radius.") - FreeCAD.Console.PrintError(msg + "\n") - return False - else: - (pA, pB) = self._makeOffsetArc(p1, p2, arcCenter, newRadius) - arc_inside = Arcs.arcFrom2Pts(pA, pB, arcCenter) + pa = FreeCAD.Vector(*arc_inside.Vertexes[0].Point[:2], 0.0) + pb = FreeCAD.Vector(*arc_inside.Vertexes[1].Point[:2], 0.0) + pc = FreeCAD.Vector(*arc_outside.Vertexes[1].Point[:2], 0.0) + pd = FreeCAD.Vector(*arc_outside.Vertexes[0].Point[:2], 0.0) - # Arc 2 - outside - # verify offset does not force radius < 0 - newRadius = arcRadius + rad - # Path.Log.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius)) - if newRadius <= 0: - msg = translate("CAM_Slot", "Current offset value produces negative radius.") - FreeCAD.Console.PrintError(msg + "\n") - return False - else: - (pC, pD) = self._makeOffsetArc(p1, p2, arcCenter, newRadius) - arc_outside = Arcs.arcFrom2Pts(pC, pD, arcCenter) - - # Make end lines to connect arcs - vA = arc_inside.Vertexes[0] - vB = arc_inside.Vertexes[1] - vC = arc_outside.Vertexes[1] - vD = arc_outside.Vertexes[0] - pa = FreeCAD.Vector(vA.X, vA.Y, 0.0) - pb = FreeCAD.Vector(vB.X, vB.Y, 0.0) - pc = FreeCAD.Vector(vC.X, vC.Y, 0.0) - pd = FreeCAD.Vector(vD.X, vD.Y, 0.0) - - # Make closed arch face and extrude e1 = Part.makeLine(pb, pc) e2 = Part.makeLine(pd, pa) edges = Part.__sortEdges__([arc_inside, e1, arc_outside, e2]) - rectFace = Part.Face(Part.Wire(edges)) - zTrans = obj.FinalDepth.Value - rectFace.BoundBox.ZMin - rectFace.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) - boxShp = rectFace.extrude(extVect) - # self._addDebugObject(boxShp, 'ArcBox') + return Part.Face(Part.Wire(edges)) - # Fuse two cylinders and box together - part1 = startShp.fuse(boxShp) - pathTravel = part1.fuse(endShp) + # Radius and extrusion direction + rad = getattr(self.tool.Diameter, "Value", self.tool.Diameter) / 2.0 + extVect = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value) + + if self.isArc == 1: + # Full circle slot: make annular ring + outer = Part.Face(Part.Wire(Part.makeCircle(arcRadius + rad, arcCenter).Edges)) + iRadius = arcRadius - rad + path = ( + outer.cut(Part.Face(Part.Wire(Part.makeCircle(iRadius, arcCenter).Edges))) + if iRadius > 0 + else outer + ) + path.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - path.BoundBox.ZMin)) + pathTravel = path.extrude(extVect) + + else: + # Arc slot with entry and exit cylinders + startShp = make_cylinder_at_point(p1, rad, extVect.z, obj.FinalDepth.Value) + endShp = make_cylinder_at_point(p2, rad, extVect.z, obj.FinalDepth.Value) + + # Validate inner arc + inner_radius = arcRadius - rad + if inner_radius <= 0: + FreeCAD.Console.PrintError( + translate("CAM_Slot", "Current offset value produces negative radius.") + "\n" + ) + return False + + # Validate outer arc + outer_radius = arcRadius + rad + if outer_radius <= 0: + FreeCAD.Console.PrintError( + translate("CAM_Slot", "Current offset value produces negative radius.") + "\n" + ) + return False + + rectFace = make_arc_face(p1, p2, arcCenter, inner_radius, outer_radius) + rectFace.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - rectFace.BoundBox.ZMin)) + arcShp = rectFace.extrude(extVect) + + pathTravel = startShp.fuse(arcShp).fuse(endShp) self._addDebugObject(pathTravel, "PathTravel") - # Check for collision with model try: cmn = self.base.Shape.common(pathTravel) - if cmn.Volume > 0.000001: - # print("volume=", cmn.Volume) - return True + return cmn.Volume > 1e-6 except Exception: Path.Log.debug("Failed to complete path collision check.") - - return False + return False def _addDebugObject(self, objShape, objName): if self.showDebugObjects: From 2c1d23e4e553549eb27d9cf77ce77dc9e42281db Mon Sep 17 00:00:00 2001 From: George Peden Date: Mon, 23 Jun 2025 12:24:33 -0700 Subject: [PATCH 126/126] Sketcher: Add contextual input hints to constraint commands (InputHints Phase 2) (#21751) * Sketcher: Extend InputHints infrastructure to constraint tools - Implement DrawSketchHandler::getToolHints() for constraint workflows - Add centralized hint table mapping constraint commands to step-specific InputHints - Integrate hint lookup in DrawSketchHandlerGenConstraint and dimension handler - Provide step-by-step user guidance for: - Coincident, PointOnObject, Distance (X/Y) - Horizontal, Vertical, HorVer, Lock, Block - Equal, Symmetric, Radius, Diameter, Angle - Tangent, Perpendicular, Parallel This continues the InputHints work started for drawing tools by enabling consistent, contextual guidance for constraint creation, including multi-step workflows like tangent-via-point. * Call updateHint() after selection reset to re-arm the first-step prompt when the tool stays active after apply. * Add comments to hints table structure * Sketcher: Update constraint hint text to use "pick" instead of "Select" Change constraint hint text from "Select" to "pick" to maintain consistency with existing FreeCAD UI style. This affects the DrawSketchHandlerGenConstraint hint system for various constraint operations including coincident, distance, horizontal/vertical, block, lock, symmetry, tangent, perpendicular, parallel, and distance constraints. The hints now follow the pattern: - "%1 pick first point or edge" - "%1 pick second point or edge" - "%1 pick line or two points" etc. This provides consistent terminology throughout the sketcher constraint creation workflow. * - Remove redundant 'first' from initial selection hints - Improve consistency in hint text formatting per Developer Guidelines - Add consistent spacing in comment sections" * Per PR feedback for DrawSketchHandlerDimension hints: * Change 'Click to' to "pick" * Simplify hint wording * Combine redundant else * Use direct return pattern instead of building hints list * Update lookupConstraintHints() to use C++20 std:ranges::find_if form per PR review feedback * Sketcher: Refine constraint hints per PR feedback - Use consistent 'point or edge' phrasing in Distance and DistanceX/Y tools - Reword Horizontal/Vertical step 0 to avoid misleading 'two points' - Generalize Tangent and Perpendicular hints to 'edge' with optional point - Simplify legacy Distance to 'point or edge' * Add dynamic hint handling for PointOnObject constraint - Implemented contextual hints in getToolHints() to generate an appropriate step 2 hint based on step 1 selection type - Preserved static lookupConstraintHints() for all other tools * Sketcher: Convert constraint hint table to C++20 designated initializer syntax - Refactored static constraint hint table to follow Sketcher hint development guidelines - Uses C++20 designated initializers for clarity and maintainability - No changes to hint logic or behavior; content is identical to previous version --- src/Mod/Sketcher/Gui/CommandConstraints.cpp | 218 +++++++++++++++++++- src/Mod/Sketcher/Gui/DrawSketchHandler.h | 6 + 2 files changed, 223 insertions(+), 1 deletion(-) diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.cpp b/src/Mod/Sketcher/Gui/CommandConstraints.cpp index 894cd063c3..0d43068a92 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.cpp +++ b/src/Mod/Sketcher/Gui/CommandConstraints.cpp @@ -1089,6 +1089,9 @@ public: selSeq.clear(); resetOngoingSequences(); + // Re-arm hint for next operation + updateHint(); + return true; } _tempOnSequences.insert(*token); @@ -1102,11 +1105,211 @@ public: seqIndex++; selFilterGate->setAllowedSelTypes(allowedSelTypes); } + updateHint(); return true; } + std::list getToolHints() const override { + const std::string commandName = cmd->getName(); + const int selectionStep = seqIndex; + + // Special case for Sketcher_ConstrainPointOnObject to generate dynamic step hint + if (commandName == "Sketcher_ConstrainPointOnObject") { + if (selectionStep == 0) { + return {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else if (selectionStep == 1 && !selSeq.empty()) { + if (isVertex(selSeq[0].GeoId, selSeq[0].PosId)) { + return {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else { + return {{QObject::tr("%1 pick point"), {Gui::InputHint::UserInput::MouseLeft}}}; + } + } + } + + // For everything else, use the static table + return lookupConstraintHints(commandName, selectionStep); +} + private: + struct ConstraintHintEntry { + std::string commandName; // FreeCAD command name (e.g., "Sketcher_ConstrainSymmetric") + int selectionStep; // 0-indexed step in the selection sequence + std::list hints; // Hint text and input types for this step + }; + + using ConstraintHintTable = std::vector; + + // Constraint hint lookup table + // Format: {command_name, selection_step, {hint_text, input_types}} + // Steps are 0-indexed and correspond to DrawSketchHandlerGenConstraint::seqIndex + // Each step provides contextual guidance for what the user should select next + static ConstraintHintTable getConstraintHintTable() { + return { + // Coincident + {.commandName = "Sketcher_ConstrainCoincidentUnified", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainCoincidentUnified", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Distance X/Y + {.commandName = "Sketcher_ConstrainDistanceX", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistanceX", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistanceY", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistanceY", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Horizontal/Vertical + {.commandName = "Sketcher_ConstrainHorizontal", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge or first point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainHorizontal", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainVertical", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge or first point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainVertical", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainHorVer", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge or first point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainHorVer", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Block/Lock + {.commandName = "Sketcher_ConstrainBlock", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge to block"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainLock", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point to lock"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Coincident (individual) + {.commandName = "Sketcher_ConstrainCoincident", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or curve"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainCoincident", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or curve"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainEqual", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainEqual", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Radius/Diameter + {.commandName = "Sketcher_ConstrainRadius", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick circle or arc"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDiameter", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick circle or arc"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Angle + {.commandName = "Sketcher_ConstrainAngle", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainAngle", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Symmetry + {.commandName = "Sketcher_ConstrainSymmetric", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainSymmetric", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainSymmetric", + .selectionStep = 2, + .hints = {{QObject::tr("%1 pick symmetry line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Tangent + {.commandName = "Sketcher_ConstrainTangent", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainTangent", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainTangent", + .selectionStep = 2, + .hints = {{QObject::tr("%1 pick optional tangent point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Perpendicular + {.commandName = "Sketcher_ConstrainPerpendicular", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainPerpendicular", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainPerpendicular", + .selectionStep = 2, + .hints = {{QObject::tr("%1 pick optional perpendicular point"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Parallel + {.commandName = "Sketcher_ConstrainParallel", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainParallel", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second line"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + // Distance + {.commandName = "Sketcher_ConstrainDistance", + .selectionStep = 0, + .hints = {{QObject::tr("%1 pick point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + + {.commandName = "Sketcher_ConstrainDistance", + .selectionStep = 1, + .hints = {{QObject::tr("%1 pick second point or edge"), {Gui::InputHint::UserInput::MouseLeft}}}}, + }; + } + + static std::list lookupConstraintHints(const std::string& commandName, int selectionStep) { + const auto constraintHintTable = getConstraintHintTable(); + auto it = std::ranges::find_if(constraintHintTable, + [&commandName, selectionStep](const ConstraintHintEntry& entry) { + return entry.commandName == commandName && entry.selectionStep == selectionStep; + }); + + return (it != constraintHintTable.end()) ? it->hints : std::list{}; + } + void activated() override { selFilterGate = new GenericConstraintSelection(sketchgui->getObject()); @@ -1602,6 +1805,8 @@ public: ss.str().c_str()); sketchgui->draw(false, false); // Redraw } + + updateHint(); return true; } @@ -1616,6 +1821,17 @@ public: DrawSketchHandler::quit(); } } + +std::list getToolHints() const override { + if (selectionEmpty()) { + return {{QObject::tr("%1 pick geometry"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else if (selPoints.size() == 1 && selLine.empty() && selCircleArc.empty()) { + return {{QObject::tr("%1 pick second point or geometry"), {Gui::InputHint::UserInput::MouseLeft}}}; + } else { + return {{QObject::tr("%1 place dimension"), {Gui::InputHint::UserInput::MouseLeft}}}; + } +} + protected: SpecialConstraint specialConstraint; AvailableConstraint availableConstraint; @@ -1759,7 +1975,7 @@ protected: && !contains(selEllipseAndCo, elem); } - bool selectionEmpty() + bool selectionEmpty() const { return selPoints.empty() && selLine.empty() && selCircleArc.empty() && selEllipseAndCo.empty(); } diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandler.h b/src/Mod/Sketcher/Gui/DrawSketchHandler.h index 607f4ee50a..ca7ad4e700 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandler.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandler.h @@ -32,6 +32,8 @@ #include #include #include +#include + #include #include @@ -160,6 +162,10 @@ public: return false; } + virtual std::list getToolHints() const + { + return {}; + } void quit() override; friend class ViewProviderSketch;