diff --git a/.clang-format b/.clang-format index 6ba545491f..45623c68c9 100644 --- a/.clang-format +++ b/.clang-format @@ -47,7 +47,7 @@ ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PackConstructorInitializers: Never PointerAlignment: Left -ReflowComments: false +ReflowComments: true SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false diff --git a/cMake/FindOpenCV.cmake b/cMake/FindOpenCV.cmake index 8b06b13815..e5f2d37ddd 100644 --- a/cMake/FindOpenCV.cmake +++ b/cMake/FindOpenCV.cmake @@ -3,10 +3,10 @@ # # The following variables are optionally searched for defaults # OpenCV_ROOT_DIR: Base directory of OpenCv tree to use. -# OpenCV_FIND_REQUIRED_COMPONENTS : FIND_PACKAGE(OpenCV COMPONENTS ..) +# OpenCV_FIND_REQUIRED_COMPONENTS : FIND_PACKAGE(OpenCV COMPONENTS ..) # compatible interface. typically CV CXCORE CVAUX HIGHGUI CVCAM .. etc. # -# The following are set after configuration is done: +# The following are set after configuration is done: # OpenCV_FOUND # OpenCV_INCLUDE_DIR # OpenCV_LIBRARIES @@ -16,10 +16,10 @@ # OPENCV_* uppercase replaced by case sensitive OpenCV_* # OPENCV_EXE_LINKER_FLAGS # OPENCV_INCLUDE_DIR : replaced by plural *_DIRS -# -# 2004/05 Jan Woetzel, Friso, Daniel Grest +# +# 2004/05 Jan Woetzel, Friso, Daniel Grest # 2006/01 complete rewrite by Jan Woetzel -# 1006/09 2nd rewrite introducing ROOT_DIR and PATH_SUFFIXES +# 1006/09 2nd rewrite introducing ROOT_DIR and PATH_SUFFIXES # to handle multiple installed versions gracefully by Jan Woetzel # # tested with: @@ -42,14 +42,14 @@ IF (NOT OpenCV_FIND_COMPONENTS) SET(OpenCV_FIND_REQUIRED_COMPONENTS CV CXCORE CVAUX HIGHGUI ) IF (WIN32) LIST(APPEND OpenCV_FIND_REQUIRED_COMPONENTS CVCAM ) # WIN32 only actually - ENDIF(WIN32) + ENDIF(WIN32) ENDIF (NOT OpenCV_FIND_COMPONENTS) # typical root dirs of installations, exactly one of them is used SET (OpenCV_POSSIBLE_ROOT_DIRS "${OpenCV_ROOT_DIR}" - "$ENV{OpenCV_ROOT_DIR}" + "$ENV{OpenCV_ROOT_DIR}" "$ENV{OPENCV_DIR}" # only for backward compatibility deprecated by ROOT_DIR "$ENV{OPENCV_HOME}" # only for backward compatibility "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Intel(R) Open Source Computer Vision Library_is1;Inno Setup: App Path]" @@ -59,17 +59,17 @@ SET (OpenCV_POSSIBLE_ROOT_DIRS ) -# MIP Uni Kiel /opt/net network installation +# MIP Uni Kiel /opt/net network installation # get correct prefix for current gcc compiler version for gcc 3.x 4.x IF (${CMAKE_COMPILER_IS_GNUCXX}) IF (NOT OpenCV_FIND_QUIETLY) MESSAGE(STATUS "Checking GNUCXX version 3/4 to determine OpenCV /opt/net/ path") ENDIF (NOT OpenCV_FIND_QUIETLY) - EXEC_PROGRAM(${CMAKE_CXX_COMPILER} ARGS --version OUTPUT_VARIABLE CXX_COMPILER_VERSION) + EXEC_PROGRAM(${CMAKE_CXX_COMPILER} ARGS --version OUTPUT_VARIABLE CXX_COMPILER_VERSION) IF (CXX_COMPILER_VERSION MATCHES ".*3\\.[0-9].*") SET(IS_GNUCXX3 TRUE) LIST(APPEND OpenCV_POSSIBLE_ROOT_DIRS /opt/net/gcc33/OpenCV ) - ENDIF(CXX_COMPILER_VERSION MATCHES ".*3\\.[0-9].*") + ENDIF(CXX_COMPILER_VERSION MATCHES ".*3\\.[0-9].*") IF (CXX_COMPILER_VERSION MATCHES ".*4\\.[0-9].*") SET(IS_GNUCXX4 TRUE) LIST(APPEND OpenCV_POSSIBLE_ROOT_DIRS /opt/net/gcc41/OpenCV ) @@ -79,15 +79,15 @@ ENDIF (${CMAKE_COMPILER_IS_GNUCXX}) #DBG_MSG("DBG (OpenCV_POSSIBLE_ROOT_DIRS=${OpenCV_POSSIBLE_ROOT_DIRS}") # -# select exactly ONE OpenCV base directory/tree +# select exactly ONE OpenCV base directory/tree # to avoid mixing different version headers and libs # -FIND_PATH(OpenCV_ROOT_DIR - NAMES +FIND_PATH(OpenCV_ROOT_DIR + NAMES cv/include/cv.h # windows include/opencv/cv.h # linux /opt/net - include/cv/cv.h - include/cv.h + include/cv/cv.h + include/cv.h PATHS ${OpenCV_POSSIBLE_ROOT_DIRS}) DBG_MSG("OpenCV_ROOT_DIR=${OpenCV_ROOT_DIR}") @@ -106,7 +106,7 @@ SET(OpenCV_INCDIR_SUFFIXES otherlibs/_graphics/include ) -# library linkdir suffixes appended to OpenCV_ROOT_DIR +# library linkdir suffixes appended to OpenCV_ROOT_DIR SET(OpenCV_LIBDIR_SUFFIXES lib OpenCV/lib @@ -119,56 +119,56 @@ SET(OpenCV_LIBDIR_SUFFIXES # find incdir for each lib # FIND_PATH(OpenCV_CV_INCLUDE_DIR - NAMES cv.h - PATHS ${OpenCV_ROOT_DIR} + NAMES cv.h + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_INCDIR_SUFFIXES} ) -FIND_PATH(OpenCV_CXCORE_INCLUDE_DIR +FIND_PATH(OpenCV_CXCORE_INCLUDE_DIR NAMES cxcore.h - PATHS ${OpenCV_ROOT_DIR} + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_INCDIR_SUFFIXES} ) -FIND_PATH(OpenCV_CVAUX_INCLUDE_DIR +FIND_PATH(OpenCV_CVAUX_INCLUDE_DIR NAMES cvaux.h - PATHS ${OpenCV_ROOT_DIR} + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_INCDIR_SUFFIXES} ) -FIND_PATH(OpenCV_HIGHGUI_INCLUDE_DIR - NAMES highgui.h - PATHS ${OpenCV_ROOT_DIR} +FIND_PATH(OpenCV_HIGHGUI_INCLUDE_DIR + NAMES highgui.h + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_INCDIR_SUFFIXES} ) -FIND_PATH(OpenCV_CVCAM_INCLUDE_DIR - NAMES cvcam.h - PATHS ${OpenCV_ROOT_DIR} +FIND_PATH(OpenCV_CVCAM_INCLUDE_DIR + NAMES cvcam.h + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_INCDIR_SUFFIXES} ) # -# find sbsolute path to all libraries +# find sbsolute path to all libraries # some are optionally, some may not exist on Linux # -FIND_LIBRARY(OpenCV_CV_LIBRARY +FIND_LIBRARY(OpenCV_CV_LIBRARY NAMES cv opencv - PATHS ${OpenCV_ROOT_DIR} + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) FIND_LIBRARY(OpenCV_CVAUX_LIBRARY NAMES cvaux PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) -FIND_LIBRARY(OpenCV_CVCAM_LIBRARY +FIND_LIBRARY(OpenCV_CVCAM_LIBRARY NAMES cvcam - PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) FIND_LIBRARY(OpenCV_CVHAARTRAINING_LIBRARY NAMES cvhaartraining - PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) -FIND_LIBRARY(OpenCV_CXCORE_LIBRARY + PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) +FIND_LIBRARY(OpenCV_CXCORE_LIBRARY NAMES cxcore PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) -FIND_LIBRARY(OpenCV_CXTS_LIBRARY +FIND_LIBRARY(OpenCV_CXTS_LIBRARY NAMES cxts PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) -FIND_LIBRARY(OpenCV_HIGHGUI_LIBRARY +FIND_LIBRARY(OpenCV_HIGHGUI_LIBRARY NAMES highgui PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) -FIND_LIBRARY(OpenCV_ML_LIBRARY +FIND_LIBRARY(OpenCV_ML_LIBRARY NAMES ml PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) -FIND_LIBRARY(OpenCV_TRS_LIBRARY +FIND_LIBRARY(OpenCV_TRS_LIBRARY NAMES trs PATHS ${OpenCV_ROOT_DIR} PATH_SUFFIXES ${OpenCV_LIBDIR_SUFFIXES} ) @@ -181,7 +181,7 @@ SET(OpenCV_FOUND ON) DBG_MSG("OpenCV_FIND_REQUIRED_COMPONENTS=${OpenCV_FIND_REQUIRED_COMPONENTS}") FOREACH(NAME ${OpenCV_FIND_REQUIRED_COMPONENTS} ) - # only good if header and library both found + # only good if header and library both found IF (OpenCV_${NAME}_INCLUDE_DIR AND OpenCV_${NAME}_LIBRARY) LIST(APPEND OpenCV_INCLUDE_DIRS ${OpenCV_${NAME}_INCLUDE_DIR} ) LIST(APPEND OpenCV_LIBRARIES ${OpenCV_${NAME}_LIBRARY} ) @@ -192,13 +192,13 @@ FOREACH(NAME ${OpenCV_FIND_REQUIRED_COMPONENTS} ) "\nOpenCV_${NAME}_LIBRARY=${OpenCV_${NAME}_LIBRARY} ") SET(OpenCV_FOUND OFF) ENDIF (OpenCV_${NAME}_INCLUDE_DIR AND OpenCV_${NAME}_LIBRARY) - + ENDFOREACH(NAME) DBG_MSG("OpenCV_INCLUDE_DIRS=${OpenCV_INCLUDE_DIRS}") DBG_MSG("OpenCV_LIBRARIES=${OpenCV_LIBRARIES}") -# get the link directory for rpath to be used with LINK_DIRECTORIES: +# get the link directory for rpath to be used with LINK_DIRECTORIES: IF (OpenCV_CV_LIBRARY) GET_FILENAME_COMPONENT(OpenCV_LINK_DIRECTORIES ${OpenCV_CV_LIBRARY} PATH) ENDIF (OpenCV_CV_LIBRARY) @@ -239,7 +239,7 @@ IF(NOT OpenCV_FOUND) MESSAGE(FATAL_ERROR "OpenCV required but some headers or libs not found. Please specify it's location with OpenCV_ROOT_DIR env. variable.") ELSE(OpenCV_FIND_REQUIRED) - MESSAGE(STATUS + MESSAGE(STATUS "ERROR: OpenCV was not found.") ENDIF(OpenCV_FIND_REQUIRED) ENDIF(NOT OpenCV_FIND_QUIETLY) diff --git a/cMake/FindPyCXX.cmake b/cMake/FindPyCXX.cmake index 1b703e967a..c15c9ec367 100644 --- a/cMake/FindPyCXX.cmake +++ b/cMake/FindPyCXX.cmake @@ -53,7 +53,7 @@ else(PYCXX_INCLUDE_DIR) endif(PyCXX_FIND_REQUIRED) endif(NOT PYCXX_INCLUDE_DIR) endif(PYCXX_INCLUDE_DIR) - + # find the sources directory if(PYCXX_SOURCE_DIR) # source directory specified, they'd better be there diff --git a/cMake/FindRift.cmake b/cMake/FindRift.cmake index 55dd21b519..8082e8da92 100644 --- a/cMake/FindRift.cmake +++ b/cMake/FindRift.cmake @@ -6,7 +6,7 @@ # OCULUS_LIBRARIES # # Copyright (c) 2012 I-maginer -# +# # This program 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 of the License, or (at your option) any later @@ -42,11 +42,11 @@ IF(OCULUS_ROOT) ${OCULUS_INCLUDE_SEARCH_DIRS} ${OCULUS_ROOT}/include ) - SET(OCULUS_LIBRARY_SEARCH_RELEASE_DIRS + SET(OCULUS_LIBRARY_SEARCH_RELEASE_DIRS ${OCULUS_LIBRARY_SEARCH_DIRS} ${OCULUS_ROOT}/Lib/x64/VS2012 ) - SET(OCULUS_LIBRARY_SEARCH_DEBUG_DIRS + SET(OCULUS_LIBRARY_SEARCH_DEBUG_DIRS ${OCULUS_LIBRARY_SEARCH_DIRS} ${OCULUS_ROOT}/Lib/x64/VS2012 ) diff --git a/cMake/FreeCAD_Helpers/PrintFinalReport.cmake b/cMake/FreeCAD_Helpers/PrintFinalReport.cmake index 845eae98a1..cb59d7e15f 100644 --- a/cMake/FreeCAD_Helpers/PrintFinalReport.cmake +++ b/cMake/FreeCAD_Helpers/PrintFinalReport.cmake @@ -99,7 +99,7 @@ macro(PrintFinalReport) value(FREECAD_LIBPACK_USE) section_end() - + ################ Libraries ################## section_begin(Libraries) diff --git a/cMake/FreeCAD_Helpers/SetupQt.cmake b/cMake/FreeCAD_Helpers/SetupQt.cmake index 2f3d71b223..34c454adba 100644 --- a/cMake/FreeCAD_Helpers/SetupQt.cmake +++ b/cMake/FreeCAD_Helpers/SetupQt.cmake @@ -15,7 +15,7 @@ if(BUILD_GUI) if (BUILD_WEB) list (APPEND FREECAD_QT_COMPONENTS WebEngineWidgets) endif() - if(BUILD_DESIGNER_PLUGIN) + if(BUILD_DESIGNER_PLUGIN) list (APPEND FREECAD_QT_COMPONENTS Designer) endif() endif() diff --git a/cMake/FreeCAD_Helpers/SetupShibokenAndPyside.cmake b/cMake/FreeCAD_Helpers/SetupShibokenAndPyside.cmake index b7a1c73b12..43e650fe85 100644 --- a/cMake/FreeCAD_Helpers/SetupShibokenAndPyside.cmake +++ b/cMake/FreeCAD_Helpers/SetupShibokenAndPyside.cmake @@ -190,7 +190,7 @@ macro(find_pip_package PACKAGE) STRING(SUBSTRING "${LINE}" 9 -1 PIP_PACKAGE_LOCATION) endif() endforeach() - file(TO_NATIVE_PATH "${PIP_PACKAGE_LOCATION}/${PIP_PACKAGE_NAME}/include" INCLUDE_DIR) + file(TO_NATIVE_PATH "${PIP_PACKAGE_LOCATION}/${PIP_PACKAGE_NAME}/include" INCLUDE_DIR) file(TO_NATIVE_PATH "${PIP_PACKAGE_LOCATION}/${PIP_PACKAGE_NAME}/lib" LIBRARY) set(${PACKAGE}_INCLUDE_DIRS ${INCLUDE_DIR} PARENT_SCOPE) set(${PACKAGE}_LIBRARIES ${LIBRARY} PARENT_SCOPE) @@ -200,7 +200,7 @@ macro(find_pip_package PACKAGE) endmacro() -# Macros similar to FindQt4.cmake's WRAP_UI and WRAP_RC, for the automatic generation of Python +# Macros similar to FindQt4.cmake's WRAP_UI and WRAP_RC, for the automatic generation of Python # code from Qt4's user interface ('.ui') and resource ('.qrc') files. These macros are called: # - PYSIDE_WRAP_UI # - PYSIDE_WRAP_RC diff --git a/cMake/UseLibPackCLbundler.cmake b/cMake/UseLibPackCLbundler.cmake index 082a27f8b5..22177651c5 100644 --- a/cMake/UseLibPackCLbundler.cmake +++ b/cMake/UseLibPackCLbundler.cmake @@ -44,7 +44,7 @@ set (Qt5XmlPatterns_DIR ${Qt5_ROOT_DIR}/lib/cmake/Qt5XmlPatterns CACHE PATH "") find_library(XercesC_LIBRARY_RELEASE xerces-c_3 "${FREECAD_LIBPACK_DIR}/lib") find_library(XercesC_LIBRARY_DEBUG xerces-c_3D "${FREECAD_LIBPACK_DIR}/lib") set (XercesC_LIBRARIES debug ${XercesC_LIBRARY_DEBUG} optimized ${XercesC_LIBRARY_RELEASE}) -set(XercesC_FOUND TRUE) +set(XercesC_FOUND TRUE) find_library(COIN3D_LIBRARY_RELEASE coin4 "${FREECAD_LIBPACK_DIR}/lib") find_library(COIN3D_LIBRARY_DEBUG coin4d "${FREECAD_LIBPACK_DIR}/lib") diff --git a/src/App/FreeCADInit.py b/src/App/FreeCADInit.py index 123c7522ca..f085c24718 100644 --- a/src/App/FreeCADInit.py +++ b/src/App/FreeCADInit.py @@ -947,6 +947,17 @@ class PropertyType(IntEnum): App.PropertyType = PropertyType +class ReturnType(IntEnum): + PyObject = 0 + DocObject = 1 + DocAndPyObject = 2 + Placement = 3 + Matrix = 4 + LinkAndPlacement = 5 + LinkAndMatrix = 6 + +App.ReturnType = ReturnType + # clean up namespace del(InitApplications) del(test_ascii) diff --git a/src/Doc/sphinx/conf.py b/src/Doc/sphinx/conf.py index 6146780a80..b94c1953f6 100644 --- a/src/Doc/sphinx/conf.py +++ b/src/Doc/sphinx/conf.py @@ -2,8 +2,8 @@ #*************************************************************************** #* * -#* Copyright (c) 2012 * -#* Yorik van Havre * +#* Copyright (c) 2012 * +#* Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * @@ -52,7 +52,7 @@ if os.path.exists(os.path.abspath(os.path.join(os.environ["HOME"],"FreeCAD/lib") elif commands.getstatusoutput("locate FreeCAD/lib")[0] == 0: path = commands.getstatusoutput("locate FreeCAD/lib")[1].split()[0] sys.path.append(path) - + # locate TemplatePyMod if commands.getstatusoutput("locate TemplatePyMod")[0] == 0: path = commands.getstatusoutput("locate TemplatePyMod")[1].split()[0] diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index a43253c775..df84585d76 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -918,6 +918,22 @@ public: } } + void trySaveUserParameter() + { + // update the XML structure and save the user parameter to disk (#0001989) + bool saveParameter = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/General")->GetBool("SaveUserParameter", true); + if (saveParameter) { + saveUserParameter(); + } + } + + void saveUserParameter() + { + ParameterManager* parmgr = App::GetApplication().GetParameterSet("User parameter"); + parmgr->SaveDocument(App::Application::Config()["UserParameter"].c_str()); + } + public: RecentFilesAction *master; ParameterGrp::handle handle; @@ -954,13 +970,7 @@ void RecentFilesAction::appendFile(const QString& filename) setFiles(files); save(); - // update the XML structure and save the user parameter to disk (#0001989) - bool saveParameter = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/General")->GetBool("SaveUserParameter", true); - if (saveParameter) { - ParameterManager* parmgr = App::GetApplication().GetParameterSet("User parameter"); - parmgr->SaveDocument(App::Application::Config()["UserParameter"].c_str()); - } + _pimpl->trySaveUserParameter(); } /** @@ -1022,6 +1032,7 @@ void RecentFilesAction::activateFile(int id) QMessageBox::critical(getMainWindow(), tr("File not found"), tr("The file '%1' cannot be opened.").arg(filename)); files.removeAll(filename); setFiles(files); + save(); } else { // invokes appendFile() diff --git a/src/Gui/DlgAddProperty.cpp b/src/Gui/DlgAddProperty.cpp index 2fe4d34065..40ab59df9d 100644 --- a/src/Gui/DlgAddProperty.cpp +++ b/src/Gui/DlgAddProperty.cpp @@ -56,6 +56,8 @@ DlgAddProperty::DlgAddProperty(QWidget* parent, std::vector types; Base::Type::getAllDerivedFrom(Base::Type::fromName("App::Property"),types); + std::sort(types.begin(), types.end(), [](Base::Type a, Base::Type b) { return strcmp(a.getName(), b.getName()) < 0; }); + for(const auto& type : types) { ui->comboType->addItem(QString::fromLatin1(type.getName())); if(type == defType) diff --git a/src/Gui/DlgSettingsNavigation.cpp b/src/Gui/DlgSettingsNavigation.cpp index 29c2b1c5d4..f01380a236 100644 --- a/src/Gui/DlgSettingsNavigation.cpp +++ b/src/Gui/DlgSettingsNavigation.cpp @@ -144,7 +144,7 @@ void DlgSettingsNavigation::loadSettings() ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath ("User parameter:BaseApp/Preferences/View"); - std::string model = hGrp->GetASCII("NavigationStyle",CADNavigationStyle::getClassTypeId().getName()); + std::string model = hGrp->GetASCII("NavigationStyle", CADNavigationStyle::getClassTypeId().getName()); int index = ui->comboNavigationStyle->findData(QByteArray(model.c_str())); if (index > -1) ui->comboNavigationStyle->setCurrentIndex(index); @@ -196,11 +196,25 @@ void DlgSettingsNavigation::loadSettings() QStringList familyNames = QFontDatabase::families(QFontDatabase::Any); #endif ui->naviCubeFontName->addItems(familyNames); + + // if the parameter has not yet been set, do so immediately + // this assures it is set even if the user cancels the dialog + if (hGrp->GetASCII("FontString", "").empty()) + hGrp->SetASCII("FontString", defaultSansserifFont.constData()); int indexFamilyNames = familyNames.indexOf( - QString::fromLatin1(hGrp->GetASCII("FontString", defaultSansserifFont).c_str())); + QString::fromStdString(hGrp->GetASCII("FontString", defaultSansserifFont))); if (indexFamilyNames < 0) indexFamilyNames = 0; ui->naviCubeFontName->setCurrentIndex(indexFamilyNames); + + // if the FontSize parameter does not yet exist, set the default value + // the default is defined in NaviCubeImplementation::getDefaultFontSize() + // but not accessible if there is no cube yet drawn + if (hGrp->GetInt("FontSize", 0) == 0) { + // the "4" is the hardcoded m_OverSample from getDefaultFontSize() + hGrp->SetInt("FontSize", int(0.18 * 4 * ui->prefCubeSize->value())); + ui->naviCubeFontSize->onRestore(); + } } void DlgSettingsNavigation::onMouseButtonClicked() diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp index 2815d9c2ea..37c1a97832 100644 --- a/src/Gui/MainWindow.cpp +++ b/src/Gui/MainWindow.cpp @@ -280,6 +280,7 @@ MainWindow::MainWindow(QWidget * parent, Qt::WindowFlags f) tab->setTabsClosable(true); // The tabs might be very wide tab->setExpanding(false); + tab->setObjectName(QString::fromLatin1("mdiAreaTabBar")); } d->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); diff --git a/src/Gui/NaviCube.cpp b/src/Gui/NaviCube.cpp index e489e8c6f0..9c384540f9 100644 --- a/src/Gui/NaviCube.cpp +++ b/src/Gui/NaviCube.cpp @@ -296,24 +296,18 @@ void NaviCube::setFontSize(int size) // the Helvetica font is a good start for most OSes QFont NaviCube::getDefaultSansserifFont() { - QFont font(QString::fromLatin1("Helvetica")); - if (font.styleHint() & QFont::SansSerif) + // Windows versions since 2017 have the 'Bahnschrift' font (a condensed + // sans serif font, optimized for readability despite being condensed), + // we first check for that. + QFont font(QString::fromLatin1("Bahnschrift")); + if (font.exactMatch()) return font; - // on Windows 10 or newer there is no Helvetica font - // therefore if we didn't find a Helvetica font check - // for the Bahnschrift font (in all Windows since 2017) - // if this too is unavailable (on Win 7), we check for the - // DejaVu Sans fonts - font.setFamily(QString::fromLatin1("Bahnschrift")); - // on Windows 11 sansserif fonts like Bahnschrift do not have the - // styleHint QFont::SansSerif but QFont::AnyStyle - // however, in future they might have, thus allow both - if (font.styleHint() == QFont::SansSerif || font.styleHint() == QFont::AnyStyle) - return font; - font.setFamily(QString::fromLatin1("DejaVu Sans")); - if (font.styleHint() == QFont::SansSerif || font.styleHint() == QFont::AnyStyle) - return font; - return font; // We failed, but return whatever we have anyway + + // On systems without 'Bahnschrift' we check for 'Helvetica' or its closest match + // as default sans serif font. (For Windows 7 this will e.g. result in 'Arial'.) + font = QString::fromLatin1("Helvetica"); + font.setStyleHint(QFont::SansSerif); + return font; } int NaviCube::getDefaultFontSize() diff --git a/src/MacAppBundle/CMakeLists.txt b/src/MacAppBundle/CMakeLists.txt index a6c72dcecd..a5fac6d82c 100644 --- a/src/MacAppBundle/CMakeLists.txt +++ b/src/MacAppBundle/CMakeLists.txt @@ -146,7 +146,7 @@ file(GLOB CONFIG_GCC "${HOMEBREW_PREFIX}/opt/gcc/lib/gcc/current") execute_process( COMMAND find -L /usr/local/Cellar/nglib -name MacOS OUTPUT_VARIABLE CONFIG_NGLIB) - + install(CODE "message(STATUS \"Making bundle relocatable...\") # The top-level CMakeLists.txt should prevent multiple package manager diff --git a/src/Main/freecad.rc.cmake b/src/Main/freecad.rc.cmake index c319021bb6..3c2a09094d 100644 --- a/src/Main/freecad.rc.cmake +++ b/src/Main/freecad.rc.cmake @@ -12,7 +12,7 @@ IDI_ICON1 ICON DISCARDABLE "icon.ico" // File info for the FreeCAD.exe // -1 VERSIONINFO +1 VERSIONINFO FILEVERSION ${PACKAGE_VERSION_MAJOR},${PACKAGE_VERSION_MINOR},${PACKAGE_VERSION_PATCH},${PACKAGE_BUILD_VERSION} BEGIN BLOCK "StringFileInfo" diff --git a/src/Main/freecadCmd.rc.cmake b/src/Main/freecadCmd.rc.cmake index 0d701a1039..9dc76dc9d1 100644 --- a/src/Main/freecadCmd.rc.cmake +++ b/src/Main/freecadCmd.rc.cmake @@ -12,7 +12,7 @@ IDI_ICON1 ICON DISCARDABLE "icon.ico" // File info for the FreeCADCmd.exe // -1 VERSIONINFO +1 VERSIONINFO FILEVERSION ${PACKAGE_VERSION_MAJOR},${PACKAGE_VERSION_MINOR},${PACKAGE_VERSION_PATCH},${PACKAGE_BUILD_VERSION} BEGIN BLOCK "StringFileInfo" diff --git a/src/Mod/AddonManager/AddonManagerTest/app/mocks.py b/src/Mod/AddonManager/AddonManagerTest/app/mocks.py index f15ee8c81a..49550229ee 100644 --- a/src/Mod/AddonManager/AddonManagerTest/app/mocks.py +++ b/src/Mod/AddonManager/AddonManagerTest/app/mocks.py @@ -22,17 +22,20 @@ """Mock objects for use when testing the addon manager non-GUI code.""" +# pylint: disable=too-few-public-methods,too-many-instance-attributes,missing-function-docstring + import os from typing import Union, List import xml.etree.ElementTree as ElemTree -class GitFailed (RuntimeError): +class GitFailed(RuntimeError): pass class MockConsole: """Mock for the FreeCAD.Console -- does NOT print anything out, just logs it.""" + def __init__(self): self.log = [] self.messages = [] @@ -71,6 +74,8 @@ class MockConsole: class MockMetadata: + """Minimal implementation of a Metadata-like object.""" + def __init__(self): self.Name = "MockMetadata" self.Urls = {"repository": {"location": "file://localhost/", "branch": "main"}} @@ -83,6 +88,8 @@ class MockMetadata: """Don't use the real metadata class, but try to read in the parameters we care about from the given metadata file (or file-like object, as the case probably is). This allows us to test whether the data is being passed around correctly.""" + + # pylint: disable=too-many-branches xml = None root = None try: @@ -120,12 +127,14 @@ class MockMetadata: class MockAddon: """Minimal Addon class""" + # pylint: disable=too-many-instance-attributes + def __init__( - self, - name: str = None, - url: str = None, - status: object = None, - branch: str = "main", + self, + name: str = None, + url: str = None, + status: object = None, + branch: str = "main", ): test_dir = os.path.join(os.path.dirname(__file__), "..", "data") if name: @@ -188,12 +197,13 @@ class MockMacro: def install(self, location: os.PathLike): """Installer function for the mock macro object: creates a file with the src_filename attribute, and optionally an icon, xpm, and other_files. The data contained in these files - is not usable and serves only as a placeholder for the existence of the files.""" + is not usable and serves only as a placeholder for the existence of the files. + """ with open( - os.path.join(location, self.filename), - "w", - encoding="utf-8", + os.path.join(location, self.filename), + "w", + encoding="utf-8", ) as f: f.write("Test file for macro installation unit tests") if self.icon: @@ -201,7 +211,7 @@ class MockMacro: f.write(b"Fake icon data - nothing to see here\n") if self.xpm: with open( - os.path.join(location, "MockMacro_icon.xpm"), "w", encoding="utf-8" + os.path.join(location, "MockMacro_icon.xpm"), "w", encoding="utf-8" ) as f: f.write(self.xpm) for name in self.other_files: @@ -224,6 +234,15 @@ class MockMacro: class SignalCatcher: + """Object to track signals that it has caught. + + Usage: + catcher = SignalCatcher() + my_signal.connect(catcher.catch_signal) + do_things_that_emit_the_signal() + self.assertTrue(catcher.caught) + """ + def __init__(self): self.caught = False self.killed = False @@ -238,6 +257,8 @@ class SignalCatcher: class AddonSignalCatcher: + """Signal catcher specifically designed for catching emitted addons.""" + def __init__(self): self.addons = [] @@ -246,6 +267,10 @@ class AddonSignalCatcher: class CallCatcher: + """Generic call monitor -- use to override functions that are not themselves under + test so that you can detect when the function has been called, and how many times. + """ + def __init__(self): self.called = False self.call_count = 0 @@ -260,7 +285,8 @@ class CallCatcher: class MockGitManager: """A mock git manager: does NOT require a git installation. Takes no actions, only records which functions are called for instrumentation purposes. Can be forced to appear to fail as - needed. Various member variables can be set to emulate necessary return responses.""" + needed. Various member variables can be set to emulate necessary return responses. + """ def __init__(self): self.called_methods = [] @@ -288,7 +314,9 @@ class MockGitManager: self.called_methods.append("clone") self._check_for_failure() - def async_clone(self, _remote, _local_path, _progress_monitor, _args: List[str] = None): + def async_clone( + self, _remote, _local_path, _progress_monitor, _args: List[str] = None + ): self.called_methods.append("async_clone") self._check_for_failure() @@ -380,7 +408,7 @@ class MockSignal: class MockNetworkManager: """Instrumented mock for the NetworkManager. Does no network access, is not asynchronous, and - does not require a running event loop. No submitted requests ever complete.""" + does not require a running event loop. No submitted requests ever complete.""" def __init__(self): self.urls = [] @@ -418,8 +446,28 @@ class MockNetworkManager: class MockByteArray: + """Mock for QByteArray. Only provides the data() access member.""" + def __init__(self, data_to_wrap="data".encode("utf-8")): self.wrapped = data_to_wrap def data(self) -> bytes: return self.wrapped + + +class MockThread: + """Mock for QThread for use when threading is not being used, but interruption + needs to be tested. Set interrupt_after_n_calls to the call number to stop at.""" + + def __init__(self): + self.interrupt_after_n_calls = 0 + self.interrupt_check_counter = 0 + + def isInterruptionRequested(self): + self.interrupt_check_counter += 1 + if ( + self.interrupt_after_n_calls + and self.interrupt_check_counter >= self.interrupt_after_n_calls + ): + return True + return False diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py b/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py index 3c53df90a0..0e1e493515 100644 --- a/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py +++ b/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py @@ -220,7 +220,7 @@ class TestAddon(unittest.TestCase): self.assertEqual(addon.repo_type, Addon.Kind.MACRO) self.assertEqual(addon.name, "DoNothing") self.assertEqual( - addon.macro.comment, "Do absolutely nothing. For Addon Manager unit tests." + addon.macro.comment, "Do absolutely nothing. For Addon Manager integration tests." ) self.assertEqual(addon.url, "https://github.com/FreeCAD/FreeCAD") self.assertEqual(addon.macro.version, "1.0") @@ -228,7 +228,7 @@ class TestAddon(unittest.TestCase): self.assertEqual(addon.macro.author, "Chris Hennes") self.assertEqual(addon.macro.date, "2022-02-28") self.assertEqual(addon.macro.icon, "not_real.png") - self.assertEqual(addon.macro.xpm, "") + self.assertNotEqual(addon.macro.xpm, "") def test_cache(self): addon = Addon( diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_macro_parser.py b/src/Mod/AddonManager/AddonManagerTest/app/test_macro_parser.py new file mode 100644 index 0000000000..f10a0fc347 --- /dev/null +++ b/src/Mod/AddonManager/AddonManagerTest/app/test_macro_parser.py @@ -0,0 +1,347 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2022-2023 FreeCAD Project Association * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** + +"""Tests for the MacroParser class""" + +import io +import os +import sys +import unittest + +sys.path.append("../../") # So the IDE can find the classes to run with + +from addonmanager_macro_parser import MacroParser +from AddonManagerTest.app.mocks import MockConsole, CallCatcher, MockThread + + +# pylint: disable=protected-access, too-many-public-methods + + +class TestMacroParser(unittest.TestCase): + """Test the MacroParser class""" + + def setUp(self) -> None: + self.test_object = MacroParser("UnitTestMacro") + self.test_object.console = MockConsole() + self.test_object.current_thread = MockThread() + + def tearDown(self) -> None: + pass + + def test_fill_details_from_code_normal(self): + """Test to make sure _process_line gets called as expected""" + catcher = CallCatcher() + self.test_object._process_line = catcher.catch_call + fake_macro_data = self.given_some_lines(20, 10) + self.test_object.fill_details_from_code(fake_macro_data) + self.assertEqual(catcher.call_count, 10) + + def test_fill_details_from_code_too_many_lines(self): + """Test to make sure _process_line gets limited as expected""" + catcher = CallCatcher() + self.test_object._process_line = catcher.catch_call + self.test_object.MAX_LINES_TO_SEARCH = 5 + fake_macro_data = self.given_some_lines(20, 10) + self.test_object.fill_details_from_code(fake_macro_data) + self.assertEqual(catcher.call_count, 5) + + def test_fill_details_from_code_thread_interrupted(self): + """Test to make sure _process_line gets stopped as expected""" + catcher = CallCatcher() + self.test_object._process_line = catcher.catch_call + self.test_object.current_thread.interrupt_after_n_calls = 6 # Stop on the 6th + fake_macro_data = self.given_some_lines(20, 10) + self.test_object.fill_details_from_code(fake_macro_data) + self.assertEqual(catcher.call_count, 5) + + @staticmethod + def given_some_lines(num_lines, num_dunder_lines) -> str: + """Generate fake macro header data with the given number of lines and number of + lines beginning with a double-underscore.""" + result = "" + for i in range(num_lines): + if i < num_dunder_lines: + result += f"__something_{i}__ = 'Test{i}' # A line to be scanned\n" + else: + result += f"# Nothing to see on line {i}\n" + return result + + def test_process_line_known_lines(self): + """Lines starting with keys are processed""" + test_lines = ["__known_key__ = 'Test'", "__another_known_key__ = 'Test'"] + for line in test_lines: + with self.subTest(line=line): + self.test_object.remaining_item_map = { + "__known_key__": "known_key", + "__another_known_key__": "another_known_key", + } + content_lines = io.StringIO(line) + read_in_line = content_lines.readline() + catcher = CallCatcher() + self.test_object._process_key = catcher.catch_call + self.test_object._process_line(read_in_line, content_lines) + self.assertTrue( + catcher.called, "_process_key was not called for a known key" + ) + + def test_process_line_unknown_lines(self): + """Lines starting with non-keys are not processed""" + test_lines = [ + "# Just a line with a comment", + "\n", + "__dont_know_this_one__ = 'Who cares?'", + "# __known_key__ = 'Aha, but it is commented out!'", + ] + for line in test_lines: + with self.subTest(line=line): + self.test_object.remaining_item_map = { + "__known_key__": "known_key", + "__another_known_key__": "another_known_key", + } + content_lines = io.StringIO(line) + read_in_line = content_lines.readline() + catcher = CallCatcher() + self.test_object._process_key = catcher.catch_call + self.test_object._process_line(read_in_line, content_lines) + self.assertFalse( + catcher.called, "_process_key was called for an unknown key" + ) + + def test_process_key_standard(self): + """Normal expected data is processed""" + self.test_object._reset_map() + in_memory_data = '__comment__ = "Test"' + content_lines = io.StringIO(in_memory_data) + line = content_lines.readline() + self.test_object._process_key("__comment__", line, content_lines) + self.assertTrue(self.test_object.parse_results["comment"], "Test") + + def test_process_key_special(self): + """Special handling for version = date is processed""" + self.test_object._reset_map() + self.test_object.parse_results["date"] = "2001-01-01" + in_memory_data = "__version__ = __date__" + content_lines = io.StringIO(in_memory_data) + line = content_lines.readline() + self.test_object._process_key("__version__", line, content_lines) + self.assertTrue(self.test_object.parse_results["version"], "2001-01-01") + + def test_handle_backslash_continuation_no_backslashes(self): + """The backslash handling code doesn't change a line with no backslashes""" + in_memory_data = '"Not a backslash in sight"' + content_lines = io.StringIO(in_memory_data) + line = content_lines.readline() + result = self.test_object._handle_backslash_continuation(line, content_lines) + self.assertEqual(result, in_memory_data) + + def test_handle_backslash_continuation(self): + """Lines ending in a backslash get stripped and concatenated""" + in_memory_data = '"Line1\\\nLine2\\\nLine3\\\nLine4"' + content_lines = io.StringIO(in_memory_data) + line = content_lines.readline() + result = self.test_object._handle_backslash_continuation(line, content_lines) + self.assertEqual(result, '"Line1Line2Line3Line4"') + + def test_handle_triple_quoted_string_no_triple_quotes(self): + """The triple-quote handler leaves alone lines without triple-quotes""" + in_memory_data = '"Line1"' + content_lines = io.StringIO(in_memory_data) + line = content_lines.readline() + result, was_triple_quoted = self.test_object._handle_triple_quoted_string( + line, content_lines + ) + self.assertEqual(result, in_memory_data) + self.assertFalse(was_triple_quoted) + + def test_handle_triple_quoted_string(self): + """Data is extracted across multiple lines for a triple-quoted string""" + in_memory_data = '"""Line1\nLine2\nLine3\nLine4"""\nLine5\n' + content_lines = io.StringIO(in_memory_data) + line = content_lines.readline() + result, was_triple_quoted = self.test_object._handle_triple_quoted_string( + line, content_lines + ) + self.assertEqual(result, '"""Line1\nLine2\nLine3\nLine4"""') + self.assertTrue(was_triple_quoted) + + def test_strip_quotes_single(self): + """Single quotes are stripped from the final string""" + expected = "test" + quoted = f"'{expected}'" + actual = self.test_object._strip_quotes(quoted) + self.assertEqual(actual, expected) + + def test_strip_quotes_double(self): + """Double quotes are stripped from the final string""" + expected = "test" + quoted = f'"{expected}"' + actual = self.test_object._strip_quotes(quoted) + self.assertEqual(actual, expected) + + def test_strip_quotes_triple(self): + """Triple quotes are stripped from the final string""" + expected = "test" + quoted = f'"""{expected}"""' + actual = self.test_object._strip_quotes(quoted) + self.assertEqual(actual, expected) + + def test_strip_quotes_unquoted(self): + """Unquoted data results in None""" + unquoted = "This has no quotation marks of any kind" + actual = self.test_object._strip_quotes(unquoted) + self.assertIsNone(actual) + + def test_standard_extraction_string(self): + """String variables are extracted and stored""" + string_keys = [ + "comment", + "url", + "wiki", + "version", + "author", + "date", + "icon", + "xpm", + ] + for key in string_keys: + with self.subTest(key=key): + self.test_object._standard_extraction(key, "test") + self.assertEqual(self.test_object.parse_results[key], "test") + + def test_standard_extraction_list(self): + """List variable is extracted and stored""" + key = "other_files" + self.test_object._standard_extraction(key, "test1, test2, test3") + self.assertIn("test1", self.test_object.parse_results[key]) + self.assertIn("test2", self.test_object.parse_results[key]) + self.assertIn("test3", self.test_object.parse_results[key]) + + def test_apply_special_handling_version(self): + """If the tag is __version__, apply our special handling""" + self.test_object._reset_map() + self.test_object._apply_special_handling("__version__", 42) + self.assertNotIn("__version__", self.test_object.remaining_item_map) + self.assertEqual(self.test_object.parse_results["version"], "42") + + def test_apply_special_handling_not_version(self): + """If the tag is not __version__, raise an error""" + self.test_object._reset_map() + with self.assertRaises(SyntaxError): + self.test_object._apply_special_handling("__not_version__", 42) + self.assertIn("__version__", self.test_object.remaining_item_map) + + def test_process_noncompliant_version_date(self): + """Detect and allow __date__ for the __version__""" + self.test_object.parse_results["date"] = "1/2/3" + self.test_object._process_noncompliant_version("__date__") + self.assertEqual( + self.test_object.parse_results["version"], + self.test_object.parse_results["date"], + ) + + def test_process_noncompliant_version_float(self): + """Detect and allow floats for the __version__""" + self.test_object._process_noncompliant_version(1.2) + self.assertEqual(self.test_object.parse_results["version"], "1.2") + + def test_process_noncompliant_version_int(self): + """Detect and allow integers for the __version__""" + self.test_object._process_noncompliant_version(42) + self.assertEqual(self.test_object.parse_results["version"], "42") + + def test_detect_illegal_content_prefixed_string(self): + """Detect and raise an error for various kinds of prefixed strings""" + illegal_strings = [ + "f'Some fancy {thing}'", + 'f"Some fancy {thing}"', + "r'Some fancy {thing}'", + 'r"Some fancy {thing}"', + "u'Some fancy {thing}'", + 'u"Some fancy {thing}"', + "fr'Some fancy {thing}'", + 'fr"Some fancy {thing}"', + "rf'Some fancy {thing}'", + 'rf"Some fancy {thing}"', + ] + for test_string in illegal_strings: + with self.subTest(test_string=test_string): + with self.assertRaises(SyntaxError): + MacroParser._detect_illegal_content(test_string) + + def test_detect_illegal_content_not_a_string(self): + """Detect and raise an error for (some) non-strings""" + illegal_strings = [ + "no quotes", + "do_stuff()", + 'print("A function call sporting quotes!")', + "__name__", + "__version__", + "1.2.3", + ] + for test_string in illegal_strings: + with self.subTest(test_string=test_string): + with self.assertRaises(SyntaxError): + MacroParser._detect_illegal_content(test_string) + + def test_detect_illegal_content_no_failure(self): + """Recognize strings of various kinds, plus ints, and floats""" + legal_strings = [ + '"Some legal value in double quotes"', + "'Some legal value in single quotes'", + '"""Some legal value in triple quotes"""', + "__date__", + "42", + "4.2", + ] + for test_string in legal_strings: + with self.subTest(test_string=test_string): + MacroParser._detect_illegal_content(test_string) + + ##################### + # INTEGRATION TESTS # + ##################### + + def test_macro_parser(self): + """INTEGRATION TEST: Given "real" data, ensure the parsing yields the expected results.""" + data_dir = os.path.join(os.path.dirname(__file__), "../data") + macro_file = os.path.join(data_dir, "DoNothing.FCMacro") + with open(macro_file, "r", encoding="utf-8") as f: + code = f.read() + self.test_object.fill_details_from_code(code) + self.assertEqual(len(self.test_object.console.errors), 0) + self.assertEqual(len(self.test_object.console.warnings), 0) + self.assertEqual(self.test_object.parse_results["author"], "Chris Hennes") + self.assertEqual(self.test_object.parse_results["version"], "1.0") + self.assertEqual(self.test_object.parse_results["date"], "2022-02-28") + self.assertEqual( + self.test_object.parse_results["comment"], + "Do absolutely nothing. For Addon Manager integration tests.", + ) + self.assertEqual( + self.test_object.parse_results["url"], "https://github.com/FreeCAD/FreeCAD" + ) + self.assertEqual(self.test_object.parse_results["icon"], "not_real.png") + self.assertListEqual( + self.test_object.parse_results["other_files"], + ["file1.py", "file2.py", "file3.py"], + ) + self.assertNotEqual(self.test_object.parse_results["xpm"], "") diff --git a/src/Mod/AddonManager/AddonManagerTest/data/DoNothing.FCMacro b/src/Mod/AddonManager/AddonManagerTest/data/DoNothing.FCMacro index dafdb02d2b..4ccec1e450 100644 --- a/src/Mod/AddonManager/AddonManagerTest/data/DoNothing.FCMacro +++ b/src/Mod/AddonManager/AddonManagerTest/data/DoNothing.FCMacro @@ -4,7 +4,7 @@ __Title__ = 'Do Nothing' __Author__ = 'Chris Hennes' __Version__ = '1.0' __Date__ = '2022-02-28' -__Comment__ = 'Do absolutely nothing. For Addon Manager unit tests.' +__Comment__ = 'Do absolutely nothing. For Addon Manager integration tests.' __Web__ = 'https://github.com/FreeCAD/FreeCAD' __Wiki__ = '' __Icon__ = 'not_real.png' @@ -13,5 +13,18 @@ __Status__ = 'Very Stable' __Requires__ = '' __Communication__ = 'Shout into the void' __Files__ = 'file1.py, file2.py, file3.py' +__Xpm__ = """/* XPM */ +static char * blarg_xpm[] = { +"16 7 2 1", +"* c #000000", +". c #ffffff", +"**..*...........", +"*.*.*...........", +"**..*..**.**..**", +"*.*.*.*.*.*..*.*", +"**..*..**.*...**", +"...............*", +".............**." +};""" print("Well, not quite *nothing*... it does print this line out.") \ No newline at end of file diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index 8d4f182580..dad3d3d455 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -23,6 +23,7 @@ SET(AddonManager_SRCS addonmanager_installer.py addonmanager_installer_gui.py addonmanager_macro.py + addonmanager_macro_parser.py addonmanager_update_all_gui.py addonmanager_uninstaller.py addonmanager_uninstaller_gui.py @@ -85,6 +86,7 @@ SET(AddonManagerTestsApp_SRCS AddonManagerTest/app/test_git.py AddonManagerTest/app/test_installer.py AddonManagerTest/app/test_macro.py + AddonManagerTest/app/test_macro_parser.py AddonManagerTest/app/test_utilities.py AddonManagerTest/app/test_uninstaller.py ) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index e966a05f8d..e24831410f 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -33,9 +33,8 @@ from typing import Dict, Tuple, List, Union, Optional import FreeCAD import NetworkManager -from PySide import QtCore -from addonmanager_utilities import is_float +from addonmanager_macro_parser import MacroParser translate = FreeCAD.Qt.translate @@ -109,7 +108,8 @@ class Macro: def is_installed(self): """Returns True if this macro is currently installed (that is, if it exists in the user macro directory), or False if it is not. Both the exact filename, as well as - the filename prefixed with "Macro", are considered an installation of this macro.""" + the filename prefixed with "Macro", are considered an installation of this macro. + """ if self.on_git and not self.src_filename: return False return os.path.exists( @@ -125,144 +125,12 @@ class Macro: self.fill_details_from_code(self.code) def fill_details_from_code(self, code: str) -> None: - """Reads in the macro code from the given string and parses it for its metadata.""" - # Number of parsed fields of metadata. Overrides anything set previously (the code is - # considered authoritative). - # For now: - # __Comment__ - # __Web__ - # __Wiki__ - # __Version__ - # __Files__ - # __Author__ - # __Date__ - # __Icon__ - max_lines_to_search = 200 - line_counter = 0 - - string_search_mapping = { - "__comment__": "comment", - "__web__": "url", - "__wiki__": "wiki", - "__version__": "version", - "__files__": "other_files", - "__author__": "author", - "__date__": "date", - "__icon__": "icon", - "__xpm__": "xpm", - } - - string_search_regex = re.compile(r"\s*(['\"])(.*)\1") - f = io.StringIO(code) - while f and line_counter < max_lines_to_search: - line = f.readline() - if not line: - break - if QtCore.QThread.currentThread().isInterruptionRequested(): - return - line_counter += 1 - if not line.startswith("__"): - # Speed things up a bit... this comparison is very cheap - continue - - lowercase_line = line.lower() - for key, value in string_search_mapping.items(): - if lowercase_line.startswith(key): - _, _, after_equals = line.partition("=") - match = re.match(string_search_regex, after_equals) - - # We do NOT support triple-quoted strings, except for the icon XPM data - # Most cases should be caught by this code - if match and '"""' not in after_equals: - self._standard_extraction(value, match.group(2)) - string_search_mapping.pop(key) - break - - # For cases where either there is no match, or we found a triple quote, - # more processing is needed - - # Macro authors are supposed to be providing strings here, but in some - # cases they are not doing so. If this is the "__version__" tag, try - # to apply some special handling to accepts numbers, and "__date__" - if key == "__version__": - self._process_noncompliant_version(after_equals) - string_search_mapping.pop(key) - break - - # Icon data can be actual embedded XPM data, inside a triple-quoted string - if key in ("__icon__", "__xpm__"): - self._process_icon(f, key, after_equals) - string_search_mapping.pop(key) - break - - FreeCAD.Console.PrintError( - translate( - "AddonsInstaller", - "Syntax error while reading {} from macro {}", - ).format(key, self.name) - + "\n" - ) - FreeCAD.Console.PrintError(line + "\n") - - # Do some cleanup of the values: - if self.comment: - self.comment = re.sub("<.*?>", "", self.comment) # Strip any HTML tags - - # Truncate long comments to speed up searches, and clean up display - if len(self.comment) > 512: - self.comment = self.comment[:511] + "…" - - # Make sure the icon is not an absolute path, etc. - self.clean_icon() - + parser = MacroParser(self.name, code) + for key, value in parser.parse_results.items(): + if value: + self.__dict__[key] = value self.parsed = True - def _standard_extraction(self, value: str, match_group): - """For most macro metadata values, this extracts the required data""" - if isinstance(self.__dict__[value], str): - self.__dict__[value] = match_group - elif isinstance(self.__dict__[value], list): - self.__dict__[value] = [of.strip() for of in match_group.split(",")] - else: - FreeCAD.Console.PrintError( - "Internal Error: bad type in addonmanager_macro class.\n" - ) - - def _process_noncompliant_version(self, after_equals): - if "__date__" in after_equals.lower(): - self.version = self.date - elif is_float(after_equals): - self.version = str(after_equals).strip() - else: - FreeCAD.Console.PrintLog( - f"Unrecognized value for __version__ in macro {self.name}" - ) - self.version = "(Unknown)" - - def _process_icon(self, f, key, after_equals): - # If this is an icon, it's possible that the icon was actually directly - # specified in the file as XPM data. This data **must** be between - # triple double quotes in order for the Addon Manager to recognize it. - if '"""' in after_equals: - _, _, xpm_data = after_equals.partition('"""') - while True: - line = f.readline() - if not line: - FreeCAD.Console.PrintError( - translate( - "AddonsInstaller", - "Syntax error while reading {} from macro {}", - ).format(key, self.name) - + "\n" - ) - break - if '"""' in line: - last_line, _, _ = line.partition('"""') - xpm_data += last_line - break - xpm_data += line - self.xpm = xpm_data - def fill_details_from_wiki(self, url): """For a given URL, download its data and attempt to get the macro's metadata out of it. If the macro's code is hosted elsewhere, as specified by a "rawcodeurl" found on diff --git a/src/Mod/AddonManager/addonmanager_macro_parser.py b/src/Mod/AddonManager/addonmanager_macro_parser.py new file mode 100644 index 0000000000..27e45bca22 --- /dev/null +++ b/src/Mod/AddonManager/addonmanager_macro_parser.py @@ -0,0 +1,249 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2023 FreeCAD Project Association * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** + +"""Contains the parser class for extracting metadata from a FreeCAD macro""" + +# pylint: disable=too-few-public-methods + +import io +import re +from typing import Any, Tuple + +try: + from PySide import QtCore +except ImportError: + QtCore = None + +try: + import FreeCAD +except ImportError: + FreeCAD = None + + +class DummyThread: + @classmethod + def isInterruptionRequested(cls): + return False + + +class MacroParser: + """Extracts metadata information from a FreeCAD macro""" + + MAX_LINES_TO_SEARCH = 200 # To speed up parsing: some files are VERY large + + def __init__(self, name: str, code: str = ""): + """Create a parser for the macro named "name". Note that the name is only + used as the context for error messages, it is not otherwise important.""" + self.name = name + self.parse_results = { + "comment": "", + "url": "", + "wiki": "", + "version": "", + "other_files": [""], + "author": "", + "date": "", + "icon": "", + "xpm": "", + } + self.remaining_item_map = {} + self.console = None if FreeCAD is None else FreeCAD.Console + self.current_thread = ( + DummyThread() if QtCore is None else QtCore.QThread.currentThread() + ) + if code: + self.fill_details_from_code(code) + + def _reset_map(self): + """This map tracks which items we've already read. If the same parser is used + twice, it has to be reset.""" + self.remaining_item_map = { + "__comment__": "comment", + "__web__": "url", + "__wiki__": "wiki", + "__version__": "version", + "__files__": "other_files", + "__author__": "author", + "__date__": "date", + "__icon__": "icon", + "__xpm__": "xpm", + } + + def fill_details_from_code(self, code: str) -> None: + """Reads in the macro code from the given string and parses it for its + metadata.""" + + self._reset_map() + line_counter = 0 + content_lines = io.StringIO(code) + while content_lines and line_counter < self.MAX_LINES_TO_SEARCH: + line = content_lines.readline() + if not line: + break + if self.current_thread.isInterruptionRequested(): + return + line_counter += 1 + if not line.startswith("__"): + # Speed things up a bit... this comparison is very cheap + continue + try: + self._process_line(line, content_lines) + except SyntaxError as e: + err_string = f"Syntax error when parsing macro {self.name}:\n{str(e)}" + if self.console: + self.console.PrintWarning(err_string) + else: + print(err_string) + + def _process_line(self, line: str, content_lines: io.StringIO): + """Given a single line of the macro file, see if it matches one of our items, + and if so, extract the data.""" + + lowercase_line = line.lower() + for key in self.remaining_item_map: + if lowercase_line.startswith(key): + self._process_key(key, line, content_lines) + break + + def _process_key(self, key: str, line: str, content_lines: io.StringIO): + """Given a line that starts with a known key, extract the data for that key, + possibly reading in additional lines (if it contains a line continuation + character, or is a triple-quoted string).""" + + line = self._handle_backslash_continuation(line, content_lines) + line, was_triple_quoted = self._handle_triple_quoted_string(line, content_lines) + + _, _, line = line.partition("=") + if not was_triple_quoted: + line, _, _ = line.partition("#") + self._detect_illegal_content(line) + final_content_line = line.strip() + + stripped_of_quotes = self._strip_quotes(final_content_line) + if stripped_of_quotes is not None: + self._standard_extraction(self.remaining_item_map[key], stripped_of_quotes) + self.remaining_item_map.pop(key) + else: + self._apply_special_handling(key, line) + + @staticmethod + def _handle_backslash_continuation(line, content_lines) -> str: + while line.strip().endswith("\\"): + line = line.strip()[:-1] + concat_line = content_lines.readline() + line += concat_line.strip() + return line + + @staticmethod + def _handle_triple_quoted_string(line, content_lines) -> Tuple[str, bool]: + result = line + was_triple_quoted = False + if '"""' in result: + was_triple_quoted = True + while True: + new_line = content_lines.readline() + if not new_line: + raise SyntaxError("Syntax error while reading macro") + if '"""' in new_line: + last_line, _, _ = new_line.partition('"""') + result += last_line + '"""' + break + result += new_line + return result, was_triple_quoted + + @staticmethod + def _strip_quotes(line) -> str: + line = line.strip() + stripped_of_quotes = None + if line.startswith('"""') and line.endswith('"""'): + stripped_of_quotes = line[3:-3] + elif (line[0] == '"' and line[-1] == '"') or ( + line[0] == "'" and line[-1] == "'" + ): + stripped_of_quotes = line[1:-1] + return stripped_of_quotes + + def _standard_extraction(self, value: str, match_group: str): + """For most macro metadata values, this extracts the required data""" + if isinstance(self.parse_results[value], str): + self.parse_results[value] = match_group + if value == "comment": + self._cleanup_comment() + elif isinstance(self.parse_results[value], list): + self.parse_results[value] = [of.strip() for of in match_group.split(",")] + else: + raise SyntaxError(f"Conflicting data type for {value}") + + def _cleanup_comment(self): + """Remove HTML from the comment line, and truncate it at 512 characters.""" + + self.parse_results["comment"] = re.sub( + "<.*?>", "", self.parse_results["comment"] + ) + if len(self.parse_results["comment"]) > 512: + self.parse_results["comment"] = self.parse_results["comment"][:511] + "…" + + def _apply_special_handling(self, key: str, line: str): + # Macro authors are supposed to be providing strings here, but in some + # cases they are not doing so. If this is the "__version__" tag, try + # to apply some special handling to accept numbers, and "__date__" + if key == "__version__": + self._process_noncompliant_version(line) + self.remaining_item_map.pop(key) + return + + raise SyntaxError(f"Failed to process {key} from {line}") + + def _process_noncompliant_version(self, after_equals): + if is_float(after_equals): + self.parse_results["version"] = str(after_equals).strip() + elif "__date__" in after_equals.lower() and self.parse_results["date"]: + self.parse_results["version"] = self.parse_results["date"] + else: + self.parse_results["version"] = "(Unknown)" + raise SyntaxError(f"Unrecognized version string {after_equals}") + + @staticmethod + def _detect_illegal_content(line: str): + """Raise a syntax error if this line contains something we can't handle""" + + lower_line = line.strip().lower() + if lower_line.startswith("'") and lower_line.endswith("'"): + return + if lower_line.startswith('"') and lower_line.endswith('"'): + return + if is_float(lower_line): + return + if lower_line == "__date__": + return + raise SyntaxError(f"Metadata is expected to be a static string, but got {line}") + + +# Borrowed from Stack Overflow: +# https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float +def is_float(element: Any) -> bool: + """Determine whether a given item can be converted to a floating-point number""" + try: + float(element) + return True + except ValueError: + return False diff --git a/src/Mod/Arch/ArchComponent.py b/src/Mod/Arch/ArchComponent.py index 1951d17124..df2ac49100 100644 --- a/src/Mod/Arch/ArchComponent.py +++ b/src/Mod/Arch/ArchComponent.py @@ -314,28 +314,28 @@ class Component(ArchIFC.IfcProduct): if prop == "Placement": if hasattr(self,"oldPlacement"): if self.oldPlacement: - import DraftVecUtils deltap = obj.Placement.Base.sub(self.oldPlacement.Base) if deltap.Length == 0: deltap = None - v = FreeCAD.Vector(0,0,1) - deltar = FreeCAD.Rotation(self.oldPlacement.Rotation.multVec(v),obj.Placement.Rotation.multVec(v)) - #print "Rotation",deltar.Axis,deltar.Angle + deltar = obj.Placement.Rotation * self.oldPlacement.Rotation.inverted() if deltar.Angle < 0.0001: deltar = None for child in self.getMovableChildren(obj): - #print "moving ",child.Label if deltar: - #child.Placement.Rotation = child.Placement.Rotation.multiply(deltar) - not enough, child must also move - # use shape methods to obtain a correct placement - import Part,math - shape = Part.Shape() - shape.Placement = child.Placement - #print("angle before rotation:",shape.Placement.Rotation.Angle) - #print("rotation angle:",math.degrees(deltar.Angle)) - shape.rotate(DraftVecUtils.tup(self.oldPlacement.Base), DraftVecUtils.tup(deltar.Axis), math.degrees(deltar.Angle)) - #print("angle after rotation:",shape.Placement.Rotation.Angle) - child.Placement = shape.Placement + import math + # Code for V1.0: + # child.Placement.rotate(self.oldPlacement.Base, + # deltar.Axis, + # math.degrees(deltar.Angle), + # comp=True) + + # Workaround solution for V0.20.3 backport: + # See: https://forum.freecadweb.org/viewtopic.php?p=613196#p613196 + offset_rotation = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), + FreeCAD.Rotation(deltar.Axis, math.degrees(deltar.Angle)), + self.oldPlacement.Base) + child.Placement = offset_rotation * child.Placement + # End workaround solution. if deltap: child.Placement.move(deltap) diff --git a/src/Mod/Arch/ArchSite.py b/src/Mod/Arch/ArchSite.py index c4b16cb9df..3ef39c8aa9 100644 --- a/src/Mod/Arch/ArchSite.py +++ b/src/Mod/Arch/ArchSite.py @@ -24,7 +24,8 @@ containers for Arch objects, and also define a terrain surface. """ -import FreeCAD,Draft,ArchCommands,math,re,datetime,ArchIFC +import FreeCAD,Draft,ArchCommands,ArchComponent,math,re,datetime,ArchIFC + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtGui,QtCore @@ -718,24 +719,16 @@ class _Site(ArchIFC.IfcProduct): if vobj.Proxy is not None: vobj.Proxy.updateDisplaymodeTerrainSwitches(vobj) - def onChanged(self,obj,prop): - """Method called when the object has a property changed. + def onBeforeChange(self, obj, prop): + ArchComponent.Component.onBeforeChange(self, obj, prop) - If Terrain has changed, hide the base object terrain. + def onChanged(self, obj, prop): + ArchComponent.Component.onChanged(self, obj, prop) + if prop == "Terrain" and obj.Terrain and FreeCAD.GuiUp: + obj.Terrain.ViewObject.hide() - Also call ArchIFC.IfcProduct.onChanged(). - - Parameters - ---------- - prop: string - The name of the property that has changed. - """ - - ArchIFC.IfcProduct.onChanged(self, obj, prop) - if prop == "Terrain": - if obj.Terrain: - if FreeCAD.GuiUp: - obj.Terrain.ViewObject.hide() + def getMovableChildren(self, obj): + return obj.Additions + obj.Subtractions def computeAreas(self,obj): """Compute the area, perimeter length, and volume of the terrain shape. diff --git a/src/Mod/Arch/ArchStairs.py b/src/Mod/Arch/ArchStairs.py index 4db3b35b7b..5d2aadc4e0 100644 --- a/src/Mod/Arch/ArchStairs.py +++ b/src/Mod/Arch/ArchStairs.py @@ -161,28 +161,18 @@ def makeRailing(stairs): if side == "L": outlineLR = stair.OutlineLeft outlineLRAll = stair.OutlineLeftAll - stairs0RailingLR = "RailingLeft" # stairs0OutlineWireLR = "OutlineWireLeft" - stairRailingLR = "RailingLeft" # stairOutlineWireLR = "OutlineWireLeft" + stairRailingLR = "RailingLeft" elif side == "R": outlineLR = stair.OutlineRight outlineLRAll = stair.OutlineRightAll - stairs0RailingLR = "RailingRight" # stairs0OutlineWireLR = "OutlineWireRight" - stairRailingLR = "RailingRight" # stairOutlineWireLR = "OutlineWireRight" + stairRailingLR = "RailingRight" if outlineLR or outlineLRAll: lrRail = ArchPipe.makePipe(baseobj=None,diameter=0,length=0,placement=None,name="Rail") if outlineLRAll: - #lrRail.Base = lrRailWire # no need to set here as _Stairs will do - setattr(stair, stairRailingLR, lrRail.Name) # setattr(stair, stairOutlineWireLR, lrRailWire.Name) - railList = stairs[0].Additions - railList.append(lrRail) - stairs[0].Additions = railList + setattr(stair, stairRailingLR, lrRail) break elif outlineLR: - #lrRail.Base = lrRailWire # no need to set here as _Stairs will do - setattr(stair, stairRailingLR, lrRail.Name) # setattr(stair, stairOutlineWireLR, lrRailWire.Name) - railList = stair.Additions - railList.append(lrRail) - stair.Additions = railList + setattr(stair, stairRailingLR, lrRail) if stairs is None: sel = FreeCADGui.Selection.getSelection() @@ -246,7 +236,7 @@ class _CommandStairs: FreeCADGui.addModule("Draft") for obj in stairs: - Draft.autogroup(obj) # seems not working? + Draft.autogroup(obj) # seems not working? FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() @@ -342,25 +332,9 @@ class _Stairs(ArchComponent.Component): self.OutlineRailArcRight = [] if not hasattr(obj,"RailingLeft"): - obj.addProperty("App::PropertyString","RailingLeft","Segment and Parts","Name of Railing object (left) created") - # Migration - if hasattr(obj,"OutlineWireLeft"): - outlineWireLeftObject = FreeCAD.ActiveDocument.getObject(obj.OutlineWireLeft) - try: - obj.RailingLeft = outlineWireLeftObject.InList[0].Name - obj.removeProperty("OutlineWireLeft") - except Exception: - pass + obj.addProperty("App::PropertyLinkHidden","RailingLeft","Segment and Parts","Name of Railing object (left) created") if not hasattr(obj,"RailingRight"): - obj.addProperty("App::PropertyString","RailingRight","Segment and Parts","Name of Railing object (right) created") - # Migration - if hasattr(obj,"OutlineWireRight"): - outlineWireRightObject = FreeCAD.ActiveDocument.getObject(obj.OutlineWireRight) - try: - obj.RailingRight = outlineWireRightObject.InList[0].Name - obj.removeProperty("OutlineWireRight") - except Exception: - pass + obj.addProperty("App::PropertyLinkHidden","RailingRight","Segment and Parts","Name of Railing object (right) created") if not hasattr(obj,"OutlineLeftAll"): obj.addProperty("App::PropertyVectorList","OutlineLeftAll","Segment and Parts",QT_TRANSLATE_NOOP("App::Property","The 'left outline' of all segments of stairs")) @@ -426,6 +400,57 @@ class _Stairs(ArchComponent.Component): ArchComponent.Component.onDocumentRestored(self,obj) self.setProperties(obj) + if hasattr(obj,"OutlineWireLeft"): + self.update_properties_0v18_to_0v20(obj) + + if obj.getTypeIdOfProperty("RailingLeft") == "App::PropertyString": + self.update_properties_0v19_to_0v20(obj) + + def update_properties_0v18_to_0v20(self, obj): + doc = FreeCAD.ActiveDocument + outlineWireLeftObject = doc.getObject(obj.OutlineWireLeft) + outlineWireRightObject = doc.getObject(obj.OutlineWireRight) + try: + obj.RailingLeft = outlineWireLeftObject.InList[0] + except Exception: + pass + try: + obj.RailingRight = outlineWireRightObject.InList[0] + except Exception: + pass + obj.removeProperty("OutlineWireLeft") + obj.removeProperty("OutlineWireRight") + self.update_properties_to_0v20(obj) + from draftutils.messages import _wrn + _wrn("v0.20.3, " + obj.Label + ", " + + translate("Arch", "removed properties 'OutlineWireLeft' and 'OutlineWireRight', and added properties 'RailingLeft' and 'RailingRight'")) + + def update_properties_0v19_to_0v20(self, obj): + doc = FreeCAD.ActiveDocument + railingLeftObject = doc.getObject(obj.RailingLeft) + railingRightObject = doc.getObject(obj.RailingRight) + obj.removeProperty("RailingLeft") + obj.removeProperty("RailingRight") + self.setProperties(obj) + obj.RailingLeft = railingLeftObject + obj.RailingRight = railingRightObject + self.update_properties_to_0v20(obj) + from draftutils.messages import _wrn + _wrn("v0.20.3, " + obj.Label + ", " + + translate("Arch", "changed the type of properties 'RailingLeft' and 'RailingRight'")) + + def update_properties_to_0v20(self, obj): + additions = obj.Additions + if obj.RailingLeft in additions: + additions.remove(obj.RailingLeft) + if obj.RailingRight in additions: + additions.remove(obj.RailingRight) + obj.Additions = additions + if obj.RailingLeft is not None: + obj.RailingLeft.Visibility = True + if obj.RailingRight is not None: + obj.RailingRight.Visibility = True + def execute(self,obj): "constructs the shape of the stairs" @@ -481,7 +506,7 @@ class _Stairs(ArchComponent.Component): else: if obj.Landings == "At center": landings = 1 - self.makeCurvedStairsWithLandings(obj,edge) + self.makeCurvedStairsWithLanding(obj,edge) else: self.makeCurvedStairs(obj,edge) @@ -515,23 +540,22 @@ class _Stairs(ArchComponent.Component): railingLeftObject, railWireL = None, None railingRightObject, railWireR = None, None + doc = FreeCAD.ActiveDocument if obj.RailingLeft: - railingLeftObject = FreeCAD.ActiveDocument.getObject(obj.RailingLeft) - if railingLeftObject: # TODO - need to update if railing is deleted by user? This become None if deleted. + railingLeftObject = obj.RailingLeft if obj.OutlineLeftAll: - railWireL, NU = _Stairs.returnOutlineWireFace(obj.OutlineLeftAll, self.OutlineRailArcLeftAll, mode = "notFaceAlso") #(outlinePoints, pArc, mode="wire or faceAlso") + railWireL, NU = _Stairs.returnOutlineWireFace(obj.OutlineLeftAll, self.OutlineRailArcLeftAll, mode = "notFaceAlso") elif obj.OutlineLeft: railWireL, NU = _Stairs.returnOutlineWireFace(obj.OutlineLeft, self.OutlineRailArcLeft, mode = "notFaceAlso") else: print (" No obj.OutlineLeftAll or obj.OutlineLeft") if railWireL: - # Migration - if Draft.getType(railingLeftObject.Base) != "Part": # None or not "Part" - railingLeftWireObject = FreeCAD.ActiveDocument.addObject("Part::Feature","RailingWire") - if railingLeftObject.Base: # if has railingLeftObject.Base but that != "Part" - railingLeftObject.Document.removeObject(railingLeftObject.Base.Name) # Delete the previous Base object... # Not Using FreeCAD.ActiveDocument... + if Draft.getType(railingLeftObject.Base) != "Part::Feature": # Base can have wrong type or be None. + if railingLeftObject.Base: + doc.removeObject(railingLeftObject.Base.Name) + railingLeftWireObject = doc.addObject("Part::Feature","RailingWire") railingLeftObject.Base = railingLeftWireObject # update the Base object shape railingLeftObject.Base.Shape = railWireL @@ -539,23 +563,19 @@ class _Stairs(ArchComponent.Component): print (" No railWireL created ") if obj.RailingRight: - railingRightObject = FreeCAD.ActiveDocument.getObject(obj.RailingRight) - if railingRightObject: # TODO - need to update if railing is deleted by user? This become None if deleted. + railingRightObject = obj.RailingRight if obj.OutlineRightAll: - print (" DEBUG - has obj.OutlineRightAll ") - railWireR, NU = _Stairs.returnOutlineWireFace(obj.OutlineRightAll, self.OutlineRailArcRightAll, mode = "notFaceAlso") #(outlinePoints, pArc, mode="wire or faceAlso") + railWireR, NU = _Stairs.returnOutlineWireFace(obj.OutlineRightAll, self.OutlineRailArcRightAll, mode = "notFaceAlso") elif obj.OutlineLeft: - print (" DEBUG - has obj.OutlineLeft ") railWireR, NU = _Stairs.returnOutlineWireFace(obj.OutlineLeft, self.OutlineRailArcRight, mode = "notFaceAlso") else: print (" No obj.OutlineRightAll or obj.OutlineLeft") if railWireR: - # Migration - if Draft.getType(railingRightObject.Base) != "Part": - railingRightWireObject = FreeCAD.ActiveDocument.addObject("Part::Feature","RailingWire") + if Draft.getType(railingRightObject.Base) != "Part::Feature": # Base can have wrong type or be None. if railingRightObject.Base: - railingRightObject.Document.removeObject(railingRightObject.Base.Name) + doc.removeObject(railingRightObject.Base.Name) + railingRightWireObject = doc.addObject("Part::Feature","RailingWire") railingRightObject.Base = railingRightWireObject # update the Base object shape railingRightObject.Base.Shape = railWireR @@ -1308,24 +1328,25 @@ class _Stairs(ArchComponent.Component): "builds a straight staircase with/without a landing in the middle" - if obj.NumberOfSteps < 3: + if obj.NumberOfSteps < 2: + print("Fewer than 2 steps, unable to create/update stairs") return v = DraftGeomUtils.vec(edge) landing = 0 if obj.TreadDepthEnforce == 0: - if obj.Landings == "At center": + if obj.Landings == "At center" and obj.NumberOfSteps > 3: if obj.LandingDepth: reslength = edge.Length - obj.LandingDepth.Value else: reslength = edge.Length - obj.Width.Value - treadDepth = float(reslength)/(obj.NumberOfSteps-2) # why needs 'float'? + treadDepth = reslength/(obj.NumberOfSteps-2) obj.TreadDepth = treadDepth vLength = DraftVecUtils.scaleTo(v,treadDepth) else: reslength = edge.Length - treadDepth = float(reslength)/(obj.NumberOfSteps-1) # why needs 'float'? + treadDepth = reslength/(obj.NumberOfSteps-1) obj.TreadDepth = treadDepth vLength = DraftVecUtils.scaleTo(v,treadDepth) else: @@ -1348,7 +1369,7 @@ class _Stairs(ArchComponent.Component): h = obj.RiserHeightEnforce.Value * (obj.NumberOfSteps) hstep = obj.RiserHeightEnforce.Value obj.RiserHeight = hstep - if obj.Landings == "At center": + if obj.Landings == "At center" and obj.NumberOfSteps > 3: landing = int(obj.NumberOfSteps/2) else: landing = obj.NumberOfSteps @@ -1360,7 +1381,7 @@ class _Stairs(ArchComponent.Component): obj.AbsTop = p1.add(Vector(0,0,h)) p2 = p1.add(DraftVecUtils.scale(vLength,landing-1).add(Vector(0,0,landing*hstep))) - if obj.Landings == "At center": + if obj.Landings == "At center" and obj.NumberOfSteps > 3: if obj.LandingDepth: p3 = p2.add(DraftVecUtils.scaleTo(vLength,obj.LandingDepth.Value)) else: @@ -1392,13 +1413,16 @@ class _Stairs(ArchComponent.Component): self.makeStraightStairs(obj,Part.LineSegment(p1,p2).toShape(),obj.DownSlabThickness.Value,obj.RiserHeight.Value,landing,None,'toSlabThickness') else: + if obj.Landings == "At center": + print("Fewer than 4 steps, unable to create landing") self.makeStraightStairs(obj,Part.LineSegment(p1,p2).toShape(),obj.DownSlabThickness.Value,obj.UpSlabThickness.Value,landing,None,None) print (p1, p2) - if obj.Landings == "At center" and obj.Flight not in ["HalfTurnLeft", "HalfTurnRight"]: - print (p3, p4) - elif obj.Landings == "At center" and obj.Flight in ["HalfTurnLeft", "HalfTurnRight"]: - print (p3r, p4r) + if obj.Landings == "At center" and obj.NumberOfSteps > 3: + if obj.Flight not in ["HalfTurnLeft", "HalfTurnRight"]: + print (p3, p4) + elif obj.Flight in ["HalfTurnLeft", "HalfTurnRight"]: + print (p3r, p4r) edge = Part.LineSegment(p1,p2).toShape() @@ -1475,6 +1499,26 @@ class _ViewProviderStairs(ArchComponent.ViewProviderComponent): import Arch_rc return ":/icons/Arch_Stairs_Tree.svg" + def claimChildren(self): + + "Define which objects will appear as children in the tree view" + + if hasattr(self, "Object"): + obj = self.Object + lst = [] + if hasattr(obj, "Base"): + lst.append(obj.Base) + if hasattr(obj, "RailingLeft"): + lst.append(obj.RailingLeft) + if hasattr(obj, "RailingRight"): + lst.append(obj.RailingRight) + if hasattr(obj, "Additions"): + lst.extend(obj.Additions) + if hasattr(obj, "Subtractions"): + lst.extend(obj.Subtractions) + return lst + return [] + if FreeCAD.GuiUp: FreeCADGui.addCommand('Arch_Stairs',_CommandStairs()) diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index f71330a256..7ad2bbb44d 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -2237,6 +2237,11 @@ def getRepresentation( loops = [] verts = [v.Point for v in fcface.OuterWire.OrderedVertexes] c = fcface.CenterOfMass + if len(verts) < 1: + print("Warning: OuterWire returned no ordered Vertexes in ", obj.Label) + # Part.show(fcface) + # Part.show(fcsolid) + continue v1 = verts[0].sub(c) v2 = verts[1].sub(c) try: diff --git a/src/Mod/Draft/draftfunctions/mirror.py b/src/Mod/Draft/draftfunctions/mirror.py index ea00f26e8e..206cd5dce2 100644 --- a/src/Mod/Draft/draftfunctions/mirror.py +++ b/src/Mod/Draft/draftfunctions/mirror.py @@ -109,8 +109,8 @@ def mirror(objlist, p1, p2): result = [] for obj in objlist: - mir = App.ActiveDocument.addObject("Part::Mirroring", "mirror") - mir.Label = obj.Label + " (" + translate("draft","mirrored") + ") " + mir = App.ActiveDocument.addObject("Part::Mirroring", "Mirror") + mir.Label = obj.Label + " (" + translate("draft", "mirrored") + ")" mir.Source = obj mir.Base = p1 mir.Normal = pnorm diff --git a/src/Mod/Draft/draftmake/make_dimension.py b/src/Mod/Draft/draftmake/make_dimension.py index 1bc3d90fda..aecd4b48c5 100644 --- a/src/Mod/Draft/draftmake/make_dimension.py +++ b/src/Mod/Draft/draftmake/make_dimension.py @@ -195,7 +195,7 @@ def make_linear_dimension(p1, p2, dim_line=None): positioned from the measured segment that goes from `p1` to `p2`. If it is `None`, this point will be calculated from the intermediate - distance betwwen `p1` and `p2`. + distance between `p1` and `p2`. Returns ------- @@ -288,7 +288,7 @@ def make_linear_dimension_obj(edge_object, i1=1, i2=2, dim_line=None): positioned from the measured segment in `edge_object`. If it is `None`, this point will be calculated from the intermediate - distance betwwen the vertices defined by `i1` and `i2`. + distance between the vertices defined by `i1` and `i2`. Returns ------- diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py index 5f34ba4329..f2c77a7c44 100644 --- a/src/Mod/Draft/draftutils/init_draft_statusbar.py +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -67,7 +67,7 @@ draft_scales_eng_imperial = ["1in=10ft", "1in=20ft", "1in=30ft", def get_scales(unit_system = 0): """ - returns the list of preset scales accordin to unit system. + returns the list of preset scales according to unit system. Parameters: unit_system = 0 : default from user preferences diff --git a/src/Mod/Fem/App/AppFem.cpp b/src/Mod/Fem/App/AppFem.cpp index b65f17d3ec..fe004396fc 100644 --- a/src/Mod/Fem/App/AppFem.cpp +++ b/src/Mod/Fem/App/AppFem.cpp @@ -178,6 +178,7 @@ PyMOD_INIT_FUNC(Fem) Fem::FemPostPipeline ::init(); Fem::FemPostFilter ::init(); Fem::FemPostClipFilter ::init(); + Fem::FemPostContoursFilter ::init(); Fem::FemPostCutFilter ::init(); Fem::FemPostDataAlongLineFilter ::init(); Fem::FemPostDataAtPointFilter ::init(); diff --git a/src/Mod/Fem/App/FemPostFilter.cpp b/src/Mod/Fem/App/FemPostFilter.cpp index 633282a1b5..19b5a6abe1 100644 --- a/src/Mod/Fem/App/FemPostFilter.cpp +++ b/src/Mod/Fem/App/FemPostFilter.cpp @@ -24,6 +24,7 @@ #ifndef _PreComp_ # include +# include # include #endif @@ -45,51 +46,50 @@ FemPostFilter::FemPostFilter() } FemPostFilter::~FemPostFilter() +{} + +void FemPostFilter::addFilterPipeline(const FemPostFilter::FilterPipeline& p, std::string name) { - -} - -void FemPostFilter::addFilterPipeline(const FemPostFilter::FilterPipeline& p, std::string name) { m_pipelines[name] = p; } -FemPostFilter::FilterPipeline& FemPostFilter::getFilterPipeline(std::string name) { +FemPostFilter::FilterPipeline& FemPostFilter::getFilterPipeline(std::string name) +{ return m_pipelines[name]; } -void FemPostFilter::setActiveFilterPipeline(std::string name) { - +void FemPostFilter::setActiveFilterPipeline(std::string name) +{ if (m_activePipeline != name && isValid()) { m_activePipeline = name; } } -DocumentObjectExecReturn* FemPostFilter::execute() { - +DocumentObjectExecReturn* FemPostFilter::execute() +{ if (!m_pipelines.empty() && !m_activePipeline.empty()) { FemPostFilter::FilterPipeline& pipe = m_pipelines[m_activePipeline]; - if (m_activePipeline.length() >= 11) { - std::string LineClip = m_activePipeline.substr(0, 13); - std::string PointClip = m_activePipeline.substr(0, 11); - if ((LineClip == "DataAlongLine") || (PointClip == "DataAtPoint")) { + vtkSmartPointer data = getInputData(); + if (!data || !data->IsA("vtkDataSet")) + return StdReturn; + + if ((m_activePipeline == "DataAlongLine") || (m_activePipeline == "DataAtPoint")) { pipe.filterSource->SetSourceData(getInputData()); pipe.filterTarget->Update(); - Data.setValue(pipe.filterTarget->GetOutputDataObject(0)); - } } else { - pipe.source->SetInputDataObject(getInputData()); + pipe.source->SetInputDataObject(data); pipe.target->Update(); Data.setValue(pipe.target->GetOutputDataObject(0)); } - } + return StdReturn; } -vtkDataObject* FemPostFilter::getInputData() { - +vtkDataObject* FemPostFilter::getInputData() +{ if (Input.getValue()) { if (Input.getValue()->getTypeId().isDerivedFrom(Base::Type::fromName("Fem::FemPostObject"))) return Input.getValue()->Data.getValue(); @@ -111,100 +111,18 @@ vtkDataObject* FemPostFilter::getInputData() { return nullptr; } - // *************************************************************************** -// clip filter -PROPERTY_SOURCE(Fem::FemPostClipFilter, Fem::FemPostFilter) - -FemPostClipFilter::FemPostClipFilter() : FemPostFilter() { - - ADD_PROPERTY_TYPE(Function, - (nullptr), - "Clip", - App::Prop_None, - "The function object which defines the clip regions"); - ADD_PROPERTY_TYPE(InsideOut, (false), "Clip", App::Prop_None, "Invert the clip direction"); - ADD_PROPERTY_TYPE( - CutCells, - (false), - "Clip", - App::Prop_None, - "Decides if cells are cuttet and interpolated or if the cells are kept as a whole"); - - FilterPipeline clip; - m_clipper = vtkSmartPointer::New(); - clip.source = m_clipper; - clip.target = m_clipper; - addFilterPipeline(clip, "clip"); - - FilterPipeline extr; - m_extractor = vtkSmartPointer::New(); - extr.source = m_extractor; - extr.target = m_extractor; - addFilterPipeline(extr, "extract"); - - m_extractor->SetExtractInside(0); - setActiveFilterPipeline("extract"); -} - -FemPostClipFilter::~FemPostClipFilter() { - -} - -void FemPostClipFilter::onChanged(const Property* prop) { - - if (prop == &Function) { - - if (Function.getValue() - && Function.getValue()->isDerivedFrom(FemPostFunction::getClassTypeId())) { - m_clipper->SetClipFunction( - static_cast(Function.getValue())->getImplicitFunction()); - m_extractor->SetImplicitFunction( - static_cast(Function.getValue())->getImplicitFunction()); - } - } - else if (prop == &InsideOut) { - - m_clipper->SetInsideOut(InsideOut.getValue()); - m_extractor->SetExtractInside((InsideOut.getValue()) ? 1 : 0); - } - else if (prop == &CutCells) { - - if (!CutCells.getValue()) - setActiveFilterPipeline("extract"); - else - setActiveFilterPipeline("clip"); - }; - - Fem::FemPostFilter::onChanged(prop); -} - -short int FemPostClipFilter::mustExecute() const { - - if (Function.isTouched() || - InsideOut.isTouched() || - CutCells.isTouched()) { - - return 1; - } - else return App::DocumentObject::mustExecute(); -} - -DocumentObjectExecReturn* FemPostClipFilter::execute() { - - if (!m_extractor->GetImplicitFunction()) - return StdReturn; - - return Fem::FemPostFilter::execute(); -} +// in the following, the different filters sorted alphabetically +// *************************************************************************** // *************************************************************************** -// data along a line +// data along line filter PROPERTY_SOURCE(Fem::FemPostDataAlongLineFilter, Fem::FemPostFilter) -FemPostDataAlongLineFilter::FemPostDataAlongLineFilter() : FemPostFilter() { - +FemPostDataAlongLineFilter::FemPostDataAlongLineFilter() + : FemPostFilter() +{ ADD_PROPERTY_TYPE(Point1, (Base::Vector3d(0.0, 0.0, 0.0)), "DataAlongLine", @@ -258,12 +176,11 @@ FemPostDataAlongLineFilter::FemPostDataAlongLineFilter() : FemPostFilter() { setActiveFilterPipeline("DataAlongLine"); } -FemPostDataAlongLineFilter::~FemPostDataAlongLineFilter() { - -} - -DocumentObjectExecReturn* FemPostDataAlongLineFilter::execute() { +FemPostDataAlongLineFilter::~FemPostDataAlongLineFilter() +{} +DocumentObjectExecReturn* FemPostDataAlongLineFilter::execute() +{ //recalculate the filter return Fem::FemPostFilter::execute(); } @@ -288,7 +205,8 @@ void FemPostDataAlongLineFilter::handleChangedPropertyType(Base::XMLReader& read } } -void FemPostDataAlongLineFilter::onChanged(const Property* prop) { +void FemPostDataAlongLineFilter::onChanged(const Property* prop) +{ if (prop == &Point1) { const Base::Vector3d& vec1 = Point1.getValue(); m_line->SetPoint1(vec1.x, vec1.y, vec1.z); @@ -306,24 +224,23 @@ void FemPostDataAlongLineFilter::onChanged(const Property* prop) { Fem::FemPostFilter::onChanged(prop); } -short int FemPostDataAlongLineFilter::mustExecute() const { - - if (Point1.isTouched() || - Point2.isTouched() || - Resolution.isTouched()) { - +short int FemPostDataAlongLineFilter::mustExecute() const +{ + if (Point1.isTouched() || Point2.isTouched() || Resolution.isTouched()) return 1; - } - else return App::DocumentObject::mustExecute(); + else + return App::DocumentObject::mustExecute(); } -void FemPostDataAlongLineFilter::GetAxisData() { - +void FemPostDataAlongLineFilter::GetAxisData() +{ std::vector coords; std::vector values; vtkSmartPointer data = m_probe->GetOutputDataObject(0); vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return; vtkDataArray* pdata = dset->GetPointData()->GetArray(PlotData.getValue()); // VTK cannot deliver data when the filer relies e.g. on a scalar clip filter // whose value is set so that all data are clipped @@ -363,11 +280,12 @@ void FemPostDataAlongLineFilter::GetAxisData() { // *************************************************************************** -// data point filter +// data at point filter PROPERTY_SOURCE(Fem::FemPostDataAtPointFilter, Fem::FemPostFilter) -FemPostDataAtPointFilter::FemPostDataAtPointFilter() : FemPostFilter() { - +FemPostDataAtPointFilter::FemPostDataAtPointFilter() + : FemPostFilter() +{ ADD_PROPERTY_TYPE(Center, (Base::Vector3d(0.0, 0.0, 0.0)), "DataAtPoint", @@ -408,17 +326,17 @@ FemPostDataAtPointFilter::FemPostDataAtPointFilter() : FemPostFilter() { setActiveFilterPipeline("DataAtPoint"); } -FemPostDataAtPointFilter::~FemPostDataAtPointFilter() { - -} - -DocumentObjectExecReturn* FemPostDataAtPointFilter::execute() { +FemPostDataAtPointFilter::~FemPostDataAtPointFilter() +{} +DocumentObjectExecReturn* FemPostDataAtPointFilter::execute() +{ //recalculate the filter return Fem::FemPostFilter::execute(); } -void FemPostDataAtPointFilter::onChanged(const Property* prop) { +void FemPostDataAtPointFilter::onChanged(const Property* prop) +{ if (prop == &Center) { const Base::Vector3d& vec = Center.getValue(); m_point->SetCenter(vec.x, vec.y, vec.z); @@ -427,20 +345,22 @@ void FemPostDataAtPointFilter::onChanged(const Property* prop) { Fem::FemPostFilter::onChanged(prop); } -short int FemPostDataAtPointFilter::mustExecute() const { - +short int FemPostDataAtPointFilter::mustExecute() const +{ if (Center.isTouched()) return 1; else return App::DocumentObject::mustExecute(); } -void FemPostDataAtPointFilter::GetPointData() { - +void FemPostDataAtPointFilter::GetPointData() +{ std::vector values; vtkSmartPointer data = m_probe->GetOutputDataObject(0); vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return; vtkDataArray* pdata = dset->GetPointData()->GetArray(FieldName.getValue()); // VTK cannot deliver data when the filer relies e.g. on a scalar clip filter // whose value is set so that all data are clipped @@ -467,197 +387,342 @@ void FemPostDataAtPointFilter::GetPointData() { // *************************************************************************** -// scalar clip filter -PROPERTY_SOURCE(Fem::FemPostScalarClipFilter, Fem::FemPostFilter) +// clip filter +PROPERTY_SOURCE(Fem::FemPostClipFilter, Fem::FemPostFilter) -FemPostScalarClipFilter::FemPostScalarClipFilter() : FemPostFilter() { - - ADD_PROPERTY_TYPE( - Value, (0), "Clip", App::Prop_None, "The scalar value used to clip the selected field"); - ADD_PROPERTY_TYPE(Scalars, (long(0)), "Clip", App::Prop_None, "The field used to clip"); +FemPostClipFilter::FemPostClipFilter() + : FemPostFilter() +{ + ADD_PROPERTY_TYPE(Function, + (nullptr), + "Clip", + App::Prop_None, + "The function object which defines the clip regions"); ADD_PROPERTY_TYPE(InsideOut, (false), "Clip", App::Prop_None, "Invert the clip direction"); - - Value.setConstraints(&m_constraints); + ADD_PROPERTY_TYPE( + CutCells, + (false), + "Clip", + App::Prop_None, + "Decides if cells are cut and interpolated or if the cells are kept as a whole"); FilterPipeline clip; - m_clipper = vtkSmartPointer::New(); + m_clipper = vtkSmartPointer::New(); clip.source = m_clipper; clip.target = m_clipper; addFilterPipeline(clip, "clip"); - setActiveFilterPipeline("clip"); + + FilterPipeline extr; + m_extractor = vtkSmartPointer::New(); + extr.source = m_extractor; + extr.target = m_extractor; + addFilterPipeline(extr, "extract"); + + m_extractor->SetExtractInside(0); + setActiveFilterPipeline("extract"); } -FemPostScalarClipFilter::~FemPostScalarClipFilter() { +FemPostClipFilter::~FemPostClipFilter() +{} -} +void FemPostClipFilter::onChanged(const Property* prop) +{ + if (prop == &Function) { -DocumentObjectExecReturn* FemPostScalarClipFilter::execute() { - - std::string val; - if (Scalars.getValue() >= 0) - val = Scalars.getValueAsString(); - - std::vector ScalarsArray; - - vtkSmartPointer data = getInputData(); - if (!data || !data->IsA("vtkDataSet")) - return StdReturn; - - vtkDataSet* dset = vtkDataSet::SafeDownCast(data); - vtkPointData* pd = dset->GetPointData(); - - // get all scalar fields - for (int i = 0; i < pd->GetNumberOfArrays(); ++i) { - if (pd->GetArray(i)->GetNumberOfComponents() == 1) - ScalarsArray.emplace_back(pd->GetArrayName(i)); - } - - App::Enumeration empty; - Scalars.setValue(empty); - m_scalarFields.setEnums(ScalarsArray); - Scalars.setValue(m_scalarFields); - - // search if the current field is in the available ones and set it - std::vector::iterator it = - std::find(ScalarsArray.begin(), ScalarsArray.end(), val); - if (!val.empty() && it != ScalarsArray.end()) - Scalars.setValue(val.c_str()); - - //recalculate the filter - return Fem::FemPostFilter::execute(); -} - -void FemPostScalarClipFilter::onChanged(const Property* prop) { - - if (prop == &Value) { - m_clipper->SetValue(Value.getValue()); + if (Function.getValue() + && Function.getValue()->isDerivedFrom(FemPostFunction::getClassTypeId())) { + m_clipper->SetClipFunction( + static_cast(Function.getValue())->getImplicitFunction()); + m_extractor->SetImplicitFunction( + static_cast(Function.getValue())->getImplicitFunction()); + } } else if (prop == &InsideOut) { + m_clipper->SetInsideOut(InsideOut.getValue()); + m_extractor->SetExtractInside((InsideOut.getValue()) ? 1 : 0); } - else if (prop == &Scalars && (Scalars.getValue() >= 0)) { - m_clipper->SetInputArrayToProcess(0, 0, 0, - vtkDataObject::FIELD_ASSOCIATION_POINTS, Scalars.getValueAsString()); - setConstraintForField(); - } + else if (prop == &CutCells) { + + if (!CutCells.getValue()) + setActiveFilterPipeline("extract"); + else + setActiveFilterPipeline("clip"); + }; Fem::FemPostFilter::onChanged(prop); } -short int FemPostScalarClipFilter::mustExecute() const { +short int FemPostClipFilter::mustExecute() const +{ + if (Function.isTouched() || InsideOut.isTouched() || CutCells.isTouched()) - if (Value.isTouched() || - InsideOut.isTouched() || - Scalars.isTouched()) return 1; else return App::DocumentObject::mustExecute(); } -void FemPostScalarClipFilter::setConstraintForField() { - - vtkSmartPointer data = getInputData(); - if (!data || !data->IsA("vtkDataSet")) - return; - - vtkDataSet* dset = vtkDataSet::SafeDownCast(data); - - vtkDataArray* pdata = dset->GetPointData()->GetArray(Scalars.getValueAsString()); - // VTK cannot deliver data when the filer relies e.g. on a cut clip filter - // whose value is set so that all data are cut - if (!pdata) - return; - double p[2]; - pdata->GetRange(p); - m_constraints.LowerBound = p[0]; - m_constraints.UpperBound = p[1]; - m_constraints.StepSize = (p[1] - p[0]) / 100.; -} - - -// *************************************************************************** -// warp vector filter -PROPERTY_SOURCE(Fem::FemPostWarpVectorFilter, Fem::FemPostFilter) - -FemPostWarpVectorFilter::FemPostWarpVectorFilter() : FemPostFilter() { - - ADD_PROPERTY_TYPE(Factor, - (0), - "Warp", - App::Prop_None, - "The factor by which the vector is added to the node positions"); - ADD_PROPERTY_TYPE( - Vector, (long(0)), "Warp", App::Prop_None, "The field added to the node position"); - - FilterPipeline warp; - m_warp = vtkSmartPointer::New(); - warp.source = m_warp; - warp.target = m_warp; - addFilterPipeline(warp, "warp"); - setActiveFilterPipeline("warp"); -} - -FemPostWarpVectorFilter::~FemPostWarpVectorFilter() { - -} - -DocumentObjectExecReturn* FemPostWarpVectorFilter::execute() { - - std::string val; - if (Vector.getValue() >= 0) - val = Vector.getValueAsString(); - - std::vector VectorArray; - - vtkSmartPointer data = getInputData(); - if (!data || !data->IsA("vtkDataSet")) +DocumentObjectExecReturn* FemPostClipFilter::execute() +{ + if (!m_extractor->GetImplicitFunction()) return StdReturn; - vtkDataSet* dset = vtkDataSet::SafeDownCast(data); - vtkPointData* pd = dset->GetPointData(); - - // get all vector fields - for (int i = 0; i < pd->GetNumberOfArrays(); ++i) { - if (pd->GetArray(i)->GetNumberOfComponents() == 3) - VectorArray.emplace_back(pd->GetArrayName(i)); - } - - App::Enumeration empty; - Vector.setValue(empty); - m_vectorFields.setEnums(VectorArray); - Vector.setValue(m_vectorFields); - - // search if the current field is in the available ones and set it - std::vector::iterator it = - std::find(VectorArray.begin(), VectorArray.end(), val); - if (!val.empty() && it != VectorArray.end()) - Vector.setValue(val.c_str()); - - //recalculate the filter return Fem::FemPostFilter::execute(); } -void FemPostWarpVectorFilter::onChanged(const Property* prop) { +// *************************************************************************** +// contours filter +PROPERTY_SOURCE(Fem::FemPostContoursFilter, Fem::FemPostFilter) - if (prop == &Factor) - // since our mesh is in mm, we must scale the factor - m_warp->SetScaleFactor(1000 * Factor.getValue()); - else if (prop == &Vector && (Vector.getValue() >= 0)) - m_warp->SetInputArrayToProcess(0, 0, 0, - vtkDataObject::FIELD_ASSOCIATION_POINTS, Vector.getValueAsString()); +FemPostContoursFilter::FemPostContoursFilter() + : FemPostFilter() +{ + ADD_PROPERTY_TYPE(NumberOfContours, (10), "Contours", App::Prop_None, "The number of contours"); + ADD_PROPERTY_TYPE(Field, (long(0)), "Clip", App::Prop_None, "The field used to clip"); + ADD_PROPERTY_TYPE( + VectorMode, ((long)0), "Contours", App::Prop_None, "Select what vector field"); + ADD_PROPERTY_TYPE(NoColor, (false), "Contours", + PropertyType(Prop_Hidden), "Don't color the contours"); + + m_contourConstraints.LowerBound = 1; + m_contourConstraints.UpperBound = 1000; + m_contourConstraints.StepSize = 1; + NumberOfContours.setConstraints(&m_contourConstraints); + + FilterPipeline contours; + m_contours = vtkSmartPointer::New(); + m_contours->ComputeScalarsOn(); + contours.source = m_contours; + contours.target = m_contours; + addFilterPipeline(contours, "contours"); + setActiveFilterPipeline("contours"); +} + +FemPostContoursFilter::~FemPostContoursFilter() +{} + +DocumentObjectExecReturn* FemPostContoursFilter::execute() +{ + // update list of available fields and their vectors + if (!m_blockPropertyChanges) { + refreshFields(); + refreshVectors(); + } + + // recalculate the filter + auto returnObject = Fem::FemPostFilter::execute(); + + // delete contour field + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return returnObject; + dset->GetPointData()->RemoveArray(contourFieldName.c_str()); + // refresh fields to reflect the deletion + if (!m_blockPropertyChanges) + refreshFields(); + + return returnObject; +} + +void FemPostContoursFilter::onChanged(const Property* prop) +{ + if (m_blockPropertyChanges) + return; + + if (prop == &Field && (Field.getValue() >= 0)) + refreshVectors(); + + // note that we need to calculate also in case of a Data change + // otherwise the contours output would be empty and the ViewProviderFemPostObject + // would not get any data + if ((prop == &Field || prop == &VectorMode || prop == &NumberOfContours || prop == &Data) + && (Field.getValue() >= 0)) { + double p[2]; + + // get the field and its data + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return; + vtkDataArray* pdata = dset->GetPointData()->GetArray(Field.getValueAsString()); + if (!pdata) + return; + if (pdata->GetNumberOfComponents() == 1) { + // if we have a scalar, we can directly use the array + m_contours->SetInputArrayToProcess( + 0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_POINTS, Field.getValueAsString()); + pdata->GetRange(p); + recalculateContours(p[0], p[1]); + } + else { + // The contour filter handles vectors by taking always its first component. + // There is no other solution than to make the desired vectorn component a + // scalar array and append this temporarily to the data. (vtkExtractVectorComponents + // does not work because our data is an unstructured data set.) + int component = -1; + if (VectorMode.getValue() == 1) + component = 0; + else if (VectorMode.getValue() == 2) + component = 1; + else if (VectorMode.getValue() == 3) + component = 2; + // extract the component to a new array + vtkSmartPointer componentArray = vtkSmartPointer::New(); + componentArray->SetNumberOfComponents(1); + vtkIdType numTuples = pdata->GetNumberOfTuples(); + componentArray->SetNumberOfTuples(numTuples); + + if (component >= 0) { + for (vtkIdType tupleIdx = 0; tupleIdx < numTuples; ++tupleIdx) { + componentArray->SetComponent( + tupleIdx, 0, pdata->GetComponent(tupleIdx, component)); + } + } + else { + for (vtkIdType tupleIdx = 0; tupleIdx < numTuples; ++tupleIdx) { + componentArray->SetComponent( + tupleIdx, + 0, + std::sqrt( + pdata->GetComponent(tupleIdx, 0) * pdata->GetComponent(tupleIdx, 0) + + pdata->GetComponent(tupleIdx, 1) * pdata->GetComponent(tupleIdx, 1) + + pdata->GetComponent(tupleIdx, 2) * pdata->GetComponent(tupleIdx, 2))); + } + } + // name the array + contourFieldName = std::string(Field.getValueAsString()) + "_contour"; + componentArray->SetName(contourFieldName.c_str()); + + // add the array as new field and use it for the contour filter + dset->GetPointData()->AddArray(componentArray); + m_contours->SetInputArrayToProcess( + 0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_POINTS, contourFieldName.c_str()); + componentArray->GetRange(p); + recalculateContours(p[0], p[1]); + if (prop == &Data) { + // we must recalculate to pass the new created contours field + // to ViewProviderFemPostObject + m_blockPropertyChanges = true; + execute(); + m_blockPropertyChanges = false; + } + } + } Fem::FemPostFilter::onChanged(prop); } -short int FemPostWarpVectorFilter::mustExecute() const { - - if (Factor.isTouched() || - Vector.isTouched()) +short int FemPostContoursFilter::mustExecute() const +{ + if (Field.isTouched() || VectorMode.isTouched() || NumberOfContours.isTouched() + || Data.isTouched()) return 1; else return App::DocumentObject::mustExecute(); } +void FemPostContoursFilter::recalculateContours(double min, double max) +{ + // As the min and max contours are not visible, an input of "3" leads + // to 1 visible contour. To not confuse the user, take the visible contours + // for NumberOfContours + int visibleNum = NumberOfContours.getValue() + 2; + m_contours->GenerateValues(visibleNum, min, max); +} + +void FemPostContoursFilter::refreshFields() +{ + m_blockPropertyChanges = true; + + std::string fieldName; + if (Field.getValue() >= 0) + fieldName = Field.getValueAsString(); + + std::vector FieldsArray; + + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) { + m_blockPropertyChanges = false; + return; + } + vtkPointData* pd = dset->GetPointData(); + + // get all fields + for (int i = 0; i < pd->GetNumberOfArrays(); ++i) { + FieldsArray.emplace_back(pd->GetArrayName(i)); + } + + App::Enumeration empty; + Field.setValue(empty); + m_fields.setEnums(FieldsArray); + Field.setValue(m_fields); + + // search if the current field is in the available ones and set it + std::vector::iterator it = + std::find(FieldsArray.begin(), FieldsArray.end(), fieldName); + if (!fieldName.empty() && it != FieldsArray.end()) { + Field.setValue(fieldName.c_str()); + } + else { + m_blockPropertyChanges = false; + // select the first field + Field.setValue(long(0)); + fieldName = Field.getValueAsString(); + } + + m_blockPropertyChanges = false; +} + +void FemPostContoursFilter::refreshVectors() +{ + // refreshes the list of available vectors for the current Field + m_blockPropertyChanges = true; + + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) { + m_blockPropertyChanges = false; + return; + } + vtkDataArray* fieldArray = dset->GetPointData()->GetArray(Field.getValueAsString()); + if (!fieldArray) { + m_blockPropertyChanges = false; + return; + } + + // store name if already set + std::string vectorName; + if (VectorMode.hasEnums() && VectorMode.getValue() >= 0) + vectorName = VectorMode.getValueAsString(); + + std::vector vectorArray; + if (fieldArray->GetNumberOfComponents() == 1) + vectorArray.emplace_back("Not a vector"); + else { + vectorArray.emplace_back("Magnitude"); + if (fieldArray->GetNumberOfComponents() >= 2) { + vectorArray.emplace_back("X"); + vectorArray.emplace_back("Y"); + } + if (fieldArray->GetNumberOfComponents() >= 3) { + vectorArray.emplace_back("Z"); + } + } + App::Enumeration empty; + VectorMode.setValue(empty); + m_vectors.setEnums(vectorArray); + VectorMode.setValue(m_vectors); + + // apply stored name + auto it = std::find(vectorArray.begin(), vectorArray.end(), vectorName); + if (!vectorName.empty() && it != vectorArray.end()) + VectorMode.setValue(vectorName.c_str()); + + m_blockPropertyChanges = false; +} + // *************************************************************************** // cut filter @@ -673,7 +738,7 @@ FemPostCutFilter::FemPostCutFilter() "The function object which defines the cut function"); FilterPipeline cut; - m_cutter = vtkSmartPointer::New(); + m_cutter = vtkSmartPointer::New(); cut.source = m_cutter; cut.target = m_cutter; addFilterPipeline(cut, "cut"); @@ -711,3 +776,191 @@ DocumentObjectExecReturn* FemPostCutFilter::execute() return Fem::FemPostFilter::execute(); } + + +// *************************************************************************** +// scalar clip filter +PROPERTY_SOURCE(Fem::FemPostScalarClipFilter, Fem::FemPostFilter) + +FemPostScalarClipFilter::FemPostScalarClipFilter() : FemPostFilter() { + + ADD_PROPERTY_TYPE( + Value, (0), "Clip", App::Prop_None, "The scalar value used to clip the selected field"); + ADD_PROPERTY_TYPE(Scalars, (long(0)), "Clip", App::Prop_None, "The field used to clip"); + ADD_PROPERTY_TYPE(InsideOut, (false), "Clip", App::Prop_None, "Invert the clip direction"); + + Value.setConstraints(&m_constraints); + + FilterPipeline clip; + m_clipper = vtkSmartPointer::New(); + clip.source = m_clipper; + clip.target = m_clipper; + addFilterPipeline(clip, "clip"); + setActiveFilterPipeline("clip"); +} + +FemPostScalarClipFilter::~FemPostScalarClipFilter() { + +} + +DocumentObjectExecReturn* FemPostScalarClipFilter::execute() +{ + std::string val; + if (Scalars.getValue() >= 0) + val = Scalars.getValueAsString(); + + std::vector ScalarsArray; + + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return StdReturn; + vtkPointData* pd = dset->GetPointData(); + + // get all scalar fields + for (int i = 0; i < pd->GetNumberOfArrays(); ++i) { + if (pd->GetArray(i)->GetNumberOfComponents() == 1) + ScalarsArray.emplace_back(pd->GetArrayName(i)); + } + + App::Enumeration empty; + Scalars.setValue(empty); + m_scalarFields.setEnums(ScalarsArray); + Scalars.setValue(m_scalarFields); + + // search if the current field is in the available ones and set it + std::vector::iterator it = + std::find(ScalarsArray.begin(), ScalarsArray.end(), val); + if (!val.empty() && it != ScalarsArray.end()) + Scalars.setValue(val.c_str()); + + //recalculate the filter + return Fem::FemPostFilter::execute(); +} + +void FemPostScalarClipFilter::onChanged(const Property* prop) +{ + if (prop == &Value) { + m_clipper->SetValue(Value.getValue()); + } + else if (prop == &InsideOut) { + m_clipper->SetInsideOut(InsideOut.getValue()); + } + else if (prop == &Scalars && (Scalars.getValue() >= 0)) { + m_clipper->SetInputArrayToProcess(0, 0, 0, + vtkDataObject::FIELD_ASSOCIATION_POINTS, Scalars.getValueAsString()); + setConstraintForField(); + } + + Fem::FemPostFilter::onChanged(prop); +} + +short int FemPostScalarClipFilter::mustExecute() const +{ + if (Value.isTouched() || + InsideOut.isTouched() || + Scalars.isTouched()) + return 1; + else + return App::DocumentObject::mustExecute(); +} + +void FemPostScalarClipFilter::setConstraintForField() +{ + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return; + + vtkDataArray* pdata = dset->GetPointData()->GetArray(Scalars.getValueAsString()); + // VTK cannot deliver data when the filer relies e.g. on a cut clip filter + // whose value is set so that all data are cut + if (!pdata) + return; + double p[2]; + pdata->GetRange(p); + m_constraints.LowerBound = p[0]; + m_constraints.UpperBound = p[1]; + m_constraints.StepSize = (p[1] - p[0]) / 100.; +} + + +// *************************************************************************** +// warp vector filter +PROPERTY_SOURCE(Fem::FemPostWarpVectorFilter, Fem::FemPostFilter) + +FemPostWarpVectorFilter::FemPostWarpVectorFilter() : FemPostFilter() +{ + ADD_PROPERTY_TYPE(Factor, + (0), + "Warp", + App::Prop_None, + "The factor by which the vector is added to the node positions"); + ADD_PROPERTY_TYPE( + Vector, (long(0)), "Warp", App::Prop_None, "The field added to the node position"); + + FilterPipeline warp; + m_warp = vtkSmartPointer::New(); + warp.source = m_warp; + warp.target = m_warp; + addFilterPipeline(warp, "warp"); + setActiveFilterPipeline("warp"); +} + +FemPostWarpVectorFilter::~FemPostWarpVectorFilter() +{} + +DocumentObjectExecReturn* FemPostWarpVectorFilter::execute() +{ + std::string val; + if (Vector.getValue() >= 0) + val = Vector.getValueAsString(); + + std::vector VectorArray; + + vtkSmartPointer data = getInputData(); + vtkDataSet* dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return StdReturn; + vtkPointData* pd = dset->GetPointData(); + + // get all vector fields + for (int i = 0; i < pd->GetNumberOfArrays(); ++i) { + if (pd->GetArray(i)->GetNumberOfComponents() == 3) + VectorArray.emplace_back(pd->GetArrayName(i)); + } + + App::Enumeration empty; + Vector.setValue(empty); + m_vectorFields.setEnums(VectorArray); + Vector.setValue(m_vectorFields); + + // search if the current field is in the available ones and set it + std::vector::iterator it = + std::find(VectorArray.begin(), VectorArray.end(), val); + if (!val.empty() && it != VectorArray.end()) + Vector.setValue(val.c_str()); + + //recalculate the filter + return Fem::FemPostFilter::execute(); +} + +void FemPostWarpVectorFilter::onChanged(const Property* prop) +{ + if (prop == &Factor) + // since our mesh is in mm, we must scale the factor + m_warp->SetScaleFactor(1000 * Factor.getValue()); + else if (prop == &Vector && (Vector.getValue() >= 0)) + m_warp->SetInputArrayToProcess(0, 0, 0, + vtkDataObject::FIELD_ASSOCIATION_POINTS, Vector.getValueAsString()); + + Fem::FemPostFilter::onChanged(prop); +} + +short int FemPostWarpVectorFilter::mustExecute() const +{ + if (Factor.isTouched() || Vector.isTouched()) + return 1; + else + return App::DocumentObject::mustExecute(); +} diff --git a/src/Mod/Fem/App/FemPostFilter.h b/src/Mod/Fem/App/FemPostFilter.h index 867997b7a9..a00edb7d9b 100644 --- a/src/Mod/Fem/App/FemPostFilter.h +++ b/src/Mod/Fem/App/FemPostFilter.h @@ -23,9 +23,12 @@ #ifndef Fem_FemPostFilter_H #define Fem_FemPostFilter_H +#include #include #include +#include #include +#include #include #include #include @@ -72,33 +75,15 @@ private: std::string m_activePipeline; }; -class FemExport FemPostClipFilter : public FemPostFilter { +// *************************************************************************** +// in the following, the different filters sorted alphabetically +// *************************************************************************** - PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostClipFilter); -public: - FemPostClipFilter(); - ~FemPostClipFilter() override; - - App::PropertyLink Function; - App::PropertyBool InsideOut; - App::PropertyBool CutCells; - - const char* getViewProviderName() const override { - return "FemGui::ViewProviderFemPostClip"; - } - short int mustExecute() const override; - App::DocumentObjectExecReturn* execute() override; - -protected: - void onChanged(const App::Property* prop) override; - -private: - vtkSmartPointer m_clipper; - vtkSmartPointer m_extractor; -}; - -class FemExport FemPostDataAlongLineFilter : public FemPostFilter { +// *************************************************************************** +// data along line filter +class FemExport FemPostDataAlongLineFilter: public FemPostFilter +{ PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostDataAlongLineFilter); @@ -113,8 +98,9 @@ public: App::PropertyFloatList YAxisData; App::PropertyString PlotData; - const char* getViewProviderName() const override { - return "FemGui::ViewProviderFemPostDataAlongLine"; + const char* getViewProviderName() const override + { + return "FemGui::ViewProviderFemPostDataAlongLine"; } short int mustExecute() const override; void GetAxisData(); @@ -126,13 +112,15 @@ protected: App::Property* prop) override; private: - - vtkSmartPointer m_line; - vtkSmartPointer m_probe; - + vtkSmartPointer m_line; + vtkSmartPointer m_probe; }; -class FemExport FemPostDataAtPointFilter : public FemPostFilter { + +// *************************************************************************** +// data at point filter +class FemExport FemPostDataAtPointFilter: public FemPostFilter +{ PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostDataAtPointFilter); @@ -146,8 +134,9 @@ public: App::PropertyFloatList PointData; App::PropertyString Unit; - const char* getViewProviderName() const override { - return "FemGui::ViewProviderFemPostDataAtPoint"; + const char* getViewProviderName() const override + { + return "FemGui::ViewProviderFemPostDataAtPoint"; } short int mustExecute() const override; @@ -157,13 +146,115 @@ protected: void GetPointData(); private: - - vtkSmartPointer m_point; - vtkSmartPointer m_probe; - + vtkSmartPointer m_point; + vtkSmartPointer m_probe; }; -class FemExport FemPostScalarClipFilter : public FemPostFilter { + +// *************************************************************************** +// clip filter +class FemExport FemPostClipFilter: public FemPostFilter +{ + + PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostClipFilter); + +public: + FemPostClipFilter(); + ~FemPostClipFilter() override; + + App::PropertyLink Function; + App::PropertyBool InsideOut; + App::PropertyBool CutCells; + + const char* getViewProviderName() const override + { + return "FemGui::ViewProviderFemPostClip"; + } + short int mustExecute() const override; + App::DocumentObjectExecReturn* execute() override; + +protected: + void onChanged(const App::Property* prop) override; + +private: + vtkSmartPointer m_clipper; + vtkSmartPointer m_extractor; +}; + + +// *************************************************************************** +// contours filter +class FemExport FemPostContoursFilter: public FemPostFilter +{ + + PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostContoursFilter); + +public: + FemPostContoursFilter(); + ~FemPostContoursFilter() override; + + App::PropertyEnumeration Field; + App::PropertyIntegerConstraint NumberOfContours; + App::PropertyEnumeration VectorMode; + App::PropertyBool NoColor; + + const char* getViewProviderName() const override + { + return "FemGui::ViewProviderFemPostContours"; + } + short int mustExecute() const override; + +protected: + App::DocumentObjectExecReturn* execute() override; + void onChanged(const App::Property* prop) override; + void recalculateContours(double min, double max); + void refreshFields(); + void refreshVectors(); + bool m_blockPropertyChanges = false; + std::string contourFieldName; + +private: + vtkSmartPointer m_contours; + vtkSmartPointer m_extractor; + vtkSmartPointer m_norm; + App::Enumeration m_fields; + App::Enumeration m_vectors; + App::PropertyIntegerConstraint::Constraints m_contourConstraints; +}; + + +// *************************************************************************** +// cut filter +class FemExport FemPostCutFilter: public FemPostFilter +{ + + PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostCutFilter); + +public: + FemPostCutFilter(); + ~FemPostCutFilter() override; + + App::PropertyLink Function; + + const char* getViewProviderName() const override + { + return "FemGui::ViewProviderFemPostCut"; + } + short int mustExecute() const override; + App::DocumentObjectExecReturn* execute() override; + +protected: + void onChanged(const App::Property* prop) override; + +private: + vtkSmartPointer m_cutter; +}; + + +// *************************************************************************** +// scalar clip filter +class FemExport FemPostScalarClipFilter: public FemPostFilter +{ PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostScalarClipFilter); @@ -171,11 +262,12 @@ public: FemPostScalarClipFilter(); ~FemPostScalarClipFilter() override; - App::PropertyBool InsideOut; + App::PropertyBool InsideOut; App::PropertyFloatConstraint Value; - App::PropertyEnumeration Scalars; + App::PropertyEnumeration Scalars; - const char* getViewProviderName() const override { + const char* getViewProviderName() const override + { return "FemGui::ViewProviderFemPostScalarClip"; } short int mustExecute() const override; @@ -186,11 +278,14 @@ protected: void setConstraintForField(); private: - vtkSmartPointer m_clipper; - App::Enumeration m_scalarFields; - App::PropertyFloatConstraint::Constraints m_constraints; + vtkSmartPointer m_clipper; + App::Enumeration m_scalarFields; + App::PropertyFloatConstraint::Constraints m_constraints; }; + +// *************************************************************************** +// warp vector filter class FemExport FemPostWarpVectorFilter : public FemPostFilter { PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostWarpVectorFilter); @@ -216,29 +311,6 @@ private: App::Enumeration m_vectorFields; }; -class FemExport FemPostCutFilter : public FemPostFilter { - - PROPERTY_HEADER_WITH_OVERRIDE(Fem::FemPostCutFilter); - -public: - FemPostCutFilter(); - ~FemPostCutFilter() override; - - App::PropertyLink Function; - - const char* getViewProviderName() const override { - return "FemGui::ViewProviderFemPostCut"; - } - short int mustExecute() const override; - App::DocumentObjectExecReturn* execute() override; - -protected: - void onChanged(const App::Property* prop) override; - -private: - vtkSmartPointer m_cutter; -}; - } //namespace Fem diff --git a/src/Mod/Fem/App/FemPostObject.cpp b/src/Mod/Fem/App/FemPostObject.cpp index 17d40d48db..3ad500a685 100644 --- a/src/Mod/Fem/App/FemPostObject.cpp +++ b/src/Mod/Fem/App/FemPostObject.cpp @@ -48,8 +48,9 @@ vtkBoundingBox FemPostObject::getBoundingBox() { vtkBoundingBox box; - if (Data.getValue() && Data.getValue()->IsA("vtkDataSet")) - box.AddBounds(vtkDataSet::SafeDownCast(Data.getValue())->GetBounds()); + vtkDataSet* dset = vtkDataSet::SafeDownCast(Data.getValue()); + if (dset) + box.AddBounds(dset->GetBounds()); // TODO: add calculation of multiblock and Multipiece datasets diff --git a/src/Mod/Fem/Gui/AppFemGui.cpp b/src/Mod/Fem/Gui/AppFemGui.cpp index 8401da4f02..88d93ab3ba 100644 --- a/src/Mod/Fem/Gui/AppFemGui.cpp +++ b/src/Mod/Fem/Gui/AppFemGui.cpp @@ -152,6 +152,7 @@ PyMOD_INIT_FUNC(FemGui) FemGui::ViewProviderFemPostObject ::init(); FemGui::ViewProviderFemPostPipeline ::init(); FemGui::ViewProviderFemPostClip ::init(); + FemGui::ViewProviderFemPostContours ::init(); FemGui::ViewProviderFemPostCut ::init(); FemGui::ViewProviderFemPostDataAlongLine ::init(); FemGui::ViewProviderFemPostDataAtPoint ::init(); diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 61783817c3..a7e84a43b5 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -93,6 +93,7 @@ if(BUILD_FEM_VTK) ${FemGui_UIC_SRCS} TaskPostDisplay.ui TaskPostClip.ui + TaskPostContours.ui TaskPostDataAlongLine.ui TaskPostDataAtPoint.ui TaskPostScalarClip.ui @@ -271,6 +272,7 @@ if(BUILD_FEM_VTK) PlaneWidget.ui SphereWidget.ui TaskPostClip.ui + TaskPostContours.ui TaskPostDataAlongLine.ui TaskPostDataAtPoint.ui TaskPostScalarClip.ui diff --git a/src/Mod/Fem/Gui/Command.cpp b/src/Mod/Fem/Gui/Command.cpp index 15e632524e..9a71678678 100644 --- a/src/Mod/Fem/Gui/Command.cpp +++ b/src/Mod/Fem/Gui/Command.cpp @@ -1534,13 +1534,14 @@ void setupFilter(Gui::Command* cmd, std::string Name) { // issue error if no post object if (!((selObject->getTypeId() == Base::Type::fromName("Fem::FemPostPipeline")) - || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostWarpVectorFilter")) - || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostScalarClipFilter")) - || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostCutFilter")) - || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostClipFilter")) - || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostDataAlongLineFilter")) ) - ) { - QMessageBox::warning(Gui::getMainWindow(), + || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostClipFilter")) + || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostContoursFilter")) + || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostCutFilter")) + || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostDataAlongLineFilter")) + || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostScalarClipFilter")) + || (selObject->getTypeId() == Base::Type::fromName("Fem::FemPostWarpVectorFilter")))) { + QMessageBox::warning( + Gui::getMainWindow(), qApp->translate("setupFilter", "Error: no post processing object selected."), qApp->translate("setupFilter", "The filter could not be set up.")); return; @@ -1721,20 +1722,21 @@ bool CmdFemPostClipFilter::isActive() // only allow one object if (getSelection().getSelection().size() > 1) return false; - // only activate if a result is either a post pipeline, scalar, cut or warp filter, - // itself or along line filter + // only activate if a result is either a post pipeline or a possible filter if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; else if (getSelection().getObjectsOfType().size() == 1) return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else return false; } @@ -1765,20 +1767,21 @@ bool CmdFemPostCutFilter::isActive() // only allow one object if (getSelection().getSelection().size() > 1) return false; - // only activate if a result is either a post pipeline, scalar, clip or warp filter, - // itself, or along line filter + // only activate if a result is either a post pipeline or a possible filter if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; else if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) + else if (getSelection().getObjectsOfType().size() == 1) return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else return false; } @@ -1809,14 +1812,16 @@ bool CmdFemPostDataAlongLineFilter::isActive() // only allow one object if (getSelection().getSelection().size() > 1) return false; - // only activate if a result is either a post pipeline, scalar, cut, clip or warp filter + // only activate if a result is either a post pipeline or a possible filter if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) return true; else if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) + else if (getSelection().getObjectsOfType().size() == 1) return true; else if (getSelection().getObjectsOfType().size() == 1) return true; @@ -1852,20 +1857,19 @@ bool CmdFemPostDataAtPointFilter::isActive() // only allow one object if (getSelection().getSelection().size() > 1) return false; - // only activate if a result is either a post pipeline, scalar, cut, clip, - // warp or along line filter + // only activate if a result is either a post pipeline or a possible filter if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; else if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) + else if (getSelection().getObjectsOfType().size() == 1) return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else return false; } @@ -1970,17 +1974,19 @@ bool CmdFemPostScalarClipFilter::isActive() // only allow one object if (getSelection().getSelection().size() > 1) return false; - // only activate if a result is either a post pipeline, clip, cut, warp or along line filter + // only activate if a result is either a post pipeline or a possible other filter if (getSelection().getObjectsOfType().size() == 1) return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else return false; } @@ -2011,17 +2017,63 @@ bool CmdFemPostWarpVectorFilter::isActive() // only allow one object if (getSelection().getSelection().size() > 1) return false; - // only activate if a result is either a post pipeline, scalar, clip, cut or along line filter + // only activate if a result is either a post pipeline or a possible other filter if (getSelection().getObjectsOfType().size() == 1) return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; - else if (getSelection().getObjectsOfType().size() == 1) - return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else if (getSelection().getObjectsOfType().size() == 1) return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else + return false; +} + + +//================================================================================================ +DEF_STD_CMD_A(CmdFemPostContoursFilter) + +CmdFemPostContoursFilter::CmdFemPostContoursFilter() + : Command("FEM_PostFilterContours") +{ + sAppModule = "Fem"; + sGroup = QT_TR_NOOP("Fem"); + sMenuText = QT_TR_NOOP("Contours filter"); + sToolTipText = + QT_TR_NOOP("Define/create a contours filter which displays iso contours"); + sWhatsThis = "FEM_PostFilterContours"; + sStatusTip = sToolTipText; + sPixmap = "FEM_PostFilterContours"; +} + +void CmdFemPostContoursFilter::activated(int) +{ + setupFilter(this, "Contours"); +} + +bool CmdFemPostContoursFilter::isActive() +{ + // only allow one object + if (getSelection().getSelection().size() > 1) + return false; + // only activate if a result is either a post pipeline or a possible other filter + if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; + else if (getSelection().getObjectsOfType().size() == 1) + return true; else return false; } @@ -2370,15 +2422,16 @@ void CreateFemCommands() // vtk post processing #ifdef FC_USE_VTK + rcCmdMgr.addCommand(new CmdFemPostApllyChanges); rcCmdMgr.addCommand(new CmdFemPostClipFilter); + rcCmdMgr.addCommand(new CmdFemPostContoursFilter); rcCmdMgr.addCommand(new CmdFemPostCutFilter); rcCmdMgr.addCommand(new CmdFemPostDataAlongLineFilter); rcCmdMgr.addCommand(new CmdFemPostDataAtPointFilter); rcCmdMgr.addCommand(new CmdFemPostLinearizedStressesFilter); + rcCmdMgr.addCommand(new CmdFemPostFunctions); + rcCmdMgr.addCommand(new CmdFemPostPipelineFromResult); rcCmdMgr.addCommand(new CmdFemPostScalarClipFilter); rcCmdMgr.addCommand(new CmdFemPostWarpVectorFilter); - rcCmdMgr.addCommand(new CmdFemPostFunctions); - rcCmdMgr.addCommand(new CmdFemPostApllyChanges); - rcCmdMgr.addCommand(new CmdFemPostPipelineFromResult); #endif } diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui b/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui index de8da1844e..598048f7d0 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui @@ -6,15 +6,15 @@ 0 0 - 400 - 203 + 350 + 259 Elmer - - + + @@ -39,6 +39,46 @@ + + + + false + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + <html><head/><body><p>Leave blank to use default Elmer elmer binary file</p><p><span style=" font-weight:600;">Note:</span> To use multithreading you must specify here<br> the executable variant with the suffix &quot;_mpi&quot;.</p></body></html> + + + elmerBinaryPath + + + Mod/Fem/Elmer + + + @@ -55,22 +95,6 @@ - - - - false - - - - 100 - 0 - - - - ElmerGrid binary path - - - @@ -134,6 +158,22 @@ + + + + false + + + + 100 + 0 + + + + ElmerGrid binary path + + + @@ -150,54 +190,23 @@ - - - - false - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - <html><head/><body><p>Leave blank to use default Elmer elmer binary file</p><p><span style=" font-weight:600;">Note:</span> To use multithreading you must specify here<br> the executable variant with the suffix &quot;_mpi&quot;.</p></body></html> - - - elmerBinaryPath - - - Mod/Fem/Elmer - - - - - + + + + + + + Options + + + + Multithreading: - + @@ -246,10 +255,37 @@ + + + + Multi-CPU core support: + + + + + + + The mesh volume regions processed by each CPU core +will be merged to make the volume boundaries invisible. + + + Filter results + + + true + + + FilterMultiCPUResults + + + Mod/Fem/Elmer + + + - + Qt::Vertical diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp index 40110f59d7..201bbe3f24 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp @@ -70,6 +70,7 @@ void DlgSettingsFemElmerImp::saveSettings() ui->fc_grid_binary_path->onSave(); ui->sb_elmer_num_cores->onSave(); + ui->cb_elmer_filtering->onSave(); } void DlgSettingsFemElmerImp::loadSettings() @@ -81,6 +82,7 @@ void DlgSettingsFemElmerImp::loadSettings() ui->fc_grid_binary_path->onRestore(); ui->sb_elmer_num_cores->onRestore(); + ui->cb_elmer_filtering->onRestore(); } /** diff --git a/src/Mod/Fem/Gui/PreCompiled.h b/src/Mod/Fem/Gui/PreCompiled.h index 74c53d91f2..c4ce783c2e 100644 --- a/src/Mod/Fem/Gui/PreCompiled.h +++ b/src/Mod/Fem/Gui/PreCompiled.h @@ -171,10 +171,11 @@ #include // VTK -#include #include #include +#include #include +#include #endif //_PreComp_ diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index 7f7fdcc322..25860e232f 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -70,6 +70,7 @@ icons/FEM_PostFilterClipRegion.svg icons/FEM_PostFilterClipScalar.svg + icons/FEM_PostFilterContours.svg icons/FEM_PostFilterCutFunction.svg icons/FEM_PostFilterDataAlongLine.svg icons/FEM_PostFilterDataAtPoint.svg diff --git a/src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterContours.svg b/src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterContours.svg new file mode 100644 index 0000000000..f438432a70 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/icons/FEM_PostFilterContours.svg @@ -0,0 +1,70 @@ + + + + + + + + image/svg+xml + + + + [Alexander Gryson] + + + 2017-03-11 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/ + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 264baf1664..621e8b324b 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -56,6 +56,7 @@ #include "ui_TaskPostDataAtPoint.h" #include "ui_TaskPostDisplay.h" #include "ui_TaskPostScalarClip.h" +#include "ui_TaskPostContours.h" #include "ui_TaskPostWarpVector.h" #include "TaskPostBoxes.h" @@ -69,7 +70,7 @@ using namespace FemGui; using namespace Gui; // *************************************************************************** - +// point marker PointMarker::PointMarker(Gui::View3DInventorViewer* iv, std::string ObjName) : view(iv), vp(new ViewProviderPointMarker) { @@ -117,7 +118,8 @@ void PointMarker::customEvent(QEvent*) Gui::Command::doCommand(Gui::Command::Doc, ObjectInvisible().c_str()); } -std::string PointMarker::ObjectInvisible() { +std::string PointMarker::ObjectInvisible() +{ return "for amesh in App.activeDocument().Objects:\n\ if \"Mesh\" in amesh.TypeId:\n\ aparttoshow = amesh.Name.replace(\"_Mesh\",\"\")\n\ @@ -145,8 +147,9 @@ ViewProviderPointMarker::~ViewProviderPointMarker() pCoords->unref(); } -// *************************************************************************** +// *************************************************************************** +// data marker DataMarker::DataMarker(Gui::View3DInventorViewer* iv, std::string ObjName) : view(iv), vp(new ViewProviderDataMarker) { @@ -189,7 +192,8 @@ void DataMarker::customEvent(QEvent*) Gui::Command::doCommand(Gui::Command::Doc, ObjectInvisible().c_str()); } -std::string DataMarker::ObjectInvisible() { +std::string DataMarker::ObjectInvisible() +{ return "for amesh in App.activeDocument().Objects:\n\ if \"Mesh\" in amesh.TypeId:\n\ aparttoshow = amesh.Name.replace(\"_Mesh\",\"\")\n\ @@ -227,25 +231,72 @@ ViewProviderDataMarker::~ViewProviderDataMarker() pMarker->unref(); } -// *************************************************************************** -// *************************************************************************** -// TaskDialog -// *************************************************************************** +// *************************************************************************** +// main task dialog +TaskPostBox::TaskPostBox(Gui::ViewProviderDocumentObject* view, const QPixmap& icon, + const QString& title, QWidget* parent) + : TaskBox(icon, title, true, parent), + m_object(view->getObject()), + m_view(view) +{} + +TaskPostBox::~TaskPostBox() +{} + +bool TaskPostBox::autoApply() +{ + return FemSettings().getPostAutoRecompute(); +} + +App::Document* TaskPostBox::getDocument() const +{ + App::DocumentObject* obj = getObject(); + return (obj ? obj->getDocument() : nullptr); +} + +void TaskPostBox::recompute() +{ + if (autoApply()) { + App::Document* doc = getDocument(); + if (doc) + doc->recompute(); + } +} + +void TaskPostBox::updateEnumerationList(App::PropertyEnumeration& prop, QComboBox* box) +{ + QStringList list; + std::vector vec = prop.getEnumVector(); + for (std::vector::iterator it = vec.begin(); it != vec.end(); ++it) { + list.push_back(QString::fromStdString(*it)); + } + + int index = prop.getValue(); + // be aware the QComboxBox might be connected to the Property, + // thus clearing the box will set back the property enumeration index too. + // https://forum.freecadweb.org/viewtopic.php?f=10&t=30944 + box->clear(); + box->insertItems(0, list); + box->setCurrentIndex(index); +} + + +// *************************************************************************** +// simulation dialog for the TaskView TaskDlgPost::TaskDlgPost(Gui::ViewProviderDocumentObject* view) - : TaskDialog() - , m_view(view) + : TaskDialog(), + m_view(view) { assert(view); } TaskDlgPost::~TaskDlgPost() +{} + +QDialogButtonBox::StandardButtons TaskDlgPost::getStandardButtons() const { -} - -QDialogButtonBox::StandardButtons TaskDlgPost::getStandardButtons() const { - //check if we only have gui task boxes bool guionly = true; for (std::vector::const_iterator it = m_boxes.begin(); it != m_boxes.end(); ++it) @@ -283,8 +334,8 @@ void TaskDlgPost::connectSlots() } } -void TaskDlgPost::appendBox(TaskPostBox* box) { - +void TaskDlgPost::appendBox(TaskPostBox* box) +{ m_boxes.push_back(box); Content.push_back(box); } @@ -341,63 +392,13 @@ void TaskDlgPost::modifyStandardButtons(QDialogButtonBox* box) { box->button(QDialogButtonBox::Apply)->setDefault(true); } -// *************************************************************************** -// some task box methods -TaskPostBox::TaskPostBox(Gui::ViewProviderDocumentObject* view, const QPixmap& icon, - const QString& title, QWidget* parent) - : TaskBox(icon, title, true, parent) - , m_object(view->getObject()) - , m_view(view) -{ -} - -TaskPostBox::~TaskPostBox() { - -} - -bool TaskPostBox::autoApply() { - - return FemSettings().getPostAutoRecompute(); -} - -App::Document* TaskPostBox::getDocument() const { - App::DocumentObject* obj = getObject(); - return (obj ? obj->getDocument() : nullptr); -} - -void TaskPostBox::recompute() { - - if (autoApply()) { - App::Document* doc = getDocument(); - if (doc) - doc->recompute(); - } -} - -void TaskPostBox::updateEnumerationList(App::PropertyEnumeration& prop, QComboBox* box) { - - QStringList list; - std::vector vec = prop.getEnumVector(); - for (std::vector::iterator it = vec.begin(); it != vec.end(); ++it) { - list.push_back(QString::fromStdString(*it)); - } - - int index = prop.getValue(); - // be aware the QComboxBox might be connected to the Property, - // thus clearing the box will set back the property enumeration index too. - // https://forum.freecadweb.org/viewtopic.php?f=10&t=30944 - box->clear(); - box->insertItems(0, list); - box->setCurrentIndex(index); -} - // *************************************************************************** -// post pipeline results +// box to set the coloring TaskPostDisplay::TaskPostDisplay(Gui::ViewProviderDocumentObject* view, QWidget* parent) : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_ResultShow"), tr("Result display options"), - parent) - , ui(new Ui_TaskPostDisplay) + parent), + ui(new Ui_TaskPostDisplay) { // we need a separate container widget to add all controls to proxy = new QWidget(this); @@ -407,7 +408,8 @@ TaskPostDisplay::TaskPostDisplay(Gui::ViewProviderDocumentObject* view, QWidget* this->groupLayout()->addWidget(proxy); // update all fields - updateEnumerationList(getTypedView()->DisplayMode, ui->Representation); + updateEnumerationList(getTypedView()->DisplayMode, + ui->Representation); updateEnumerationList(getTypedView()->Field, ui->Field); updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); @@ -420,29 +422,28 @@ TaskPostDisplay::TaskPostDisplay(Gui::ViewProviderDocumentObject* view, QWidget* } TaskPostDisplay::~TaskPostDisplay() -{ -} +{} void TaskPostDisplay::slotAddedFunction() { updateEnumerationList(getTypedView()->Field, ui->Field); } -void TaskPostDisplay::on_Representation_activated(int i) { - +void TaskPostDisplay::on_Representation_activated(int i) +{ getTypedView()->DisplayMode.setValue(i); updateEnumerationList(getTypedView()->Field, ui->Field); updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); } -void TaskPostDisplay::on_Field_activated(int i) { - +void TaskPostDisplay::on_Field_activated(int i) +{ getTypedView()->Field.setValue(i); updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); } -void TaskPostDisplay::on_VectorMode_activated(int i) { - +void TaskPostDisplay::on_VectorMode_activated(int i) +{ getTypedView()->VectorMode.setValue(i); } @@ -454,19 +455,16 @@ void TaskPostDisplay::on_Transparency_valueChanged(int i) { QToolTip::showText(QCursor::pos(), QString::number(i) + QString::fromLatin1(" %"), nullptr); } -void TaskPostDisplay::applyPythonCode() { +void TaskPostDisplay::applyPythonCode() +{} -} // *************************************************************************** -// ? -// the icon fem-post-geo-plane might be wrong but I do not know any better since the plane is one -// of the implicit functions +// functions TaskPostFunction::TaskPostFunction(ViewProviderDocumentObject* view, QWidget* parent) : TaskPostBox(view, Gui::BitmapFactory().pixmap("fem-post-geo-plane"), tr("Implicit function"), parent) { - assert(view->isDerivedFrom(ViewProviderFemPostFunction::getClassTypeId())); //we load the views widget @@ -476,173 +474,27 @@ TaskPostFunction::TaskPostFunction(ViewProviderDocumentObject* view, QWidget* pa this->groupLayout()->addWidget(w); } -TaskPostFunction::~TaskPostFunction() { - -} - -void TaskPostFunction::applyPythonCode() { +TaskPostFunction::~TaskPostFunction() +{} +void TaskPostFunction::applyPythonCode() +{ //we apply the views widgets python code } // *************************************************************************** -// region clip filter -TaskPostClip::TaskPostClip(ViewProviderDocumentObject* view, App::PropertyLink* function, - QWidget* parent) - : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterClipRegion"), - tr("Clip region, choose implicit function"), parent), - ui(new Ui_TaskPostClip) -{ - - assert(view->isDerivedFrom(ViewProviderFemPostClip::getClassTypeId())); - assert(function); - Q_UNUSED(function); - - fwidget = nullptr; - - //we load the views widget - proxy = new QWidget(this); - ui->setupUi(proxy); - QMetaObject::connectSlotsByName(this); - this->groupLayout()->addWidget(proxy); - - //the layout for the container widget - QVBoxLayout* layout = new QVBoxLayout(); - ui->Container->setLayout(layout); - - //fill up the combo box with possible functions - collectImplicitFunctions(); - - //add the function creation command - Gui::CommandManager& rcCmdMgr = Gui::Application::Instance->commandManager(); - Gui::Command* cmd = rcCmdMgr.getCommandByName("FEM_PostCreateFunctions"); - if (cmd && cmd->getAction()) - cmd->getAction()->addTo(ui->CreateButton); - ui->CreateButton->setPopupMode(QToolButton::InstantPopup); - - //load the default values - ui->CutCells->setChecked( - static_cast(getObject())->CutCells.getValue()); - ui->InsideOut->setChecked( - static_cast(getObject())->InsideOut.getValue()); -} - -TaskPostClip::~TaskPostClip() { - -} - -void TaskPostClip::applyPythonCode() { - -} - -void TaskPostClip::collectImplicitFunctions() { - - std::vector pipelines; - pipelines = getDocument()->getObjectsOfType(); - if (!pipelines.empty()) { - Fem::FemPostPipeline* pipeline = pipelines.front(); - if (pipeline->Functions.getValue() && pipeline->Functions.getValue()->getTypeId() - == Fem::FemPostFunctionProvider::getClassTypeId()) { - - ui->FunctionBox->clear(); - QStringList items; - std::size_t currentItem = 0; - App::DocumentObject* currentFunction = - static_cast(getObject())->Function.getValue(); - const std::vector& funcs = - static_cast(pipeline->Functions.getValue()) - ->Functions.getValues(); - for (std::size_t i = 0; i < funcs.size(); ++i) { - items.push_back(QString::fromLatin1(funcs[i]->getNameInDocument())); - if (currentFunction == funcs[i]) - currentItem = i; - } - ui->FunctionBox->addItems(items); - ui->FunctionBox->setCurrentIndex(currentItem); - } - } -} - -void TaskPostClip::on_CreateButton_triggered(QAction*) { - - int numFuncs = ui->FunctionBox->count(); - int currentItem = ui->FunctionBox->currentIndex(); - collectImplicitFunctions(); - - // if a new function was successfully added use it - int indexCount = ui->FunctionBox->count(); - if (indexCount > currentItem + 1) - ui->FunctionBox->setCurrentIndex(indexCount - 1); - - // When the first function ever was added, a signal must be emitted - if (numFuncs == 0) { - Q_EMIT emitAddedFunction(); - } - - recompute(); -} - -void TaskPostClip::on_FunctionBox_currentIndexChanged(int idx) { - - //set the correct property - std::vector pipelines; - pipelines = getDocument()->getObjectsOfType(); - if (!pipelines.empty()) { - Fem::FemPostPipeline* pipeline = pipelines.front(); - if (pipeline->Functions.getValue() && pipeline->Functions.getValue()->getTypeId() - == Fem::FemPostFunctionProvider::getClassTypeId()) { - - const std::vector& funcs = - static_cast(pipeline->Functions.getValue()) - ->Functions.getValues(); - if (idx >= 0) - static_cast(getObject())->Function.setValue(funcs[idx]); - else - static_cast(getObject())->Function.setValue(nullptr); - } - } - - //load the correct view - Fem::FemPostFunction* fobj = static_cast( - static_cast(getObject())->Function.getValue()); - Gui::ViewProvider* view = nullptr; - if (fobj) - view = Gui::Application::Instance->getViewProvider(fobj); - - if (fwidget) - fwidget->deleteLater(); - - if (view) { - fwidget = static_cast(view)->createControlWidget(); - fwidget->setParent(ui->Container); - fwidget->setViewProvider(static_cast(view)); - ui->Container->layout()->addWidget(fwidget); - } - recompute(); -} - -void TaskPostClip::on_CutCells_toggled(bool val) { - - static_cast(getObject())->CutCells.setValue(val); - recompute(); -} - -void TaskPostClip::on_InsideOut_toggled(bool val) { - - static_cast(getObject())->InsideOut.setValue(val); - recompute(); -} +// in the following, the different filters sorted alphabetically +// *************************************************************************** // *************************************************************************** -// data along a line +// data along line filter TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderDocumentObject* view, QWidget* parent) : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterDataAlongLine"), tr("Data along a line options"), parent), ui(new Ui_TaskPostDataAlongLine) { - assert(view->isDerivedFrom(ViewProviderFemPostDataAlongLine::getClassTypeId())); //we load the views widget @@ -694,20 +546,34 @@ TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderDocumentObject* view, Q int res = static_cast(getObject())->Resolution.getValue(); ui->resolution->setValue(res); - connect(ui->point1X, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAlongLine::point1Changed); - connect(ui->point1Y, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAlongLine::point1Changed); - connect(ui->point1Z, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAlongLine::point1Changed); - connect(ui->point2X, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAlongLine::point2Changed); - connect(ui->point2Y, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAlongLine::point2Changed); - connect(ui->point2Z, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAlongLine::point2Changed); - connect(ui->resolution, qOverload(&QSpinBox::valueChanged), - this, &TaskPostDataAlongLine::resolutionChanged); + connect(ui->point1X, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAlongLine::point1Changed); + connect(ui->point1Y, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAlongLine::point1Changed); + connect(ui->point1Z, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAlongLine::point1Changed); + connect(ui->point2X, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAlongLine::point2Changed); + connect(ui->point2Y, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAlongLine::point2Changed); + connect(ui->point2Z, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAlongLine::point2Changed); + connect(ui->resolution, + qOverload(&QSpinBox::valueChanged), + this, + &TaskPostDataAlongLine::resolutionChanged); //update all fields updateEnumerationList(getTypedView()->DisplayMode, @@ -716,39 +582,36 @@ TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderDocumentObject* view, Q updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); } -TaskPostDataAlongLine::~TaskPostDataAlongLine() { +TaskPostDataAlongLine::~TaskPostDataAlongLine() +{} -} +void TaskPostDataAlongLine::applyPythonCode() +{} -void TaskPostDataAlongLine::applyPythonCode() { - -} - -static const char* cursor_triangle[] = { -"32 32 3 1", -" c None", -". c #FFFFFF", -"+ c #FF0000", -" . ", -" . ", -" . ", -" . ", -" . ", -" ", -"..... ..... ", -" ", -" . ", -" . ", -" . ++ ", -" . + + ", -" . + ++ + ", -" + ++++ + ", -" + ++ ++ + ", -" + ++++++++ + ", -" ++ ++ ++ ++ " }; - -void TaskPostDataAlongLine::on_SelectPoints_clicked() { +static const char* cursor_triangle[] = {"32 32 3 1", + " c None", + ". c #FFFFFF", + "+ c #FF0000", + " . ", + " . ", + " . ", + " . ", + " . ", + " ", + "..... ..... ", + " ", + " . ", + " . ", + " . ++ ", + " . + + ", + " . + ++ + ", + " + ++++ + ", + " + ++ ++ + ", + " + ++++++++ + ", + " ++ ++ ++ ++ "}; +void TaskPostDataAlongLine::on_SelectPoints_clicked() +{ Gui::Command::doCommand(Gui::Command::Doc, ObjectVisible().c_str()); Gui::Document* doc = Gui::Application::Instance->getDocument(getDocument()); Gui::View3DInventor* view = static_cast(doc->getActiveView()); @@ -763,13 +626,14 @@ void TaskPostDataAlongLine::on_SelectPoints_clicked() { FemGui::PointMarker* marker = new FemGui::PointMarker(viewer, ObjName); viewer->addEventCallback(SoMouseButtonEvent::getClassTypeId(), - FemGui::TaskPostDataAlongLine::pointCallback, marker); - connect(marker, &PointMarker::PointsChanged, - this, &TaskPostDataAlongLine::onChange); + FemGui::TaskPostDataAlongLine::pointCallback, + marker); + connect(marker, &PointMarker::PointsChanged, this, &TaskPostDataAlongLine::onChange); } } -std::string TaskPostDataAlongLine::ObjectVisible() { +std::string TaskPostDataAlongLine::ObjectVisible() +{ return "for amesh in App.activeDocument().Objects:\n\ if \"Mesh\" in amesh.TypeId:\n\ aparttoshow = amesh.Name.replace(\"_Mesh\",\"\")\n\ @@ -778,8 +642,8 @@ std::string TaskPostDataAlongLine::ObjectVisible() { apart.ViewObject.Visibility = True\n"; } -void TaskPostDataAlongLine::on_CreatePlot_clicked() { - +void TaskPostDataAlongLine::on_CreatePlot_clicked() +{ App::DocumentObjectT objT(getObject()); std::string ObjName = objT.getObjectPython(); Gui::doCommandT(Gui::Command::Doc, "x = %s.XAxisData", ObjName); @@ -792,7 +656,6 @@ void TaskPostDataAlongLine::on_CreatePlot_clicked() { void TaskPostDataAlongLine::onChange(double x1, double y1, double z1, double x2, double y2, double z2) { - // call point1Changed only once ui->point1X->blockSignals(true); ui->point1Y->blockSignals(true); @@ -818,11 +681,13 @@ void TaskPostDataAlongLine::onChange(double x1, double y1, double z1, double x2, point2Changed(0.0); } -void TaskPostDataAlongLine::point1Changed(double) { - +void TaskPostDataAlongLine::point1Changed(double) +{ try { std::string ObjName = getObject()->getNameInDocument(); - Gui::cmdAppDocumentArgs(getDocument(), "%s.Point1 = App.Vector(%f, %f, %f)", ObjName, + Gui::cmdAppDocumentArgs(getDocument(), + "%s.Point1 = App.Vector(%f, %f, %f)", + ObjName, ui->point1X->value().getValue(), ui->point1Y->value().getValue(), ui->point1Z->value().getValue()); @@ -840,11 +705,13 @@ void TaskPostDataAlongLine::point1Changed(double) { } } -void TaskPostDataAlongLine::point2Changed(double) { - +void TaskPostDataAlongLine::point2Changed(double) +{ try { std::string ObjName = getObject()->getNameInDocument(); - Gui::cmdAppDocumentArgs(getDocument(), "%s.Point2 = App.Vector(%f, %f, %f)", ObjName, + Gui::cmdAppDocumentArgs(getDocument(), + "%s.Point2 = App.Vector(%f, %f, %f)", + ObjName, ui->point2X->value().getValue(), ui->point2Y->value().getValue(), ui->point2Z->value().getValue()); @@ -862,8 +729,8 @@ void TaskPostDataAlongLine::point2Changed(double) { } } -void TaskPostDataAlongLine::resolutionChanged(int val) { - +void TaskPostDataAlongLine::resolutionChanged(int val) +{ static_cast(getObject())->Resolution.setValue(val); // recompute the feature getObject()->recomputeFeature(); @@ -909,27 +776,28 @@ void TaskPostDataAlongLine::pointCallback(void* ud, SoEventCallback* n) } } -void TaskPostDataAlongLine::on_Representation_activated(int i) { - +void TaskPostDataAlongLine::on_Representation_activated(int i) +{ getTypedView()->DisplayMode.setValue(i); updateEnumerationList(getTypedView()->Field, ui->Field); updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); } -void TaskPostDataAlongLine::on_Field_activated(int i) { - +void TaskPostDataAlongLine::on_Field_activated(int i) +{ getTypedView()->Field.setValue(i); std::string FieldName = ui->Field->currentText().toStdString(); static_cast(getObject())->PlotData.setValue(FieldName); updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); } -void TaskPostDataAlongLine::on_VectorMode_activated(int i) { - +void TaskPostDataAlongLine::on_VectorMode_activated(int i) +{ getTypedView()->VectorMode.setValue(i); } -std::string TaskPostDataAlongLine::Plot() { +std::string TaskPostDataAlongLine::Plot() +{ auto xlabel = tr("Length", "X-Axis plot label"); std::ostringstream oss; oss << "import FreeCAD\n\ @@ -939,7 +807,8 @@ from matplotlib import pyplot as plt\n\ plt.ioff()\n\ plt.figure(title)\n\ plt.plot(x, y)\n\ -plt.xlabel(\"" << xlabel.toStdString() << "\")\n\ +plt.xlabel(\"" + << xlabel.toStdString() << "\")\n\ plt.ylabel(title)\n\ plt.title(title)\n\ plt.grid()\n\ @@ -952,13 +821,12 @@ plt.show()\n"; // *************************************************************************** -// data at point +// data at point filter TaskPostDataAtPoint::TaskPostDataAtPoint(ViewProviderDocumentObject* view, QWidget* parent) : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterDataAtPoint"), tr("Data at point options"), parent), ui(new Ui_TaskPostDataAtPoint) { - assert(view->isDerivedFrom(ViewProviderFemPostDataAtPoint::getClassTypeId())); //we load the views widget @@ -995,57 +863,62 @@ TaskPostDataAtPoint::TaskPostDataAtPoint(ViewProviderDocumentObject* view, QWidg updateEnumerationList(getTypedView()->Field, ui->Field); // read in point value - auto pointValue = static_cast(getObject())->PointData[0]; + auto pointValue = static_cast(getObject())->PointData[0]; showValue(pointValue, static_cast(getObject())->Unit.getValue()); - connect(ui->centerX, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAtPoint::centerChanged); - connect(ui->centerY, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAtPoint::centerChanged); - connect(ui->centerZ, qOverload(&Gui::QuantitySpinBox::valueChanged), - this, &TaskPostDataAtPoint::centerChanged); + connect(ui->centerX, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAtPoint::centerChanged); + connect(ui->centerY, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAtPoint::centerChanged); + connect(ui->centerZ, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskPostDataAtPoint::centerChanged); // the point filter object needs to be recomputed // to fill all fields with data at the current point getObject()->recomputeFeature(); } -TaskPostDataAtPoint::~TaskPostDataAtPoint() { +TaskPostDataAtPoint::~TaskPostDataAtPoint() +{ App::Document* doc = getDocument(); if (doc) doc->recompute(); } -void TaskPostDataAtPoint::applyPythonCode() { +void TaskPostDataAtPoint::applyPythonCode() +{} -} - -static const char* cursor_star[] = { -"32 17 3 1", -" c None", -". c #FFFFFF", -"+ c #FF0000", -" . ", -" . ", -" . ", -" . ", -" . ", -" ", -"..... ..... ", -" ", -" . ", -" . ", -" . ++ ", -" . + + ", -" . + ++ + ", -" + ++++ + ", -" + ++ ++ + ", -" + ++++++++ + ", -" ++ ++ ++ ++ " }; - -void TaskPostDataAtPoint::on_SelectPoint_clicked() { +static const char* cursor_star[] = {"32 17 3 1", + " c None", + ". c #FFFFFF", + "+ c #FF0000", + " . ", + " . ", + " . ", + " . ", + " . ", + " ", + "..... ..... ", + " ", + " . ", + " . ", + " . ++ ", + " . + + ", + " . + ++ + ", + " + ++++ + ", + " + ++ ++ + ", + " + ++++++++ + ", + " ++ ++ ++ ++ "}; +void TaskPostDataAtPoint::on_SelectPoint_clicked() +{ Gui::Command::doCommand(Gui::Command::Doc, ObjectVisible().c_str()); Gui::Document* doc = Gui::Application::Instance->getDocument(getDocument()); Gui::View3DInventor* view = static_cast(doc->getActiveView()); @@ -1060,14 +933,16 @@ void TaskPostDataAtPoint::on_SelectPoint_clicked() { FemGui::DataMarker* marker = new FemGui::DataMarker(viewer, ObjName); viewer->addEventCallback(SoMouseButtonEvent::getClassTypeId(), - FemGui::TaskPostDataAtPoint::pointCallback, marker); + FemGui::TaskPostDataAtPoint::pointCallback, + marker); connect(marker, &DataMarker::PointsChanged, this, &TaskPostDataAtPoint::onChange); } getTypedView()->DisplayMode.setValue(1); updateEnumerationList(getTypedView()->Field, ui->Field); } -std::string TaskPostDataAtPoint::ObjectVisible() { +std::string TaskPostDataAtPoint::ObjectVisible() +{ return "for amesh in App.activeDocument().Objects:\n\ if \"Mesh\" in amesh.TypeId:\n\ aparttoshow = amesh.Name.replace(\"_Mesh\",\"\")\n\ @@ -1076,7 +951,8 @@ std::string TaskPostDataAtPoint::ObjectVisible() { apart.ViewObject.Visibility = True\n"; } -void TaskPostDataAtPoint::onChange(double x, double y, double z) { +void TaskPostDataAtPoint::onChange(double x, double y, double z) +{ // call centerChanged only once ui->centerX->blockSignals(true); @@ -1091,11 +967,13 @@ void TaskPostDataAtPoint::onChange(double x, double y, double z) { centerChanged(0.0); } -void TaskPostDataAtPoint::centerChanged(double) { - +void TaskPostDataAtPoint::centerChanged(double) +{ try { std::string ObjName = getObject()->getNameInDocument(); - Gui::cmdAppDocumentArgs(getDocument(), "%s.Center = App.Vector(%f, %f, %f)", ObjName, + Gui::cmdAppDocumentArgs(getDocument(), + "%s.Center = App.Vector(%f, %f, %f)", + ObjName, ui->centerX->value().getValue(), ui->centerY->value().getValue(), ui->centerZ->value().getValue()); @@ -1147,8 +1025,8 @@ void TaskPostDataAtPoint::pointCallback(void* ud, SoEventCallback* n) } } -void TaskPostDataAtPoint::on_Field_activated(int i) { - +void TaskPostDataAtPoint::on_Field_activated(int i) +{ getTypedView()->Field.setValue(i); std::string FieldName = ui->Field->currentText().toStdString(); // there is no "None" for the FieldName property, thus return here @@ -1162,23 +1040,22 @@ void TaskPostDataAtPoint::on_Field_activated(int i) { // Set the unit for the different known result types. // CCX names - if ( (FieldName == "von Mises Stress") || (FieldName == "Tresca Stress") - || (FieldName == "Major Principal Stress") - || (FieldName == "Intermediate Principal Stress") - || (FieldName == "Minor Principal Stress") - || (FieldName == "Major Principal Stress Vector") + if ((FieldName == "von Mises Stress") || (FieldName == "Tresca Stress") + || (FieldName == "Major Principal Stress") || (FieldName == "Intermediate Principal Stress") + || (FieldName == "Minor Principal Stress") || (FieldName == "Major Principal Stress Vector") || (FieldName == "Intermediate Principal Stress Vector") - || (FieldName == "Minor Principal Stress Vector") - || (FieldName == "Stress xx component") || (FieldName == "Stress xy component") - || (FieldName == "Stress xz component") || (FieldName == "Stress yy component") - || (FieldName == "Stress yz component") || (FieldName == "Stress zz component") ) { + || (FieldName == "Minor Principal Stress Vector") || (FieldName == "Stress xx component") + || (FieldName == "Stress xy component") || (FieldName == "Stress xz component") + || (FieldName == "Stress yy component") || (FieldName == "Stress yz component") + || (FieldName == "Stress zz component")) { static_cast(getObject())->Unit.setValue("Pa"); } // The Elmer names are different. If there are EigenModes, the names are unique for // every mode. Therefore we only check for the beginning of the name. - else if ( (FieldName.find("tresca", 0) == 0) || (FieldName.find("vonmises", 0) == 0) - || (FieldName.find("stress_", 0) == 0) || (FieldName.find("principal stress", 0) == 0) ) { - static_cast(getObject())->Unit.setValue("Pa"); + else if ((FieldName.find("tresca", 0) == 0) || (FieldName.find("vonmises", 0) == 0) + || (FieldName.find("stress_", 0) == 0) + || (FieldName.find("principal stress", 0) == 0)) { + static_cast(getObject())->Unit.setValue("Pa"); } else if ((FieldName == "current density") || (FieldName == "current density re") || (FieldName == "current density im") || (FieldName == "current density abs")) { @@ -1196,7 +1073,7 @@ void TaskPostDataAtPoint::on_Field_activated(int i) { static_cast(getObject())->Unit.setValue("V/m"); } else if (FieldName == "electric flux") { - static_cast(getObject())->Unit.setValue("A*s/m^2"); + static_cast(getObject())->Unit.setValue("A*s/m^2"); } else if (FieldName == "electric force density") { static_cast(getObject())->Unit.setValue("N/m^2"); @@ -1238,7 +1115,7 @@ void TaskPostDataAtPoint::on_Field_activated(int i) { static_cast(getObject())->Unit.setValue("V"); } else if (FieldName == "potential flux") { - static_cast(getObject())->Unit.setValue("W/m^2"); + static_cast(getObject())->Unit.setValue("W/m^2"); } // potential loads are in Coulomb: https://www.elmerfem.org/forum/viewtopic.php?t=7780 else if (FieldName == "potential loads") { @@ -1252,7 +1129,7 @@ void TaskPostDataAtPoint::on_Field_activated(int i) { static_cast(getObject())->Unit.setValue("K"); } else if (FieldName == "temperature flux") { - static_cast(getObject())->Unit.setValue("W/m^2"); + static_cast(getObject())->Unit.setValue("W/m^2"); } else { static_cast(getObject())->Unit.setValue(""); @@ -1295,15 +1172,20 @@ std::string TaskPostDataAtPoint::toString(double val) const return valueStream.str(); } -// *************************************************************************** -// scalar clip filter -TaskPostScalarClip::TaskPostScalarClip(ViewProviderDocumentObject* view, QWidget* parent) - : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterClipScalar"), - tr("Scalar clip options"), parent), - ui(new Ui_TaskPostScalarClip) -{ - assert(view->isDerivedFrom(ViewProviderFemPostScalarClip::getClassTypeId())); +// *************************************************************************** +// clip filter +TaskPostClip::TaskPostClip(ViewProviderDocumentObject* view, App::PropertyLink* function, + QWidget* parent) + : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterClipRegion"), + tr("Clip region, choose implicit function"), parent), + ui(new Ui_TaskPostClip) +{ + assert(view->isDerivedFrom(ViewProviderFemPostClip::getClassTypeId())); + assert(function); + Q_UNUSED(function); + + fwidget = nullptr; //we load the views widget proxy = new QWidget(this); @@ -1311,278 +1193,274 @@ TaskPostScalarClip::TaskPostScalarClip(ViewProviderDocumentObject* view, QWidget QMetaObject::connectSlotsByName(this); this->groupLayout()->addWidget(proxy); + //the layout for the container widget + QVBoxLayout* layout = new QVBoxLayout(); + ui->Container->setLayout(layout); + + //fill up the combo box with possible functions + collectImplicitFunctions(); + + //add the function creation command + Gui::CommandManager& rcCmdMgr = Gui::Application::Instance->commandManager(); + Gui::Command* cmd = rcCmdMgr.getCommandByName("FEM_PostCreateFunctions"); + if (cmd && cmd->getAction()) + cmd->getAction()->addTo(ui->CreateButton); + ui->CreateButton->setPopupMode(QToolButton::InstantPopup); + //load the default values - updateEnumerationList(getTypedObject()->Scalars, ui->Scalar); + ui->CutCells->setChecked( + static_cast(getObject())->CutCells.getValue()); ui->InsideOut->setChecked( - static_cast(getObject())->InsideOut.getValue()); - App::PropertyFloatConstraint& scalar_prop = - static_cast(getObject())->Value; - double scalar_factor = scalar_prop.getValue(); - - // set spinbox scalar_factor, don't forget to sync the slider - ui->Value->blockSignals(true); - ui->Value->setValue(scalar_factor); - ui->Value->blockSignals(false); - - // sync the slider - // slider min = 0%, slider max = 100% - // - // scalar_factor - // slider_value = --------------- x 100 - // max - // - double max = scalar_prop.getConstraints()->UpperBound; - int slider_value = (scalar_factor / max) * 100.; - ui->Slider->blockSignals(true); - ui->Slider->setValue(slider_value); - ui->Slider->blockSignals(false); - Base::Console().Log( - "init: scalar_factor, slider_value: %f, %i: \n", scalar_factor, slider_value); + static_cast(getObject())->InsideOut.getValue()); } -TaskPostScalarClip::~TaskPostScalarClip() { +TaskPostClip::~TaskPostClip() +{} +void TaskPostClip::applyPythonCode() +{} + +void TaskPostClip::collectImplicitFunctions() +{ + std::vector pipelines; + pipelines = getDocument()->getObjectsOfType(); + if (!pipelines.empty()) { + Fem::FemPostPipeline* pipeline = pipelines.front(); + if (pipeline->Functions.getValue() && pipeline->Functions.getValue()->getTypeId() + == Fem::FemPostFunctionProvider::getClassTypeId()) { + + ui->FunctionBox->clear(); + QStringList items; + std::size_t currentItem = 0; + App::DocumentObject* currentFunction = + static_cast(getObject())->Function.getValue(); + const std::vector& funcs = + static_cast(pipeline->Functions.getValue()) + ->Functions.getValues(); + for (std::size_t i = 0; i < funcs.size(); ++i) { + items.push_back(QString::fromLatin1(funcs[i]->getNameInDocument())); + if (currentFunction == funcs[i]) + currentItem = i; + } + ui->FunctionBox->addItems(items); + ui->FunctionBox->setCurrentIndex(currentItem); + } + } } -void TaskPostScalarClip::applyPythonCode() { +void TaskPostClip::on_CreateButton_triggered(QAction*) +{ + int numFuncs = ui->FunctionBox->count(); + int currentItem = ui->FunctionBox->currentIndex(); + collectImplicitFunctions(); -} + // if a new function was successfully added use it + int indexCount = ui->FunctionBox->count(); + if (indexCount > currentItem + 1) + ui->FunctionBox->setCurrentIndex(indexCount - 1); -void TaskPostScalarClip::on_Scalar_currentIndexChanged(int idx) { + // When the first function ever was added, a signal must be emitted + if (numFuncs == 0) { + Q_EMIT emitAddedFunction(); + } - static_cast(getObject())->Scalars.setValue(idx); recompute(); - - // update constraints and values - App::PropertyFloatConstraint& scalar_prop = - static_cast(getObject())->Value; - double scalar_factor = scalar_prop.getValue(); - double min = scalar_prop.getConstraints()->LowerBound; - double max = scalar_prop.getConstraints()->UpperBound; - - ui->Maximum->setText(QString::number(min)); - ui->Minimum->setText(QString::number(max)); - - // set scalar_factor, don't forget to sync the slider - ui->Value->blockSignals(true); - ui->Value->setValue(scalar_factor); - ui->Value->blockSignals(false); - - // sync the slider - ui->Slider->blockSignals(true); - int slider_value = (scalar_factor / max) * 100.; - ui->Slider->setValue(slider_value); - ui->Slider->blockSignals(false); } -void TaskPostScalarClip::on_Slider_valueChanged(int v) { +void TaskPostClip::on_FunctionBox_currentIndexChanged(int idx) +{ + //set the correct property + std::vector pipelines; + pipelines = getDocument()->getObjectsOfType(); + if (!pipelines.empty()) { + Fem::FemPostPipeline* pipeline = pipelines.front(); + if (pipeline->Functions.getValue() && pipeline->Functions.getValue()->getTypeId() + == Fem::FemPostFunctionProvider::getClassTypeId()) { - App::PropertyFloatConstraint& value = - static_cast(getObject())->Value; - double val = value.getConstraints()->LowerBound * (1 - double(v) / 100.) - + double(v) / 100. * value.getConstraints()->UpperBound; + const std::vector& funcs = + static_cast(pipeline->Functions.getValue()) + ->Functions.getValues(); + if (idx >= 0) + static_cast(getObject())->Function.setValue(funcs[idx]); + else + static_cast(getObject())->Function.setValue(nullptr); + } + } - value.setValue(val); + //load the correct view + Fem::FemPostFunction* fobj = static_cast( + static_cast(getObject())->Function.getValue()); + Gui::ViewProvider* view = nullptr; + if (fobj) + view = Gui::Application::Instance->getViewProvider(fobj); + + if (fwidget) + fwidget->deleteLater(); + + if (view) { + fwidget = static_cast(view)->createControlWidget(); + fwidget->setParent(ui->Container); + fwidget->setViewProvider(static_cast(view)); + ui->Container->layout()->addWidget(fwidget); + } recompute(); - - //don't forget to sync the spinbox - ui->Value->blockSignals(true); - ui->Value->setValue(val); - ui->Value->blockSignals(false); } -void TaskPostScalarClip::on_Value_valueChanged(double v) { - - App::PropertyFloatConstraint& value = - static_cast(getObject())->Value; - value.setValue(v); +void TaskPostClip::on_CutCells_toggled(bool val) +{ + static_cast(getObject())->CutCells.setValue(val); recompute(); - - //don't forget to sync the slider - ui->Slider->blockSignals(true); - ui->Slider->setValue( - int(((v - value.getConstraints()->LowerBound) - / (value.getConstraints()->UpperBound - value.getConstraints()->LowerBound)) - * 100.)); - ui->Slider->blockSignals(false); } -void TaskPostScalarClip::on_InsideOut_toggled(bool val) { - - static_cast(getObject())->InsideOut.setValue(val); +void TaskPostClip::on_InsideOut_toggled(bool val) +{ + static_cast(getObject())->InsideOut.setValue(val); recompute(); } // *************************************************************************** -// warp filter -// spinbox min, slider, spinbox max -// spinbox warp factor -TaskPostWarpVector::TaskPostWarpVector(ViewProviderDocumentObject* view, QWidget* parent) - : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"), tr("Warp options"), - parent), - ui(new Ui_TaskPostWarpVector) +// contours filter +TaskPostContours::TaskPostContours(ViewProviderDocumentObject* view, QWidget* parent) + : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterContours"), + tr("Contours filter options"), parent), + ui(new Ui_TaskPostContours) { + assert(view->isDerivedFrom(ViewProviderFemPostContours::getClassTypeId())); - assert(view->isDerivedFrom(ViewProviderFemPostWarpVector::getClassTypeId())); - - // we load the views widget + // load the views widget proxy = new QWidget(this); ui->setupUi(proxy); QMetaObject::connectSlotsByName(this); this->groupLayout()->addWidget(proxy); - // load the default values for warp display - updateEnumerationList(getTypedObject()->Vector, ui->Vector); - double warp_factor = static_cast(getObject()) - ->Factor.getValue();// get the standard warp factor - - // set spinbox warp_factor, don't forget to sync the slider - ui->Value->blockSignals(true); - ui->Value->setValue(warp_factor); - ui->Value->blockSignals(false); - - // set min and max, don't forget to sync the slider - // TODO if warp is set to standard 1.0, find a smarter way for standard min, max and warp_factor - // may be depend on grid boundbox and min max vector values - ui->Max->blockSignals(true); - ui->Max->setValue(warp_factor == 0 ? 1 : warp_factor * 10.); - ui->Max->blockSignals(false); - ui->Min->blockSignals(true); - ui->Min->setValue(warp_factor == 0 ? 0 : warp_factor / 10.); - ui->Min->blockSignals(false); - - // sync slider - ui->Slider->blockSignals(true); - // slider min = 0%, slider max = 100% - // - // ( warp_factor - min ) - // slider_value = ----------------------- x 100 - // ( max - min ) - // - int slider_value = - (warp_factor - ui->Min->value()) / (ui->Max->value() - ui->Min->value()) * 100.; - ui->Slider->setValue(slider_value); - ui->Slider->blockSignals(false); - Base::Console().Log("init: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); -} - -TaskPostWarpVector::~TaskPostWarpVector() { - -} - -void TaskPostWarpVector::applyPythonCode() { - -} - -void TaskPostWarpVector::on_Vector_currentIndexChanged(int idx) { - // combobox to choose the result to warp - - static_cast(getObject())->Vector.setValue(idx); - recompute(); -} - -void TaskPostWarpVector::on_Slider_valueChanged(int slider_value) { - // slider changed, change warp factor and sync spinbox - - // - // ( max - min ) - // warp_factor = min + ( slider_value x --------------- ) - // 100 - // - double warp_factor = - ui->Min->value() + ((ui->Max->value() - ui->Min->value()) / 100.) * slider_value; - static_cast(getObject())->Factor.setValue(warp_factor); - recompute(); - - // sync the spinbox - ui->Value->blockSignals(true); - ui->Value->setValue(warp_factor); - ui->Value->blockSignals(false); - Base::Console().Log("Change: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); -} - -void TaskPostWarpVector::on_Value_valueChanged(double warp_factor) { - // spinbox changed, change warp factor and sync slider - - // TODO warp factor should not be smaller than min and greater than max, - // but problems on automate change of warp_factor, see on_Max_valueChanged - - static_cast(getObject())->Factor.setValue(warp_factor); - recompute(); - - // sync the slider, see above for formula - ui->Slider->blockSignals(true); - int slider_value = - (warp_factor - ui->Min->value()) / (ui->Max->value() - ui->Min->value()) * 100.; - ui->Slider->setValue(slider_value); - ui->Slider->blockSignals(false); - Base::Console().Log("Change: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); -} - -void TaskPostWarpVector::on_Max_valueChanged(double) { - - // TODO max should be greater than min, see a few lines later on problem on input characters - ui->Slider->blockSignals(true); - ui->Slider->setValue((ui->Value->value() - ui->Min->value()) - / (ui->Max->value() - ui->Min->value()) * 100.); - ui->Slider->blockSignals(false); - - /* - * problem, if warp_factor is 2000 one would like to input 4000 as max, one starts to input 4 - * immediately the warp_factor is changed to 4 because 4 < 2000, but one has just input - * one character of their 4000. * I do not know how to solve this, but the code to set slider - * and spinbox is fine thus I leave it ... - * - * mhh it works if "apply changes to pipeline directly" button is deactivated, - * still it really confuses if the button is active. More investigation is needed. - * - // set warp factor to max, if warp factor > max - if (ui->Value->value() > ui->Max->value()) { - double warp_factor = ui->Max->value(); - static_cast(getObject())->Factor.setValue(warp_factor); - recompute(); - - // sync the slider, see above for formula - ui->Slider->blockSignals(true); - int slider_value = (warp_factor - ui->Min->value()) - / (ui->Max->value() - ui->Min->value()) * 100.; - ui->Slider->setValue(slider_value); - ui->Slider->blockSignals(false); - // sync the spinbox, see above for formula - ui->Value->blockSignals(true); - ui->Value->setValue(warp_factor); - ui->Value->blockSignals(false); - Base::Console().Log("Change: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); + // load filter settings + updateEnumerationList(getTypedObject()->Field, ui->fieldsCB); + updateEnumerationList(getTypedObject()->VectorMode, ui->vectorsCB); + // for a new filter, initialize the coloring + auto colorState = static_cast(getObject())->NoColor.getValue(); + if (!colorState && getTypedView()->Field.getValue() == 0) { + getTypedView()->Field.setValue(1); } - */ + + ui->numberContoursSB->setValue( + static_cast(getObject())->NumberOfContours.getValue()); + ui->noColorCB->setChecked(colorState); + + // connect + connect(ui->fieldsCB, + qOverload(&QComboBox::currentIndexChanged), + this, + &TaskPostContours::onFieldsChanged); + connect(ui->vectorsCB, + qOverload(&QComboBox::currentIndexChanged), + this, + &TaskPostContours::onVectorModeChanged); + connect(ui->numberContoursSB, + qOverload(&QSpinBox::valueChanged), + this, + &TaskPostContours::onNumberOfContoursChanged); + connect(ui->noColorCB, &QCheckBox::toggled, this, &TaskPostContours::onNoColorChanged); } -void TaskPostWarpVector::on_Min_valueChanged(double) { +TaskPostContours::~TaskPostContours() +{} - // TODO min should be smaller than max - // TODO if warp factor is smaller than min, warp factor should be min, don't forget to sync - ui->Slider->blockSignals(true); - ui->Slider->setValue((ui->Value->value() - ui->Min->value()) - / (ui->Max->value() - ui->Min->value()) * 100.); - ui->Slider->blockSignals(false); +void TaskPostContours::applyPythonCode() +{} + +void TaskPostContours::updateFields() +{ + // update the ViewProvider Field + // since the ViewProvider can have another field sorting, we cannot use the same index + if (!static_cast(getObject())->NoColor.getValue()) { + std::string objectField = + getTypedObject()->Field.getValueAsString(); + getTypedView()->Field.setValue(objectField.c_str()); + } + else { + getTypedView()->Field.setValue("None"); + } + +} + +void TaskPostContours::onFieldsChanged(int idx) +{ + static_cast(getObject())->Field.setValue(idx); + + blockVectorUpdate = true; + updateEnumerationList(getTypedObject()->VectorMode, ui->vectorsCB); + blockVectorUpdate = false; + + // In > 99 % of the cases the coloring should be equal to the field, + // thus change the coloring field too. Users can override this be resetting only the coloring + // field afterwards in the properties if really necessary. + updateFields(); + + // since a new field can be e.g. no vector while the previous one was, + // we must also update the VectorMode + if (!static_cast(getObject())->NoColor.getValue()) { + auto newMode = getTypedObject()->VectorMode.getValue(); + getTypedView()->VectorMode.setValue(newMode); + } +} + +void TaskPostContours::onVectorModeChanged(int idx) +{ + static_cast(getObject())->VectorMode.setValue(idx); + recompute(); + if (!blockVectorUpdate) { + // we can have the case that the previous field had VectorMode "Z" but + // since it is a 2D field, Z is eompty thus no field is available to color + // when the user noch goes back to e.g. "Y" we must set the Field + // first to get the possible VectorModes of that field + updateFields(); + // now we can set the VectorMode + if (!static_cast(getObject())->NoColor.getValue()) + getTypedView()->VectorMode.setValue(idx); + } +} + +void TaskPostContours::onNumberOfContoursChanged(int number) +{ + static_cast(getObject())->NumberOfContours.setValue(number); + recompute(); +} + +void TaskPostContours::onNoColorChanged(bool state) +{ + static_cast(getObject())->NoColor.setValue(state); + if (state) { + // no color + getTypedView()->Field.setValue(long(0)); + } + else { + // set same field + auto currentField = getTypedObject()->Field.getValue(); + // the ViewProvider field starts with an additional entry "None", + // therefore the desired new setting is idx + 1 + getTypedView()->Field.setValue(currentField + 1); + // set the VectorMode too + auto currentMode = getTypedObject()->VectorMode.getValue(); + getTypedView()->VectorMode.setValue(currentMode); + } + recompute(); } // *************************************************************************** -// function clip filter +// cut filter TaskPostCut::TaskPostCut(ViewProviderDocumentObject* view, App::PropertyLink* function, QWidget* parent) : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterCutFunction"), tr("Function cut, choose implicit function"), parent), ui(new Ui_TaskPostCut) { - assert(view->isDerivedFrom(ViewProviderFemPostCut::getClassTypeId())); assert(function); Q_UNUSED(function) - fwidget = nullptr; + fwidget = nullptr; //we load the views widget proxy = new QWidget(this); @@ -1605,16 +1483,14 @@ TaskPostCut::TaskPostCut(ViewProviderDocumentObject* view, App::PropertyLink* fu ui->CreateButton->setPopupMode(QToolButton::InstantPopup); } -TaskPostCut::~TaskPostCut() { +TaskPostCut::~TaskPostCut() +{} -} - -void TaskPostCut::applyPythonCode() { - -} - -void TaskPostCut::collectImplicitFunctions() { +void TaskPostCut::applyPythonCode() +{} +void TaskPostCut::collectImplicitFunctions() +{ std::vector pipelines; pipelines = getDocument()->getObjectsOfType(); if (!pipelines.empty()) { @@ -1642,8 +1518,8 @@ void TaskPostCut::collectImplicitFunctions() { } } -void TaskPostCut::on_CreateButton_triggered(QAction*) { - +void TaskPostCut::on_CreateButton_triggered(QAction*) +{ int numFuncs = ui->FunctionBox->count(); int currentItem = ui->FunctionBox->currentIndex(); collectImplicitFunctions(); @@ -1661,8 +1537,8 @@ void TaskPostCut::on_CreateButton_triggered(QAction*) { recompute(); } -void TaskPostCut::on_FunctionBox_currentIndexChanged(int idx) { - +void TaskPostCut::on_FunctionBox_currentIndexChanged(int idx) +{ //set the correct property std::vector pipelines; pipelines = getDocument()->getObjectsOfType(); @@ -1701,4 +1577,274 @@ void TaskPostCut::on_FunctionBox_currentIndexChanged(int idx) { recompute(); } + +// *************************************************************************** +// scalar clip filter +TaskPostScalarClip::TaskPostScalarClip(ViewProviderDocumentObject* view, QWidget* parent) + : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterClipScalar"), + tr("Scalar clip options"), parent), + ui(new Ui_TaskPostScalarClip) +{ + assert(view->isDerivedFrom(ViewProviderFemPostScalarClip::getClassTypeId())); + + //we load the views widget + proxy = new QWidget(this); + ui->setupUi(proxy); + QMetaObject::connectSlotsByName(this); + this->groupLayout()->addWidget(proxy); + + //load the default values + updateEnumerationList(getTypedObject()->Scalars, ui->Scalar); + ui->InsideOut->setChecked( + static_cast(getObject())->InsideOut.getValue()); + App::PropertyFloatConstraint& scalar_prop = + static_cast(getObject())->Value; + double scalar_factor = scalar_prop.getValue(); + + // set spinbox scalar_factor, don't forget to sync the slider + ui->Value->blockSignals(true); + ui->Value->setValue(scalar_factor); + ui->Value->blockSignals(false); + + // sync the slider + // slider min = 0%, slider max = 100% + // + // scalar_factor + // slider_value = --------------- x 100 + // max + // + double max = scalar_prop.getConstraints()->UpperBound; + int slider_value = (scalar_factor / max) * 100.; + ui->Slider->blockSignals(true); + ui->Slider->setValue(slider_value); + ui->Slider->blockSignals(false); + Base::Console().Log( + "init: scalar_factor, slider_value: %f, %i: \n", scalar_factor, slider_value); +} + +TaskPostScalarClip::~TaskPostScalarClip() +{} + +void TaskPostScalarClip::applyPythonCode() +{} + +void TaskPostScalarClip::on_Scalar_currentIndexChanged(int idx) +{ + static_cast(getObject())->Scalars.setValue(idx); + recompute(); + + // update constraints and values + App::PropertyFloatConstraint& scalar_prop = + static_cast(getObject())->Value; + double scalar_factor = scalar_prop.getValue(); + double min = scalar_prop.getConstraints()->LowerBound; + double max = scalar_prop.getConstraints()->UpperBound; + + ui->Maximum->setText(QString::number(min)); + ui->Minimum->setText(QString::number(max)); + + // set scalar_factor, don't forget to sync the slider + ui->Value->blockSignals(true); + ui->Value->setValue(scalar_factor); + ui->Value->blockSignals(false); + + // sync the slider + ui->Slider->blockSignals(true); + int slider_value = (scalar_factor / max) * 100.; + ui->Slider->setValue(slider_value); + ui->Slider->blockSignals(false); +} + +void TaskPostScalarClip::on_Slider_valueChanged(int v) +{ + App::PropertyFloatConstraint& value = + static_cast(getObject())->Value; + double val = value.getConstraints()->LowerBound * (1 - double(v) / 100.) + + double(v) / 100. * value.getConstraints()->UpperBound; + + value.setValue(val); + recompute(); + + //don't forget to sync the spinbox + ui->Value->blockSignals(true); + ui->Value->setValue(val); + ui->Value->blockSignals(false); +} + +void TaskPostScalarClip::on_Value_valueChanged(double v) +{ + App::PropertyFloatConstraint& value = + static_cast(getObject())->Value; + value.setValue(v); + recompute(); + + //don't forget to sync the slider + ui->Slider->blockSignals(true); + ui->Slider->setValue( + int(((v - value.getConstraints()->LowerBound) + / (value.getConstraints()->UpperBound - value.getConstraints()->LowerBound)) + * 100.)); + ui->Slider->blockSignals(false); +} + +void TaskPostScalarClip::on_InsideOut_toggled(bool val) +{ + static_cast(getObject())->InsideOut.setValue(val); + recompute(); +} + + +// *************************************************************************** +// warp vector filter +TaskPostWarpVector::TaskPostWarpVector(ViewProviderDocumentObject* view, QWidget* parent) + : TaskPostBox(view, Gui::BitmapFactory().pixmap("FEM_PostFilterWarp"), tr("Warp options"), + parent), + ui(new Ui_TaskPostWarpVector) +{ + assert(view->isDerivedFrom(ViewProviderFemPostWarpVector::getClassTypeId())); + + // we load the views widget + proxy = new QWidget(this); + ui->setupUi(proxy); + QMetaObject::connectSlotsByName(this); + this->groupLayout()->addWidget(proxy); + + // load the default values for warp display + updateEnumerationList(getTypedObject()->Vector, ui->Vector); + double warp_factor = static_cast(getObject()) + ->Factor.getValue();// get the standard warp factor + + // set spinbox warp_factor, don't forget to sync the slider + ui->Value->blockSignals(true); + ui->Value->setValue(warp_factor); + ui->Value->blockSignals(false); + + // set min and max, don't forget to sync the slider + // TODO if warp is set to standard 1.0, find a smarter way for standard min, max + // and warp_factor may be depend on grid boundbox and min max vector values + ui->Max->blockSignals(true); + ui->Max->setValue(warp_factor == 0 ? 1 : warp_factor * 10.); + ui->Max->blockSignals(false); + ui->Min->blockSignals(true); + ui->Min->setValue(warp_factor == 0 ? 0 : warp_factor / 10.); + ui->Min->blockSignals(false); + + // sync slider + ui->Slider->blockSignals(true); + // slider min = 0%, slider max = 100% + // + // ( warp_factor - min ) + // slider_value = ----------------------- x 100 + // ( max - min ) + // + int slider_value = + (warp_factor - ui->Min->value()) / (ui->Max->value() - ui->Min->value()) * 100.; + ui->Slider->setValue(slider_value); + ui->Slider->blockSignals(false); + Base::Console().Log("init: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); +} + +TaskPostWarpVector::~TaskPostWarpVector() +{} + +void TaskPostWarpVector::applyPythonCode() +{} + +void TaskPostWarpVector::on_Vector_currentIndexChanged(int idx) +{ + // combobox to choose the result to warp + + static_cast(getObject())->Vector.setValue(idx); + recompute(); +} + +void TaskPostWarpVector::on_Slider_valueChanged(int slider_value) +{ + // slider changed, change warp factor and sync spinbox + + // + // ( max - min ) + // warp_factor = min + ( slider_value x --------------- ) + // 100 + // + double warp_factor = + ui->Min->value() + ((ui->Max->value() - ui->Min->value()) / 100.) * slider_value; + static_cast(getObject())->Factor.setValue(warp_factor); + recompute(); + + // sync the spinbox + ui->Value->blockSignals(true); + ui->Value->setValue(warp_factor); + ui->Value->blockSignals(false); + Base::Console().Log("Change: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); +} + +void TaskPostWarpVector::on_Value_valueChanged(double warp_factor) +{ + // spinbox changed, change warp factor and sync slider + + // TODO warp factor should not be smaller than min and greater than max, + // but problems on automate change of warp_factor, see on_Max_valueChanged + + static_cast(getObject())->Factor.setValue(warp_factor); + recompute(); + + // sync the slider, see above for formula + ui->Slider->blockSignals(true); + int slider_value = + (warp_factor - ui->Min->value()) / (ui->Max->value() - ui->Min->value()) * 100.; + ui->Slider->setValue(slider_value); + ui->Slider->blockSignals(false); + Base::Console().Log("Change: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); +} + +void TaskPostWarpVector::on_Max_valueChanged(double) +{ + // TODO max should be greater than min, see a few lines later on problem on input characters + ui->Slider->blockSignals(true); + ui->Slider->setValue((ui->Value->value() - ui->Min->value()) + / (ui->Max->value() - ui->Min->value()) * 100.); + ui->Slider->blockSignals(false); + + /* + * problem, if warp_factor is 2000 one would like to input 4000 as max, one starts to input 4 + * immediately the warp_factor is changed to 4 because 4 < 2000, but one has just input + * one character of their 4000. * I do not know how to solve this, but the code to set slider + * and spinbox is fine thus I leave it ... + * + * mhh it works if "apply changes to pipeline directly" button is deactivated, + * still it really confuses if the button is active. More investigation is needed. + * + // set warp factor to max, if warp factor > max + if (ui->Value->value() > ui->Max->value()) { + double warp_factor = ui->Max->value(); + static_cast(getObject())->Factor.setValue(warp_factor); + recompute(); + + // sync the slider, see above for formula + ui->Slider->blockSignals(true); + int slider_value = (warp_factor - ui->Min->value()) + / (ui->Max->value() - ui->Min->value()) * 100.; + ui->Slider->setValue(slider_value); + ui->Slider->blockSignals(false); + // sync the spinbox, see above for formula + ui->Value->blockSignals(true); + ui->Value->setValue(warp_factor); + ui->Value->blockSignals(false); + Base::Console().Log("Change: warp_factor, slider_value: %f, %i: \n", warp_factor, slider_value); + } + */ +} + +void TaskPostWarpVector::on_Min_valueChanged(double) +{ + // TODO min should be smaller than max + // TODO if warp factor is smaller than min, warp factor should be min, don't forget to sync + ui->Slider->blockSignals(true); + ui->Slider->setValue((ui->Value->value() - ui->Min->value()) + / (ui->Max->value() - ui->Min->value()) * 100.); + ui->Slider->blockSignals(false); +} + + #include "moc_TaskPostBoxes.cpp" diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.h b/src/Mod/Fem/Gui/TaskPostBoxes.h index 9d74b2903d..d046975a77 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.h +++ b/src/Mod/Fem/Gui/TaskPostBoxes.h @@ -34,6 +34,7 @@ class QComboBox; class Ui_TaskPostDisplay; class Ui_TaskPostClip; +class Ui_TaskPostContours; class Ui_TaskPostDataAlongLine; class Ui_TaskPostDataAtPoint; class Ui_TaskPostScalarClip; @@ -50,8 +51,11 @@ class SoEventCallback; class SoMarkerSet; -namespace FemGui { +namespace FemGui +{ +// *************************************************************************** +// point marker class ViewProviderPointMarker; class PointMarker : public QObject { @@ -77,7 +81,6 @@ private: std::string ObjectInvisible(); }; - class FemGuiExport ViewProviderPointMarker : public Gui::ViewProviderDocumentObject { PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderPointMarker); @@ -92,6 +95,8 @@ protected: }; +// *************************************************************************** +// data marker class ViewProviderDataMarker; class DataMarker : public QObject { @@ -132,8 +137,11 @@ protected: friend class DataMarker; }; -class TaskPostBox : public Gui::TaskView::TaskBox { +// *************************************************************************** +// main task dialog +class TaskPostBox : public Gui::TaskView::TaskBox +{ Q_OBJECT public: @@ -175,7 +183,8 @@ private: }; -/// simulation dialog for the TaskView +// *************************************************************************** +// simulation dialog for the TaskView class TaskDlgPost : public Gui::TaskView::TaskDialog { Q_OBJECT @@ -216,6 +225,8 @@ protected: }; +// *************************************************************************** +// box to set the coloring class TaskPostDisplay : public TaskPostBox { Q_OBJECT @@ -240,8 +251,10 @@ private: }; -class TaskPostFunction : public TaskPostBox { - +// *************************************************************************** +// functions +class TaskPostFunction : public TaskPostBox +{ Q_OBJECT public: @@ -252,8 +265,76 @@ public: }; -class TaskPostClip : public TaskPostBox { +// *************************************************************************** +// in the following, the different filters sorted alphabetically +// *************************************************************************** + +// *************************************************************************** +// data along line filter +class TaskPostDataAlongLine: public TaskPostBox +{ + Q_OBJECT + +public: + explicit TaskPostDataAlongLine(Gui::ViewProviderDocumentObject* view, + QWidget* parent = nullptr); + ~TaskPostDataAlongLine() override; + + void applyPythonCode() override; + static void pointCallback(void* ud, SoEventCallback* n); + +private Q_SLOTS: + void on_SelectPoints_clicked(); + void on_CreatePlot_clicked(); + void on_Representation_activated(int i); + void on_Field_activated(int i); + void on_VectorMode_activated(int i); + void point2Changed(double); + void point1Changed(double); + void resolutionChanged(int val); + void onChange(double x1, double y1, double z1, double x2, double y2, double z2); + +private: + std::string Plot(); + std::string ObjectVisible(); + QWidget* proxy; + std::unique_ptr ui; +}; + + +// *************************************************************************** +// data at point filter +class TaskPostDataAtPoint: public TaskPostBox +{ + Q_OBJECT + +public: + explicit TaskPostDataAtPoint(Gui::ViewProviderDocumentObject* view, QWidget* parent = nullptr); + ~TaskPostDataAtPoint() override; + + void applyPythonCode() override; + static void pointCallback(void* ud, SoEventCallback* n); + +private Q_SLOTS: + void on_SelectPoint_clicked(); + void on_Field_activated(int i); + void centerChanged(double); + void onChange(double x, double y, double z); + +private: + std::string toString(double val) const; + void showValue(double value, const char* unit); + std::string ObjectVisible(); + QWidget* proxy; + std::unique_ptr ui; +}; + + +// *************************************************************************** +// clip filter +class TaskPostClip : public TaskPostBox +{ Q_OBJECT public: @@ -282,69 +363,66 @@ private: }; -class TaskPostDataAlongLine: public TaskPostBox { - +// *************************************************************************** +// contours filter +class TaskPostContours: public TaskPostBox +{ Q_OBJECT public: - explicit TaskPostDataAlongLine(Gui::ViewProviderDocumentObject* view, - QWidget* parent = nullptr); - ~TaskPostDataAlongLine() override; + explicit TaskPostContours(Gui::ViewProviderDocumentObject* view, QWidget* parent = nullptr); + ~TaskPostContours() override; void applyPythonCode() override; - static void pointCallback(void * ud, SoEventCallback * n); private Q_SLOTS: - void on_SelectPoints_clicked(); - void on_CreatePlot_clicked(); - void on_Representation_activated(int i); - void on_Field_activated(int i); - void on_VectorMode_activated(int i); - void point2Changed(double); - void point1Changed(double); - void resolutionChanged(int val); - void onChange(double x1, double y1, double z1, double x2, double y2, double z2); - + void onFieldsChanged(int idx); + void onVectorModeChanged(int idx); + void onNumberOfContoursChanged(int number); + void onNoColorChanged(bool state); private: - std::string Plot(); - std::string ObjectVisible(); QWidget* proxy; - std::unique_ptr ui; + std::unique_ptr ui; + bool blockVectorUpdate = false; + void updateFields(); }; -class TaskPostDataAtPoint: public TaskPostBox { - +// *************************************************************************** +// cut filter +class TaskPostCut: public TaskPostBox +{ Q_OBJECT public: - explicit TaskPostDataAtPoint(Gui::ViewProviderDocumentObject* view, QWidget* parent = nullptr); - ~TaskPostDataAtPoint() override; + TaskPostCut(Gui::ViewProviderDocumentObject* view, App::PropertyLink* function, + QWidget* parent = nullptr); + ~TaskPostCut() override; void applyPythonCode() override; - static void pointCallback(void * ud, SoEventCallback * n); private Q_SLOTS: - void on_SelectPoint_clicked(); - void on_Field_activated(int i); - void centerChanged(double); - void onChange(double x, double y, double z); + void on_CreateButton_triggered(QAction*); + void on_FunctionBox_currentIndexChanged(int idx); + +Q_SIGNALS: + void emitAddedFunction(); private: - std::string toString(double val) const; - void showValue(double value, const char* unit); + void collectImplicitFunctions(); - -private: - std::string ObjectVisible(); + //App::PropertyLink* m_functionProperty; QWidget* proxy; - std::unique_ptr ui; + std::unique_ptr ui; + FunctionWidget* fwidget; }; -class TaskPostScalarClip : public TaskPostBox { - +// *************************************************************************** +// scalar clip filter +class TaskPostScalarClip : public TaskPostBox +{ Q_OBJECT public: @@ -365,8 +443,10 @@ private: }; -class TaskPostWarpVector : public TaskPostBox { - +// *************************************************************************** +// warp vector filter +class TaskPostWarpVector : public TaskPostBox +{ Q_OBJECT public: @@ -387,34 +467,6 @@ private: std::unique_ptr ui; }; - -class TaskPostCut : public TaskPostBox { - - Q_OBJECT - -public: - TaskPostCut(Gui::ViewProviderDocumentObject* view, App::PropertyLink* function, - QWidget* parent = nullptr); - ~TaskPostCut() override; - - void applyPythonCode() override; - -private Q_SLOTS: - void on_CreateButton_triggered(QAction*); - void on_FunctionBox_currentIndexChanged(int idx); - -Q_SIGNALS: - void emitAddedFunction(); - -private: - void collectImplicitFunctions(); - - //App::PropertyLink* m_functionProperty; - QWidget* proxy; - std::unique_ptr ui; - FunctionWidget* fwidget; -}; - } //namespace FemGui #endif // GUI_TASKVIEW_TaskPostDisplay_H diff --git a/src/Mod/Fem/Gui/TaskPostContours.ui b/src/Mod/Fem/Gui/TaskPostContours.ui new file mode 100644 index 0000000000..9e62a3466e --- /dev/null +++ b/src/Mod/Fem/Gui/TaskPostContours.ui @@ -0,0 +1,90 @@ + + + TaskPostContours + + + + 0 + 0 + 250 + 115 + + + + Form + + + + + + + + + + + Vector: + + + + + + + + + + Field: + + + + + + + + 0 + 0 + + + + Number of contours: + + + + + + + + 40 + 0 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + 1 + + + 1000 + + + + + + + + + Contour lines will not be colored + + + No color + + + + + + + + diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp index cc9ce29893..a8803ee909 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.cpp @@ -30,6 +30,63 @@ using namespace FemGui; +// *************************************************************************** +// in the following, the different filters sorted alphabetically +// *************************************************************************** + + +// *************************************************************************** +// data along line filter +PROPERTY_SOURCE(FemGui::ViewProviderFemPostDataAlongLine, FemGui::ViewProviderFemPostObject) + +ViewProviderFemPostDataAlongLine::ViewProviderFemPostDataAlongLine() +{ + sPixmap = "FEM_PostFilterDataAlongLine"; +} + +ViewProviderFemPostDataAlongLine::~ViewProviderFemPostDataAlongLine() +{} + +void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) +{ + //add the function box + dlg->appendBox(new TaskPostDataAlongLine(dlg->getView())); +} + + +// *************************************************************************** +// data at point filter +PROPERTY_SOURCE(FemGui::ViewProviderFemPostDataAtPoint, FemGui::ViewProviderFemPostObject) + +ViewProviderFemPostDataAtPoint::ViewProviderFemPostDataAtPoint() +{ + sPixmap = "FEM_PostFilterDataAtPoint"; +} + +void ViewProviderFemPostDataAtPoint::show() +{ + Gui::ViewProviderDocumentObject::show(); +} + +void ViewProviderFemPostDataAtPoint::onSelectionChanged(const Gui::SelectionChanges&) +{ + // do not do anything here + // For DataAtPoint the color bar must not be refreshed when it is selected + // because a single point does not make sense with a color range. +} + +ViewProviderFemPostDataAtPoint::~ViewProviderFemPostDataAtPoint() +{} + +void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) +{ + //add the function box + dlg->appendBox(new TaskPostDataAtPoint(dlg->getView())); +} + + +// *************************************************************************** +// clip filter PROPERTY_SOURCE(FemGui::ViewProviderFemPostClip, FemGui::ViewProviderFemPostObject) ViewProviderFemPostClip::ViewProviderFemPostClip() { @@ -37,9 +94,8 @@ ViewProviderFemPostClip::ViewProviderFemPostClip() { sPixmap = "FEM_PostFilterClipRegion"; } -ViewProviderFemPostClip::~ViewProviderFemPostClip() { - -} +ViewProviderFemPostClip::~ViewProviderFemPostClip() +{} void ViewProviderFemPostClip::setupTaskDialog(TaskDlgPost* dlg) { @@ -51,59 +107,56 @@ void ViewProviderFemPostClip::setupTaskDialog(TaskDlgPost* dlg) { FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); } -PROPERTY_SOURCE(FemGui::ViewProviderFemPostDataAlongLine, FemGui::ViewProviderFemPostObject) -ViewProviderFemPostDataAlongLine::ViewProviderFemPostDataAlongLine() { +// *************************************************************************** +// contours filter +PROPERTY_SOURCE(FemGui::ViewProviderFemPostContours, FemGui::ViewProviderFemPostObject) - sPixmap = "FEM_PostFilterDataAlongLine"; -} - -ViewProviderFemPostDataAlongLine::~ViewProviderFemPostDataAlongLine() { - -} - -void ViewProviderFemPostDataAlongLine::setupTaskDialog(TaskDlgPost* dlg) { - - //add the function box - dlg->appendBox(new TaskPostDataAlongLine(dlg->getView())); - -} - -PROPERTY_SOURCE(FemGui::ViewProviderFemPostDataAtPoint, FemGui::ViewProviderFemPostObject) - -ViewProviderFemPostDataAtPoint::ViewProviderFemPostDataAtPoint() { - - sPixmap = "FEM_PostFilterDataAtPoint"; -} - -void ViewProviderFemPostDataAtPoint::show() +ViewProviderFemPostContours::ViewProviderFemPostContours() { - Gui::ViewProviderDocumentObject::show(); + sPixmap = "FEM_PostFilterContours"; } -void ViewProviderFemPostDataAtPoint::onSelectionChanged(const Gui::SelectionChanges &) +ViewProviderFemPostContours::~ViewProviderFemPostContours() +{} + +void ViewProviderFemPostContours::setupTaskDialog(TaskDlgPost* dlg) { - // do not do anything here - // For DataAtPoint the color bar must not be refreshed when it is selected - // because a single point does not make sense with a color range. + // the filter-specific task panel + dlg->appendBox(new TaskPostContours(dlg->getView())); } -ViewProviderFemPostDataAtPoint::~ViewProviderFemPostDataAtPoint() { +// *************************************************************************** +// cut filter +PROPERTY_SOURCE(FemGui::ViewProviderFemPostCut, FemGui::ViewProviderFemPostObject) + +ViewProviderFemPostCut::ViewProviderFemPostCut() +{ + sPixmap = "FEM_PostFilterCutFunction"; } -void ViewProviderFemPostDataAtPoint::setupTaskDialog(TaskDlgPost* dlg) { +ViewProviderFemPostCut::~ViewProviderFemPostCut() +{} +void ViewProviderFemPostCut::setupTaskDialog(TaskDlgPost* dlg) +{ //add the function box - dlg->appendBox(new TaskPostDataAtPoint(dlg->getView())); + dlg->appendBox(new TaskPostCut( + dlg->getView(), + &static_cast(dlg->getView()->getObject())->Function)); + //add the display options + FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); } +// *************************************************************************** +// scalar clip filter PROPERTY_SOURCE(FemGui::ViewProviderFemPostScalarClip, FemGui::ViewProviderFemPostObject) -ViewProviderFemPostScalarClip::ViewProviderFemPostScalarClip() { - +ViewProviderFemPostScalarClip::ViewProviderFemPostScalarClip() +{ sPixmap = "FEM_PostFilterClipScalar"; } @@ -111,8 +164,8 @@ ViewProviderFemPostScalarClip::~ViewProviderFemPostScalarClip() { } -void ViewProviderFemPostScalarClip::setupTaskDialog(TaskDlgPost* dlg) { - +void ViewProviderFemPostScalarClip::setupTaskDialog(TaskDlgPost* dlg) +{ //add the function box dlg->appendBox(new TaskPostScalarClip(dlg->getView())); @@ -120,44 +173,24 @@ void ViewProviderFemPostScalarClip::setupTaskDialog(TaskDlgPost* dlg) { FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); } + +// *************************************************************************** +// warp vector filter PROPERTY_SOURCE(FemGui::ViewProviderFemPostWarpVector, FemGui::ViewProviderFemPostObject) -ViewProviderFemPostWarpVector::ViewProviderFemPostWarpVector() { - +ViewProviderFemPostWarpVector::ViewProviderFemPostWarpVector() +{ sPixmap = "FEM_PostFilterWarp"; } -ViewProviderFemPostWarpVector::~ViewProviderFemPostWarpVector() { - -} - -void ViewProviderFemPostWarpVector::setupTaskDialog(TaskDlgPost* dlg) { +ViewProviderFemPostWarpVector::~ViewProviderFemPostWarpVector() +{} +void ViewProviderFemPostWarpVector::setupTaskDialog(TaskDlgPost* dlg) +{ //add the function box dlg->appendBox(new TaskPostWarpVector(dlg->getView())); //add the display options FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); } - - -PROPERTY_SOURCE(FemGui::ViewProviderFemPostCut, FemGui::ViewProviderFemPostObject) - -ViewProviderFemPostCut::ViewProviderFemPostCut() { - - sPixmap = "FEM_PostFilterCutFunction"; -} - -ViewProviderFemPostCut::~ViewProviderFemPostCut() { - -} - -void ViewProviderFemPostCut::setupTaskDialog(TaskDlgPost* dlg) { - - //add the function box - dlg->appendBox(new TaskPostCut(dlg->getView(), - &static_cast(dlg->getView()->getObject())->Function)); - - //add the display options - FemGui::ViewProviderFemPostObject::setupTaskDialog(dlg); -} diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h index 7681bded57..33f326a965 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostFilter.h @@ -20,30 +20,24 @@ * * ***************************************************************************/ - #ifndef FEM_VIEWPROVIDERFEMPOSTFILTER_H #define FEM_VIEWPROVIDERFEMPOSTFILTER_H #include "ViewProviderFemPostObject.h" + namespace FemGui { -class FemGuiExport ViewProviderFemPostClip : public ViewProviderFemPostObject { +// *************************************************************************** +// in the following, the different filters sorted alphabetically +// *************************************************************************** - PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostClip); - -public: - /// constructor. - ViewProviderFemPostClip(); - ~ViewProviderFemPostClip() override; - -protected: - void setupTaskDialog(TaskDlgPost* dlg) override; -}; - -class FemGuiExport ViewProviderFemPostDataAlongLine : public ViewProviderFemPostObject { +// *************************************************************************** +// data along line filter +class FemGuiExport ViewProviderFemPostDataAlongLine : public ViewProviderFemPostObject +{ PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostDataAlongLine); public: @@ -55,8 +49,11 @@ protected: void setupTaskDialog(TaskDlgPost* dlg) override; }; -class FemGuiExport ViewProviderFemPostDataAtPoint: public ViewProviderFemPostObject { +// *************************************************************************** +// data at point filter +class FemGuiExport ViewProviderFemPostDataAtPoint: public ViewProviderFemPostObject +{ PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostDataAtPoint); public: @@ -69,34 +66,44 @@ public: protected: void setupTaskDialog(TaskDlgPost* dlg) override; }; -class FemGuiExport ViewProviderFemPostScalarClip : public ViewProviderFemPostObject { - PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostScalarClip); + +// *************************************************************************** +// clip filter +class FemGuiExport ViewProviderFemPostClip: public ViewProviderFemPostObject +{ + PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostClip); public: /// constructor. - ViewProviderFemPostScalarClip(); - ~ViewProviderFemPostScalarClip() override; + ViewProviderFemPostClip(); + ~ViewProviderFemPostClip() override; protected: void setupTaskDialog(TaskDlgPost* dlg) override; }; -class FemGuiExport ViewProviderFemPostWarpVector : public ViewProviderFemPostObject { - PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostWarpVector); +// *************************************************************************** +// contours filter +class FemGuiExport ViewProviderFemPostContours: public ViewProviderFemPostObject +{ + PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostContours); public: /// constructor. - ViewProviderFemPostWarpVector(); - ~ViewProviderFemPostWarpVector() override; + ViewProviderFemPostContours(); + ~ViewProviderFemPostContours() override; protected: void setupTaskDialog(TaskDlgPost* dlg) override; }; -class FemGuiExport ViewProviderFemPostCut : public ViewProviderFemPostObject { +// *************************************************************************** +// cut filter +class FemGuiExport ViewProviderFemPostCut: public ViewProviderFemPostObject +{ PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostCut); public: @@ -108,6 +115,38 @@ protected: void setupTaskDialog(TaskDlgPost* dlg) override; }; + +// *************************************************************************** +// scalar clip filter +class FemGuiExport ViewProviderFemPostScalarClip: public ViewProviderFemPostObject +{ + PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostScalarClip); + +public: + /// constructor. + ViewProviderFemPostScalarClip(); + ~ViewProviderFemPostScalarClip() override; + +protected: + void setupTaskDialog(TaskDlgPost* dlg) override; +}; + + +// *************************************************************************** +// warp vector filter +class FemGuiExport ViewProviderFemPostWarpVector : public ViewProviderFemPostObject +{ + PROPERTY_HEADER_WITH_OVERRIDE(FemGui::ViewProviderFemPostWarpVector); + +public: + /// constructor. + ViewProviderFemPostWarpVector(); + ~ViewProviderFemPostWarpVector() override; + +protected: + void setupTaskDialog(TaskDlgPost* dlg) override; +}; + } //namespace FemGui diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 9b9ebfdc1b..d5bce46f92 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -38,6 +38,7 @@ # include # include # include +# include # include # include @@ -55,6 +56,8 @@ #include #include #include +#include +#include #include #include "ViewProviderFemPostObject.h" @@ -265,8 +268,8 @@ void ViewProviderFemPostObject::attach(App::DocumentObject* pcObj) (void)setupPipeline(); } -SoSeparator* ViewProviderFemPostObject::getFrontRoot() const { - +SoSeparator* ViewProviderFemPostObject::getFrontRoot() const +{ return m_colorRoot; } @@ -359,8 +362,9 @@ void ViewProviderFemPostObject::updateProperties() { colorArrays.emplace_back("Not a vector"); else { int array = Field.getValue() - 1; //0 is none - vtkPolyData* pd = m_currentAlgorithm->GetOutput(); - vtkDataArray* data = pd->GetPointData()->GetArray(array); + vtkDataArray* data = point->GetArray(array); + if (!data) + return; if (data->GetNumberOfComponents() == 1) colorArrays.emplace_back("Not a vector"); @@ -491,7 +495,6 @@ void ViewProviderFemPostObject::update3D() { void ViewProviderFemPostObject::WritePointData(vtkPoints* points, vtkDataArray* normals, vtkDataArray* tcoords) { - Q_UNUSED(tcoords); if (!points) @@ -539,14 +542,15 @@ void ViewProviderFemPostObject::updateMaterial() WriteColorData(true); } -void ViewProviderFemPostObject::WriteColorData(bool ResetColorBarRange) { - +void ViewProviderFemPostObject::WriteColorData(bool ResetColorBarRange) +{ if (!setupPipeline()) return; if (Field.getEnumVector().empty() || Field.getValue() == 0) { m_material->diffuseColor.setValue(SbColor(0.8, 0.8, 0.8)); - m_material->transparency.setValue(0.); + float trans = float(Transparency.getValue()) / 100.0; + m_material->transparency.setValue(trans); m_materialBinding->value = SoMaterialBinding::OVERALL; m_materialBinding->touch(); // since there is no field, set the range to the default @@ -558,6 +562,8 @@ void ViewProviderFemPostObject::WriteColorData(bool ResetColorBarRange) { int array = Field.getValue() - 1; // 0 is none vtkPolyData* pd = m_currentAlgorithm->GetOutput(); vtkDataArray* data = pd->GetPointData()->GetArray(array); + if (!data) + return; int component = VectorMode.getValue() - 1; // 0 is either "Not a vector" or magnitude, // for -1 is correct for magnitude. @@ -606,8 +612,8 @@ void ViewProviderFemPostObject::WriteColorData(bool ResetColorBarRange) { m_triangleStrips->touch(); } -void ViewProviderFemPostObject::WriteTransparency() { - +void ViewProviderFemPostObject::WriteTransparency() +{ float trans = float(Transparency.getValue()) / 100.0; m_material->transparency.setValue(trans); @@ -616,17 +622,73 @@ void ViewProviderFemPostObject::WriteTransparency() { m_triangleStrips->touch(); } -void ViewProviderFemPostObject::updateData(const App::Property* p) { - +void ViewProviderFemPostObject::updateData(const App::Property* p) +{ if (strcmp(p->getName(), "Data") == 0) { updateVtk(); } } +void ViewProviderFemPostObject::filterArtifacts(vtkDataObject* data) +{ + // The problem is that in the surface view the boundary regions of the volumes + // calculated by the different CPU cores is always visible, independent of the + // transparency setting. Elmer is not to blame because this is a property of the + // partial VTK file reader. So this can happen with various inputs + // since FreeCAD can also be used to view VTK files without the need to perform + // an analysis. Therefore it is impossible to know in advance when a filter + // is necessary or not. + // Only for pure CCX analyses we know that no filtering is necessary. However, + // the effort to catch this case is not worth it since the filtering is + // only as time-consuming as enabling the surface filter. In fact, it is like + // performing the surface filter twice. + + // We need to set the filter clipping plane below the z-minimum of the data. + // We can either do this by checking the VTK data or by getting the info from + // the 3D view. We use here the latter because this is much faster. + + // since we will set the filter according to the visible bounding box + // assure the object is visible + bool visibility = this->Visibility.getValue(); + this->Visibility.setValue(false); + + Gui::Document* doc = this->getDocument(); + Gui::View3DInventor* view = + qobject_cast(doc->getViewOfViewProvider(this)); + + if (view) { + Gui::View3DInventorViewer* viewer = view->getViewer(); + SbBox3f boundingBox; + boundingBox = viewer->getBoundingBox(); + if (boundingBox.hasVolume()) { + // setup + vtkSmartPointer m_implicit; + auto m_plane = vtkSmartPointer::New(); + m_implicit = m_plane; + m_plane->SetNormal(0., 0., 1.); + auto extractor = vtkSmartPointer::New(); + float dx, dy, dz; + boundingBox.getSize(dx, dy, dz); + // set plane slightly below the minimum to assure there are + // no boundary cells (touching the function + m_plane->SetOrigin(0., 0., -1 * dz - 1); + extractor->SetClipFunction(m_implicit); + extractor->SetInputData(data); + extractor->Update(); + m_surface->SetInputData(extractor->GetOutputDataObject(0)); + } + else { + // for e.g. DataAtPoint filter + m_surface->SetInputData(data); + } + } + // restore initial vsibility + this->Visibility.setValue(visibility); +} + bool ViewProviderFemPostObject::setupPipeline() { vtkDataObject* data = static_cast(getObject())->Data.getValue(); - if (!data) return false; @@ -634,6 +696,8 @@ bool ViewProviderFemPostObject::setupPipeline() // add a field with an absolute value vtkSmartPointer SPdata = data; vtkDataSet* dset = vtkDataSet::SafeDownCast(SPdata); + if (!dset) + return false; std::string FieldName; auto numFields = dset->GetPointData()->GetNumberOfArrays(); for (int i = 0; i < numFields; ++i) { @@ -642,15 +706,23 @@ bool ViewProviderFemPostObject::setupPipeline() } m_outline->SetInputData(data); - m_surface->SetInputData(data); m_wireframe->SetInputData(data); m_points->SetInputData(data); + // filtering artifacts is only necessary for the surface filter + auto hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Fem/Elmer"); + bool FilterMultiCPUResults = hGrp->GetBool("FilterMultiCPUResults", 1); + if (FilterMultiCPUResults) + filterArtifacts(data); + else + m_surface->SetInputData(data); + return true; } -void ViewProviderFemPostObject::onChanged(const App::Property* prop) { - +void ViewProviderFemPostObject::onChanged(const App::Property* prop) +{ if (m_blockPropertyChanges) return; @@ -679,7 +751,8 @@ void ViewProviderFemPostObject::onChanged(const App::Property* prop) { ViewProviderDocumentObject::onChanged(prop); } -bool ViewProviderFemPostObject::doubleClicked() { +bool ViewProviderFemPostObject::doubleClicked() +{ // work around for a problem in VTK implementation: // https://forum.freecadweb.org/viewtopic.php?t=10587&start=130#p125688 // check if backlight is enabled @@ -695,8 +768,8 @@ bool ViewProviderFemPostObject::doubleClicked() { return true; } -bool ViewProviderFemPostObject::setEdit(int ModNum) { - +bool ViewProviderFemPostObject::setEdit(int ModNum) +{ if (ModNum == ViewProvider::Default || ModNum == 1) { Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog(); @@ -733,8 +806,8 @@ bool ViewProviderFemPostObject::setEdit(int ModNum) { } } -void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) { - +void ViewProviderFemPostObject::setupTaskDialog(TaskDlgPost* dlg) +{ dlg->appendBox(new TaskPostDisplay(this)); } @@ -752,7 +825,8 @@ void ViewProviderFemPostObject::unsetEdit(int ModNum) { } } -void ViewProviderFemPostObject::hide() { +void ViewProviderFemPostObject::hide() +{ Gui::ViewProviderDocumentObject::hide(); m_colorStyle->style = SoDrawStyle::INVISIBLE; // The object is now hidden but the color bar is wrong @@ -787,7 +861,8 @@ void ViewProviderFemPostObject::hide() { } } -void ViewProviderFemPostObject::show() { +void ViewProviderFemPostObject::show() +{ Gui::ViewProviderDocumentObject::show(); m_colorStyle->style = SoDrawStyle::FILLED; // we must update the color bar except for data point filters @@ -795,7 +870,8 @@ void ViewProviderFemPostObject::show() { WriteColorData(true); } -void ViewProviderFemPostObject::OnChange(Base::Subject< int >& /*rCaller*/, int /*rcReason*/) { +void ViewProviderFemPostObject::OnChange(Base::Subject< int >& /*rCaller*/, int /*rcReason*/) +{ bool ResetColorBarRange = false; WriteColorData(ResetColorBarRange); } diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h index fce127a5c5..8a5d7f0bc9 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -150,6 +151,7 @@ protected: vtkSmartPointer m_points, m_pointsSurface; private: + void filterArtifacts(vtkDataObject* data); void updateProperties(); void update3D(); void WritePointData(vtkPoints *points, vtkDataArray *normals, diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp index b59aa56c42..a3e3c0d9e6 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostPipeline.cpp @@ -155,10 +155,9 @@ void ViewProviderFemPostPipeline::transformField(char *FieldName, double FieldFa Fem::FemPostPipeline *obj = static_cast(getObject()); vtkSmartPointer data = obj->Data.getValue(); - if (!data || !data->IsA("vtkDataSet")) - return; - vtkDataSet *dset = vtkDataSet::SafeDownCast(data); + if (!dset) + return; vtkDataArray *pdata = dset->GetPointData()->GetArray(FieldName); if (!pdata) return; diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index 9be3e2e32f..70c14c5a7f 100755 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -203,6 +203,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "FEM_PostFilterClipScalar" << "FEM_PostFilterCutFunction" << "FEM_PostFilterClipRegion" + << "FEM_PostFilterContours" << "FEM_PostFilterDataAlongLine" << "FEM_PostFilterLinearizedStresses" << "FEM_PostFilterDataAtPoint" @@ -371,6 +372,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "FEM_PostFilterClipScalar" << "FEM_PostFilterCutFunction" << "FEM_PostFilterClipRegion" + << "FEM_PostFilterContours" << "FEM_PostFilterDataAlongLine" << "FEM_PostFilterLinearizedStresses" << "FEM_PostFilterDataAtPoint" diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index ec7d4f2f10..78947dd17f 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -720,6 +720,21 @@ def makePostVtkFilterWarp( return obj +def makePostVtkFilterContours( + doc, + base_vtk_result, + name="VtkFilterContours" +): + """makePostVtkFilterContours(document, base_vtk_result, [name]): + creates a FEM post processing contours filter object (vtk based)""" + obj = doc.addObject("Fem::FemPostContoursFilter", name) + tmp_filter_list = base_vtk_result.Filter + tmp_filter_list.append(obj) + base_vtk_result.Filter = tmp_filter_list + del tmp_filter_list + return obj + + def makePostVtkResult( doc, base_result, diff --git a/src/Mod/Fem/femmesh/gmshtools.py b/src/Mod/Fem/femmesh/gmshtools.py index 6af99050e5..dae0af63f5 100644 --- a/src/Mod/Fem/femmesh/gmshtools.py +++ b/src/Mod/Fem/femmesh/gmshtools.py @@ -394,11 +394,11 @@ class GmshTools(): self.group_elements[ge] = new_group_elements[ge] else: Console.PrintError(" A group with this name exists already.\n") - else: - Console.PrintMessage(" No Group meshing for analysis.\n") + #else: + # Console.PrintMessage(" No Group meshing for analysis.\n") - if self.group_elements: - Console.PrintMessage(" {}\n".format(self.group_elements)) + #if self.group_elements: + # Console.PrintMessage(" {}\n".format(self.group_elements)) def get_gmsh_version(self): self.get_gmsh_command() @@ -447,7 +447,7 @@ class GmshTools(): # print(" No mesh regions.") pass else: - Console.PrintMessage(" Mesh regions, we need to get the elements.\n") + #Console.PrintMessage(" Mesh regions, we need to get the elements.\n") # by the use of MeshRegion object and a BooleanSplitCompound # there could be problems with node numbers see # http://forum.freecadweb.org/viewtopic.php?f=18&t=18780&start=40#p149467 @@ -461,17 +461,7 @@ class GmshTools(): or femutils.is_of_type(part, "FeatureXOR") ) ): - error_message = ( - " The mesh to shape is a boolean split tools Compound " - "and the mesh has mesh region list. " - "Gmsh could return unexpected meshes in such circumstances. " - "It is strongly recommended to extract the shape to mesh " - "from the Compound and use this one." - ) - Console.PrintError(error_message + "\n") - # TODO: no gui popup because FreeCAD will be in a endless output loop - # as long as the pop up is on --> maybe find a better solution for - # either of both --> thus the pop up is in task panel + self.outputCompoundWarning for mr_obj in self.mesh_obj.MeshRegionList: # print(mr_obj.Name) # print(mr_obj.CharacteristicLength) @@ -540,8 +530,8 @@ class GmshTools(): ele_shape = geomtools.get_element(self.part_obj, eleml) ele_vertexes = geomtools.get_vertexes_by_element(self.part_obj.Shape, ele_shape) self.ele_node_map[eleml] = ele_vertexes - Console.PrintMessage(" {}\n".format(self.ele_length_map)) - Console.PrintMessage(" {}\n".format(self.ele_node_map)) + #Console.PrintMessage(" {}\n".format(self.ele_length_map)) + #Console.PrintMessage(" {}\n".format(self.ele_node_map)) def get_boundary_layer_data(self): # mesh boundary layer @@ -553,16 +543,11 @@ class GmshTools(): # print(" No mesh boundary layer setting document object.") pass else: - Console.PrintMessage(" Mesh boundary layers, we need to get the elements.\n") + #Console.PrintMessage(" Mesh boundary layers, we need to get the elements.\n") if self.part_obj.Shape.ShapeType == "Compound": # see http://forum.freecadweb.org/viewtopic.php?f=18&t=18780&start=40#p149467 and # http://forum.freecadweb.org/viewtopic.php?f=18&t=18780&p=149520#p149520 - err = ( - "Gmsh could return unexpected meshes for a boolean split tools Compound. " - "It is strongly recommended to extract the shape to mesh " - "from the Compound and use this one." - ) - Console.PrintError(err + "\n") + self.outputCompoundWarning for mr_obj in self.mesh_obj.MeshBoundaryLayerList: if mr_obj.MinimumThickness and Units.Quantity(mr_obj.MinimumThickness).Value > 0: if mr_obj.References: @@ -960,6 +945,17 @@ class GmshTools(): else: Console.PrintError("No mesh was created.\n") + def outputCompoundWarning(self): + error_message = ( + "The mesh to shape is a Boolean Split Tools compound " + "and the mesh has mesh region list.\n" + "Gmsh could return unexpected meshes in such circumstances.\n" + "If this is the case, use the part workbench and " + "apply a Compound Filter on the compound.\n" + "Use the Compound Filter as input for the mesh." + ) + Console.PrintWarning(error_message + "\n") + ## @} diff --git a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py index 30aac36737..a7e33f97aa 100644 --- a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py +++ b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py @@ -199,6 +199,8 @@ class _TaskPanel: ) def run_gmsh(self): + from femmesh import gmshtools + gmsh_mesh = gmshtools.GmshTools(self.mesh_obj, self.analysis) QApplication.setOverrideCursor(Qt.WaitCursor) part = self.mesh_obj.Part if ( @@ -209,43 +211,26 @@ class _TaskPanel: or is_of_type(part, "FeatureXOR") ) ): - error_message = ( - "The shape to mesh is a boolean split tools Compound " - "and the mesh has mesh region list. " - "Gmsh could return unexpected meshes in such circumstances. " - "It is strongly recommended to extract the shape " - "to mesh from the Compound and use this one." - ) - qtbox_title = ( - "Shape to mesh is a BooleanFragmentsCompound " - "and mesh regions are defined" - ) - QtGui.QMessageBox.critical( - None, - qtbox_title, - error_message - ) + gmsh_mesh.outputCompoundWarning() self.Start = time.time() self.form.l_time.setText("Time: {0:4.1f}: ".format(time.time() - self.Start)) self.console_message_gmsh = "" self.gmsh_runs = True self.console_log("We are going to start ...") self.get_active_analysis() - from femmesh import gmshtools - gmsh_mesh = gmshtools.GmshTools(self.mesh_obj, self.analysis) self.console_log("Start Gmsh ...") error = "" try: error = gmsh_mesh.create_mesh() except Exception: error = sys.exc_info()[1] - FreeCAD.Console.PrintMessage( + FreeCAD.Console.PrintError( "Unexpected error when creating mesh: {}\n" .format(error) ) if error: - FreeCAD.Console.PrintMessage("Gmsh had warnings ...\n") - FreeCAD.Console.PrintMessage("{}\n".format(error)) + FreeCAD.Console.PrintWarning("Gmsh had warnings:\n") + FreeCAD.Console.PrintWarning("{}\n".format(error)) self.console_log("Gmsh had warnings ...") self.console_log(error, "#FF0000") else: @@ -265,7 +250,7 @@ class _TaskPanel: else: for m in analysis.Group: if m.Name == self.mesh_obj.Name: - FreeCAD.Console.PrintMessage( + FreeCAD.Console.PrintLog( "Active analysis found: {}\n" .format(analysis.Name) ) diff --git a/src/Mod/Fem/femtest/app/test_femimport.py b/src/Mod/Fem/femtest/app/test_femimport.py index f6add3ad73..f5d1208a6c 100644 --- a/src/Mod/Fem/femtest/app/test_femimport.py +++ b/src/Mod/Fem/femtest/app/test_femimport.py @@ -154,6 +154,7 @@ class TestObjectExistance(unittest.TestCase): expected_vtk_obj_types = [ "Fem::FemPostClipFilter", + "Fem::FemPostContoursFilter", "Fem::FemPostCutFilter", "Fem::FemPostDataAlongLineFilter", "Fem::FemPostDataAtPointFilter", diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index 0f150527f1..87011ac19a 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -87,11 +87,11 @@ class TestObjectCreate(unittest.TestCase): # solver children: equations --> 8 # gmsh mesh children: group, region, boundary layer --> 3 # resule children: mesh result --> 1 - # post pipeline children: region, scalar, cut, wrap --> 4 + # post pipeline children: region, scalar, cut, wrap --> 5 # analysis itself is not in analysis group --> 1 - # thus: -17 + # thus: -18 - self.assertEqual(len(doc.Analysis.Group), count_defmake - 17) + self.assertEqual(len(doc.Analysis.Group), count_defmake - 18) self.assertEqual(len(doc.Objects), count_defmake) fcc_print("doc objects count: {}, method: {}".format( @@ -1858,6 +1858,7 @@ def create_all_fem_objects_doc( vres = analysis.addObject(ObjectsFem.makePostVtkResult(doc, res))[0] ObjectsFem.makePostVtkFilterClipRegion(doc, vres) ObjectsFem.makePostVtkFilterClipScalar(doc, vres) + ObjectsFem.makePostVtkFilterContours(doc, vres) ObjectsFem.makePostVtkFilterCutFunction(doc, vres) ObjectsFem.makePostVtkFilterWarp(doc, vres) diff --git a/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py b/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py index 81be7da090..c68270163f 100644 --- a/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py +++ b/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py @@ -36,7 +36,6 @@ import FemGui from PySide import QtGui from femtaskpanels import task_mesh_gmsh from femtools.femutils import is_of_type -# from . import view_base_femobject # TODO use VPBaseFemObject from view_base_femobject @@ -64,21 +63,21 @@ class VPMeshGmsh: def setEdit(self, vobj, mode): # hide all FEM meshes and VTK FemPost* objects - for o in vobj.Object.Document.Objects: + for obj in vobj.Object.Document.Objects: if ( - o.isDerivedFrom("Fem::FemMeshObject") - or o.isDerivedFrom("Fem::FemPostPipeline") - or o.isDerivedFrom("Fem::FemPostClipFilter") - or o.isDerivedFrom("Fem::FemPostScalarClipFilter") - or o.isDerivedFrom("Fem::FemPostWarpVectorFilter") - or o.isDerivedFrom("Fem::FemPostDataAlongLineFilter") - or o.isDerivedFrom("Fem::FemPostDataAtPointFilter") - or o.isDerivedFrom("Fem::FemPostCutFilter") - or o.isDerivedFrom("Fem::FemPostDataAlongLineFilter") - or o.isDerivedFrom("Fem::FemPostPlaneFunction") - or o.isDerivedFrom("Fem::FemPostSphereFunction") + obj.isDerivedFrom("Fem::FemMeshObject") + or obj.isDerivedFrom("Fem::FemPostClipFilter") + or obj.isDerivedFrom("Fem::FemPostContoursFilter") + or obj.isDerivedFrom("Fem::FemPostCutFilter") + or obj.isDerivedFrom("Fem::FemPostDataAlongLineFilter") + or obj.isDerivedFrom("Fem::FemPostDataAtPointFilter") + or obj.isDerivedFrom("Fem::FemPostPipeline") + or obj.isDerivedFrom("Fem::FemPostPlaneFunction") + or obj.isDerivedFrom("Fem::FemPostScalarClipFilter") + or obj.isDerivedFrom("Fem::FemPostSphereFunction") + or obj.isDerivedFrom("Fem::FemPostWarpVectorFilter") ): - o.ViewObject.hide() + obj.ViewObject.hide() # show the mesh we like to edit self.ViewObject.show() # show task panel diff --git a/src/Mod/Part/App/Geometry.cpp b/src/Mod/Part/App/Geometry.cpp index 99b83b5d4c..1663571d95 100644 --- a/src/Mod/Part/App/Geometry.cpp +++ b/src/Mod/Part/App/Geometry.cpp @@ -2244,29 +2244,23 @@ GeomBSplineCurve* GeomCircle::toNurbs(double first, double last) const return GeomConic::toNurbs(first, last); } - double radius = getRadius(); - Handle(Geom_Conic) conic = Handle(Geom_Conic)::DownCast(handle()); - gp_Ax1 axis = conic->Axis(); - //gp_Dir xdir = conic->XAxis().Direction(); - //Standard_Real angle = gp_Dir(1,0,0).Angle(xdir) + first; - Standard_Real angle = first; - const gp_Pnt& loc = axis.Location(); - //Note: If the matching this way doesn't work reliably then we must compute the - //angle so that the point of the curve for 'first' matches the first pole - //gp_Pnt pnt = conic->Value(first); + Handle(Geom_Circle) conic = Handle(Geom_Circle)::DownCast(handle()); + double radius = conic->Radius(); TColgp_Array1OfPnt poles(1, 7); - poles(1) = loc.Translated(gp_Vec(radius, 0, 0)); - poles(2) = loc.Translated(gp_Vec(radius, 2*radius, 0)); - poles(3) = loc.Translated(gp_Vec(-radius, 2*radius, 0)); - poles(4) = loc.Translated(gp_Vec(-radius, 0, 0)); - poles(5) = loc.Translated(gp_Vec(-radius, -2*radius, 0)); - poles(6) = loc.Translated(gp_Vec(radius, -2*radius, 0)); - poles(7) = loc.Translated(gp_Vec(radius, 0, 0)); + poles(1) = gp_Pnt(radius, 0, 0); + poles(2) = gp_Pnt(radius, 2*radius, 0); + poles(3) = gp_Pnt(-radius, 2*radius, 0); + poles(4) = gp_Pnt(-radius, 0, 0); + poles(5) = gp_Pnt(-radius, -2*radius, 0); + poles(6) = gp_Pnt(radius, -2*radius, 0); + poles(7) = gp_Pnt(radius, 0, 0); + gp_Trsf trsf; + trsf.SetTransformation(conic->Position(), gp_Ax3()); TColStd_Array1OfReal weights(1,7); for (int i=1; i<=7; i++) { - poles(i).Rotate(axis, angle); + poles(i).Transform(trsf); weights(i) = 1; } weights(1) = 3; @@ -2285,7 +2279,6 @@ GeomBSplineCurve* GeomCircle::toNurbs(double first, double last) const Handle(Geom_BSplineCurve) spline = new Geom_BSplineCurve(poles, weights,knots, mults, 3, Standard_False, Standard_True); - spline->Segment(0, last-first); return new GeomBSplineCurve(spline); } @@ -2671,25 +2664,23 @@ GeomBSplineCurve* GeomEllipse::toNurbs(double first, double last) const } Handle(Geom_Ellipse) conic = Handle(Geom_Ellipse)::DownCast(handle()); - gp_Ax1 axis = conic->Axis(); Standard_Real majorRadius = conic->MajorRadius(); Standard_Real minorRadius = conic->MinorRadius(); - gp_Dir xdir = conic->XAxis().Direction(); - Standard_Real angle = atan2(xdir.Y(), xdir.X()); - const gp_Pnt& loc = axis.Location(); TColgp_Array1OfPnt poles(1, 7); - poles(1) = loc.Translated(gp_Vec(majorRadius, 0, 0)); - poles(2) = loc.Translated(gp_Vec(majorRadius, 2*minorRadius, 0)); - poles(3) = loc.Translated(gp_Vec(-majorRadius, 2*minorRadius, 0)); - poles(4) = loc.Translated(gp_Vec(-majorRadius, 0, 0)); - poles(5) = loc.Translated(gp_Vec(-majorRadius, -2*minorRadius, 0)); - poles(6) = loc.Translated(gp_Vec(majorRadius, -2*minorRadius, 0)); - poles(7) = loc.Translated(gp_Vec(majorRadius, 0, 0)); + poles(1) = gp_Pnt(majorRadius, 0, 0); + poles(2) = gp_Pnt(majorRadius, 2*minorRadius, 0); + poles(3) = gp_Pnt(-majorRadius, 2*minorRadius, 0); + poles(4) = gp_Pnt(-majorRadius, 0, 0); + poles(5) = gp_Pnt(-majorRadius, -2*minorRadius, 0); + poles(6) = gp_Pnt(majorRadius, -2*minorRadius, 0); + poles(7) = gp_Pnt(majorRadius, 0, 0); + gp_Trsf trsf; + trsf.SetTransformation(conic->Position(), gp_Ax3()); TColStd_Array1OfReal weights(1,7); for (int i=1; i<=7; i++) { - poles(i).Rotate(axis, angle); + poles(i).Transform(trsf); weights(i) = 1; } weights(1) = 3; diff --git a/src/Mod/Part/TestPartApp.py b/src/Mod/Part/TestPartApp.py index 1c252fa5cc..14ad04c6bc 100644 --- a/src/Mod/Part/TestPartApp.py +++ b/src/Mod/Part/TestPartApp.py @@ -175,6 +175,48 @@ class PartTestBSplineCurve(unittest.TestCase): #closing doc FreeCAD.closeDocument("PartTest") +class PartTestCurveToNurbs(unittest.TestCase): + def testCircleToNurbs(self): + mat = Base.Matrix() + mat.rotateX(1) + mat.rotateY(1) + mat.rotateZ(1) + + circle = Part.Circle() + circle.Radius = 5 + + circle.transform(mat) + nurbs = circle.toNurbs() + self.assertEqual(circle.value(0), nurbs.value(0)) + + arc = circle.trim(0, 2) + nurbs = arc.toNurbs() + self.assertEqual(circle.value(0), nurbs.value(0)) + + spline = circle.toBSpline() + self.assertAlmostEqual(circle.value(0).distanceToPoint(spline.value(0)), 0) + + def testEllipseToNurbs(self): + mat = Base.Matrix() + mat.rotateX(1) + mat.rotateY(1) + mat.rotateZ(1) + + ellipse = Part.Ellipse() + ellipse.MajorRadius = 5 + ellipse.MinorRadius = 3 + + ellipse.transform(mat) + nurbs = ellipse.toNurbs() + self.assertEqual(ellipse.value(0), nurbs.value(0)) + + arc = ellipse.trim(0, 2) + nurbs = arc.toNurbs() + self.assertEqual(ellipse.value(0), nurbs.value(0)) + + spline = ellipse.toBSpline() + self.assertAlmostEqual(ellipse.value(0).distanceToPoint(spline.value(0)), 0) + class PartTestBSplineSurface(unittest.TestCase): def testTorusToSpline(self): to = Part.Toroid() diff --git a/src/Mod/Sketcher/Gui/EditModeCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeCoinManager.cpp index 6e570cede3..9abd818da6 100644 --- a/src/Mod/Sketcher/Gui/EditModeCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeCoinManager.cpp @@ -148,6 +148,8 @@ void EditModeCoinManager::ParameterObserver::initParameters() [this, drawingParameters = Client.drawingParameters](const std::string & param){updateColor(drawingParameters.PreselectColor, param);}}, {"SelectionColor", [this, drawingParameters = Client.drawingParameters](const std::string & param){updateColor(drawingParameters.SelectColor, param);}}, + {"CursorTextColor", + [this, drawingParameters = Client.drawingParameters](const std::string & param){updateColor(drawingParameters.CursorTextColor, param);}}, }; for( auto & val : str2updatefunction){ @@ -230,12 +232,12 @@ void EditModeCoinManager::ParameterObserver::updateElementSizeParameters(const s // simple scaling factor for hardcoded pixel values in the Sketcher Client.drawingParameters.pixelScalingFactor = viewScalingFactor * dpi / 96; // 96 ppi is the standard pixel density for which pixel quantities were calculated - // Coin documentation indicates the size of a font is: - // SoSFFloat SoFont::size Size of font. Defaults to 10.0. + // About sizes: + // SoDatumLabel takes the size in points, not in pixels. This is because it uses QFont internally. + // Coin, at least our coin at this time, takes pixels, not points. // - // For 2D rendered bitmap fonts (like for SoText2), this value is the height of a character in screen pixels. For 3D text, this value is the world-space coordinates height of a character in the current units setting (see documentation for SoUnits node). - // - // However, with hdpi monitors, the coin font labels do not respect the size passed in pixels: + // DPI considerations: + // With hdpi monitors, the coin font labels do not respect the size passed in pixels: // https://forum.freecadweb.org/viewtopic.php?f=3&t=54347&p=467610#p467610 // https://forum.freecadweb.org/viewtopic.php?f=10&t=49972&start=40#p467471 // @@ -247,7 +249,8 @@ void EditModeCoinManager::ParameterObserver::updateElementSizeParameters(const s // This means that the following correction does not have a documented basis, but appears necessary so that the Sketcher is usable in // HDPI monitors. - Client.drawingParameters.coinFontSize = std::lround(sketcherfontSize * 96.0f / dpi); + Client.drawingParameters.coinFontSize = std::lround(sketcherfontSize * 96.0f / dpi); // this is in pixels + Client.drawingParameters.labelFontSize = std::lround(sketcherfontSize * 72.0f / dpi); // this is in points, as SoDatumLabel uses points Client.drawingParameters.constraintIconSize = std::lround(0.8 * sketcherfontSize); // For marker size the global default is used. @@ -269,6 +272,8 @@ void EditModeCoinManager::ParameterObserver::updateColor(SbColor &sbcolor, const unsigned long color = (unsigned long)(sbcolor.getPackedValue()); color = hGrp->GetUnsigned(parametername.c_str(), color); sbcolor.setPackedValue((uint32_t)color, transparency); + + Client.updateInventorColors(); } void EditModeCoinManager::ParameterObserver::subscribeToParameters() @@ -705,11 +710,6 @@ void EditModeCoinManager::createEditModeInventorNodes() editModeScenegraphNodes.EditCurveSet->setName("EditCurveLineSet"); editCurvesRoot->addChild(editModeScenegraphNodes.EditCurveSet); - ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/View"); - float transparency; - SbColor cursorTextColor(0,0,1); - cursorTextColor.setPackedValue((uint32_t)hGrp->GetUnsigned("CursorTextColor", cursorTextColor.getPackedValue()), transparency); - // stuff for the EditMarkers +++++++++++++++++++++++++++++++++++++++ SoSeparator* editMarkersRoot = new SoSeparator; editModeScenegraphNodes.EditRoot->addChild(editMarkersRoot); @@ -740,15 +740,16 @@ void EditModeCoinManager::createEditModeInventorNodes() // no caching for frequently-changing data structures Coordsep->renderCaching = SoSeparator::OFF; - SoMaterial *CoordTextMaterials = new SoMaterial; - CoordTextMaterials->setName("CoordTextMaterials"); - CoordTextMaterials->diffuseColor = cursorTextColor; - Coordsep->addChild(CoordTextMaterials); + editModeScenegraphNodes.textMaterial = new SoMaterial; + editModeScenegraphNodes.textMaterial->setName("CoordTextMaterials"); + editModeScenegraphNodes.textMaterial->diffuseColor = drawingParameters.CursorTextColor; + Coordsep->addChild(editModeScenegraphNodes.textMaterial); - SoFont *font = new SoFont(); - font->size.setValue(drawingParameters.coinFontSize); + editModeScenegraphNodes.textFont = new SoFont(); + editModeScenegraphNodes.textFont->name.setValue("Helvetica"); + editModeScenegraphNodes.textFont->size.setValue(drawingParameters.coinFontSize); - Coordsep->addChild(font); + Coordsep->addChild(editModeScenegraphNodes.textFont); editModeScenegraphNodes.textPos = new SoTranslation(); Coordsep->addChild(editModeScenegraphNodes.textPos); @@ -828,9 +829,18 @@ void EditModeCoinManager::updateInventorNodeSizes() editModeScenegraphNodes.ConstraintDrawStyle->lineWidth = 1 * drawingParameters.pixelScalingFactor; editModeScenegraphNodes.InformationDrawStyle->lineWidth = 1 * drawingParameters.pixelScalingFactor; + editModeScenegraphNodes.textFont->size.setValue(drawingParameters.coinFontSize); + pEditModeConstraintCoinManager->rebuildConstraintNodes(); } +void EditModeCoinManager::updateInventorColors() +{ + editModeScenegraphNodes.RootCrossMaterials->diffuseColor.set1Value(0, drawingParameters.CrossColorH); + editModeScenegraphNodes.RootCrossMaterials->diffuseColor.set1Value(1, drawingParameters.CrossColorV); + editModeScenegraphNodes.textMaterial->diffuseColor = drawingParameters.CursorTextColor; +} + /************************ Edit node access ************************/ SoSeparator* EditModeCoinManager::getRootEditNode() diff --git a/src/Mod/Sketcher/Gui/EditModeCoinManager.h b/src/Mod/Sketcher/Gui/EditModeCoinManager.h index a032847ebb..beb5659e49 100644 --- a/src/Mod/Sketcher/Gui/EditModeCoinManager.h +++ b/src/Mod/Sketcher/Gui/EditModeCoinManager.h @@ -262,6 +262,8 @@ private: void updateInventorNodeSizes(); + void updateInventorColors(); + /** @name coin nodes creation*/ void createEditModeInventorNodes(); //@} diff --git a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.cpp b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.cpp index f6cf090c9e..207274c138 100644 --- a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.cpp +++ b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.cpp @@ -51,5 +51,6 @@ SbColor DrawingParameters::ConstrIcoColor (1.0f, 0.14 SbColor DrawingParameters::NonDrivingConstrDimColor (0.0f, 0.149f, 1.0f); // #0026FF -> ( 0, 38,255) SbColor DrawingParameters::ExprBasedConstrDimColor (1.0f, 0.5f, 0.149f); // #FF7F26 -> (255, 127,38) SbColor DrawingParameters::DeactivatedConstrDimColor (0.8f, 0.8f, 0.8f); // #CCCCCC -> (204,204,204) +SbColor DrawingParameters::CursorTextColor (0.0f, 0.0f, 1.0f); // #0000FF -> (0,0,255) const MultiFieldId MultiFieldId::Invalid = MultiFieldId(); diff --git a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h index 5ff0a18aee..9e4781a9d5 100644 --- a/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h +++ b/src/Mod/Sketcher/Gui/EditModeCoinManagerParameters.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include #include + #include #include @@ -112,12 +114,14 @@ struct DrawingParameters { static SbColor NonDrivingConstrDimColor; // Color used for non-driving (reference) dimensional constraints static SbColor ExprBasedConstrDimColor; // Color used for expression based dimensional constraints static SbColor DeactivatedConstrDimColor; // Color used for deactivated dimensional constraints + static SbColor CursorTextColor; // Color used by the edit mode cursor //@} /** @name Rendering sizes (also to support HDPI monitors) **/ //@{ double pixelScalingFactor = 1.0; // Scaling factor to be used for pixels int coinFontSize = 17; // Font size to be used by coin + int labelFontSize = 17; // Font size to be used by SoDatumLabel, which uses a QPainter and a QFont internally int constraintIconSize = 15; // Size of constraint icons int markerSize = 7; // Size used for markers //@} @@ -326,6 +330,8 @@ struct EditModeScenegraphNodes { //@{ SoText2 *textX; SoTranslation *textPos; + SoFont *textFont; + SoMaterial *textMaterial; //@} /** @name Constraint nodes*/ diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index eaceec9db7..a56404a6a1 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -1440,7 +1440,7 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes(const GeoListFacade & drawingParameters.ConstrDimColor :drawingParameters.NonDrivingConstrDimColor) :drawingParameters.DeactivatedConstrDimColor; - text->size.setValue(drawingParameters.coinFontSize); + text->size.setValue(drawingParameters.labelFontSize); text->lineWidth = 2 * drawingParameters.pixelScalingFactor; text->useAntialiasing = false; SoAnnotation *anno = new SoAnnotation(); diff --git a/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp b/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp index 52446f6cb1..3fd78714b9 100644 --- a/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp +++ b/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp @@ -365,9 +365,9 @@ void EditModeInformationOverlayCoinConverter::addNode(const Result & result) { else setText(result.strings[i], text); - sep->addChild(translate); sep->addChild(mat); sep->addChild(font); + sep->addChild(translate); sep->addChild(text); sw->addChild(sep); diff --git a/src/Mod/Start/StartPage/StartPage.py b/src/Mod/Start/StartPage/StartPage.py index 1e38609f87..972c9b5490 100644 --- a/src/Mod/Start/StartPage/StartPage.py +++ b/src/Mod/Start/StartPage/StartPage.py @@ -253,7 +253,7 @@ def getDefaultIcon(): def buildCard(filename,method,arg=None): - """builds an html
  • element representing a file. + """builds an html
  • element representing a file. method is a script + a keyword, for ex. url.py?key=""" result = "" diff --git a/src/Mod/TechDraw/CMakeLists.txt b/src/Mod/TechDraw/CMakeLists.txt index 1678c83be8..b4a796218b 100644 --- a/src/Mod/TechDraw/CMakeLists.txt +++ b/src/Mod/TechDraw/CMakeLists.txt @@ -21,14 +21,17 @@ set(TechDraw_ToolsScripts TechDrawTools/CommandMoveView.py TechDrawTools/CommandShareView.py TechDrawTools/CommandAxoLengthDimension.py + TechDrawTools/CommandHoleShaftFit.py TechDrawTools/TaskMoveView.py TechDrawTools/TaskShareView.py + TechDrawTools/TaskHoleShaftFit.py TechDrawTools/TDToolsUtil.py TechDrawTools/TDToolsMovers.py ) set(TechDraw_ToolsGui Gui/TaskMoveView.ui + Gui/TaskHoleShaftFit.ui Gui/DlgPageChooser.ui ) diff --git a/src/Mod/TechDraw/Gui/Resources/TechDraw.qrc b/src/Mod/TechDraw/Gui/Resources/TechDraw.qrc index b142946ba5..aa405f1538 100644 --- a/src/Mod/TechDraw/Gui/Resources/TechDraw.qrc +++ b/src/Mod/TechDraw/Gui/Resources/TechDraw.qrc @@ -29,6 +29,7 @@ icons/actions/TechDraw_Midpoints.svg icons/actions/TechDraw_MoveView.svg icons/actions/TechDraw_AxoLengthDimension.svg + icons/actions/TechDraw_HoleShaftFit.svg icons/actions/TechDraw_Multiview.svg icons/actions/TechDraw_PageDefault.svg icons/actions/TechDraw_PageTemplate.svg diff --git a/src/Mod/TechDraw/Gui/Resources/icons/actions/TechDraw_HoleShaftFit.svg b/src/Mod/TechDraw/Gui/Resources/icons/actions/TechDraw_HoleShaftFit.svg new file mode 100644 index 0000000000..11b6f7ac31 --- /dev/null +++ b/src/Mod/TechDraw/Gui/Resources/icons/actions/TechDraw_HoleShaftFit.svg @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + [WandererFan] + + + TechDraw_Dimension + 2016-04-27 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/TechDraw/Gui/Resources/icons/TechDraw_Dimension.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + arrow + double arrow + diagonal + + + Double arrow at an angle between two diagonal lines + + + + + + + + + + + + + + + diff --git a/src/Mod/TechDraw/Gui/TaskHoleShaftFit.ui b/src/Mod/TechDraw/Gui/TaskHoleShaftFit.ui new file mode 100644 index 0000000000..67d83d6cf6 --- /dev/null +++ b/src/Mod/TechDraw/Gui/TaskHoleShaftFit.ui @@ -0,0 +1,164 @@ + + + TechDrawGui::TaskSurfaceFinishSymbols + + + true + + + + 0 + 0 + 274 + 162 + + + + + 0 + 0 + + + + + 250 + 0 + + + + Hole /Shaft Fit ISO 286 + + + + + + + + shaft fit + + + true + + + + + + + hole fit + + + + + + + + + + + true + + + + c11 + + + + + f7 + + + + + h6 + + + + + h7 + + + + + h9 + + + + + h9 + + + + + h9 + + + + + h6 + + + + + h6 + + + + + h6 + + + + + h6 + + + + + h6 + + + + + k6 + + + + + n6 + + + + + r6 + + + + + s6 + + + + + + + + loose fit + + + + + + + H11/ + + + + + + + + + + + + diff --git a/src/Mod/TechDraw/Gui/Workbench.cpp b/src/Mod/TechDraw/Gui/Workbench.cpp index 9ba7b30a03..00559b6e0d 100644 --- a/src/Mod/TechDraw/Gui/Workbench.cpp +++ b/src/Mod/TechDraw/Gui/Workbench.cpp @@ -226,6 +226,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const *draw << "TechDraw_ShowAll"; *draw << "TechDraw_WeldSymbol"; *draw << "TechDraw_SurfaceFinishSymbols"; + *draw << "TechDraw_HoleShaftFit"; *draw << "Separator"; *draw << "TechDraw_ProjectShape"; return root; @@ -376,6 +377,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const *anno << "TechDraw_ShowAll"; *anno << "TechDraw_WeldSymbol"; *anno << "TechDraw_SurfaceFinishSymbols"; + *anno << "TechDraw_HoleShaftFit"; return root; } @@ -525,6 +527,7 @@ Gui::ToolBarItem* Workbench::setupCommandBars() const *anno << "TechDraw_ShowAll"; *anno << "TechDraw_WeldSymbol"; *anno << "TechDraw_SurfaceFinishSymbols"; + *anno << "TechDraw_HoleShaftFit"; return root; } diff --git a/src/Mod/TechDraw/TechDrawTools/CommandHoleShaftFit.py b/src/Mod/TechDraw/TechDrawTools/CommandHoleShaftFit.py new file mode 100644 index 0000000000..94b027ae12 --- /dev/null +++ b/src/Mod/TechDraw/TechDrawTools/CommandHoleShaftFit.py @@ -0,0 +1,67 @@ +# *************************************************************************** +# * Copyright (c) 2023 edi * +# * * +# * 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 * +# * * +# *************************************************************************** +"""Provides the TechDraw HoleShaftFit GuiCommand.""" + +__title__ = "TechDrawTools.CommandHoleShaftFit" +__author__ = "edi" +__url__ = "https://www.freecadweb.org" +__version__ = "00.01" +__date__ = "2023/02/07" + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui + +import TechDrawTools + +class CommandHoleShaftFit: + """Adds a hole or shaft fit to a selected dimension.""" + + def GetResources(self): + """Return a dictionary with data that will be used by the button or menu item.""" + return {'Pixmap': 'actions/TechDraw_HoleShaftFit.svg', + 'Accel': "", + 'MenuText': QT_TRANSLATE_NOOP("TechDraw_HoleShaftFit", "Add hole or shaft fit"), + 'ToolTip': QT_TRANSLATE_NOOP("TechDraw_HoleShaftFit", "Add a hole or shaft fit to a dimension
    \ + - select one length dimension or diameter dimension
    \ + - click the tool button, a panel openes
    \ + - select shaft fit / hole fit
    \ + - select the desired ISO 286 fit field using the combo box")} + + def Activated(self): + """Run the following code when the command is activated (button press).""" + sel = Gui.Selection.getSelectionEx() + #if sel and sel[0].Object.TypeId == 'TechDraw::DrawViewDimension': + if sel[0].Object.TypeId == 'TechDraw::DrawViewDimension': + self.ui = TechDrawTools.TaskHoleShaftFit(sel) + Gui.Control.showDialog(self.ui) + + def IsActive(self): + """Return True when the command should be active or False when it should be disabled (greyed).""" + if App.ActiveDocument: + return TechDrawTools.TDToolsUtil.havePage() and TechDrawTools.TDToolsUtil.haveView() + else: + return False + +# +# The command must be "registered" with a unique name by calling its class. +Gui.addCommand('TechDraw_HoleShaftFit', CommandHoleShaftFit()) \ No newline at end of file diff --git a/src/Mod/TechDraw/TechDrawTools/TaskHoleShaftFit.py b/src/Mod/TechDraw/TechDrawTools/TaskHoleShaftFit.py new file mode 100644 index 0000000000..fcd39194fe --- /dev/null +++ b/src/Mod/TechDraw/TechDrawTools/TaskHoleShaftFit.py @@ -0,0 +1,184 @@ +# *************************************************************************** +# * Copyright (c) 2023 edi * +# * * +# * 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 * +# * * +# *************************************************************************** +"""Provides the TechDraw HoleShaftFit Task Dialog.""" + +__title__ = "TechDrawTools.TaskHoleShaftFit" +__author__ = "edi" +__url__ = "https://www.freecadweb.org" +__version__ = "00.01" +__date__ = "2023/02/07" + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui + +from functools import partial + +import os + +class TaskHoleShaftFit: + def __init__(self,sel): + + loose = QT_TRANSLATE_NOOP("TechDraw_HoleShaftFit", "loose") + snug = QT_TRANSLATE_NOOP("TechDraw_HoleShaftFit", "snug") + press = QT_TRANSLATE_NOOP("TechDraw_HoleShaftFit", "press") + self.isHole = True + self.sel = sel + self.holeValues = [["h9","D10",loose],["h9","E9",loose],["h9","F8",loose],["h6","G7",loose], + ["c11","H11",loose],["f7","H8",loose],["h6","H7",loose],["h7","H8",loose], + ["k6","H7",snug],["n6","H7",snug],["r6","H7",press],["s6","H7",press], + ["h6","K7",snug],["h6","N7",snug],["h6","R7",press],["h6","S7",press]] + self.shaftValues = [["H11","c11",loose],["H8","f7",loose],["H7","h6",loose],["H8","h7",loose], + ["D10","h9",loose],["E9","h9",loose],["F8","h9",loose],["G7","h6",loose], + ["K7","h6",snug],["N7","h6",snug],["R7","h6",press],["S7","h6",press], + ["H7","k6",snug],["H7","n6",snug],["H7","r6",press],["H7","s6",press]] + + self._uiPath = App.getHomePath() + self._uiPath = os.path.join(self._uiPath, "Mod/TechDraw/TechDrawTools/Gui/TaskHoleShaftFit.ui") + self.form = Gui.PySideUic.loadUi(self._uiPath) + + self.form.setWindowTitle(QT_TRANSLATE_NOOP("TechDraw_HoleShaftFit", "Hole / Shaft Fit ISO 286")) + + self.form.rbHoleBase.clicked.connect(partial(self.on_HoleShaftChanged,True)) + self.form.rbShaftBase.clicked.connect(partial(self.on_HoleShaftChanged,False)) + self.form.cbField.currentIndexChanged.connect(self.on_FieldChanged) + + def setHoleFields(self): + '''set hole fields in the combo box''' + for i in range(self.form.cbField.count()): + self.form.cbField.removeItem(0) + for value in self.holeValues: + self.form.cbField.addItem(value[1]) + self.form.lbBaseField.setText(' '+self.holeValues[0][0]+" /") + self.form.lbFitType.setText(self.holeValues[0][2]+" fit") + + def setShaftFields(self): + '''set shaft fields in the combo box''' + for i in range(self.form.cbField.count()): + self.form.cbField.removeItem(0) + for value in self.shaftValues: + self.form.cbField.addItem(value[1]) + self.form.lbBaseField.setText(' '+self.shaftValues[0][0]+" /") + self.form.lbFitType.setText(self.shaftValues[0][2]+" fit") + + def on_HoleShaftChanged(self,isHole): + '''slot: change the used base fit hole/shaft''' + if isHole: + self.isHole = isHole + self.setShaftFields() + else: + self.isHole = isHole + self.setHoleFields() + + def on_FieldChanged(self): + '''slot: change of the desired field''' + currentIndex = self.form.cbField.currentIndex() + if self.isHole: + self.form.lbBaseField.setText(' '+self.shaftValues[currentIndex][0]+" /") + self.form.lbFitType.setText(self.shaftValues[currentIndex][2]+" fit") + else: + self.form.lbBaseField.setText(' '+self.holeValues[currentIndex][0]+" /") + self.form.lbFitType.setText(self.holeValues[currentIndex][2]+" fit") + + def accept(self): + '''slot: OK pressed''' + currentIndex = self.form.cbField.currentIndex() + if self.isHole: + selectedField = self.shaftValues[currentIndex][1] + else: + selectedField = self.holeValues[currentIndex][1] + fieldChar = selectedField[0] + quality = int(selectedField[1:]) + dim = self.sel[0].Object + value = dim.getRawValue() + iso = ISO286() + iso.calculate(value,fieldChar,quality) + rangeValues = iso.getValues() + mainFormat = dim.FormatSpec + dim.FormatSpec = mainFormat+selectedField + dim.EqualTolerance = False + dim.FormatSpecOverTolerance = '(%+.3f)' + dim.OverTolerance = rangeValues[0] + dim.UnderTolerance = rangeValues[1] + Gui.Control.closeDialog() + + def reject(self): + return True + +class ISO286: + '''This class represents a subset of the ISO 286 standard''' + + def getNominalRange(self,measureValue): + '''return index of selected nominal range field, 0 < measureValue < 500 mm''' + measureRanges = [0,3,6,10,14,18,24,30,40,50,65,80,100,120,140,160,180,200,225,250,280,315,355,400,450,500] + index = 1 + while measureValue > measureRanges[index]: + index = index+1 + return index-1 + + def getITValue(self,valueQuality,valueNominalRange): + '''return IT-value (value of quality in micrometers)''' + '''tables IT6 to IT11 from 0 to 500 mm''' + IT6 = [6,8,9,11,11,13,13,16,16,19,19,22,22,25,25,25,29,29,29,32,32,36,36,40,40] + IT7 = [10,12,15,18,18,21,21,25,25,30,30,35,35,40,40,40,46,46,46,52,52,57,57,63,63] + IT8 = [14,18,22,27,27,33,33,39,39,46,46,54,54,63,63,63,72,72,72,81,81,89,89,97,97] + IT9 = [25,30,36,43,43,52,52,62,62,74,74,87,87,100,100,100,115,115,115,130,130,140,140,155,155] + IT10 = [40,48,58,70,70,84,84,100,100,120,120,140,140,160,160,160,185,185,185,210,210,230,230,250,250] + IT11 = [60,75,90,110,110,130,130,160,160,190,190,220,220,250,250,250,290,290,290,320,320,360,360,400,400] + qualityTable = [IT6,IT7,IT8,IT9,IT10,IT11] + return qualityTable[valueQuality-6][valueNominalRange] + + def getFieldValue(self,fieldCharacter,valueNominalRange): + '''return es or ES value of the field in micrometers''' + cField = [-60,-70,-80,-95,-95,-110,-110,-120,-130,-140,-150,-170,-180,-200,-210,-230,-240,-260,-280,-300,-330,-360,-400,-440,-480] + fField = [-6,-10,-13,-16,-16,-20,-20,-25,-25,-30,-30,-36,-36,-43,-43,-43,-50,-50,-50,-56,-56,-62,-62,-68,-68] + gField = [-2,-4,-5,-6,-6,-7,-7,-9,-9,-10,-10,-12,-12,-14,-14,-14,-15,-15,-15,-17,-17,-18,-18,-20,-20] + hField = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + kField = [6,9,10,12,12,15,15,18,18,21,21,25,25,28,28,28,33,33,33,36,36,40,40,45,45] + nField = [10,16,19,23,23,28,28,33,33,39,39,45,45,52,52,60,60,66,66,73,73,80,80] + rField = [16,23,28,34,34,41,41,50,50,60,62,73,76,88,90,93,106,109,113,126,130,144,150,166,172] + sField = [20,27,32,39,39,48,48,59,59,72,78,93,101,117,125,133,151,159,169,190,202,226,244,272,292] + DField = [60,78,98,120,120,149,149,180,180,220,220,260,260,305,305,305,355,355,355,400,400,440,440,480,480] + EField = [39,50,61,75,75,92,92,112,112,134,134,159,159,185,185,185,215,215,215,240,240,265,265,290,290] + FField = [20,28,35,43,43,53,53,64,64,76,76,90,90,106,106,106,122,122,122,137,137,151,151,165,165] + GField = [12,16,20,24,24,28,28,34,34,40,40,47,47,54,54,54,61,61,61,69,69,75,75,83,83] + HField = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + KField = [0,3,5,6,6,6,6,7,7,9,9,10,10,12,12,12,13,13,13,16,16,17,17,18,18] + NField = [-4,-4,-4,-5,-5,-7,-7,-8,-8,-9,-9,-10,-10,-12,-12,-12,-14,-14,-14,-14,-14,-16,-16,-17,-17] + RField = [-10,-11,-13,-16,-16,-20,-20,-25,-25,-30,-32,-38,-41,-48,-50,-53,-60,-63,-67,-74,-78,-87,-93,-103,-109] + SField = [-14,-15,-17,-21,-21,-27,-27,-34,-34,-42,-48,-58,-66,-77,-85,-93,-105,-113,-123,-138,-150,-169,-187,-209,-229] + fieldDict = {'c':cField,'f':fField,'g':gField,'h':hField,'k':kField,'n':nField,'r':rField,'s':sField, + 'D':DField,'E':EField,'F':FField,'G':GField,'H':HField,'K':KField,'N':NField,'R':RField,'S':SField} + return fieldDict[fieldCharacter][valueNominalRange] + + def calculate(self,value,fieldChar,quality): + '''calculate upper and lower field values''' + self.nominalRange = self. getNominalRange(value) + self.upperValue = self.getFieldValue(fieldChar,self.nominalRange) + self.lowerValue = self.upperValue-self.getITValue(quality,self.nominalRange) + if fieldChar == 'H': + self.upperValue = -self.lowerValue + self.lowerValue = 0 + + def getValues(self): + '''return range values in mm''' + return (self.upperValue/1000,self.lowerValue/1000) \ No newline at end of file diff --git a/src/Mod/TechDraw/TechDrawTools/__init__.py b/src/Mod/TechDraw/TechDrawTools/__init__.py index 37cabe6849..dd21abe35b 100644 --- a/src/Mod/TechDraw/TechDrawTools/__init__.py +++ b/src/Mod/TechDraw/TechDrawTools/__init__.py @@ -34,5 +34,7 @@ from .TDToolsUtil import * from .CommandShareView import CommandShareView from .CommandMoveView import CommandMoveView from .CommandAxoLengthDimension import CommandAxoLengthDimension +from .CommandHoleShaftFit import CommandHoleShaftFit from .TaskShareView import TaskShareView from .TaskMoveView import TaskMoveView +from .TaskHoleShaftFit import TaskHoleShaftFit diff --git a/tests/src/App/Metadata.cpp b/tests/src/App/Metadata.cpp index 59fe73923d..97389d3cb1 100644 --- a/tests/src/App/Metadata.cpp +++ b/tests/src/App/Metadata.cpp @@ -1,7 +1,32 @@ +/************************************************************************** +* * +* Copyright (c) 2021-2023 FreeCAD Project Association * +* * +* 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 * +* . * +* * +***************************************************************************/ + +// NOLINTNEXTLINE #include "gtest/gtest.h" #include "App/Metadata.h" +// NOLINTBEGIN(readability-named-parameter) + TEST(ContactTest, ContactDefaultConstruction){ auto contact = App::Meta::Contact(); ASSERT_EQ(contact.name,""); @@ -128,6 +153,7 @@ TEST(VersionTest, VersionOperatorComparison){ auto version_2_2_4_delta = App::Meta::Version(2,2,4,"delta"); auto version_2_4_4_delta = App::Meta::Version(2,4,4,"delta"); auto version_2_3_3_delta = App::Meta::Version(2,3,3,"delta"); + // NOLINTNEXTLINE Five is not really a "magic number" in this test auto version_2_3_5_delta = App::Meta::Version(2,3,5,"delta"); auto version_2_3_4_epsilon = App::Meta::Version(2,3,4,"epsilon"); auto version_2_3_4_beta = App::Meta::Version(2,3,4,"beta"); @@ -147,19 +173,14 @@ TEST(VersionTest, VersionOperatorComparison){ class MetadataTest : public ::testing::Test { protected: void SetUp() override { - try { - xercesc_3_2::XMLPlatformUtils::Initialize(); - } - catch (const xercesc_3_2::XMLException& toCatch) { - // Some kind of Google Test error condition? - } + xercesc_3_2::XMLPlatformUtils::Initialize(); } void TearDown() override { xercesc_3_2::XMLPlatformUtils::Terminate(); } std::string GivenSimpleMetadataXMLString() { - std::stringstream stream; + std::ostringstream stream; stream << "\n" << "\n" << " " << _name << "\n" @@ -176,7 +197,7 @@ protected: ASSERT_EQ(testObject.description(), _description); ASSERT_EQ(testObject.version(), App::Meta::Version(_version)); } - +private: std::string _name = "TestAddon"; std::string _description = "A package.xml file for unit testing."; std::string _version = "1.2.3beta"; @@ -188,3 +209,4 @@ TEST_F(MetadataTest, MetadataInMemoryConstruction) { AssertMetadataMatches(testObject); } +// NOLINTEND(readability-named-parameter)