diff --git a/.travis.yml b/.travis.yml index 6347a0e33b9e..6823b925f189 100755 --- a/.travis.yml +++ b/.travis.yml @@ -19,34 +19,40 @@ ## env: global: - - FREECAD_RELEASE="0.18" - - DEPLOY_RELEASE=${DEPLOY_RELEASE:-$FREECAD_RELEASE} - - CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} - - OSX_PORTS_CACHE=${OSX_PORTS_CACHE:-FreeCAD/FreeCAD-ports-cache} - - DEPLOY=${DEPLOY:-0} - GENERATOR="Unix Makefiles" - matrix: + - CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} +# - FREECAD_RELEASE="0.18" +# - DEPLOY_RELEASE=${DEPLOY_RELEASE:-$FREECAD_RELEASE} +# - OSX_PORTS_CACHE=${OSX_PORTS_CACHE:-FreeCAD/FreeCAD-ports-cache} +# - DEPLOY=${DEPLOY:-0} + +# jobs: # chunk.io key (if needed, obtain it with Yorik, PrzemoF, Kunda1) - secure: MJu0ZU/9Yqut6bUkHoSrXTV/c/WhCLR0KnHKCsnEU081PYoukzH6ngzgKk7/trAH2In080d/ra4B2OmTNl/LAgV6DXKFY9dO1aG8QwcrHgaMPf0pHYUy/OfwQSFYFByQDV2OEMAHcIWc/dtNkzK2QUi44Kn7d0GtSEiN4s816lriWtjg0vmEGAU8MjvcAGss4gKyn05Xm1NUCYPKgpgIHsywLbpE76lv0eOYoosEuKv5Q9Pb4FMQts02+JUlqE8eY4ZZ3nV8iQbgIDdseOSA7Ixn05zWjU/ZRZ74TrYxMnzfUAwQcJe9OcqoESq+pPWQt5HYG66VmeVxQim1gmsiDASH51U/nswKt0Q4bISj3tVk0YZMFV8Ax+SzPvLEmFZJQGfgO1mg7HdNcz9N9G5JHPawrV19DwYIEFbAw8MCSAoIXFOcPQZUWXCbtjm7NO9vCjMrqyVJMDD9L8omvQajHoajuHbOT8KB250gFokeLj3z8yu++Tz+IrZX5inUMrXsARVt/ALXpi8rJPXmoFMpMUjyWmDPqPWlnqUhLtTtEtKpuOWP8ZnWVwkg4QYOUhCy95C1okJSGkG+ylHWncWfY4mS+UBT525laoh+GOhH+sRW+p2xkI21xGFRqg1oHjjgY1yIYF6nnSHPzxMBRYmZwagyXsjkFG5FPMWR2oYk0Yg -cache: - ccache: true - directories: - - $HOME/.ccache + # secure: MJu0ZU/9Yqut6bUkHoSrXTV/c/WhCLR0KnHKCsnEU081PYoukzH6ngzgKk7/trAH2In080d/ra4B2OmTNl/LAgV6DXKFY9dO1aG8QwcrHgaMPf0pHYUy/OfwQSFYFByQDV2OEMAHcIWc/dtNkzK2QUi44Kn7d0GtSEiN4s816lriWtjg0vmEGAU8MjvcAGss4gKyn05Xm1NUCYPKgpgIHsywLbpE76lv0eOYoosEuKv5Q9Pb4FMQts02+JUlqE8eY4ZZ3nV8iQbgIDdseOSA7Ixn05zWjU/ZRZ74TrYxMnzfUAwQcJe9OcqoESq+pPWQt5HYG66VmeVxQim1gmsiDASH51U/nswKt0Q4bISj3tVk0YZMFV8Ax+SzPvLEmFZJQGfgO1mg7HdNcz9N9G5JHPawrV19DwYIEFbAw8MCSAoIXFOcPQZUWXCbtjm7NO9vCjMrqyVJMDD9L8omvQajHoajuHbOT8KB250gFokeLj3z8yu++Tz+IrZX5inUMrXsARVt/ALXpi8rJPXmoFMpMUjyWmDPqPWlnqUhLtTtEtKpuOWP8ZnWVwkg4QYOUhCy95C1okJSGkG+ylHWncWfY4mS+UBT525laoh+GOhH+sRW+p2xkI21xGFRqg1oHjjgY1yIYF6nnSHPzxMBRYmZwagyXsjkFG5FPMWR2oYk0Yg + +git: + depth: 5000 -language: cpp -python: - - 2.7 - - 3.6 +notifications: + email: false + webhooks: + urls: + - https://webhooks.gitter.im/e/479456663cdf5c84e4d8 + on_success: always + on_failure: always + on_start: change # Inject osx build into matrix - needed to specify image/dist -matrix: - # allow_failures: - # - python: 3.7 +jobs: +# allow_failures: +# - python: 3.7 fast_finish: true # https://blog.travis-ci.com/2013-11-27-fast-finishing-builds include: - os: linux dist: bionic + language: cpp compiler: clang + cache: ccache addons: apt: sources: @@ -58,52 +64,51 @@ matrix: env: - CC=clang-9 - CXX=clang++-9 - - CMAKE_OPTS="-DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_FEM_NETGEN=ON -DBUILD_QT5=ON" - - PYTHON_MAJOR_VERSION=3 + - CMAKE_ARGS="-DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_FEM_NETGEN=ON -DBUILD_QT5=ON" - CACHE_NAME=JOB1 - # - os: linux - # dist: bionic - # compiler: gcc - # addons: - # apt: - # sources: - # - sourceline: 'ppa:ubuntu-toolchain-r/test' - # packages: - # - gcc-9 - # - g++-9 - # env: - # - CC=gcc-9 - # - CXX=g++-9 - # - CMAKE_OPTS="-DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_FEM_NETGEN=ON -DBUILD_QT5=ON" - # - PYTHON_MAJOR_VERSION=3 - # - CACHE_NAME=JOB2 - - os: linux dist: bionic - compiler: gcc + language: cpp + compiler: gcc-9 + cache: ccache + addons: + apt: + sources: + - sourceline: 'ppa:ubuntu-toolchain-r/test' + packages: + - gcc-9 + - g++-9 env: - - CMAKE_OPTS="-DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_FEM_NETGEN=ON -DBUILD_QT5=ON" - - PYTHON_MAJOR_VERSION=3 - - CACHE_NAME=JOB3 + - CC=gcc-9 + - CXX=g++-9 + - CC_FOR_BUILD=gcc-9 + - CXX_FOR_BUILD=g++-9 + - CMAKE_ARGS="-DCMAKE_CXX_COMPILER=/usr/bin/c++ -DCMAKE_C_COMPILER=/usr/bin/cc -DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_FEM_NETGEN=ON -DBUILD_QT5=ON" + - CACHE_NAME=JOB2 - os: linux dist: bionic - compiler: gcc - env: - - PYTHON_MAJOR_VERSION=2 - - CACHE_NAME=JOB4 + language: cpp + compiler: gcc-7 + cache: ccache + env: + - CC=gcc-7 + - CXX=g++-7 + - CC_FOR_BUILD=gcc-7 + - CXX_FOR_BUILD=g++-7 + - CMAKE_ARGS="-DCMAKE_CXX_COMPILER=/usr/bin/c++ -DCMAKE_C_COMPILER=/usr/bin/cc" + - CACHE_NAME=JOB3 - os: windows language: cpp - filter_secrets: false env: - CMAKE_OPTS="-DBUILD_FEM_NETGEN=ON -DFREECAD_RELEASE_PDB=OFF" - GENERATOR="Visual Studio 15 2017 Win64" - - PYTHON_MAJOR_VERSION=3 + - PYTHON_MAJOR_VERSION=3 - MSBUILD_PATH="c:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin" - TEST_PATH="C:\Users\travis\build\FreeCAD\FreeCAD\build\bin" - - CLCACHE_PATH="C:\Users\travis\build\FreeCAD\FreeCAD\" + - CLCACHE_PATH="C:\Users\travis\build\FreeCAD\FreeCAD" - VS15=true - CCACHE_TEMPDIR=/tmp/.ccache-temp - CCACHE_COMPRESS=1 @@ -123,20 +128,9 @@ matrix: # # fail the build if there are Python syntax errors or undefined names # script: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics -git: - depth: 5000 - -notifications: - email: false - webhooks: - urls: - - https://webhooks.gitter.im/e/479456663cdf5c84e4d8 - on_success: always - on_failure: always - on_start: change before_install: -- eval "$(curl -fsSL "https://raw.githubusercontent.com/${OSX_PORTS_CACHE}/v${FREECAD_RELEASE}/travis-helpers.sh")" +# - eval "$(curl -fsSL "https://raw.githubusercontent.com/${OSX_PORTS_CACHE}/v${FREECAD_RELEASE}/travis-helpers.sh")" - | case "${TRAVIS_OS_NAME}" in @@ -192,57 +186,50 @@ before_install: libmetis-dev \ libspnav-dev # Runtime deps - sudo apt-get install -y --no-install-recommends freecad-daily-python3 + sudo apt-get install -y --no-install-recommends freecad-daily-python3 python-ply python3-ply export DISPLAY=:99.0 sh -e /etc/init.d/xvfb start - export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/netgen/:$LD_LIRBARY_PATH - export CCACHE_CPP2=YES - if [[ ${PYTHON_MAJOR_VERSION} == 2 ]] - then - export CMAKE_ARGS="${CMAKE_OPTS} -DPYTHON_EXECUTABLE=/usr/bin/python" - else - export CMAKE_ARGS="${CMAKE_OPTS} -DPYTHON_EXECUTABLE=/usr/bin/python3" - fi export INSTALLED_APP_PATH="/usr/local/bin/FreeCAD" + export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/netgen/:$LD_LIRBARY_PATH ;; "osx") - xcodebuild -version -sdk - brew --config - if [ "${OSX_PORTS_CACHE}X" != "X" ]; then - brew install jq - cacheContext=$(create_helper_context repo=${OSX_PORTS_CACHE} auth_token=${GH_TOKEN} release=${FREECAD_RELEASE}) - travis_wait prime_local_ports_cache $cacheContext - fi - brew update >/dev/null - brew --config + #xcodebuild -version -sdk + #brew --config + #if [ "${OSX_PORTS_CACHE}X" != "X" ]; then + # brew install jq + # cacheContext=$(create_helper_context repo=${OSX_PORTS_CACHE} auth_token=${GH_TOKEN} release=${FREECAD_RELEASE}) + # travis_wait prime_local_ports_cache $cacheContext + #fi + #brew update >/dev/null + #brew --config - brew tap FreeCAD/freecad + #brew tap FreeCAD/freecad - brew install --verbose --only-dependencies freecad --with-packaging-utils - pip install six - # Qt5: Set Qt5 build flag and CMAKE_PREFIX - QT5_CMAKE_PREFIX=$(ls -d $(brew --cellar)/qt/*/lib/cmake) - QT5_WEBKIT_CMAKE_PREFIX=$(ls -d $(brew --cellar)/qtwebkit/*/lib/cmake) - CMAKE_OPTS="${CMAKE_OPTS} -DBUILD_QT5=ON -DCMAKE_PREFIX_PATH=${QT5_CMAKE_PREFIX};${QT5_WEBKIT_CMAKE_PREFIX}" + #brew install --verbose --only-dependencies freecad --with-packaging-utils + #pip install six + ## Qt5: Set Qt5 build flag and CMAKE_PREFIX + #QT5_CMAKE_PREFIX=$(ls -d $(brew --cellar)/qt/*/lib/cmake) + #QT5_WEBKIT_CMAKE_PREFIX=$(ls -d $(brew --cellar)/qtwebkit/*/lib/cmake) + #CMAKE_OPTS="${CMAKE_OPTS} -DBUILD_QT5=ON -DCMAKE_PREFIX_PATH=${QT5_CMAKE_PREFIX};${QT5_WEBKIT_CMAKE_PREFIX}" - #Install the 3DConnexion frameworks - if [ "${DEPLOY}" == "1" ]; then - if [ ! -d /Library/Frameworks/3DconnexionClient.framework ]; then - curl -o /tmp/3dFW.dmg -L 'http://www.3dconnexion.com/index.php?eID=sdl&ext=tx_iccsoftware&oid=a273bdbc-c289-e10d-816b-567043331c9e&filename=3DxWareMac_v10-4-1_r2428.dmg' - hdiutil attach -readonly /tmp/3dFW.dmg - sudo installer -package /Volumes/3Dconnexion\ Software/Install\ 3Dconnexion\ software.pkg -target / - diskutil eject /Volumes/3Dconnexion\ Software - fi - export CMAKE_OPTS="${CMAKE_OPTS} -DFREECAD_CREATE_MAC_APP=ON" - export INSTALLED_APP_PATH="/usr/local/FreeCAD.app/Contents/MacOS/FreeCAD" - else - export INSTALLED_APP_PATH="/usr/local/MacOS/FreeCAD" - fi + ##Install the 3DConnexion frameworks + #if [ "${DEPLOY}" == "1" ]; then + # if [ ! -d /Library/Frameworks/3DconnexionClient.framework ]; then + # curl -o /tmp/3dFW.dmg -L 'http://www.3dconnexion.com/index.php?eID=sdl&ext=tx_iccsoftware&oid=a273bdbc-c289-e10d-816b-567043331c9e&filename=3DxWareMac_v10-4-1_r2428.dmg' + # hdiutil attach -readonly /tmp/3dFW.dmg + # sudo installer -package /Volumes/3Dconnexion\ Software/Install\ 3Dconnexion\ software.pkg -target / + # diskutil eject /Volumes/3Dconnexion\ Software + # fi + # export CMAKE_OPTS="${CMAKE_OPTS} -DFREECAD_CREATE_MAC_APP=ON" + # export INSTALLED_APP_PATH="/usr/local/FreeCAD.app/Contents/MacOS/FreeCAD" + #else + # export INSTALLED_APP_PATH="/usr/local/MacOS/FreeCAD" + #fi - export CMAKE_ARGS="${CMAKE_OPTS} -DFREECAD_USE_EXTERNAL_KDL=ON" + #export CMAKE_ARGS="${CMAKE_OPTS} -DFREECAD_USE_EXTERNAL_KDL=ON" ;; "windows") @@ -254,16 +241,16 @@ before_install: export PATH=$CLCACHE_PATH:$PATH export PATH=$TEST_PATH:$PATH #reset clcache hit stats - cmd.exe /C 'C:\Users\travis\build\FreeCAD\FreeCAD\clcache.exe -z' + cmd.exe //C 'C:\Users\travis\build\FreeCAD\FreeCAD\clcache.exe -z' # clcache stats before compilation - cmd.exe /C 'C:\Users\travis\build\FreeCAD\FreeCAD\clcache.exe -s' + cmd.exe //C 'C:\Users\travis\build\FreeCAD\FreeCAD\clcache.exe -s' curl -L https://github.com/FreeCAD/FreeCAD/releases/download/0.19_pre/FreeCADLibs_12.1.4_x64_VC15.7z --output FreeCADLibs.7z 7z x FreeCADLibs.7z -oFreeCADLibs > /dev/null rm -f FreeCADLibs.7z - export LIBPACK_DIR="$TRAVIS_BUILD_DIR\FreeCADLibs" + export FREECAD_LIBPACK_DIR="$TRAVIS_BUILD_DIR\FreeCADLibs\FreeCADLibs_12.1.4_x64_VC15" - export CMAKE_ARGS="${CMAKE_OPTS} -DFREECAD_LIBPACK_DIR=$TRAVIS_BUILD_DIR\FreeCADLibs -DPYTHON_EXECUTABLE=$TRAVIS_BUILD_DIR\FreeCADLibs\bin\python.exe \ + export CMAKE_ARGS="${CMAKE_OPTS} -DFREECAD_LIBPACK_DIR=$TRAVIS_BUILD_DIR\FreeCADLibs\FreeCADLibs_12.1.4_x64_VC15 -DPYTHON_EXECUTABLE=$TRAVIS_BUILD_DIR\FreeCADLibs\FreeCADLibs_12.1.4_x64_VC15\bin\python.exe \ -DBUILD_QT5=TRUE" export PATH=$MSBUILD_PATH:$PATH ;; @@ -287,17 +274,18 @@ script: # MSBuild.exe /m FreeCAD.sln - | if [ "${TRAVIS_OS_NAME}" == "windows" ]; then + # https://travis-ci.community/t/vcvarsall-bat-freezes-on-new-1809-based-windows-images/7098/6 # call msbuild using clcache - #cmd.exe /C '"C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64 && MSBuild.exe FreeCAD.sln /p:CLToolExe=clcache.exe /p:TrackFileAccess=false /p:CLToolPath=C:\Users\travis\build\FreeCAD\FreeCAD /m:2 /nologo /verbosity:minimal /p:Configuration=Release /p:Platform=x64' - cmd.exe /C 'C:\Users\travis\build\FreeCAD\FreeCAD\.travis\build.bat' + #cmd.exe //C '"C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64 && MSBuild.exe FreeCAD.sln /p:CLToolExe=clcache.exe /p:TrackFileAccess=false /p:CLToolPath=C:\Users\travis\build\FreeCAD\FreeCAD /m:2 /nologo /verbosity:minimal /p:Configuration=Release /p:Platform=x64' + cmd.exe //C 'C:\Users\travis\build\FreeCAD\FreeCAD\.travis\build.bat' # ls -lahR - du -hs bin/ - mv ../FreeCADLibs/bin/* bin/ + mkdir bin/ + mv ../FreeCADLibs/FreeCADLibs_12.1.4_x64_VC15/bin/* bin/ du -hs bin/ # show clcache hit stats - cmd.exe /C 'C:\Users\travis\build\FreeCAD\FreeCAD\clcache.exe -s' + cmd.exe //C 'C:\Users\travis\build\FreeCAD\FreeCAD\clcache.exe -s' # run the tests - #cmd.exe /C 'cd C:\Users\travis\build\FreeCAD\FreeCAD\build\bin && FreeCADCmd.exe --run-test 0' + #cmd.exe //C 'cd C:\Users\travis\build\FreeCAD\FreeCAD\build\bin && FreeCADCmd.exe --run-test 0' # Make build fail if ANY of the following fails #set -ev winpty.exe -Xallow-non-tty -Xplain /C/Users/travis/build/FreeCAD/FreeCAD/build/bin/FreeCADCmd.exe --run-test 0 | tee runlog.txt @@ -305,9 +293,12 @@ script: else # Stop compiling (GCC) after 2 hrs 50 min (3 hrs limit). # Preserves created ccache for the next build job. - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then sudo timeout -k 175m 170m make -j2 install || true; fi - if [ "${TRAVIS_OS_NAME}" == "osx" ]; then sudo make -j2 install; fi - # sudo make -j2 install + # if [ "${TRAVIS_OS_NAME}" == "linux" ]; then sudo timeout -k 175m 170m make -j2 install || true; fi + # if [ "${TRAVIS_OS_NAME}" == "osx" ]; then sudo make -j2 install; fi + cat $HOME/.ccache/ccache.conf + ccache -z -s + sudo make -j2 install + ccache -s ${INSTALLED_APP_PATH} --console --run-test 0 || travis_terminate 1 ${INSTALLED_APP_PATH} --log-file /tmp/FreeCAD_installed.log & sleep 10 && pkill FreeCAD @@ -315,21 +306,21 @@ script: grep --file=../.log_errors /tmp/FreeCAD_installed.log ; [ $? == 1 ] && echo "No errors from .log_errors file found in the log after start from /usr/local/bin" || ( echo "Error from .log_errors found!" && false ) fi -after_success: -#### +#after_success: # Package and deploy the build to GitHub. This will only run for builds on # master (i.e. pull requests are only built and tested but not deployed). # # GH_TOKEN must be set in order to deploy releases to GitHub -## -- | - if [ "${TRAVIS_OS_NAME}" == "osx" -a "${TRAVIS_PULL_REQUEST}" == "false" -a "${DEPLOY}" == "1" ]; then - brew ls --versions jq || brew install jq - npm install -g appdmg - export VSN=$(python ${TRAVIS_BUILD_DIR}/src/Tools/ArchiveNameFromVersionHeader.py ${TRAVIS_BUILD_DIR}/build/src/Build/Version.h) - export DEPLOYMENT_ARCHIVE=${VSN}-${QT}.dmg - appdmg ${TRAVIS_BUILD_DIR}/src/MacAppBundle/DiskImage/layout.json "${DEPLOYMENT_ARCHIVE}" - deployContext=$(create_helper_context repo=${TRAVIS_REPO_SLUG} auth_token=${GH_TOKEN} release=${DEPLOY_RELEASE}) - gitHub_deploy_asset_to_release_named $deployContext ${DEPLOYMENT_ARCHIVE} - gitHub_prune_assets_for_release_named $deployContext "-${QT}" 1 - fi +# +#- | +# if [ "${TRAVIS_OS_NAME}" == "osx" -a "${TRAVIS_PULL_REQUEST}" == "false" -a "${DEPLOY}" == "1" ]; then +# brew ls --versions jq || brew install jq +# npm install -g appdmg +# export VSN=$(python ${TRAVIS_BUILD_DIR}/src/Tools/ArchiveNameFromVersionHeader.py ${TRAVIS_BUILD_DIR}/build/src/Build/Version.h) +# export DEPLOYMENT_ARCHIVE=${VSN}-${QT}.dmg +# appdmg ${TRAVIS_BUILD_DIR}/src/MacAppBundle/DiskImage/layout.json "${DEPLOYMENT_ARCHIVE}" +# deployContext=$(create_helper_context repo=${TRAVIS_REPO_SLUG} auth_token=${GH_TOKEN} release=${DEPLOY_RELEASE}) +# gitHub_deploy_asset_to_release_named $deployContext ${DEPLOYMENT_ARCHIVE} +# gitHub_prune_assets_for_release_named $deployContext "-${QT}" 1 +# fi + diff --git a/CMakeLists.txt b/CMakeLists.txt index f5347f3b4ffe..2c54472dbf9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ set(PACKAGE_VERSION_NAME "Vulcan") set(PACKAGE_VERSION_MAJOR "0") set(PACKAGE_VERSION_MINOR "19") set(PACKAGE_VERSION_PATCH "16100") +set(PACKAGE_VERSION_SUFFIX "dev") # either "dev" for development snapshot or "" (empty string) +set(FREECAD_VERSION_PATCH "0") # number of patch release (e.g. "4" for the 0.18.4 release) set(FREECAD_VERSION "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}") set(PACKAGE_VERSION "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}") diff --git a/cMake/FreeCAD_Helpers/SetGlobalCompilerAndLinkerSettings.cmake b/cMake/FreeCAD_Helpers/SetGlobalCompilerAndLinkerSettings.cmake index 4f4ddd252716..514c4aad2451 100644 --- a/cMake/FreeCAD_Helpers/SetGlobalCompilerAndLinkerSettings.cmake +++ b/cMake/FreeCAD_Helpers/SetGlobalCompilerAndLinkerSettings.cmake @@ -13,7 +13,11 @@ macro(SetGlobalCompilerAndLinkerSettings) message(STATUS "Platform is 32-bit") endif(CMAKE_SIZEOF_VOID_P EQUAL 8) - + # check for mips64 platform + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "mips64") + message(STATUS "Architecture: mips64") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mxgot") + endif() if(MSVC) # set default compiler settings diff --git a/cMake/FreeCAD_Helpers/SetupPython.cmake b/cMake/FreeCAD_Helpers/SetupPython.cmake index f7e4cdd31ff5..3c8e10635649 100644 --- a/cMake/FreeCAD_Helpers/SetupPython.cmake +++ b/cMake/FreeCAD_Helpers/SetupPython.cmake @@ -119,6 +119,12 @@ macro(SetupPython) endif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin" AND NOT BUILD_WITH_CONDA) find_package(PythonInterp REQUIRED) + + # Issue in cmake prevents finding pythonlibs 3.x when python 2.7 is present + # setting NOTFOUND here resolves the issue + set(PYTHON_INCLUDE_DIR "NOTFOUND") + set(PYTHON_LIBRARY "NOTFOUND") + set(Python_ADDITIONAL_VERSIONS ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}) if (NOT DEFINED PYTHON_VERSION_STRING) find_package(PythonLibs REQUIRED) diff --git a/src/App/Application.cpp b/src/App/Application.cpp index 53a0bd44f4cc..64aeef02d1f0 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -621,7 +621,7 @@ std::vector Application::openDocuments(const std::vector for (auto &name : filenames) _pendingDocs.push_back(name.c_str()); - std::deque > newDocs; + std::map newDocs; FC_TIME_INIT(t); @@ -655,7 +655,7 @@ std::vector Application::openDocuments(const std::vector auto doc = openDocumentPrivate(path, name, label, isMainDoc, createView, objNames); FC_DURATION_PLUS(timing.d1,t1); if (doc) - newDocs.emplace_front(doc,timing); + newDocs.emplace(doc,timing); if (isMainDoc) res[count] = doc; @@ -703,19 +703,50 @@ std::vector Application::openDocuments(const std::vector _pendingDocMap.clear(); Base::SequencerLauncher seq("Postprocessing...", newDocs.size()); + + std::vector docs; + docs.reserve(newDocs.size()); for (auto &v : newDocs) { + // Notify ProeprtyXLink to attach newly opened documents and restore + // relavant external links + PropertyXLink::restoreDocument(*v.first); + docs.push_back(v.first); + } + + // After external links has been restored, we can now sort the document + // according to their dependency order. + docs = Document::getDependentDocuments(docs, true); + for (auto it=docs.begin(); it!=docs.end();) { + Document *doc = *it; + // It is possible that the newly opened document depends on an existing + // document, which will be included with the above call to + // Document::getDependentDocuments(). Make sure to exclude that. + auto dit = newDocs.find(doc); + if (dit == newDocs.end()) { + it = docs.erase(it); + continue; + } + ++it; FC_TIME_INIT(t1); - v.first->afterRestore(true); - FC_DURATION_PLUS(v.second.d2,t1); + // Finalize document restoring with the correct order + doc->afterRestore(true); + FC_DURATION_PLUS(dit->second.d2,t1); seq.next(); } - if (!newDocs.empty()) - setActiveDocument(newDocs.back().first); + // Set the active document using the first successfully restored main + // document (i.e. documents explicitly asked for by caller). + for (auto doc : res) { + if (doc) { + setActiveDocument(doc); + break; + } + } - for (auto &v : newDocs) { - FC_DURATION_LOG(v.second.d1, v.first->getName() << " restore"); - FC_DURATION_LOG(v.second.d2, v.first->getName() << " postprocess"); + for (auto doc : docs) { + auto &timing = newDocs[doc]; + FC_DURATION_LOG(timing.d1, doc->getName() << " restore"); + FC_DURATION_LOG(timing.d2, doc->getName() << " postprocess"); } FC_TIME_LOG(t,"total"); @@ -1702,6 +1733,7 @@ void Application::initTypes(void) App ::PropertyXLink ::init(); App ::PropertyXLinkSub ::init(); App ::PropertyXLinkSubList ::init(); + App ::PropertyXLinkList ::init(); App ::PropertyXLinkContainer ::init(); App ::PropertyMatrix ::init(); App ::PropertyVector ::init(); diff --git a/src/App/Application.h b/src/App/Application.h index 1605a48ce469..642cfce019eb 100644 --- a/src/App/Application.h +++ b/src/App/Application.h @@ -48,6 +48,7 @@ class DocumentObject; class ApplicationObserver; class Property; class AutoTransaction; +class ExtensionContainer; enum GetLinkOption { /// Get all links (both directly and in directly) linked to the given object @@ -261,6 +262,18 @@ class AppExport Application /// signal on about changing the editor mode of a property boost::signals2::signal signalChangePropertyEditor; //@} + + /** @name Signals of extension changes + * These signals are emitted on dynamic extension addition. Dynamic extensions are the ones added by python (c++ ones are part + * of the class definition, hence not dynamic) + * The extension in question is provided as parameter. + */ + //@{ + /// signal before adding the extension + boost::signals2::signal signalBeforeAddingDynamicExtension; + /// signal after the extension was added + boost::signals2::signal signalAddedDynamicExtension; + //@} /** @name methods for parameter handling */ diff --git a/src/App/AutoTransaction.cpp b/src/App/AutoTransaction.cpp index 82ccc8cec9d1..6094ef207040 100644 --- a/src/App/AutoTransaction.cpp +++ b/src/App/AutoTransaction.cpp @@ -21,7 +21,9 @@ ****************************************************************************/ #include "PreCompiled.h" + #include +#include #include "Application.h" #include "Transactions.h" #include "Document.h" @@ -31,6 +33,9 @@ FC_LOG_LEVEL_INIT("App",true,true) using namespace App; +static int _TransactionLock; +static int _TransactionClosed; + AutoTransaction::AutoTransaction(const char *name, bool tmpName) { auto &app = GetApplication(); if(name && app._activeTransactionGuard>=0) { @@ -125,7 +130,11 @@ int Application::setActiveTransaction(const char *name, bool persist) { AutoTransaction::setEnable(false); return 0; } - }else{ + } else if (_TransactionLock) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Transaction locked, ignore new transaction '" << name << "'"); + return 0; + } else { FC_LOG("set active transaction '" << name << "'"); _activeTransactionID = 0; for(auto &v : DocMap) @@ -156,10 +165,17 @@ void Application::closeActiveTransaction(bool abort, int id) { return; } + if(_TransactionLock) { + if(_TransactionClosed >= 0) + _TransactionLock = abort?-1:1; + FC_LOG("pending " << (abort?"abort":"close") << " transaction"); + return; + } + FC_LOG("close transaction '" << _activeTransactionName << "' " << abort); _activeTransactionID = 0; - TransactionSignaller siganller(abort,false); + TransactionSignaller signaller(abort,false); for(auto &v : DocMap) { if(v.second->getTransactionID(true) != id) continue; @@ -170,3 +186,57 @@ void Application::closeActiveTransaction(bool abort, int id) { } } +//////////////////////////////////////////////////////////////////////// + +TransactionLocker::TransactionLocker(bool lock) + :active(lock) +{ + if(lock) + ++_TransactionLock; +} + +TransactionLocker::~TransactionLocker() +{ + if(active) { + try { + activate(false); + return; + } catch (Base::Exception &e) { + e.ReportException(); + } catch (Py::Exception &) { + Base::PyException e; + e.ReportException(); + } catch (std::exception &e) { + FC_ERR(e.what()); + } catch (...) { + } + FC_ERR("Exception when unlocking transaction"); + } +} + +void TransactionLocker::activate(bool enable) +{ + if(active == enable) + return; + + active = enable; + if(active) { + ++_TransactionLock; + return; + } + + if(--_TransactionLock != 0) + return; + + if(_TransactionClosed) { + bool abort = (_TransactionClosed<0); + _TransactionClosed = 0; + GetApplication().closeActiveTransaction(abort); + } +} + +bool TransactionLocker::isLocked() { + return _TransactionLock > 0; +} + + diff --git a/src/App/AutoTransaction.h b/src/App/AutoTransaction.h index f22fc123a4b6..e792b41b8956 100644 --- a/src/App/AutoTransaction.h +++ b/src/App/AutoTransaction.h @@ -25,6 +25,8 @@ namespace App { +class Application; + /// Helper class to manager transaction (i.e. undo/redo) class AppExport AutoTransaction { private: @@ -79,6 +81,51 @@ class AppExport AutoTransaction { int tid = 0; }; + +/** Helper class to lock a transaction from being closed or aborted. + * + * The helper class is used to protect some critical transaction from being + * closed prematurely, e.g. when deleting some object. + */ +class AppExport TransactionLocker { +public: + + /** Constructor + * @param lock: whether to activate the lock + */ + TransactionLocker(bool lock=true); + + /** Destructor + * Unlock the transaction is this locker is active + */ + ~TransactionLocker(); + + /** Activate or deactivate this locker + * @param enable: whether to activate the locker + * + * An internal counter is used to support recursive locker. When activated, + * the current active transaction cannot be closed or aborted. But the + * closing call (Application::closeActiveTransaction()) will be remembered, + * and performed when the internal lock counter reaches zero. + */ + void activate(bool enable); + + /// Check if the locker is active + bool isActive() const {return active;} + + /// Check if transaction is being locked + static bool isLocked(); + + friend class Application; + +private: + /// Private new operator to prevent heap allocation + void* operator new(size_t size); + +private: + bool active; +}; + } // namespace App #endif // APP_AUTOTRANSACTION_H diff --git a/src/App/Document.cpp b/src/App/Document.cpp index cf54a7924e3d..10d639de1c68 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -91,6 +91,7 @@ recompute path. Also, it enables more complicated dependencies beyond trees. #include #include +#include "AutoTransaction.h" #include "Document.h" #include "Application.h" #include "DocumentObject.h" @@ -1831,7 +1832,8 @@ void Document::writeObjects(const std::vector& obj, if(!isExporting(0)) { for(auto o : obj) { - const auto &outList = o->getOutList(DocumentObject::OutListNoHidden); + const auto &outList = o->getOutList(DocumentObject::OutListNoHidden + | DocumentObject::OutListNoXLinked); writer.Stream() << writer.ind() << "<" FC_ELEMENT_OBJECT_DEPS " " FC_ATTR_DEP_OBJ_NAME "=\"" << o->getNameInDocument() << "\" " FC_ATTR_DEP_COUNT "=\"" << outList.size(); @@ -3764,6 +3766,8 @@ void Document::removeObject(const char* sName) return; } + TransactionLocker tlock; + _checkTransaction(pos->second,0,__LINE__); #if 0 @@ -3865,6 +3869,8 @@ void Document::_removeObject(DocumentObject* pcObject) return; } + TransactionLocker tlock; + // TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer) _checkTransaction(pcObject,0,__LINE__); diff --git a/src/App/DocumentObject.cpp b/src/App/DocumentObject.cpp index 72b5b309cc9d..a3e88e9f358e 100644 --- a/src/App/DocumentObject.cpp +++ b/src/App/DocumentObject.cpp @@ -284,14 +284,24 @@ void DocumentObject::getOutList(int options, std::vector &res) std::vector props; getPropertyList(props); bool noHidden = !!(options & OutListNoHidden); - bool noXLinked = !!(options & OutListNoXLinked); + std::size_t size = res.size(); for(auto prop : props) { auto link = dynamic_cast(prop); - if(link && (!noXLinked || !PropertyXLink::supportXLink(prop))) + if(link) link->getLinks(res,noHidden); } if(!(options & OutListNoExpression)) ExpressionEngine.getLinks(res); + + if(options & OutListNoXLinked) { + for(auto it=res.begin()+size;it!=res.end();) { + auto obj = *it; + if(obj && obj->getDocument()!=getDocument()) + it = res.erase(it); + else + ++it; + } + } } std::vector DocumentObject::getOutListOfProperty(App::Property* prop) const diff --git a/src/App/DocumentObserver.cpp b/src/App/DocumentObserver.cpp index 18122c157adb..c88043d628fb 100644 --- a/src/App/DocumentObserver.cpp +++ b/src/App/DocumentObserver.cpp @@ -29,10 +29,13 @@ #include +#include #include "Application.h" #include "Document.h" #include "DocumentObject.h" #include "DocumentObserver.h" +#include "ComplexGeoData.h" +#include "GeoFeature.h" using namespace App; @@ -77,7 +80,7 @@ Document* DocumentT::getDocument() const return GetApplication().getDocument(document.c_str()); } -std::string DocumentT::getDocumentName() const +const std::string &DocumentT::getDocumentName() const { return document; } @@ -85,15 +88,7 @@ std::string DocumentT::getDocumentName() const std::string DocumentT::getDocumentPython() const { std::stringstream str; - Document* doc = GetApplication().getActiveDocument(); - if (doc && document == doc->getName()) { - str << "App.ActiveDocument"; - } - else { - str << "App.getDocument(\"" - << document - << "\")"; - } + str << "App.getDocument(\"" << document << "\")"; return str.str(); } @@ -103,11 +98,19 @@ DocumentObjectT::DocumentObjectT() { } +DocumentObjectT::DocumentObjectT(const DocumentObjectT &other) +{ + *this = other; +} + +DocumentObjectT::DocumentObjectT(DocumentObjectT &&other) +{ + *this = std::move(other); +} + DocumentObjectT::DocumentObjectT(const DocumentObject* obj) { - object = obj->getNameInDocument(); - label = obj->Label.getValue(); - document = obj->getDocument()->getName(); + *this = obj; } DocumentObjectT::DocumentObjectT(const Property* prop) @@ -115,35 +118,78 @@ DocumentObjectT::DocumentObjectT(const Property* prop) *this = prop; } +DocumentObjectT::DocumentObjectT(const char *docName, const char *objName) +{ + if(docName) + document = docName; + if(objName) + object = objName; +} + DocumentObjectT::~DocumentObjectT() { } -void DocumentObjectT::operator=(const DocumentObjectT& obj) +DocumentObjectT &DocumentObjectT::operator=(const DocumentObjectT& obj) { if (this == &obj) - return; + return *this; object = obj.object; label = obj.label; document = obj.document; property = obj.property; + return *this; +} + +DocumentObjectT &DocumentObjectT::operator=(DocumentObjectT&& obj) +{ + if (this == &obj) + return *this; + object = std::move(obj.object); + label = std::move(obj.label); + document = std::move(obj.document); + property = std::move(obj.property); + return *this; } void DocumentObjectT::operator=(const DocumentObject* obj) { - object = obj->getNameInDocument(); - label = obj->Label.getValue(); - document = obj->getDocument()->getName(); - property.clear(); + if(!obj || !obj->getNameInDocument()) { + object.clear(); + label.clear(); + document.clear(); + property.clear(); + } else { + object = obj->getNameInDocument(); + label = obj->Label.getValue(); + document = obj->getDocument()->getName(); + property.clear(); + } } void DocumentObjectT::operator=(const Property *prop) { - auto obj = dynamic_cast(prop->getContainer()); - assert(obj); - object = obj->getNameInDocument(); - label = obj->Label.getValue(); - document = obj->getDocument()->getName(); - property = prop->getName(); + if(!prop || !prop->getName() + || !prop->getContainer() + || !prop->getContainer()->isDerivedFrom(App::DocumentObject::getClassTypeId())) + { + object.clear(); + label.clear(); + document.clear(); + property.clear(); + } else { + auto obj = static_cast(prop->getContainer()); + object = obj->getNameInDocument(); + label = obj->Label.getValue(); + document = obj->getDocument()->getName(); + property = prop->getName(); + } +} + +bool DocumentObjectT::operator==(const DocumentObjectT &other) const { + return document == other.document + && object == other.object + && label == other.label + && property == other.property; } Document* DocumentObjectT::getDocument() const @@ -151,7 +197,7 @@ Document* DocumentObjectT::getDocument() const return GetApplication().getDocument(document.c_str()); } -std::string DocumentObjectT::getDocumentName() const +const std::string& DocumentObjectT::getDocumentName() const { return document; } @@ -159,15 +205,7 @@ std::string DocumentObjectT::getDocumentName() const std::string DocumentObjectT::getDocumentPython() const { std::stringstream str; - Document* doc = GetApplication().getActiveDocument(); - if (doc && document == doc->getName()) { - str << "App.ActiveDocument"; - } - else { - str << "App.getDocument(\"" - << document - << "\")"; - } + str << "FreeCAD.getDocument(\"" << document << "\")"; return str.str(); } @@ -181,12 +219,12 @@ DocumentObject* DocumentObjectT::getObject() const return obj; } -std::string DocumentObjectT::getObjectName() const +const std::string &DocumentObjectT::getObjectName() const { return object; } -std::string DocumentObjectT::getObjectLabel() const +const std::string &DocumentObjectT::getObjectLabel() const { return label; } @@ -194,21 +232,11 @@ std::string DocumentObjectT::getObjectLabel() const std::string DocumentObjectT::getObjectPython() const { std::stringstream str; - Document* doc = GetApplication().getActiveDocument(); - if (doc && document == doc->getName()) { - str << "App.ActiveDocument."; - } - else { - str << "App.getDocument(\"" - << document - << "\")."; - } - - str << object; + str << "FreeCAD.getDocument('" << document << "').getObject('" << object << "')"; return str.str(); } -std::string DocumentObjectT::getPropertyName() const { +const std::string &DocumentObjectT::getPropertyName() const { return property; } @@ -231,6 +259,134 @@ Property *DocumentObjectT::getProperty() const { } // ----------------------------------------------------------------------------- +SubObjectT::SubObjectT() +{} + +SubObjectT::SubObjectT(const SubObjectT &other) + :DocumentObjectT(other), subname(other.subname) +{ +} + +SubObjectT::SubObjectT(SubObjectT &&other) + :DocumentObjectT(std::move(other)), subname(std::move(other.subname)) +{ +} + +SubObjectT::SubObjectT(const DocumentObject *obj, const char *s) + :DocumentObjectT(obj),subname(s?s:"") +{} + +SubObjectT::SubObjectT(const char *docName, const char *objName, const char *s) + :DocumentObjectT(docName,objName), subname(s?s:"") +{ +} + +bool SubObjectT::operator<(const SubObjectT &other) const { + if(getDocumentName() < other.getDocumentName()) + return true; + if(getDocumentName() > other.getDocumentName()) + return false; + if(getObjectName() < other.getObjectName()) + return true; + if(getObjectName() > other.getObjectName()) + return false; + if(getSubName() < other.getSubName()) + return true; + if(getSubName() > other.getSubName()) + return false; + return getPropertyName() < other.getPropertyName(); +} + +SubObjectT &SubObjectT::operator=(const SubObjectT& other) +{ + if (this == &other) + return *this; + static_cast(*this) = other; + subname = other.subname; + return *this; +} + +SubObjectT &SubObjectT::operator=(SubObjectT &&other) +{ + if (this == &other) + return *this; + static_cast(*this) = std::move(other); + subname = std::move(other.subname); + return *this; +} + +bool SubObjectT::operator==(const SubObjectT &other) const { + return static_cast(*this) == other + && subname == other.subname; +} + +void SubObjectT::setSubName(const char *s) { + subname = s?s:""; +} + +const std::string &SubObjectT::getSubName() const { + return subname; +} + +std::string SubObjectT::getSubNameNoElement() const { + return Data::ComplexGeoData::noElementName(subname.c_str()); +} + +const char *SubObjectT::getElementName() const { + return Data::ComplexGeoData::findElementName(subname.c_str()); +} + +std::string SubObjectT::getNewElementName() const { + std::pair element; + auto obj = getObject(); + if(!obj) + return std::string(); + GeoFeature::resolveElement(obj,subname.c_str(),element); + return std::move(element.first); +} + +std::string SubObjectT::getOldElementName(int *index) const { + std::pair element; + auto obj = getObject(); + if(!obj) + return std::string(); + GeoFeature::resolveElement(obj,subname.c_str(),element); + if(!index) + return std::move(element.second); + std::size_t pos = element.second.find_first_of("0123456789"); + if(pos == std::string::npos) + *index = -1; + else { + *index = std::atoi(element.second.c_str()+pos); + element.second.resize(pos); + } + return std::move(element.second); +} + +App::DocumentObject *SubObjectT::getSubObject() const { + auto obj = getObject(); + if(obj) + return obj->getSubObject(subname.c_str()); + return 0; +} + +std::string SubObjectT::getSubObjectPython(bool force) const { + if(!force && subname.empty()) + return getObjectPython(); + std::stringstream str; + str << "(" << getObjectPython() << ",u'" + << Base::Tools::escapedUnicodeFromUtf8(subname.c_str()) << "')"; + return str.str(); +} + +std::vector SubObjectT::getSubObjectList() const { + auto obj = getObject(); + if(obj) + return obj->getSubObjectList(subname.c_str()); + return {}; +} + +// ----------------------------------------------------------------------------- DocumentObserver::DocumentObserver() : _document(0) { this->connectApplicationCreatedDocument = App::GetApplication().signalNewDocument.connect(boost::bind diff --git a/src/App/DocumentObserver.h b/src/App/DocumentObserver.h index abb38423bcba..dcf793630432 100644 --- a/src/App/DocumentObserver.h +++ b/src/App/DocumentObserver.h @@ -62,7 +62,7 @@ class AppExport DocumentT /*! Get a pointer to the document or 0 if it doesn't exist any more. */ Document* getDocument() const; /*! Get the name of the document. */ - std::string getDocumentName() const; + const std::string &getDocumentName() const; /*! Get the document as Python command. */ std::string getDocumentPython() const; @@ -83,22 +83,32 @@ class AppExport DocumentObjectT /*! Constructor */ DocumentObjectT(); /*! Constructor */ + DocumentObjectT(const DocumentObjectT &); + /*! Constructor */ + DocumentObjectT(DocumentObjectT &&); + /*! Constructor */ DocumentObjectT(const DocumentObject*); /*! Constructor */ + DocumentObjectT(const char *docName, const char *objName); + /*! Constructor */ DocumentObjectT(const Property*); /*! Destructor */ ~DocumentObjectT(); /*! Assignment operator */ - void operator=(const DocumentObjectT&); + DocumentObjectT &operator=(const DocumentObjectT&); + /*! Assignment operator */ + DocumentObjectT &operator=(DocumentObjectT &&); /*! Assignment operator */ void operator=(const DocumentObject*); /*! Assignment operator */ void operator=(const Property*); + /*! Equality operator */ + bool operator==(const DocumentObjectT&) const; /*! Get a pointer to the document or 0 if it doesn't exist any more. */ Document* getDocument() const; /*! Get the name of the document. */ - std::string getDocumentName() const; + const std::string &getDocumentName() const; /*! Get the document as Python command. */ std::string getDocumentPython() const; /*! Get a pointer to the document object or 0 if it doesn't exist any more. */ @@ -106,11 +116,11 @@ class AppExport DocumentObjectT /*! Get a pointer to the property or 0 if it doesn't exist any more. */ Property* getProperty() const; /*! Get the name of the document object. */ - std::string getObjectName() const; + const std::string &getObjectName() const; /*! Get the label of the document object. */ - std::string getObjectLabel() const; + const std::string &getObjectLabel() const; /*! Get the name of the property. */ - std::string getPropertyName() const; + const std::string &getPropertyName() const; /*! Get the document object as Python command. */ std::string getObjectPython() const; /*! Get the property as Python command. */ @@ -134,6 +144,67 @@ class AppExport DocumentObjectT std::string property; }; +class AppExport SubObjectT: public DocumentObjectT +{ +public: + /*! Constructor */ + SubObjectT(); + + /*! Constructor */ + SubObjectT(const SubObjectT &); + + /*! Constructor */ + SubObjectT(SubObjectT &&); + + /*! Constructor */ + SubObjectT(const DocumentObject*, const char *subname); + + /*! Constructor */ + SubObjectT(const char *docName, const char *objName, const char *subname); + + /*! Assignment operator */ + SubObjectT &operator=(const SubObjectT&); + + /*! Assignment operator */ + SubObjectT &operator=(SubObjectT &&); + + /*! Equality operator */ + bool operator==(const SubObjectT&) const; + + /// Set the subname path to the sub-object + void setSubName(const char *subname); + + /// Return the subname path + const std::string &getSubName() const; + + /// Return the subname path without sub-element + std::string getSubNameNoElement() const; + + /// Return the sub-element (Face, Edge, etc) of the subname path + const char *getElementName() const; + + /// Return the new style sub-element name + std::string getNewElementName() const; + + /** Return the old style sub-element name + * @param index: if given, then return the element type, and extract the index + */ + std::string getOldElementName(int *index=0) const; + + /// Return the sub-object + DocumentObject *getSubObject() const; + + /// Return all objects along the subname path + std::vector getSubObjectList() const; + + bool operator<(const SubObjectT &other) const; + + std::string getSubObjectPython(bool force=true) const; + +private: + std::string subname; +}; + /** * The DocumentObserver class simplfies the step to write classes that listen * to what happens inside a document. diff --git a/src/App/DocumentObserverPython.cpp b/src/App/DocumentObserverPython.cpp index edaeef5eb083..40b74d447f1e 100644 --- a/src/App/DocumentObserverPython.cpp +++ b/src/App/DocumentObserverPython.cpp @@ -111,6 +111,8 @@ DocumentObserverPython::DocumentObserverPython(const Py::Object& obj) : inst(obj FC_PY_ELEMENT_ARG1(AppendDynamicProperty, AppendDynamicProperty) FC_PY_ELEMENT_ARG1(RemoveDynamicProperty, RemoveDynamicProperty) FC_PY_ELEMENT_ARG2(ChangePropertyEditor, ChangePropertyEditor) + FC_PY_ELEMENT_ARG2(BeforeAddingDynamicExtension, BeforeAddingDynamicExtension) + FC_PY_ELEMENT_ARG2(AddedDynamicExtension, AddedDynamicExtension) } DocumentObserverPython::~DocumentObserverPython() @@ -541,3 +543,34 @@ void DocumentObserverPython::slotFinishSaveDocument(const App::Document& doc, co e.ReportException(); } } + +void DocumentObserverPython::slotBeforeAddingDynamicExtension(const App::ExtensionContainer& extcont, std::string extension) +{ + Base::PyGILStateLocker lock; + try { + Py::Tuple args(2); + args.setItem(0, Py::Object(const_cast(extcont).getPyObject())); + args.setItem(1, Py::String(extension)); + Base::pyCall(pyBeforeAddingDynamicExtension.ptr(),args.ptr()); + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotAddedDynamicExtension(const App::ExtensionContainer& extcont, std::string extension) +{ + Base::PyGILStateLocker lock; + try { + Py::Tuple args(2); + args.setItem(0, Py::Object(const_cast(extcont).getPyObject())); + args.setItem(1, Py::String(extension)); + Base::pyCall(pyAddedDynamicExtension.ptr(),args.ptr()); + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + diff --git a/src/App/DocumentObserverPython.h b/src/App/DocumentObserverPython.h index b1d0411d9dc0..b6e36b92cb69 100644 --- a/src/App/DocumentObserverPython.h +++ b/src/App/DocumentObserverPython.h @@ -28,6 +28,8 @@ #include #include +#include + namespace App { @@ -105,6 +107,11 @@ class AppExport DocumentObserverPython void slotStartSaveDocument(const App::Document&, const std::string&); /** Called when an document has been saved*/ void slotFinishSaveDocument(const App::Document&, const std::string&); + /** Called before an object gets a new extension added*/ + void slotBeforeAddingDynamicExtension(const App::ExtensionContainer&, std::string extension); + /** Called when an object gets a dynamic extension added*/ + void slotAddedDynamicExtension(const App::ExtensionContainer&, std::string extension); + private: Py::Object inst; @@ -145,6 +152,8 @@ class AppExport DocumentObserverPython Connection pyAppendDynamicProperty; Connection pyRemoveDynamicProperty; Connection pyChangePropertyEditor; + Connection pyBeforeAddingDynamicExtension; + Connection pyAddedDynamicExtension; }; } //namespace App diff --git a/src/App/ExtensionContainerPyImp.cpp b/src/App/ExtensionContainerPyImp.cpp index 36750d223dba..4c3dfe85813b 100644 --- a/src/App/ExtensionContainerPyImp.cpp +++ b/src/App/ExtensionContainerPyImp.cpp @@ -207,7 +207,7 @@ PyObject* ExtensionContainerPy::addExtension(PyObject *args) { str << "No extension found of type '" << typeId << "'" << std::ends; throw Py::Exception(Base::BaseExceptionFreeCADError,str.str()); } - + //register the extension App::Extension* ext = static_cast(extension.createInstance()); //check if this really is a python extension! @@ -217,7 +217,8 @@ PyObject* ExtensionContainerPy::addExtension(PyObject *args) { str << "Extension is not a python addable version: '" << typeId << "'" << std::ends; throw Py::Exception(Base::BaseExceptionFreeCADError,str.str()); } - + + GetApplication().signalBeforeAddingDynamicExtension(*getExtensionContainerPtr(), typeId); ext->initExtension(getExtensionContainerPtr()); //set the proxy to allow python overrides @@ -260,6 +261,9 @@ PyObject* ExtensionContainerPy::addExtension(PyObject *args) { } Py_DECREF(obj); + + //throw the appropriate event + GetApplication().signalAddedDynamicExtension(*getExtensionContainerPtr(), typeId); Py_Return; } diff --git a/src/App/Link.cpp b/src/App/Link.cpp index 90acca1b395e..a07c967c7cdf 100644 --- a/src/App/Link.cpp +++ b/src/App/Link.cpp @@ -182,8 +182,60 @@ App::DocumentObjectExecReturn *LinkBaseExtension::extensionExecute(void) { // recomputed. _LinkTouched.touch(); - if(getLinkedObjectProperty() && !getTrueLinkedObject(true)) - return new App::DocumentObjectExecReturn("Link broken"); + if(getLinkedObjectProperty()) { + DocumentObject *linked = getTrueLinkedObject(true); + if(!linked) + return new App::DocumentObjectExecReturn("Link broken"); + + App::DocumentObject *container = getContainer(); + PropertyPythonObject *proxy = 0; + if(getLinkExecuteProperty() + && !boost::iequals(getLinkExecuteValue(), "none") + && (!myOwner || !container->getDocument()->getObjectByID(myOwner))) + { + // Check if this is an element link. Do not invoke appLinkExecute() + // if so, because it will be called from the link array. + proxy = Base::freecad_dynamic_cast( + linked->getPropertyByName("Proxy")); + } + if(proxy) { + Base::PyGILStateLocker lock; + const char *errMsg = "Linked proxy execute failed"; + try { + Py::Tuple args(3); + Py::Object proxyValue = proxy->getValue(); + const char *method = getLinkExecuteValue(); + if(!method || !method[0]) + method = "appLinkExecute"; + Py::Object attr = proxyValue.getAttr(method); + if(attr.ptr() && attr.isCallable()) { + Py::Tuple args(4); + args.setItem(0, Py::asObject(linked->getPyObject())); + args.setItem(1, Py::asObject(container->getPyObject())); + if(!_getElementCountValue()) { + Py::Callable(attr).apply(args); + } else { + const auto &elements = _getElementListValue(); + for(int i=0; i<_getElementCountValue(); ++i) { + args.setItem(2, Py::Int(i)); + if(i < (int)elements.size()) + args.setItem(3, Py::asObject(elements[i]->getPyObject())); + else + args.setItem(3, Py::Object()); + Py::Callable(attr).apply(args); + } + } + } + } catch (Py::Exception &) { + Base::PyException e; + e.ReportException(); + return new App::DocumentObjectExecReturn(errMsg); + } catch (Base::Exception &e) { + e.ReportException(); + return new App::DocumentObjectExecReturn(errMsg); + } + } + } return inherited::extensionExecute(); } @@ -906,17 +958,6 @@ void LinkBaseExtension::update(App::DocumentObject *parent, const Property *prop auto placementProp = getPlacementListProperty(); auto scaleProp = getScaleListProperty(); const auto &vis = getVisibilityListValue(); - auto proxy = freecad_dynamic_cast(parent->getPropertyByName("Proxy")); - Py::Callable method; - Py::Tuple args(3); - if(proxy) { - Py::Object proxyValue = proxy->getValue(); - const char *fname = "onCreateLinkElement"; - if (proxyValue.hasAttr(fname)) { - method = proxyValue.getAttr(fname); - args.setItem(0,Py::Object(parent->getPyObject(),true)); - } - } auto owner = getContainer(); long ownerID = owner?owner->getID():0; @@ -932,13 +973,7 @@ void LinkBaseExtension::update(App::DocumentObject *parent, const Property *prop if(obj && (!obj->myOwner || obj->myOwner==ownerID)) obj->Visibility.setValue(false); else { - if(!method.isNone()) { - obj = new LinkElementPython; - args.setItem(1,Py::Object(obj->getPyObject(),true)); - args.setItem(2,Py::Int((int)i)); - method.apply(args); - } else - obj = new LinkElement; + obj = new LinkElement; parent->getDocument()->addObject(obj,name.c_str()); } diff --git a/src/App/Link.h b/src/App/Link.h index 1f32a261455f..503ea3a45781 100644 --- a/src/App/Link.h +++ b/src/App/Link.h @@ -126,6 +126,10 @@ class AppExport LinkBaseExtension : public App::DocumentObjectExtension #define LINK_PARAM_MODE(...) \ (LinkMode, long, App::PropertyEnumeration, ((long)0), "Link group mode", ##__VA_ARGS__) +#define LINK_PARAM_LINK_EXECUTE(...) \ + (LinkExecute, const char*, App::PropertyString, (""),\ + "Link execute function. Default to 'appLinkExecute'. 'None' to disable.", ##__VA_ARGS__) + #define LINK_PARAM_COLORED_ELEMENTS(...) \ (ColoredElements, App::DocumentObject*, App::PropertyLinkSubHidden, \ 0, "Link colored elements", ##__VA_ARGS__) @@ -155,7 +159,8 @@ class AppExport LinkBaseExtension : public App::DocumentObjectExtension LINK_PARAM(ELEMENTS)\ LINK_PARAM(SHOW_ELEMENT)\ LINK_PARAM(MODE)\ - LINK_PARAM(COLORED_ELEMENTS) + LINK_PARAM(LINK_EXECUTE)\ + LINK_PARAM(COLORED_ELEMENTS)\ enum PropIndex { #define LINK_PINDEX_DEFINE(_1,_2,_param) LINK_PINDEX(_param), @@ -446,6 +451,7 @@ class AppExport Link : public App::DocumentObject, public App::LinkExtension LINK_PARAM_EXT(PLACEMENT)\ LINK_PARAM_EXT(SHOW_ELEMENT)\ LINK_PARAM_EXT_TYPE(COUNT,App::PropertyIntegerConstraint)\ + LINK_PARAM_EXT(LINK_EXECUTE)\ LINK_PARAM_EXT_ATYPE(COLORED_ELEMENTS,App::Prop_Hidden)\ LINK_PROPS_DEFINE(LINK_PARAMS_LINK) diff --git a/src/App/Property.h b/src/App/Property.h index 95c263a775ff..8e4ff24061d4 100644 --- a/src/App/Property.h +++ b/src/App/Property.h @@ -376,8 +376,7 @@ template class AtomicPropertyChangeInterface { P & mProp; /**< Referenced to property we work on */ }; -private: - +protected: int signalCounter; /**< Counter for invoking transaction start/stop */ bool hasChanged; }; diff --git a/src/App/PropertyContainerPyImp.cpp b/src/App/PropertyContainerPyImp.cpp index e4935ba3d04d..2f09ab3562bd 100644 --- a/src/App/PropertyContainerPyImp.cpp +++ b/src/App/PropertyContainerPyImp.cpp @@ -555,11 +555,8 @@ int PropertyContainerPy::setCustomAttributes(const char* attr, PyObject *obj) throw Py::AttributeError(s.str()); } - PY_TRY { - FC_TRACE("Set property " << prop->getFullName()); - prop->setPyObject(obj); - }_PY_CATCH(return(-1)) - + FC_TRACE("Set property " << prop->getFullName()); + prop->setPyObject(obj); return 1; } diff --git a/src/App/PropertyLinks.cpp b/src/App/PropertyLinks.cpp index e90d369d3571..390a773a79e7 100644 --- a/src/App/PropertyLinks.cpp +++ b/src/App/PropertyLinks.cpp @@ -1035,22 +1035,28 @@ void PropertyLinkSub::setPyObject(PyObject *value) throw Base::ValueError("Expect input sequence of size 2"); else if (PyObject_TypeCheck(seq[0].ptr(), &(DocumentObjectPy::Type))) { DocumentObjectPy *pcObj = (DocumentObjectPy*)seq[0].ptr(); + static const char *errMsg = "type of second element in tuple must be str or sequence of str"; + PropertyString propString; if (seq[1].isString()) { std::vector vals; - vals.push_back((std::string)Py::String(seq[1])); + propString.setPyObject(seq[1].ptr()); + vals.emplace_back(propString.getValue()); setValue(pcObj->getDocumentObjectPtr(),std::move(vals)); } else if (seq[1].isSequence()) { Py::Sequence list(seq[1]); std::vector vals(list.size()); unsigned int i=0; - for (Py::Sequence::iterator it = list.begin();it!=list.end();++it,++i) - vals[i] = Py::String(*it); + for (Py::Sequence::iterator it = list.begin();it!=list.end();++it,++i) { + if(!(*it).isString()) + throw Base::TypeError(errMsg); + propString.setPyObject((*it).ptr()); + vals[i] = propString.getValue(); + } setValue(pcObj->getDocumentObjectPtr(),std::move(vals)); } else { - std::string error = std::string("type of second element in tuple must be str or sequence of str"); - throw Base::TypeError(error); + throw Base::TypeError(errMsg); } } else { @@ -1964,12 +1970,11 @@ void PropertyLinkSubList::setPyObject(PyObject *value) return; }catch(...) {} -#define SUBLIST_THROW \ - throw Base::TypeError(\ - "Expects sequence of items of type DocObj, (DocObj,SubName), or (DocObj, (SubName,...))") + static const char *errMsg = + "Expects sequence of items of type DocObj, (DocObj,SubName), or (DocObj, (SubName,...))"; if (!PyTuple_Check(value) && !PyList_Check(value)) - SUBLIST_THROW; + throw Base::TypeError(errMsg); Py::Sequence list(value); Py::Sequence::size_type size = list.size(); @@ -1984,17 +1989,22 @@ void PropertyLinkSubList::setPyObject(PyObject *value) Py::Sequence seq(item); if (PyObject_TypeCheck(seq[0].ptr(), &(DocumentObjectPy::Type))){ auto obj = static_cast(seq[0].ptr())->getDocumentObjectPtr(); + PropertyString propString; if (seq[1].isString()) { values.push_back(obj); - SubNames.push_back(Py::String(seq[1])); + propString.setPyObject(seq[1].ptr()); + SubNames.emplace_back(propString.getValue()); } else if (seq[1].isSequence()) { Py::Sequence list(seq[1]); for (Py::Sequence::iterator it = list.begin(); it != list.end(); ++it) { + if(!(*it).isString()) + throw Base::TypeError(errMsg); values.push_back(obj); - SubNames.push_back(Py::String(*it)); + propString.setPyObject((*it).ptr()); + SubNames.emplace_back(propString.getValue()); } } else - SUBLIST_THROW; + throw Base::TypeError(errMsg); } } else if (PyObject_TypeCheck(*item, &(DocumentObjectPy::Type))) { DocumentObjectPy *pcObj; @@ -2002,7 +2012,7 @@ void PropertyLinkSubList::setPyObject(PyObject *value) values.push_back(pcObj->getDocumentObjectPtr()); SubNames.emplace_back(); } else - SUBLIST_THROW; + throw Base::TypeError(errMsg); } setValues(values,SubNames); } @@ -2511,6 +2521,14 @@ class App::DocInfo : auto ret = _DocInfoMap.insert(std::make_pair(path,info)); info->init(ret.first,objName,l); } + + if(info->pcDoc) { + // make sure to attach only external object + auto owner = Base::freecad_dynamic_cast(l->getContainer()); + if(owner && owner->getDocument() == info->pcDoc) + return info; + } + info->links.insert(l); return info; } @@ -2623,6 +2641,13 @@ class App::DocInfo : } } + static void restoreDocument(const App::Document &doc) { + auto it = _DocInfoMap.find(getFullPath(doc.FileName.getValue())); + if(it==_DocInfoMap.end()) + return; + it->second->slotFinishRestoreDocument(doc); + } + void slotFinishRestoreDocument(const App::Document &doc) { if(pcDoc) return; QString fullpath(getFullPath()); @@ -3288,9 +3313,9 @@ Property *PropertyXLink::CopyOnImportExternal( if(subs.empty() && linked==_pcLink) return 0; - PropertyXLink *p= createInstance(); + std::unique_ptr p(new PropertyXLink); copyTo(*p,linked,&subs); - return p; + return p.release(); } Property *PropertyXLink::CopyOnLinkReplace(const App::DocumentObject *parent, @@ -3299,13 +3324,9 @@ Property *PropertyXLink::CopyOnLinkReplace(const App::DocumentObject *parent, auto res = tryReplaceLinkSubs(getContainer(),_pcLink,parent,oldObj,newObj,_SubList); if(!res.first) return 0; - PropertyXLink *p= createInstance(); + std::unique_ptr p(new PropertyXLink); copyTo(*p,res.first,&res.second); - return p; -} - -PropertyXLink *PropertyXLink::createInstance() const { - return new PropertyXLink(); + return p.release(); } Property *PropertyXLink::CopyOnLabelChange(App::DocumentObject *obj, @@ -3317,9 +3338,9 @@ Property *PropertyXLink::CopyOnLabelChange(App::DocumentObject *obj, auto subs = updateLinkSubs(_pcLink,_SubList,&updateLabelReference,obj,ref,newLabel); if(subs.empty()) return 0; - PropertyXLink *p= createInstance(); + std::unique_ptr p(new PropertyXLink); copyTo(*p,_pcLink,&subs); - return p; + return p.release(); } void PropertyXLink::copyTo(PropertyXLink &other, @@ -3330,6 +3351,8 @@ void PropertyXLink::copyTo(PropertyXLink &other, if(linked && linked->getNameInDocument()) { other.docName = linked->getDocument()->getName(); other.objectName = linked->getNameInDocument(); + other.docInfo.reset(); + other.filePath.clear(); }else{ other.objectName = objectName; other.docName.clear(); @@ -3345,9 +3368,9 @@ void PropertyXLink::copyTo(PropertyXLink &other, Property *PropertyXLink::Copy(void) const { - PropertyXLink *p= createInstance(); + std::unique_ptr p(new PropertyXLink); copyTo(*p); - return p; + return p.release(); } void PropertyXLink::Paste(const Property &from) @@ -3407,6 +3430,10 @@ bool PropertyXLink::hasXLink( return ret; } +void PropertyXLink::restoreDocument(const App::Document &doc) { + DocInfo::restoreDocument(doc); +} + std::map > PropertyXLink::getDocumentOutList(App::Document *doc) { std::map > ret; @@ -3573,10 +3600,6 @@ PropertyXLinkSub::PropertyXLinkSub(bool allowPartial, PropertyLinkBase *parent) PropertyXLinkSub::~PropertyXLinkSub() { } -PropertyXLink *PropertyXLinkSub::createInstance() const{ - return new PropertyXLinkSub(); -} - bool PropertyXLinkSub::upgrade(Base::XMLReader &reader, const char *typeName) { if(strcmp(typeName, PropertyLinkSubGlobal::getClassTypeId().getName())==0 || strcmp(typeName, PropertyLinkSub::getClassTypeId().getName())==0 || @@ -3697,7 +3720,7 @@ void PropertyXLinkSubList::setValues( FC_THROWM(Base::ValueError,"invalid document object"); } - aboutToSetValue(); + atomic_change guard(*this); for(auto it=_Links.begin(),itNext=it;it!=_Links.end();it=itNext) { ++itNext; @@ -3714,7 +3737,7 @@ void PropertyXLinkSubList::setValues( _Links.emplace_back(testFlag(LinkAllowPartial),this); _Links.back().setValue(v.first,std::move(v.second)); } - hasSetValue(); + guard.tryInvoke(); } void PropertyXLinkSubList::addValue(App::DocumentObject *obj, @@ -3742,10 +3765,10 @@ void PropertyXLinkSubList::addValue(App::DocumentObject *obj, return; } } - aboutToSetValue(); + atomic_change guard(*this); _Links.emplace_back(testFlag(LinkAllowPartial),this); _Links.back().setValue(obj,std::move(subs)); - hasSetValue(); + guard.tryInvoke(); } void PropertyXLinkSubList::setValue(DocumentObject* lValue, const std::vector &SubList) @@ -3756,6 +3779,41 @@ void PropertyXLinkSubList::setValue(DocumentObject* lValue, const std::vector &values) { + atomic_change guard(*this); + _Links.clear(); + for(auto obj : values) { + _Links.emplace_back(testFlag(LinkAllowPartial),this); + _Links.back().setValue(obj); + } + guard.tryInvoke(); +} + +void PropertyXLinkSubList::set1Value(int idx, + DocumentObject *value, + const std::vector &SubList) +{ + if(idx < -1 || idx > getSize()) + throw Base::RuntimeError("index out of bound"); + + if(idx < 0 || idx+1 == getSize()) { + if(SubList.empty()) { + addValue(value,SubList); + return; + } + atomic_change guard(*this); + _Links.emplace_back(testFlag(LinkAllowPartial),this); + _Links.back().setValue(value); + guard.tryInvoke(); + return; + } + + auto it = _Links.begin(); + for(;idx;--idx) + ++it; + it->setValue(value,SubList); +} + const string PropertyXLinkSubList::getPyReprString() const { if (_Links.empty()) @@ -3790,15 +3848,18 @@ DocumentObject *PropertyXLinkSubList::getValue() const int PropertyXLinkSubList::removeValue(App::DocumentObject *lValue) { + atomic_change guard(*this,false); int ret = 0; - auto it = std::find_if(_Links.begin(),_Links.end(), - [=](const PropertyXLinkSub &l){return l.getValue()==lValue;}); - if(it != _Links.end()) { - ret = (int)it->getSubValues().size(); - if(!ret) - ret = 1; - _Links.erase(it); + for(auto it=_Links.begin();it!=_Links.end();) { + if(it->getValue() != lValue) + ++it; + else { + guard.aboutToChange(); + it = _Links.erase(it); + ++ret; + } } + guard.tryInvoke(); return ret; } @@ -3809,6 +3870,7 @@ PyObject *PropertyXLinkSubList::getPyObject(void) auto obj = link.getValue(); if(!obj || !obj->getNameInDocument()) continue; + Py::Tuple tup(2); tup[0] = Py::asObject(obj->getPyObject()); @@ -3898,12 +3960,14 @@ void PropertyXLinkSubList::Restore(Base::XMLReader &reader) reader.hasAttribute("partial") && reader.getAttributeAsInteger("partial")); int count = reader.getAttributeAsInteger("count"); + atomic_change guard(*this,false); _Links.clear(); for(int i=0;i &objs, if(obj && obj->getNameInDocument()) count += l.getSubValues().size(); } + if(!count) { + objs.reserve(objs.size()+_Links.size()); + for(auto &l : _Links) { + auto obj = l.getValue(); + if(obj && obj->getNameInDocument()) + objs.push_back(obj); + } + return; + } + objs.reserve(objs.size()+count); subs->reserve(subs->size()+count); for(auto &l : _Links) { @@ -4107,18 +4181,14 @@ void PropertyXLinkSubList::breakLink(App::DocumentObject *obj, bool clear) { setValue(0); return; } - bool touched = false; + atomic_change guard(*this,false); for(auto &l : _Links) { if(l.getValue() == obj) { - if(!touched) { - touched = true; - aboutToSetValue(); - } + guard.aboutToChange(); l.setValue(0); } } - if(touched) - hasSetValue(); + guard.tryInvoke(); } bool PropertyXLinkSubList::adjustLink(const std::set &inList) { @@ -4163,9 +4233,18 @@ int PropertyXLinkSubList::checkRestore(std::string *msg) const { } bool PropertyXLinkSubList::upgrade(Base::XMLReader &reader, const char *typeName) { - if(strcmp(typeName, PropertyLinkSubListGlobal::getClassTypeId().getName())==0 || - strcmp(typeName, PropertyLinkSubList::getClassTypeId().getName())==0 || - strcmp(typeName, PropertyLinkSubListChild::getClassTypeId().getName())==0) + if(strcmp(typeName, PropertyLinkListGlobal::getClassTypeId().getName())==0 || + strcmp(typeName, PropertyLinkList::getClassTypeId().getName())==0 || + strcmp(typeName, PropertyLinkListChild::getClassTypeId().getName())==0) + { + PropertyLinkList linkProp; + linkProp.setContainer(getContainer()); + linkProp.Restore(reader); + setValues(linkProp.getValues()); + return true; + } else if (strcmp(typeName, PropertyLinkSubListGlobal::getClassTypeId().getName())==0 || + strcmp(typeName, PropertyLinkSubList::getClassTypeId().getName())==0 || + strcmp(typeName, PropertyLinkSubListChild::getClassTypeId().getName())==0) { PropertyLinkSubList linkProp; linkProp.setContainer(getContainer()); @@ -4195,11 +4274,67 @@ void PropertyXLinkSubList::setAllowPartial(bool enable) { } void PropertyXLinkSubList::hasSetChildValue(Property &) { - hasSetValue(); + if(!signalCounter) + hasSetValue(); } void PropertyXLinkSubList::aboutToSetChildValue(Property &) { - aboutToSetValue(); + if(!signalCounter || !hasChanged) { + aboutToSetValue(); + if(signalCounter) + hasChanged = true; + } +} + +//************************************************************************** +// PropertyXLinkList +//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +TYPESYSTEM_SOURCE(App::PropertyXLinkList , App::PropertyXLinkSubList) + +//************************************************************************** +// Construction/Destruction + +PropertyXLinkList::PropertyXLinkList() +{ +} + +PropertyXLinkList::~PropertyXLinkList() +{ +} + +PyObject *PropertyXLinkList::getPyObject(void) +{ + for(auto &link : _Links) { + auto obj = link.getValue(); + if(!obj || !obj->getNameInDocument()) + continue; + if(link.hasSubName()) + return PropertyXLinkSubList::getPyObject(); + } + + Py::List list; + for(auto &link : _Links) { + auto obj = link.getValue(); + if(!obj || !obj->getNameInDocument()) + continue; + list.append(Py::asObject(obj->getPyObject())); + } + return Py::new_reference_to(list); +} + +void PropertyXLinkList::setPyObject(PyObject *value) +{ + try { //try PropertyLinkList syntax + PropertyLinkList dummy; + dummy.setAllowExternal(true); + dummy.setPyObject(value); + this->setValues(dummy.getValues()); + return; + } + catch (Base::Exception&) {} + + PropertyXLinkSubList::setPyObject(value); } //************************************************************************** diff --git a/src/App/PropertyLinks.h b/src/App/PropertyLinks.h index 3c214acf7055..9dfa74e8ecbc 100644 --- a/src/App/PropertyLinks.h +++ b/src/App/PropertyLinks.h @@ -762,8 +762,6 @@ class AppExport PropertyLinkListHidden : public PropertyLinkList PropertyLinkListHidden() {_pcScope = LinkScope::Hidden;}; }; -class PropertyXLinkSub; - /** the Link Property with sub elements * This property links an object and a defined sequence of * sub elements. These subelements (like Edges of a Shape) @@ -834,6 +832,9 @@ class AppExport PropertyLinkSub : public PropertyLinkBase virtual Property *Copy(void) const override; virtual void Paste(const Property &from) override; + virtual const char* getEditorName(void) const override + { return "Gui::PropertyEditor::PropertyLinkItem"; } + /// Return a copy of the property if any changes caused by importing external object virtual Property *CopyOnImportExternal(const std::map &nameMap) const override; @@ -976,6 +977,9 @@ class AppExport PropertyLinkSubList : public PropertyLinkBase virtual Property *Copy(void) const override; virtual void Paste(const Property &from) override; + virtual const char* getEditorName(void) const override + { return "Gui::PropertyEditor::PropertyLinkListItem"; } + /// Return a copy of the property if any changes caused by importing external object virtual Property *CopyOnImportExternal(const std::map &nameMap) const override; @@ -1103,6 +1107,7 @@ class AppExport PropertyXLink : public PropertyLinkGlobal static bool hasXLink(const std::vector &objs, std::vector *unsaved=0); static std::map > getDocumentOutList(App::Document *doc=0); static std::map > getDocumentInList(App::Document *doc=0); + static void restoreDocument(const App::Document &doc); virtual void updateElementReference( DocumentObject *feature,bool reverse=false, bool notify=false) override; @@ -1129,16 +1134,14 @@ class AppExport PropertyXLink : public PropertyLinkGlobal return filePath.c_str(); } + virtual bool upgrade(Base::XMLReader &reader, const char *typeName); + protected: void unlink(); void detach(); void restoreLink(App::DocumentObject *); - virtual PropertyXLink *createInstance() const; - - virtual bool upgrade(Base::XMLReader &reader, const char *typeName); - void copyTo(PropertyXLink &other, App::DocumentObject *linked=0, std::vector *subs=0) const; virtual void aboutToSetValue() override; @@ -1174,16 +1177,21 @@ class AppExport PropertyXLinkSub: public PropertyXLink { virtual PyObject *getPyObject(void) override; -protected: - virtual PropertyXLink *createInstance() const override; + virtual const char* getEditorName(void) const override + { return "Gui::PropertyEditor::PropertyLinkItem"; } }; /** Link to one or more (sub)object(s) of one or more object(s) from the same or different document */ -class AppExport PropertyXLinkSubList: public PropertyLinkBase { +class AppExport PropertyXLinkSubList: public PropertyLinkBase + , public AtomicPropertyChangeInterface +{ TYPESYSTEM_HEADER_WITH_OVERRIDE(); + typedef typename AtomicPropertyChangeInterface::AtomicPropertyChange atomic_change; + friend atomic_change; + public: PropertyXLinkSubList(); virtual ~PropertyXLinkSubList(); @@ -1197,6 +1205,9 @@ class AppExport PropertyXLinkSubList: public PropertyLinkBase { * setValue(0, whatever) clears the property */ void setValue(DocumentObject*,const char*); + void setValues(const std::vector&); + void set1Value(int idx, DocumentObject *value, const std::vector &SubList={}); + void setValues(const std::vector&,const std::vector&); void setValues(const std::vector&,const std::vector&); void setValues(std::map > &&); @@ -1209,7 +1220,7 @@ class AppExport PropertyXLinkSubList: public PropertyLinkBase { * @brief setValue: PropertyLinkSub-compatible overload * @param SubList */ - void setValue(App::DocumentObject *lValue, const std::vector &SubList=std::vector()); + void setValue(App::DocumentObject *lValue, const std::vector &SubList={}); std::vector getValues(void); @@ -1244,6 +1255,9 @@ class AppExport PropertyXLinkSubList: public PropertyLinkBase { virtual Property *Copy(void) const override; virtual void Paste(const Property &from) override; + virtual const char* getEditorName(void) const override + { return "Gui::PropertyEditor::PropertyLinkListItem"; } + virtual Property *CopyOnImportExternal(const std::map &nameMap) const override; virtual Property *CopyOnLabelChange(App::DocumentObject *obj, @@ -1279,6 +1293,25 @@ class AppExport PropertyXLinkSubList: public PropertyLinkBase { std::list _Links; }; + +/** Link to one or more (sub)object(s) of one or more object(s) from the same or different document + * + * The only difference for PropertyXLinkList and PropertyXLinkSubList is in + * their getPyObject(). PropertyXLinkList will return a list of object is + * there is no sub-object/sub-elements in the property. + */ +class AppExport PropertyXLinkList: public PropertyXLinkSubList { + TYPESYSTEM_HEADER_WITH_OVERRIDE(); + +public: + PropertyXLinkList(); + virtual ~PropertyXLinkList(); + + virtual PyObject *getPyObject(void) override; + virtual void setPyObject(PyObject *) override; +}; + + /** Abstract property that can link to multiple external objects * * @sa See PropertyExpressionEngine for example usage diff --git a/src/Base/QuantityPy.xml b/src/Base/QuantityPy.xml index 328c35489903..f4c487157b38 100644 --- a/src/Base/QuantityPy.xml +++ b/src/Base/QuantityPy.xml @@ -48,6 +48,14 @@ Quantity(string) -- arbitrary mixture of numbers and chars defining a Quantity + + + +Return the Integral closest to x, rounding half toward even. +When an argument is passed, work like built-in round(x, ndigits). + + + Numeric Value of the Quantity (in internal system mm,kg,s) diff --git a/src/Base/QuantityPyImp.cpp b/src/Base/QuantityPyImp.cpp index 1e1772dd48f8..c90dbbcee7b5 100644 --- a/src/Base/QuantityPyImp.cpp +++ b/src/Base/QuantityPyImp.cpp @@ -205,6 +205,17 @@ PyObject* QuantityPy::getValueAs(PyObject *args) return new QuantityPy(new Quantity(quant)); } +PyObject * QuantityPy::__round__ (PyObject *args) +{ + double val= getQuantityPtr()->getValue(); + Unit unit = getQuantityPtr()->getUnit(); + Py::Float flt(val); + Py::Callable func(flt.getAttr("__round__")); + double rnd = static_cast(Py::Float(func.apply(args))); + + return new QuantityPy(new Quantity(rnd, unit)); +} + PyObject * QuantityPy::number_float_handler (PyObject *self) { if (!PyObject_TypeCheck(self, &(QuantityPy::Type))) { diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index b4330209f6ca..be4cef5e98a3 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -971,6 +971,8 @@ Gui::MDIView* Application::editViewOfNode(SoNode *node) const } void Application::setEditDocument(Gui::Document *doc) { + if(doc == d->editDocument) + return; if(!doc) d->editDocument = 0; for(auto &v : d->documents) diff --git a/src/Gui/CommandDoc.cpp b/src/Gui/CommandDoc.cpp index e091e32835e8..c1beaad45aa0 100644 --- a/src/Gui/CommandDoc.cpp +++ b/src/Gui/CommandDoc.cpp @@ -1130,6 +1130,9 @@ void StdCmdDelete::activated(int iMsg) commitCommand(); return; } + + App::TransactionLocker tlock; + Gui::getMainWindow()->setUpdatesEnabled(false); auto editDoc = Application::Instance->editDocument(); ViewProviderDocumentObject *vpedit = 0; @@ -1288,13 +1291,22 @@ StdCmdRefresh::StdCmdRefresh() sAccel = keySequenceToAccel(QKeySequence::Refresh); eType = AlterDoc | Alter3DView | AlterSelection | ForEdit; bCanLog = false; + + // Make it optional to create a transaction for a recompute. + // The new default behaviour is quite cumbersome in some cases because when + // undoing the last transaction the manual recompute will clear the redo stack. + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Document"); + bool create = hGrp->GetBool("TransactionOnRecompute", true); + if (!create) + eType = eType | NoTransaction; } void StdCmdRefresh::activated(int iMsg) { Q_UNUSED(iMsg); if (getActiveGuiDocument()) { - App::AutoTransaction trans("Recompute"); + App::AutoTransaction trans((eType & NoTransaction) ? nullptr : "Recompute"); try { doCommand(Doc,"App.activeDocument().recompute(None,True,True)"); } diff --git a/src/Gui/DlgPropertyLink.cpp b/src/Gui/DlgPropertyLink.cpp index d415149bf94a..f00a7915f0c5 100644 --- a/src/Gui/DlgPropertyLink.cpp +++ b/src/Gui/DlgPropertyLink.cpp @@ -27,92 +27,77 @@ # include # include # include +# include #endif +#include + +#include #include #include #include #include +#include +#include "Document.h" +#include "View3DInventor.h" +#include "Tree.h" +#include "Selection.h" +#include "PropertyView.h" #include "BitmapFactory.h" #include "DlgPropertyLink.h" #include "Application.h" #include "ViewProviderDocumentObject.h" +#include "MetaTypes.h" #include "ui_DlgPropertyLink.h" using namespace Gui::Dialog; -/* TRANSLATOR Gui::Dialog::DlgPropertyLink */ - -DlgPropertyLink::DlgPropertyLink(const QStringList& list, QWidget* parent, Qt::WindowFlags fl, bool xlink) - : QDialog(parent, fl), link(list), ui(new Ui_DlgPropertyLink) -{ -#ifdef FC_DEBUG - assert(list.size() >= 4); -#endif +class ItemDelegate: public QStyledItemDelegate { +public: + ItemDelegate(QObject* parent=0): QStyledItemDelegate(parent) {} - // populate inList to filter out any objects that contains the owner object - // of the editing link property - auto doc = App::GetApplication().getDocument(qPrintable(link[0])); - if(doc) { - auto obj = doc->getObject(qPrintable(link[3])); - if(obj && obj->getNameInDocument()) { - inList = obj->getInListEx(true); - inList.insert(obj); - } + virtual QWidget* createEditor(QWidget *parent, + const QStyleOptionViewItem &option, const QModelIndex &index) const + { + if(index.column() != 1) + return nullptr; + return QStyledItemDelegate::createEditor(parent, option, index); } +}; + +/* TRANSLATOR Gui::Dialog::DlgPropertyLink */ +DlgPropertyLink::DlgPropertyLink(QWidget* parent) + : QDialog(parent), SelectionObserver(false,0) + , ui(new Ui_DlgPropertyLink) +{ ui->setupUi(this); ui->typeTree->hide(); + ui->searchBox->installEventFilter(this); + ui->searchBox->setNoProperty(true); - if(!xlink) - ui->comboBox->hide(); - else { - std::string linkDoc = qPrintable(link[0]); - for(auto doc : App::GetApplication().getDocuments()) { - QString name(QString::fromUtf8(doc->Label.getValue())); - ui->comboBox->addItem(name,QVariant(QString::fromLatin1(doc->getName()))); - if(linkDoc == doc->getName()) - ui->comboBox->setCurrentIndex(ui->comboBox->count()-1); - } - } + timer = new QTimer(this); + timer->setSingleShot(true); + connect(timer, SIGNAL(timeout()), this, SLOT(onTimer())); - Base::Type baseType; - - App::Document *linkedDoc = doc; - if (link.size()>FC_XLINK_VALUE_INDEX) - linkedDoc = App::GetApplication().getDocument(qPrintable(link[FC_XLINK_VALUE_INDEX])); - if(linkedDoc) { - QString objName = link[1]; // linked object name - auto obj = linkedDoc->getObject((const char*)objName.toLatin1()); - if (obj && inList.find(obj)==inList.end()) { - Base::Type objType = obj->getTypeId(); - // get only geometric types - if (objType.isDerivedFrom(App::GeoFeature::getClassTypeId())) - baseType = App::GeoFeature::getClassTypeId(); - else - baseType = App::DocumentObject::getClassTypeId(); - - // get the direct base class of App::DocumentObject which 'obj' is derived from - while (!objType.isBad()) { - std::string name = objType.getName(); - Base::Type parType = objType.getParent(); - if (parType == baseType) { - baseType = objType; - break; - } - objType = parType; - } - } - } - if(!baseType.isBad()) { - types.insert(baseType.getName()); - ui->checkObjectType->setChecked(true); - }else - findObjects(); + ui->treeWidget->setEditTriggers(QAbstractItemView::DoubleClicked); + ui->treeWidget->setItemDelegate(new ItemDelegate(this)); + ui->treeWidget->setMouseTracking(true); + connect(ui->treeWidget, SIGNAL(itemEntered(QTreeWidgetItem*, int)), + this, SLOT(onItemEntered(QTreeWidgetItem*))); connect(ui->treeWidget, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(onItemExpanded(QTreeWidgetItem*))); + + connect(ui->treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(onItemSelectionChanged())); + + connect(ui->searchBox, SIGNAL(returnPressed()), this, SLOT(onItemSearch())); + + connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(onClicked(QAbstractButton*))); + + refreshButton = ui->buttonBox->addButton(tr("Reset"), QDialogButtonBox::ActionRole); + resetButton = ui->buttonBox->addButton(tr("Clear"), QDialogButtonBox::ResetRole); } /** @@ -120,220 +105,782 @@ DlgPropertyLink::DlgPropertyLink(const QStringList& list, QWidget* parent, Qt::W */ DlgPropertyLink::~DlgPropertyLink() { + detachObserver(); + // no need to delete child widgets, Qt does it all for us delete ui; } -void DlgPropertyLink::setSelectionMode(QAbstractItemView::SelectionMode mode) +QList DlgPropertyLink::getLinksFromProperty(const App::PropertyLinkBase *prop) { - ui->treeWidget->setSelectionMode(mode); - findObjects(); + QList res; + if(!prop) + return res; + + std::vector objs; + std::vector subs; + prop->getLinks(objs,true,&subs,false); + if(subs.empty()) { + for(auto obj : objs) + res.push_back(App::SubObjectT(obj,0)); + } else if (objs.size()==1) { + for(auto &sub : subs) + res.push_back(App::SubObjectT(objs.front(),sub.c_str())); + } else { + int i=0; + for(auto obj : objs) + res.push_back(App::SubObjectT(obj,subs[i++].c_str())); + } + return res; } -void DlgPropertyLink::accept() +QString DlgPropertyLink::formatObject(App::Document *ownerDoc, App::DocumentObject *obj, const char *sub) { - QList items = ui->treeWidget->selectedItems(); - if (items.isEmpty()) { - QMessageBox::warning(this, tr("No selection"), tr("Please select an object from the list")); - return; + if(!obj || !obj->getNameInDocument()) + return QLatin1String("?"); + + const char *objName = obj->getNameInDocument(); + std::string _objName; + if(ownerDoc && ownerDoc!=obj->getDocument()) { + _objName = obj->getFullName(); + objName = _objName.c_str(); } - QDialog::accept(); + if(!sub || !sub[0]) { + if(obj->Label.getStrValue() == obj->getNameInDocument()) + return QLatin1String(objName); + return QString::fromLatin1("%1 (%2)").arg(QLatin1String(objName), + QString::fromUtf8(obj->Label.getValue())); + } + + auto sobj = obj->getSubObject(sub); + if(!sobj || sobj->Label.getStrValue() == sobj->getNameInDocument()) + return QString::fromLatin1("%1.%2").arg(QLatin1String(objName), + QString::fromUtf8(sub)); + + return QString::fromLatin1("%1.%2 (%3)").arg(QLatin1String(objName), + QString::fromUtf8(sub), + QString::fromUtf8(sobj->Label.getValue())); } -static QStringList getLinkFromItem(const QStringList &link, QTreeWidgetItem *selItem) { - QStringList list = link; - if(link.size()>FC_XLINK_VALUE_INDEX) { - QString subname; - auto parent = selItem; - for(auto item=parent;;item=parent) { - parent = item->parent(); - if(!parent) { - list[1] = item->data(0,Qt::UserRole).toString(); - break; - } - subname = QString::fromLatin1("%1.%2"). - arg(item->data(0,Qt::UserRole).toString()).arg(subname); +static inline bool isLinkSub(QList links) +{ + for(auto &link : links) { + if(&link == &links.front()) + continue; + if(link.getDocumentName() != links.front().getDocumentName() + || link.getObjectName() != links.front().getObjectName()) + { + return false; } - list[FC_XLINK_VALUE_INDEX] = subname; - if(subname.size()) - list[2] = QString::fromLatin1("%1 (%2.%3)"). - arg(selItem->text(0)).arg(list[1]).arg(subname); - else - list[2] = selItem->text(0); - QString docName(selItem->data(0, Qt::UserRole+1).toString()); - if(list.size()>FC_XLINK_VALUE_INDEX+1) - list[FC_XLINK_VALUE_INDEX+1] = docName; - else - list << docName; - }else{ - list[1] = selItem->data(0,Qt::UserRole).toString(); - list[2] = selItem->text(0); - if (list[1].isEmpty()) - list[2] = QString::fromUtf8(""); } - return list; + return true; } -QStringList DlgPropertyLink::propertyLink() const +QString DlgPropertyLink::formatLinks(App::Document *ownerDoc, QList links) { - auto items = ui->treeWidget->selectedItems(); - if (items.isEmpty()) { - return link; + if(!ownerDoc || links.empty()) + return QString(); + + auto obj = links.front().getObject(); + if(!obj) + return QLatin1String("?"); + + if(links.size() == 1 && links.front().getSubName().empty()) + return formatObject(ownerDoc, links.front()); + + QStringList list; + if(isLinkSub(links)) { + int i = 0; + for(auto &link : links) { + list << QString::fromUtf8(link.getSubName().c_str()); + if( ++i >= 3) + break; + } + return QString::fromLatin1("%1 [%2%3]").arg(formatObject(ownerDoc,obj,0), + list.join(QLatin1String(", ")), + QLatin1String(links.size()>3?" ...":"")); } - return getLinkFromItem(link,items[0]); -} -QVariantList DlgPropertyLink::propertyLinkList() const -{ - QVariantList varList; - QList items = ui->treeWidget->selectedItems(); - if (items.isEmpty()) { - QStringList l = link; - l[1] = QString(); - l[2] = QString(); - varList << l; - } else { - for (QList::iterator it = items.begin(); it != items.end(); ++it) - varList << getLinkFromItem(link,*it); + int i = 0; + for(auto &link : links) { + list << formatObject(ownerDoc,link); + if( ++i >= 3) + break; } - return varList; + return QString::fromLatin1("[%1%2]").arg(list.join(QLatin1String(", ")), + QLatin1String(links.size()>3?" ...":"")); } -void DlgPropertyLink::findObjects() -{ - bool filterType = ui->checkObjectType->isChecked(); +void DlgPropertyLink::init(const App::DocumentObjectT &prop, bool tryFilter) { + ui->treeWidget->blockSignals(true); ui->treeWidget->clear(); + ui->treeWidget->blockSignals(false); + + ui->typeTree->blockSignals(true); + ui->typeTree->clear(); + ui->typeTree->blockSignals(false); + + oldLinks.clear(); + docItems.clear(); + typeItems.clear(); + itemMap.clear(); + inList.clear(); + selectedTypes.clear(); + currentObj = nullptr; + searchItem = nullptr; + subSelections.clear(); + selections.clear(); + + objProp = prop; + auto owner = objProp.getObject(); + if(!owner || !owner->getNameInDocument()) + return; + + ui->searchBox->setDocumentObject(owner); + + auto propLink = Base::freecad_dynamic_cast(objProp.getProperty()); + if(!propLink) + return; + + oldLinks = getLinksFromProperty(propLink); + + if(propLink->getScope() != App::LinkScope::Hidden) { + // populate inList to filter out any objects that contains the owner object + // of the editing link property + inList = owner->getInListEx(true); + inList.insert(owner); + } + + std::vector docs; + + singleSelect = false; + if(propLink->isDerivedFrom(App::PropertyXLinkSub::getClassTypeId()) + || propLink->isDerivedFrom(App::PropertyLinkSub::getClassTypeId())) + { + allowSubObject = true; + singleParent = true; + } else if (propLink->isDerivedFrom(App::PropertyLink::getClassTypeId())) { + singleSelect = true; + } + + if(App::PropertyXLink::supportXLink(propLink)) { + allowSubObject = true; + docs = App::GetApplication().getDocuments(); + } else + docs.push_back(owner->getDocument()); + + bool isLinkList = false; + if (propLink->isDerivedFrom(App::PropertyXLinkList::getClassTypeId()) + || propLink->isDerivedFrom(App::PropertyLinkList::getClassTypeId())) + { + isLinkList = true; + allowSubObject = false; + } + + if(singleSelect) { + singleParent = true; + ui->treeWidget->setSelectionMode(QAbstractItemView::SingleSelection); + } else { + ui->treeWidget->setSelectionMode(QAbstractItemView::MultiSelection); + } + + ui->checkSubObject->setVisible(allowSubObject); + + if(!allowSubObject) { + ui->treeWidget->setColumnCount(1); + } else { + ui->treeWidget->setColumnCount(2); + + // make sure to show a horizontal scrollbar if needed +#if QT_VERSION >= 0x050000 + ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); +#else + ui->treeWidget->header()->setResizeMode(0, QHeaderView::ResizeToContents); +#endif + } + + std::set expandDocs; + + if(oldLinks.empty()) { + expandDocs.insert(owner->getDocument()); + } else { + for(auto &link : oldLinks) { + auto doc = link.getDocument(); + if(doc) + expandDocs.insert(doc); + } + } + + QPixmap docIcon(Gui::BitmapFactory().pixmap("Document")); + for(auto d : docs) { + auto item = new QTreeWidgetItem(ui->treeWidget); + item->setIcon(0, docIcon); + item->setText(0, QString::fromUtf8(d->Label.getValue())); + item->setData(0, Qt::UserRole, QByteArray("")); + item->setData(0, Qt::UserRole+1, QByteArray(d->getName())); + item->setFlags(Qt::ItemIsEnabled); + item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); + if(expandDocs.count(d)) + item->setExpanded(true); + docItems[d] = item; + } - QString docName = link[0]; // document name of the owner object of this editing property - - bool isSingleSelection = (ui->treeWidget->selectionMode() == QAbstractItemView::SingleSelection); - App::Document* doc = App::GetApplication().getDocument((const char*)docName.toLatin1()); - if (doc) { - - // build list of objects names already in property so we can mark them as selected later on - std::set selectedNames; - - // Add a "None" entry on top - auto* item = new QTreeWidgetItem(ui->treeWidget); - item->setText(0,tr("None (Remove link)")); - QByteArray ba(""); - item->setData(0,Qt::UserRole, ba); - - if (!isSingleSelection) { - - QString ownerName = link[3]; - QString proName = link[4]; - auto owner = doc->getObject(ownerName.toLatin1()); - if(owner) { - App::Property* prop = owner->getPropertyByName((const char*)proName.toLatin1()); - // gather names of objects currently in property - if (prop && prop->getTypeId().isDerivedFrom(App::PropertyLinkList::getClassTypeId())) { - const App::PropertyLinkList* propll = static_cast(prop); - std::vector links = propll->getValues(); - for (std::vector::iterator it = links.begin(); it != links.end(); ++it) { - selectedNames.insert((*it)->getNameInDocument()); - } - } + if(oldLinks.isEmpty()) + return; + + if(allowSubObject) { + for(auto &link : oldLinks) { + auto sobj = link.getSubObject(); + if(sobj && sobj!=link.getObject()) { + ui->checkSubObject->setChecked(true); + break; } } + } - Base::PyGILStateLocker lock; - std::map typeInfos; + // Try to select items corresponding to the current links inside the + // property + ui->treeWidget->blockSignals(true); + for(auto &link : oldLinks) { + onSelectionChanged(Gui::SelectionChanges(SelectionChanges::AddSelection, + link.getDocumentName(), + link.getObjectName(), + link.getSubName())); + } + ui->treeWidget->blockSignals(false); - QTreeWidgetItem *selected = 0; - for(auto obj : doc->getObjects()) { - auto it = types.end(); - auto prop = Base::freecad_dynamic_cast( - obj->getPropertyByName("Proxy")); - bool pass = false; - if(prop && !prop->getValue().isNone()) { - std::string typeName = prop->getValue().type().as_string(); - if(refreshTypes) { - QIcon &icon = typeInfos[typeName]; - if(icon.isNull()) { - auto vp = Application::Instance->getViewProvider(obj); - if(vp) - icon = vp->getIcon(); - } - } - if(filterType) - pass = types.count(typeName); + // For link list type property, try to auto filter type + if(tryFilter && isLinkList) { + Base::Type objType; + for(auto link : oldLinks) { + auto obj = link.getSubObject(); + if(!obj) + continue; + if(objType.isBad()) { + objType = obj->getTypeId(); + continue; } - if(refreshTypes) { - auto type = obj->getTypeId(); - QIcon &icon = typeInfos[type.getName()]; - if(!prop && icon.isNull()) { - auto vp = Application::Instance->getViewProvider(obj); - if(vp) - icon = vp->getIcon(); - } - for(type = type.getParent();!type.isBad();type=type.getParent()) - typeInfos.emplace(type.getName(),QIcon()); + for(;objType != App::DocumentObject::getClassTypeId(); + objType = objType.getParent()) + { + if(obj->isDerivedFrom(objType)) + break; } - if(filterType && !pass && types.size()) { - for(auto type = obj->getTypeId();!type.isBad();type=type.getParent()) { - it = types.find(type.getName()); - if(it!=types.end()) - break; - } - if(it == types.end()) - continue; + } + + Base::Type baseType; + // get only geometric types + if (objType.isDerivedFrom(App::GeoFeature::getClassTypeId())) + baseType = App::GeoFeature::getClassTypeId(); + else + baseType = App::DocumentObject::getClassTypeId(); + + // get the direct base class of App::DocumentObject which 'obj' is derived from + while (!objType.isBad()) { + Base::Type parType = objType.getParent(); + if (parType == baseType) { + baseType = objType; + break; } + objType = parType; + } + + if(!baseType.isBad()) { + const char *name = baseType.getName(); + auto it = typeItems.find(QByteArray::fromRawData(name,strlen(name)+1)); + if(it != typeItems.end()) + it->second->setSelected(true); + ui->checkObjectType->setChecked(true); + } + } +} + +void DlgPropertyLink::onClicked(QAbstractButton *button) { + if(button == resetButton) { + ui->treeWidget->blockSignals(true); + ui->treeWidget->selectionModel()->clearSelection(); + for(auto item : subSelections) + item->setText(1, QString()); + ui->treeWidget->blockSignals(false); + subSelections.clear(); + Gui::Selection().clearSelection(); + } else if (button == refreshButton) { + init(objProp); + } +} + +void DlgPropertyLink::hideEvent(QHideEvent *ev) { + detachObserver(); + QDialog::hideEvent(ev); +} + +void DlgPropertyLink::closeEvent(QCloseEvent *ev) { + detachObserver(); + QDialog::closeEvent(ev); +} + +void DlgPropertyLink::attachObserver() { + if(isConnectionAttached()) + return; - auto item = createItem(obj,0); - if(item && selectedNames.count(obj->getNameInDocument())) { - if(!selected) - selected = item; - item->setSelected(true); + Gui::Selection().selStackPush(); + attachSelection(); + + if(!parentView) { + for(auto p=parent(); p; p=p->parent()) { + auto view = qobject_cast(p); + if(view) { + parentView = view; + for(auto &sel : Gui::Selection().getCompleteSelection(0)) + savedSelections.emplace_back(sel.DocName, sel.FeatName, sel.SubName); + break; } } - if(selected) - ui->treeWidget->scrollToItem(selected); - - if(refreshTypes) { - refreshTypes = false; - ui->typeTree->blockSignals(true); - ui->typeTree->clear(); - QTreeWidgetItem *selected = 0; - QIcon icon = BitmapFactory().pixmap("px"); - for(auto &v : typeInfos) { - auto item = new QTreeWidgetItem(ui->typeTree); - item->setText(0,QString::fromLatin1(v.first.c_str())); - item->setIcon(0,v.second.isNull()?icon:v.second); - if(types.count(v.first)) { - item->setSelected(true); - if(!selected) - selected = item; - } + } + auto view = qobject_cast(parentView.data()); + if(view) + view->blockConnection(true); +} + +void DlgPropertyLink::showEvent(QShowEvent *ev) { + attachObserver(); + QDialog::showEvent(ev); +} + +void DlgPropertyLink::onItemEntered(QTreeWidgetItem *) { + int timeout = Gui::TreeParams::Instance()->PreSelectionDelay()/2; + if(timeout < 0) + timeout = 1; + timer->start(timeout); + Gui::Selection().rmvPreselect(); +} + +void DlgPropertyLink::leaveEvent(QEvent *ev) { + Gui::Selection().rmvPreselect(); + QDialog::leaveEvent(ev); +} + +void DlgPropertyLink::detachObserver() { + if(isConnectionAttached()) + detachSelection(); + + auto view = qobject_cast(parentView.data()); + if(view && savedSelections.size()) { + Gui::Selection().clearSelection(); + for(auto &sel : savedSelections) { + if(sel.getSubObject()) + Gui::Selection().addSelection(sel.getDocumentName().c_str(), + sel.getObjectName().c_str(), + sel.getSubName().c_str()); + } + savedSelections.clear(); + } + if(view) + view->blockConnection(false); + + parentView = nullptr; +} + +void DlgPropertyLink::onItemSelectionChanged() +{ + auto newSelections = ui->treeWidget->selectedItems(); + + if(newSelections.isEmpty() || selections.contains(newSelections.back())) { + selections = newSelections; + if(newSelections.isEmpty()) + currentObj = 0; + return; + } + + selections = newSelections; + + auto sobjs = getLinkFromItem(newSelections.back()); + App::DocumentObject *obj = sobjs.size()?sobjs.front().getObject():nullptr; + if(!obj) { + Gui::Selection().clearSelection(); + return; + } + + bool focus = false; + // Do auto view switch if tree view does not do it + if(!TreeParams::Instance()->SyncView()) { + focus = ui->treeWidget->hasFocus(); + auto doc = Gui::Application::Instance->getDocument(sobjs.front().getDocumentName().c_str()); + if(doc) { + auto vp = Base::freecad_dynamic_cast( + doc->getViewProvider(obj)); + if(vp) { + doc->setActiveView(vp, Gui::View3DInventor::getClassTypeId()); } - if(selected) - ui->typeTree->scrollToItem(selected); - ui->typeTree->blockSignals(false); - if(types.size() && !selected) { - types.clear(); - findObjects(); + } + } + + // Sync 3d view selection. To give a better visual feedback, we + // only keep the latest selection. + bool blocked = blockConnection(true); + Gui::Selection().clearSelection(); + for(auto &sobj : sobjs) + Gui::Selection().addSelection(sobj.getDocumentName().c_str(), + sobj.getObjectName().c_str(), + sobj.getSubName().c_str()); + blockConnection(blocked); + + // Enforce single parent + if(singleParent && currentObj && currentObj!=obj) { + ui->treeWidget->blockSignals(true); + for(auto item : ui->treeWidget->selectedItems()) { + if(item != selections.back()) + item->setSelected(false); + } + auto last = selections.back(); + selections.clear(); + selections.append(last); + ui->treeWidget->blockSignals(false); + } + currentObj = obj; + + if(focus) { + // FIXME: does not work, why? + ui->treeWidget->setFocus(); + } +} + +QTreeWidgetItem *DlgPropertyLink::findItem( + App::DocumentObject *obj, const char *subname, bool *pfound) +{ + if(pfound) + *pfound = false; + + if(!obj || !obj->getNameInDocument()) + return 0; + + std::vector sobjs; + if(subname && subname[0]) { + if(!allowSubObject) { + obj = obj->getSubObject(subname); + if(!obj) + return 0; + } else { + sobjs = obj->getSubObjectList(subname); + } + } + + auto itDoc = docItems.find(obj->getDocument()); + if(itDoc == docItems.end()) + return 0; + onItemExpanded(itDoc->second); + + auto it = itemMap.find(obj); + if(it == itemMap.end()) + return 0; + + if(!allowSubObject) { + if(pfound) + *pfound = true; + return it->second; + } + + QTreeWidgetItem *item = it->second; + + bool first = true; + for(auto o : sobjs) { + if(first) { + first = false; + continue; + } + onItemExpanded(item); + bool found = false; + for(int i=0,count=item->childCount();ichild(i); + if(strcmp(o->getNameInDocument(), + child->data(0, Qt::UserRole).toByteArray().constData())==0) + { + item = child; + found = true; + break; } } + if(!found) + return item; + } + if(pfound) + *pfound = true; + return item; +} + +void DlgPropertyLink::onSelectionChanged(const Gui::SelectionChanges& msg) +{ + if (msg.Type != SelectionChanges::AddSelection) + return; + + bool found = false; + auto selObj = msg.Object.getObject(); + + std::pair elementName; + const char *subname = msg.pSubName; + if(!ui->checkSubObject->isChecked()) { + selObj = App::GeoFeature::resolveElement(selObj,subname,elementName); + if(!selObj) + return; + subname = elementName.second.c_str(); + } + + auto item = findItem(selObj, msg.pSubName, &found); + if(!item || !found) + return; + + if(!item->isSelected()) { + ui->treeWidget->blockSignals(true); + if(singleSelect || (singleParent && currentObj && currentObj!=selObj)) + ui->treeWidget->selectionModel()->clearSelection(); + currentObj = selObj; + item->setSelected(true); + selections.append(item); + ui->treeWidget->blockSignals(false); + } + + ui->treeWidget->scrollToItem(item); + if(allowSubObject) { + QString element = QString::fromLatin1(msg.Object.getOldElementName().c_str()); + if(element.size()) { + QStringList list; + QString text = item->text(1); + if(text.size()) + list = text.split(QLatin1Char(',')); + if(list.indexOf(element)<0) { + list << element; + item->setText(1, list.join(QLatin1String(","))); + subSelections.insert(item); + } + } else if (subSelections.erase(item)) + item->setText(1, QString()); } } -QTreeWidgetItem *DlgPropertyLink::createItem(App::DocumentObject *obj, QTreeWidgetItem *parent) { +void DlgPropertyLink::accept() +{ + QDialog::accept(); +} + +static QTreeWidgetItem *_getLinkFromItem(std::ostringstream &ss, QTreeWidgetItem *item, const char *objName) { + auto parent = item->parent(); + assert(parent); + const char *nextName = parent->data(0, Qt::UserRole).toByteArray().constData(); + if(!nextName[0]) + return item; + + item = _getLinkFromItem(ss, parent, nextName); + ss << objName << '.'; + return item; +} + +QList +DlgPropertyLink::getLinkFromItem(QTreeWidgetItem *item, bool needSubName) const +{ + QList res; + + auto parent = item->parent(); + if(!parent) + return res; + + std::ostringstream ss; + auto parentItem = _getLinkFromItem(ss, item, + item->data(0,Qt::UserRole).toByteArray().constData()); + + App::SubObjectT sobj(parentItem->data(0, Qt::UserRole+1).toByteArray().constData(), + parentItem->data(0, Qt::UserRole).toByteArray().constData(), + ss.str().c_str()); + + QString elements; + if(needSubName && allowSubObject) + elements = item->text(1); + + if(elements.isEmpty()) { + res.append(App::SubObjectT()); + res.last() = std::move(sobj); + return res; + } + + for(const QString &element : elements.split(QLatin1Char(','))) { + res.append(App::SubObjectT()); + res.last() = App::SubObjectT(sobj.getDocumentName().c_str(), + sobj.getObjectName().c_str(), + (sobj.getSubName() + element.toLatin1().constData()).c_str()); + } + return res; +} + +void DlgPropertyLink::onTimer() { + auto item = ui->treeWidget->itemAt( + ui->treeWidget->viewport()->mapFromGlobal(QCursor::pos())); + if(!item) + return; + auto sobjs = getLinkFromItem(item); + if(sobjs.isEmpty()) + return; + const auto &sobj = sobjs.front(); + Gui::Selection().setPreselect(sobj.getDocumentName().c_str(), + sobj.getObjectName().c_str(), + sobj.getSubName().c_str(), + 0,0,0,2); +} + +QList DlgPropertyLink::currentLinks() const +{ + auto items = ui->treeWidget->selectedItems(); + QList res; + for(auto item : items) + res.append(getLinkFromItem(item)); + return res; +} + +QList DlgPropertyLink::originalLinks() const +{ + return oldLinks; +} + +QString DlgPropertyLink::linksToPython(QList links) { + if(links.isEmpty()) + return QLatin1String("None"); + + if(links.size() == 1) + return QString::fromLatin1(links.front().getSubObjectPython(false).c_str()); + + std::ostringstream ss; + + if(isLinkSub(links)) { + ss << '(' << links.front().getObjectPython() << ", ["; + for(auto link : links) { + const auto &sub = link.getSubName(); + if(sub.size()) + ss << "u'" << Base::Tools::escapedUnicodeFromUtf8(sub.c_str()) << "',"; + } + ss << "])"; + } else { + ss << '['; + for(auto link : links) + ss << link.getSubObjectPython(false) << ','; + ss << ']'; + } + + return QString::fromLatin1(ss.str().c_str()); +} + +void DlgPropertyLink::filterObjects() +{ + for(int i=0, count=ui->treeWidget->topLevelItemCount(); itreeWidget->topLevelItem(i); + for(int j=0, c=item->childCount(); jchild(j)); + } +} + +void DlgPropertyLink::filterItem(QTreeWidgetItem *item) { + if(filterType(item)) { + item->setHidden(true); + return; + } + item->setHidden(false); + for(int i=0, count=item->childCount(); ichild(i)); +} + +bool DlgPropertyLink::eventFilter(QObject *obj, QEvent *e) { + if(obj == ui->searchBox + && e->type() == QEvent::KeyPress + && static_cast(e)->key() == Qt::Key_Escape) + { + ui->searchBox->setText(QString()); + return true; + } + return QDialog::eventFilter(obj,e); +} + +void DlgPropertyLink::onItemSearch() { + itemSearch(ui->searchBox->text(), true); +} + +void DlgPropertyLink::keyPressEvent(QKeyEvent *ev) +{ + if(ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { + if(ui->searchBox->hasFocus()) + return; + } + QDialog::keyPressEvent(ev); +} + +void DlgPropertyLink::itemSearch(const QString &text, bool select) { + if(searchItem) + searchItem->setBackground(0, bgBrush); + + auto owner = objProp.getObject(); + if(!owner) + return; + + std::string txt(text.toUtf8().constData()); + try { + if(txt.empty()) + return; + if(txt.find("<<") == std::string::npos) { + auto pos = txt.find('.'); + if(pos==std::string::npos) + txt += '.'; + else if(pos!=txt.size()-1) { + txt.insert(pos+1,"<<"); + if(txt.back()!='.') + txt += '.'; + txt += ">>."; + } + }else if(txt.back() != '.') + txt += '.'; + txt += "_self"; + auto path = App::ObjectIdentifier::parse(owner,txt); + if(path.getPropertyName() != "_self") + return; + + App::DocumentObject *obj = path.getDocumentObject(); + if(!obj) + return; + + bool found; + const char *subname = path.getSubObjectName().c_str(); + QTreeWidgetItem *item = findItem(obj, subname, &found); + if(!item) + return; + + if(select) { + if(!found) + return; + Gui::Selection().addSelection(obj->getDocument()->getName(), + obj->getNameInDocument(),subname); + }else{ + Selection().setPreselect(obj->getDocument()->getName(), + obj->getNameInDocument(), subname,0,0,0,2); + searchItem = item; + ui->treeWidget->scrollToItem(searchItem); + bgBrush = searchItem->background(0); + searchItem->setBackground(0, QColor(255, 255, 0, 100)); + } + } catch(...) + { + } +} + +QTreeWidgetItem *DlgPropertyLink::createItem( + App::DocumentObject *obj, QTreeWidgetItem *parent) +{ if(!obj || !obj->getNameInDocument()) return 0; if(inList.find(obj)!=inList.end()) return 0; - auto vp = Gui::Application::Instance->getViewProvider(obj); + auto vp = Base::freecad_dynamic_cast( + Application::Instance->getViewProvider(obj)); if(!vp) return 0; - QString searchText = ui->searchBox->text(); - if (!searchText.isEmpty()) { - QString label = QString::fromUtf8((obj)->Label.getValue()); - if (!label.contains(searchText,Qt::CaseInsensitive)) - return 0; - } + QTreeWidgetItem* item; if(parent) item = new QTreeWidgetItem(parent); @@ -343,54 +890,171 @@ QTreeWidgetItem *DlgPropertyLink::createItem(App::DocumentObject *obj, QTreeWidg item->setText(0, QString::fromUtf8((obj)->Label.getValue())); item->setData(0, Qt::UserRole, QByteArray(obj->getNameInDocument())); item->setData(0, Qt::UserRole+1, QByteArray(obj->getDocument()->getName())); - if(link.size()>=5) { - item->setChildIndicatorPolicy(obj->hasChildElement()||vp->getChildRoot()? + + if(allowSubObject) { + item->setChildIndicatorPolicy(obj->getLinkedObject(true)->getOutList().size()? QTreeWidgetItem::ShowIndicator:QTreeWidgetItem::DontShowIndicator); + item->setFlags(item->flags() | Qt::ItemIsEditable); + } + + const char *typeName = obj->getTypeId().getName(); + QByteArray typeData = QByteArray::fromRawData(typeName, strlen(typeName)+1); + item->setData(0, Qt::UserRole+2, typeData); + + QByteArray proxyType; + auto prop = Base::freecad_dynamic_cast( + obj->getPropertyByName("Proxy")); + if(prop) { + Base::PyGILStateLocker lock; + Py::Object proxy = prop->getValue(); + if(!proxy.isNone() && !proxy.isString()) { + const char *name = 0; + if (proxy.hasAttr("__class__")) + proxyType = QByteArray(proxy.getAttr("__class__").as_string().c_str()); + else { + name = proxy.ptr()->ob_type->tp_name; + proxyType = QByteArray::fromRawData(name, strlen(name)+1); + } + auto it = typeItems.find(proxyType); + if(it != typeItems.end()) + proxyType = it->first; + else if (name) + proxyType = QByteArray(name, proxyType.size()); + } + } + item->setData(0, Qt::UserRole+3, proxyType); + + filterItem(item); + return item; +} + +QTreeWidgetItem *DlgPropertyLink::createTypeItem(Base::Type type) { + if(type.isBad()) + return 0; + + QTreeWidgetItem *item = 0; + if(!type.isBad() && type!=App::DocumentObject::getClassTypeId()) { + Base::Type parentType = type.getParent(); + if(!parentType.isBad()) { + const char *name = parentType.getName(); + auto typeData = QByteArray::fromRawData(name,strlen(name)+1); + auto &typeItem = typeItems[typeData]; + if(!typeItem) { + typeItem = createTypeItem(parentType); + typeItem->setData(0, Qt::UserRole, typeData); + } + item = typeItem; + } } + + if(!item) + item = new QTreeWidgetItem(ui->typeTree); + else + item = new QTreeWidgetItem(item); + item->setExpanded(true); + item->setText(0, QString::fromLatin1(type.getName())); + if(type == App::DocumentObject::getClassTypeId()) + item->setFlags(Qt::ItemIsEnabled); return item; } +bool DlgPropertyLink::filterType(QTreeWidgetItem *item) { + auto proxyType = item->data(0, Qt::UserRole+3).toByteArray(); + QTreeWidgetItem *proxyItem = 0; + if(proxyType.size()) { + auto &pitem = typeItems[proxyType]; + if(!pitem) { + pitem = new QTreeWidgetItem(ui->typeTree); + pitem->setText(0,QString::fromLatin1(proxyType)); + pitem->setIcon(0,item->icon(0)); + pitem->setData(0,Qt::UserRole,proxyType); + } + proxyItem = pitem; + } + + auto typeData = item->data(0, Qt::UserRole+2).toByteArray(); + Base::Type type = Base::Type::fromName(typeData.constData()); + if(type.isBad()) + return false; + + QTreeWidgetItem *&typeItem = typeItems[typeData]; + if(!typeItem) { + typeItem = createTypeItem(type); + typeItem->setData(0, Qt::UserRole, typeData); + } + + if(!proxyType.size()) { + QIcon icon = typeItem->icon(0); + if(icon.isNull()) + typeItem->setIcon(0, item->icon(0)); + } + + if(!ui->checkObjectType->isChecked() || selectedTypes.empty()) + return false; + + if(proxyItem && selectedTypes.count(proxyType)) + return false; + + for(auto t=type; !t.isBad() && t!=App::DocumentObject::getClassTypeId(); t=t.getParent()) { + const char *name = t.getName(); + if(selectedTypes.count(QByteArray::fromRawData(name, strlen(name)+1))) + return false; + } + + return true; +} + void DlgPropertyLink::onItemExpanded(QTreeWidgetItem * item) { - if(link.size()<5 || item->childCount()) + if(item->childCount()) return; - std::string name(qPrintable(item->data(0, Qt::UserRole).toString())); - std::string docName(qPrintable(item->data(0, Qt::UserRole+1).toString())); - auto doc = App::GetApplication().getDocument(docName.c_str()); - if(doc) { - auto obj = doc->getObject(name.c_str()); + const char *docName = item->data(0, Qt::UserRole+1).toByteArray().constData(); + auto doc = App::GetApplication().getDocument(docName); + if(!doc) + return; + + const char *objName = item->data(0, Qt::UserRole).toByteArray().constData(); + if(!objName[0]) { + for(auto obj : doc->getObjects()) { + auto newItem = createItem(obj,item); + if(newItem) + itemMap[obj] = newItem; + } + } else if(allowSubObject) { + auto obj = doc->getObject(objName); if(!obj) return; - auto vp = Application::Instance->getViewProvider(obj); - if(!vp) return; - for(auto obj : vp->claimChildren()) - createItem(obj,item); + std::set childSet; + std::string sub; + for(auto child : obj->getLinkedObject(true)->getOutList()) { + if(!childSet.insert(child).second) + continue; + sub = child->getNameInDocument(); + sub += "."; + if(obj->getSubObject(sub.c_str())) + createItem(child,item); + } } } void DlgPropertyLink::on_checkObjectType_toggled(bool on) { ui->typeTree->setVisible(on); - findObjects(); + filterObjects(); } void DlgPropertyLink::on_typeTree_itemSelectionChanged() { - types.clear(); + + selectedTypes.clear(); for(auto item : ui->typeTree->selectedItems()) - types.insert(item->text(0).toLatin1().constData()); - findObjects(); -} + selectedTypes.insert(item->data(0, Qt::UserRole).toByteArray()); -void DlgPropertyLink::on_searchBox_textChanged(const QString& /*search*/) -{ - findObjects(); + if(ui->checkObjectType->isChecked()) + filterObjects(); } -void DlgPropertyLink::on_comboBox_currentIndexChanged(int index) +void DlgPropertyLink::on_searchBox_textChanged(const QString& text) { - link[0] = ui->comboBox->itemData(index).toString(); - refreshTypes = true; - findObjects(); + itemSearch(text,false); } - #include "moc_DlgPropertyLink.cpp" diff --git a/src/Gui/DlgPropertyLink.h b/src/Gui/DlgPropertyLink.h index e156a68dfd4b..29e1156c9cde 100644 --- a/src/Gui/DlgPropertyLink.h +++ b/src/Gui/DlgPropertyLink.h @@ -26,42 +26,100 @@ #include #include +#include +#include +#include #define FC_XLINK_VALUE_INDEX 5 namespace Gui { namespace Dialog { class Ui_DlgPropertyLink; -class DlgPropertyLink : public QDialog +class DlgPropertyLink : public QDialog, public Gui::SelectionObserver { Q_OBJECT public: - DlgPropertyLink(const QStringList& list, QWidget* parent = 0, Qt::WindowFlags fl = 0, bool xlink=false); + DlgPropertyLink(QWidget* parent = 0); ~DlgPropertyLink(); - void setSelectionMode(QAbstractItemView::SelectionMode mode); void accept(); - QStringList propertyLink() const; - QVariantList propertyLinkList() const; + + QList currentLinks() const; + QList originalLinks() const; + + void init(const App::DocumentObjectT &prop, bool tryFilter=true); + + static QString linksToPython(QList links); + + static QList getLinksFromProperty(const App::PropertyLinkBase *prop); + + static QString formatObject(App::Document *ownerDoc, App::DocumentObject *obj, const char *sub); + + static inline QString formatObject(App::Document *ownerDoc, const App::SubObjectT &sobj) { + return formatObject(ownerDoc, sobj.getObject(), sobj.getSubName().c_str()); + } + + static QString formatLinks(App::Document *ownerDoc, QList links); + +protected: + void showEvent(QShowEvent *); + void hideEvent(QHideEvent *); + void closeEvent (QCloseEvent * e); + void leaveEvent(QEvent *); + bool eventFilter(QObject *obj, QEvent *ev); + void keyPressEvent(QKeyEvent *ev); + + void detachObserver(); + void attachObserver(); + + void onSelectionChanged(const Gui::SelectionChanges& msg); private Q_SLOTS: void on_checkObjectType_toggled(bool); void on_typeTree_itemSelectionChanged(); void on_searchBox_textChanged(const QString&); - void on_comboBox_currentIndexChanged(int); void onItemExpanded(QTreeWidgetItem * item); + void onItemSelectionChanged(); + void onItemEntered(QTreeWidgetItem *item); + void onItemSearch(); + void onTimer(); + void onClicked(QAbstractButton *); private: QTreeWidgetItem *createItem(App::DocumentObject *obj, QTreeWidgetItem *parent); - void findObjects(); + QTreeWidgetItem *createTypeItem(Base::Type type); + void filterObjects(); + void filterItem(QTreeWidgetItem *item); + bool filterType(QTreeWidgetItem *item); + QTreeWidgetItem *findItem(App::DocumentObject *obj, const char *subname=0, bool *found=nullptr); + void itemSearch(const QString &text, bool select); + QList getLinkFromItem(QTreeWidgetItem *, bool needSubName=true) const; private: - QStringList link; Ui_DlgPropertyLink* ui; + QTimer *timer; + QPushButton *resetButton; + QPushButton *refreshButton; + + QPointer parentView; + std::vector savedSelections; + + App::DocumentObjectT objProp; std::set inList; - std::set types; - bool refreshTypes = true; + std::map docItems; + std::map itemMap; + std::map typeItems; + std::set subSelections; + QList selections; + std::set selectedTypes; + QList oldLinks; + bool allowSubObject = false; + bool singleSelect = false; + bool singleParent = false; + App::DocumentObject *currentObj = nullptr; + QTreeWidgetItem *searchItem = nullptr; + QBrush bgBrush; }; } // namespace Dialog diff --git a/src/Gui/DlgPropertyLink.ui b/src/Gui/DlgPropertyLink.ui index 133e3f47ed7d..6f9691749771 100644 --- a/src/Gui/DlgPropertyLink.ui +++ b/src/Gui/DlgPropertyLink.ui @@ -15,28 +15,6 @@ - - - Filter by type - - - - - - - true - - - false - - - - 1 - - - - - @@ -46,7 +24,7 @@ - + A search pattern to filter the results above @@ -54,7 +32,28 @@ - + + + + QAbstractItemView::ExtendedSelection + + + false + + + true + + + false + + + + 1 + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok @@ -62,14 +61,17 @@ - - - - - - QAbstractItemView::ExtendedSelection + + + QAbstractItemView::NoEditTriggers - + + false + + + true + + false @@ -82,8 +84,42 @@ + + + + + + Filter by type + + + + + + + If enabled, then 3D view selection will be syncrhonize with full object hierarchy. + + + Sync sub-object selection + + + + + + + + Gui::ExpressionLineEdit + QLineEdit +
Gui/ExpressionCompleter.h
+
+
+ + treeWidget + typeTree + searchBox + buttonBox + diff --git a/src/Gui/DlgSettings3DView.ui b/src/Gui/DlgSettings3DView.ui index 5dabdba1b483..a4bd3e0b5f28 100644 --- a/src/Gui/DlgSettings3DView.ui +++ b/src/Gui/DlgSettings3DView.ui @@ -408,10 +408,10 @@ The value is the diameter of the sphere to fit on the screen.
10000000.000000000000000 - NewDocumentCameraScale + NewDocumentCameraScale - View + View 100.000000000000000 diff --git a/src/Gui/DlgSettingsDocument.ui b/src/Gui/DlgSettingsDocument.ui index 5bf2e0dd139a..129007a9794a 100644 --- a/src/Gui/DlgSettingsDocument.ui +++ b/src/Gui/DlgSettingsDocument.ui @@ -187,10 +187,10 @@ This feature may slightly increase recomputation time.
Allow aborting recomputation - CanAbortRecompute + CanAbortRecompute - Document + Document diff --git a/src/Gui/Document.cpp b/src/Gui/Document.cpp index e28f44242b8d..e17726700f7d 100644 --- a/src/Gui/Document.cpp +++ b/src/Gui/Document.cpp @@ -462,7 +462,12 @@ void Document::_resetEdit(void) } d->_editViewProvider->finishEditing(); - if (d->_editViewProvider->isDerivedFrom(ViewProviderDocumentObject::getClassTypeId())) + + // Have to check d->_editViewProvider below, because there is a chance + // the editing object gets deleted inside the above call to + // 'finishEditing()', which will trigger our slotDeletedObject(), which + // nullifies _editViewProvider. + if (d->_editViewProvider && d->_editViewProvider->isDerivedFrom(ViewProviderDocumentObject::getClassTypeId())) signalResetEdit(*(static_cast(d->_editViewProvider))); d->_editViewProvider = 0; diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp index 8de8c6cae29f..0c377627b68c 100644 --- a/src/Gui/ExpressionCompleter.cpp +++ b/src/Gui/ExpressionCompleter.cpp @@ -36,6 +36,10 @@ class ExpressionCompleterModel: public QAbstractItemModel { setDocumentObject(obj); } + void setNoProperty(bool enabled) { + noProperty = enabled; + } + void setDocumentObject(const App::DocumentObject *obj) { beginResetModel(); if(obj) { @@ -337,6 +341,13 @@ void ExpressionCompleter::setDocumentObject(const App::DocumentObject *obj) { static_cast(m)->setDocumentObject(obj); } +void ExpressionCompleter::setNoProperty(bool enabled) { + noProperty = enabled; + auto m = model(); + if(m) + static_cast(m)->setNoProperty(enabled); +} + QString ExpressionCompleter::pathFromIndex ( const QModelIndex & index ) const { auto m = model(); @@ -545,6 +556,12 @@ void ExpressionLineEdit::setDocumentObject(const App::DocumentObject * currentDo } } +void ExpressionLineEdit::setNoProperty(bool enabled) { + noProperty = enabled; + if(completer) + completer->setNoProperty(enabled); +} + bool ExpressionLineEdit::completerActive() const { return completer && completer->popup() && completer->popup()->isVisible(); diff --git a/src/Gui/ExpressionCompleter.h b/src/Gui/ExpressionCompleter.h index 7370f16d7bc5..df75a4ccdfa1 100644 --- a/src/Gui/ExpressionCompleter.h +++ b/src/Gui/ExpressionCompleter.h @@ -42,6 +42,8 @@ class GuiExport ExpressionCompleter : public QCompleter void setDocumentObject(const App::DocumentObject*); + void setNoProperty(bool enabled=true); + public Q_SLOTS: void slotUpdate(const QString &prefix, int pos); @@ -65,6 +67,7 @@ class GuiExport ExpressionLineEdit : public QLineEdit { void setDocumentObject(const App::DocumentObject *currentDocObj); bool completerActive() const; void hideCompleter(); + void setNoProperty(bool enabled=true); Q_SIGNALS: void textChanged2(QString text, int pos); public Q_SLOTS: diff --git a/src/Gui/MetaTypes.h b/src/Gui/MetaTypes.h index 1ff64406b175..8825c2b9c0aa 100644 --- a/src/Gui/MetaTypes.h +++ b/src/Gui/MetaTypes.h @@ -27,6 +27,7 @@ #include #include #include +#include Q_DECLARE_METATYPE(Base::Vector3f) Q_DECLARE_METATYPE(Base::Vector3d) @@ -34,5 +35,7 @@ Q_DECLARE_METATYPE(Base::Matrix4D) Q_DECLARE_METATYPE(Base::Placement) Q_DECLARE_METATYPE(Base::Quantity) Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(App::SubObjectT) +Q_DECLARE_METATYPE(QList) #endif // GUI_METATYPES_H diff --git a/src/Gui/ProgressBar.cpp b/src/Gui/ProgressBar.cpp index 400c33dbef61..9f9dc61b6637 100644 --- a/src/Gui/ProgressBar.cpp +++ b/src/Gui/ProgressBar.cpp @@ -571,6 +571,9 @@ bool ProgressBar::eventFilter(QObject* o, QEvent* e) case QEvent::Enter: case QEvent::Leave: case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: + case QEvent::NativeGesture: case QEvent::ContextMenu: { return true; diff --git a/src/Gui/Selection.cpp b/src/Gui/Selection.cpp index 70ed8f3f49a6..469944a6346f 100644 --- a/src/Gui/Selection.cpp +++ b/src/Gui/Selection.cpp @@ -361,7 +361,7 @@ bool SelectionSingleton::hasSelection() const } bool SelectionSingleton::hasPreselection() const { - return !CurrentPreselection.ObjName.empty(); + return !CurrentPreselection.Object.getObjectName().empty(); } std::vector SelectionSingleton::getCompleteSelection(int resolve) const @@ -584,9 +584,7 @@ void SelectionSingleton::notify(SelectionChanges &&Chng) { break; case SelectionChanges::SetPreselect: notify = CurrentPreselection.Type==SelectionChanges::SetPreselect - && CurrentPreselection.DocName == msg.DocName - && CurrentPreselection.ObjName == msg.ObjName - && CurrentPreselection.SubName == msg.SubName; + && CurrentPreselection.Object == msg.Object; break; case SelectionChanges::RmvPreselect: notify = CurrentPreselection.Type==SelectionChanges::ClrSelection; @@ -719,14 +717,12 @@ void SelectionSingleton::slotSelectionChanged(const SelectionChanges& msg) { msg.Type == SelectionChanges::HideSelection) return; - if(msg.DocName.size() && msg.ObjName.size() && msg.SubName.size()) { - App::Document* pDoc = getDocument(msg.pDocName); - if(!pDoc) return; + if(msg.Object.getSubName().size()) { + auto pParent = msg.Object.getObject(); + if(!pParent) return; std::pair elementName; auto &newElementName = elementName.first; auto &oldElementName = elementName.second; - auto pParent = pDoc->getObject(msg.pObjectName); - if(!pParent) return; auto pObject = App::GeoFeature::resolveElement(pParent,msg.pSubName,elementName); if (!pObject) return; SelectionChanges msg2(msg.Type,pObject->getDocument()->getName(), @@ -735,12 +731,10 @@ void SelectionSingleton::slotSelectionChanged(const SelectionChanges& msg) { pObject->getTypeId().getName(), msg.x,msg.y,msg.z); msg2.pOriginalMsg = &msg; - msg2.pParentObject = pParent; - msg2.pSubObject = pObject; signalSelectionChanged3(msg2); - msg2.SubName = oldElementName; - msg2.pSubName = msg2.SubName.c_str(); + msg2.Object.setSubName(oldElementName.c_str()); + msg2.pSubName = msg2.Object.getSubName().c_str(); signalSelectionChanged2(msg2); }else { @@ -854,7 +848,7 @@ void SelectionSingleton::setPreselectCoord( float x, float y, float z) static char buf[513]; // if nothing is in preselect ignore - if(!CurrentPreselection.pObjectName || CurrentPreselection.ObjName.empty()) return; + if(CurrentPreselection.Object.getObjectName().empty()) return; CurrentPreselection.x = x; CurrentPreselection.y = y; @@ -1053,7 +1047,7 @@ bool SelectionSingleton::addSelection(const char* pDocName, const char* pObjectN SelectionChanges Chng(SelectionChanges::AddSelection, temp.DocName,temp.FeatName,temp.SubName,temp.TypeName, x,y,z); - FC_LOG("Add Selection "<getTypeId().getName()); - FC_LOG("Update Selection "<updateActions(); @@ -1674,7 +1668,7 @@ void SelectionSingleton::slotDeletedObject(const App::DocumentObject& Obj) } if(changes.size()) { for(auto &Chng : changes) { - FC_LOG("Rmv Selection "<updateActions(); diff --git a/src/Gui/Selection.h b/src/Gui/Selection.h index 4add9f4c0e9e..b982d4cf5efa 100644 --- a/src/Gui/Selection.h +++ b/src/Gui/Selection.h @@ -38,7 +38,7 @@ #include #include #include - +#include #include namespace App @@ -85,13 +85,11 @@ class GuiExport SelectionChanges float x=0, float y=0, float z=0, int subtype=0) : Type(type),SubType(subtype) , x(x),y(y),z(z) + , Object(docName,objName,subName) { - if(docName) DocName=docName; - pDocName = DocName.c_str(); - if(objName) ObjName = objName; - pObjectName = ObjName.c_str(); - if(subName) SubName = subName; - pSubName = SubName.c_str(); + pDocName = Object.getDocumentName().c_str(); + pObjectName = Object.getObjectName().c_str(); + pSubName = Object.getSubName().c_str(); if(typeName) TypeName = typeName; pTypeName = TypeName.c_str(); } @@ -104,14 +102,12 @@ class GuiExport SelectionChanges float x=0,float y=0,float z=0, int subtype=0) : Type(type), SubType(subtype) , x(x),y(y),z(z) - , DocName(docName) - , ObjName(objName) - , SubName(subName) + , Object(docName.c_str(), objName.c_str(), subName.c_str()) , TypeName(typeName) { - pDocName = DocName.c_str(); - pObjectName = ObjName.c_str(); - pSubName = SubName.c_str(); + pDocName = Object.getDocumentName().c_str(); + pObjectName = Object.getObjectName().c_str(); + pSubName = Object.getSubName().c_str(); pTypeName = TypeName.c_str(); } @@ -125,17 +121,13 @@ class GuiExport SelectionChanges x = other.x; y = other.y; z = other.z; - DocName = other.DocName; - ObjName = other.ObjName; - SubName = other.SubName; + Object = other.Object; TypeName = other.TypeName; - pDocName = DocName.c_str(); - pObjectName = ObjName.c_str(); - pSubName = SubName.c_str(); + pDocName = Object.getDocumentName().c_str(); + pObjectName = Object.getObjectName().c_str(); + pSubName = Object.getSubName().c_str(); pTypeName = TypeName.c_str(); pOriginalMsg = other.pOriginalMsg; - pSubObject = other.pSubObject; - pParentObject = other.pParentObject; return *this; } @@ -149,17 +141,13 @@ class GuiExport SelectionChanges x = other.x; y = other.y; z = other.z; - DocName = std::move(other.DocName); - ObjName = std::move(other.ObjName); - SubName = std::move(other.SubName); + Object = std::move(other.Object); TypeName = std::move(other.TypeName); - pDocName = DocName.c_str(); - pObjectName = ObjName.c_str(); - pSubName = SubName.c_str(); + pDocName = Object.getDocumentName().c_str(); + pObjectName = Object.getObjectName().c_str(); + pSubName = Object.getSubName().c_str(); pTypeName = TypeName.c_str(); pOriginalMsg = other.pOriginalMsg; - pSubObject = other.pSubObject; - pParentObject = other.pParentObject; return *this; } @@ -174,19 +162,9 @@ class GuiExport SelectionChanges float y; float z; - // For more robust selection notification (e.g. in case user make selection - // change inside selection notification handler), the notification message - // shall make a copy of all the strings here. - std::string DocName; - std::string ObjName; - std::string SubName; + App::SubObjectT Object; std::string TypeName; - // Resolved sub object in case resolve!=0, otherwise this is null - App::DocumentObject *pSubObject = 0; - // Resolved parent object in case resolve!=0, otherwise this is null - App::DocumentObject *pParentObject = 0; - // Original selection message in case resolve!=0 const SelectionChanges *pOriginalMsg = 0; }; diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index eda68903ff21..83196b85b111 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -861,7 +861,7 @@ void View3DInventorViewer::checkGroupOnTop(const SelectionChanges &Reason) { return; } if(childRoot->findChild(childVp->getRoot())<0) { - FC_WARN("cannot find '" << childVp->getObject()->getFullName() + FC_LOG("cannot find '" << childVp->getObject()->getFullName() << "' in geo group '" << grp->getNameInDocument() << "'"); break; } diff --git a/src/Gui/propertyeditor/PropertyEditor.cpp b/src/Gui/propertyeditor/PropertyEditor.cpp index 0167c3cf32cd..e839aedf4b6a 100644 --- a/src/Gui/propertyeditor/PropertyEditor.cpp +++ b/src/Gui/propertyeditor/PropertyEditor.cpp @@ -159,16 +159,7 @@ void PropertyEditor::editorDestroyed (QObject * editor) // When editing expression through context menu, the editor (ExpLineEditor) // deletes itself when finished, so it won't trigger closeEditor signal. We // must handle it here to perform auto update. - if (autoupdate) { - App::Document* doc = App::GetApplication().getActiveDocument(); - if (doc) { - if (!doc->isTransactionEmpty()) { - if (doc->isTouched()) - doc->recompute(); - } - } - App::GetApplication().closeActiveTransaction(); - } + closeTransaction(); } void PropertyEditor::currentChanged ( const QModelIndex & current, const QModelIndex & previous ) @@ -246,10 +237,8 @@ void PropertyEditor::onItemActivated ( const QModelIndex & index ) setupTransaction(index); } -void PropertyEditor::closeEditor (QWidget * editor, QAbstractItemDelegate::EndEditHint hint) +void PropertyEditor::closeTransaction() { - QTreeView::closeEditor(editor, hint); - if (autoupdate) { App::Document* doc = App::GetApplication().getActiveDocument(); if (doc) { @@ -262,6 +251,13 @@ void PropertyEditor::closeEditor (QWidget * editor, QAbstractItemDelegate::EndEd } App::GetApplication().closeActiveTransaction(); } +} + +void PropertyEditor::closeEditor (QWidget * editor, QAbstractItemDelegate::EndEditHint hint) +{ + QTreeView::closeEditor(editor, hint); + + closeTransaction(); QModelIndex indexSaved = currentIndex(); FC_LOG("index saved " << indexSaved.row() << ", " << indexSaved.column()); @@ -329,6 +325,9 @@ void PropertyEditor::buildUp(PropertyModel::PropertyList &&props, bool checkDocu return; } + if(this->state() == EditingState) + closeTransaction(); + QModelIndex index = this->currentIndex(); QStringList propertyPath = propertyModel->propertyPathFromIndex(index); if (!propertyPath.isEmpty()) diff --git a/src/Gui/propertyeditor/PropertyEditor.h b/src/Gui/propertyeditor/PropertyEditor.h index 775a04b625d2..a41087b0fe96 100644 --- a/src/Gui/propertyeditor/PropertyEditor.h +++ b/src/Gui/propertyeditor/PropertyEditor.h @@ -107,6 +107,7 @@ protected Q_SLOTS: void setEditorMode(const QModelIndex & parent, int start, int end); void updateItemEditor(bool enable, int column, const QModelIndex& parent); void setupTransaction(const QModelIndex &); + void closeTransaction(); private: PropertyItemDelegate *delegate; diff --git a/src/Gui/propertyeditor/PropertyItem.cpp b/src/Gui/propertyeditor/PropertyItem.cpp index 0af57b952d52..c9757b38e009 100644 --- a/src/Gui/propertyeditor/PropertyItem.cpp +++ b/src/Gui/propertyeditor/PropertyItem.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,7 @@ #include using namespace Gui::PropertyEditor; +using namespace Gui::Dialog; Gui::PropertyEditor::PropertyItemFactory* Gui::PropertyEditor::PropertyItemFactory::_singleton = NULL; @@ -3572,7 +3574,7 @@ QVariant PropertyTransientFileItem::editorData(QWidget *editor) const // --------------------------------------------------------------- -LinkSelection::LinkSelection(const QStringList& list) : link(list) +LinkSelection::LinkSelection(const App::SubObjectT &link) : link(link) { } @@ -3582,15 +3584,23 @@ LinkSelection::~LinkSelection() void LinkSelection::select() { + auto sobj = link.getSubObject(); + if(!sobj) { + QMessageBox::critical(getMainWindow(), tr("Error"), tr("Object not found")); + return; + } + Gui::Selection().selStackPush(); Gui::Selection().clearSelection(); - Gui::Selection().addSelection((const char*)link[0].toLatin1(), - (const char*)link[1].toLatin1()); + Gui::Selection().addSelection(link.getDocumentName().c_str(), + link.getObjectName().c_str(), + link.getSubName().c_str()); this->deleteLater(); } // --------------------------------------------------------------- -LinkLabel::LinkLabel (QWidget * parent, bool xlink) : QWidget(parent), isXLink(xlink) +LinkLabel::LinkLabel (QWidget * parent, const App::Property *prop) + : QWidget(parent), objProp(prop), dlg(nullptr) { QHBoxLayout *layout = new QHBoxLayout(this); layout->setMargin(0); @@ -3599,6 +3609,8 @@ LinkLabel::LinkLabel (QWidget * parent, bool xlink) : QWidget(parent), isXLink(x label = new QLabel(this); label->setAutoFillBackground(true); label->setTextFormat(Qt::RichText); + // Below is necessary for the hytperlink to be clickable without losing focus + label->setTextInteractionFlags(Qt::TextBrowserInteraction); layout->addWidget(label); editButton = new QPushButton(QLatin1String("..."), this); @@ -3620,23 +3632,42 @@ LinkLabel::~LinkLabel() { } -void LinkLabel::setPropertyLink(const QStringList& o) -{ - link = o; - QString linkcolor = QApplication::palette().color(QPalette::Link).name(); - QString text = QString::fromLatin1( - "" - "

" - "%4" - "

" - ) - .arg(link[0], link[1], linkcolor, link[2]); +void LinkLabel::updatePropertyLink() +{ + QString text; + auto owner = objProp.getObject(); + auto prop = Base::freecad_dynamic_cast(objProp.getProperty()); + + link = QVariant(); + + if(owner && prop) { + auto links = DlgPropertyLink::getLinksFromProperty(prop); + if(links.size() == 1) { + auto &sobj = links.front(); + link = QVariant::fromValue(sobj); + QString linkcolor = QApplication::palette().color(QPalette::Link).name(); + text = QString::fromLatin1( + "" + "

" + "%5" + "

" + ) + .arg(QLatin1String(sobj.getDocumentName().c_str()), + QLatin1String(sobj.getObjectName().c_str()), + QString::fromUtf8(sobj.getSubName().c_str()), + linkcolor, + DlgPropertyLink::formatObject( + owner->getDocument(), sobj.getObject(), sobj.getSubName().c_str())); + } else if (links.size()) { + text = DlgPropertyLink::formatLinks(owner->getDocument(), links); + } + } label->setText(text); } -QStringList LinkLabel::propertyLink() const +QVariant LinkLabel::propertyLink() const { return link; } @@ -3644,50 +3675,71 @@ QStringList LinkLabel::propertyLink() const void LinkLabel::onLinkActivated (const QString& s) { Q_UNUSED(s); - LinkSelection* select = new LinkSelection(link); + LinkSelection* select = new LinkSelection(qvariant_cast(link)); QTimer::singleShot(50, select, SLOT(select())); } void LinkLabel::onEditClicked () { - Gui::Dialog::DlgPropertyLink dlg(link, this, 0, isXLink); - if (dlg.exec() == QDialog::Accepted) { - setPropertyLink(dlg.propertyLink()); - /*emit*/ linkChanged(link); + if(!dlg) { + dlg = new DlgPropertyLink(this); + dlg->init(objProp,true); + connect(dlg, SIGNAL(accepted()), this, SLOT(onLinkChanged())); + } else + dlg->init(objProp,false); + dlg->show(); +} + +void LinkLabel::onLinkChanged() { + if(dlg) { + auto links = dlg->currentLinks(); + if(links != dlg->originalLinks()) { + link = QVariant::fromValue(links); + /*emit*/ linkChanged(link); + updatePropertyLink(); + } } } void LinkLabel::resizeEvent(QResizeEvent* e) { editButton->setFixedWidth(e->size().height()); - editButton->setFixedHeight(e->size().height()); } +// -------------------------------------------------------------------- PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyLinkItem) -PropertyLinkItem::PropertyLinkItem():isXLink(false) +PropertyLinkItem::PropertyLinkItem() { } QVariant PropertyLinkItem::toString(const QVariant& prop) const { - QStringList list = prop.toStringList(); - return QVariant(list[2]); + QString res; + if(propertyItems.size()) { + App::DocumentObjectT owner(propertyItems[0]); + res = DlgPropertyLink::formatLinks(owner.getDocument(), + qvariant_cast >(prop)); + } + return res; } QVariant PropertyLinkItem::data(int column, int role) const { if(propertyItems.size() && column == 1 && (role == Qt::TextColorRole || role == Qt::ToolTipRole)) { - auto xlink = Base::freecad_dynamic_cast(propertyItems[0]); - if(xlink) { - if(role==Qt::TextColorRole && xlink->checkRestore()>1) + auto propLink = Base::freecad_dynamic_cast(propertyItems[0]); + if(propLink) { + if(role==Qt::TextColorRole && propLink->checkRestore()>1) return QVariant::fromValue(QColor(0xff,0,0)); else if(role == Qt::ToolTipRole) { - const char *filePath = xlink->getFilePath(); - if(filePath && filePath[0]) - return QVariant::fromValue(QString::fromUtf8(filePath)); + auto xlink = Base::freecad_dynamic_cast(propertyItems[0]); + if(xlink) { + const char *filePath = xlink->getFilePath(); + if(filePath && filePath[0]) + return QVariant::fromValue(QString::fromUtf8(filePath)); + } } } } @@ -3696,365 +3748,55 @@ QVariant PropertyLinkItem::data(int column, int role) const { QVariant PropertyLinkItem::value(const App::Property* prop) const { - assert(prop && prop->getTypeId().isDerivedFrom(App::PropertyLink::getClassTypeId())); - - auto xlink = Base::freecad_dynamic_cast(prop); - isXLink = xlink!=0; - - const App::PropertyLink* prop_link = static_cast(prop); - App::PropertyContainer* c = prop_link->getContainer(); - - // The list has five mandatory elements: - // - // document name of the container, - // internal name of the linked object, - // label, - // internal name of container, - // property name - // - // and two additional elements if it is a PropertyXLink - // - // subname - // (optional) document name of linked object if it is different from the container - // - - App::DocumentObject* obj = prop_link->getValue(); - QStringList list; - if (obj) { - list << QString::fromLatin1(obj->getDocument()->getName()); - list << QString::fromLatin1(obj->getNameInDocument()); - - std::string _objName; - const char *objName = obj->getNameInDocument(); - auto owner = Base::freecad_dynamic_cast(c); - if(!objName || (owner && owner->getDocument()!=obj->getDocument())) { - _objName = obj->getFullName(); - objName = _objName.c_str(); - } - - if(xlink && xlink->getSubValues().size()) { - int count = 0; - std::stringstream ss; - ss << objName << ' '; - - std::string prevSub; - for(const auto &sub : xlink->getSubValues(false)) { - if(++count > 3) - break; - - if(count>1) - ss << ','; - else - ss << '('; - - auto element = Data::ComplexGeoData::findElementName(sub.c_str()); - if(prevSub.size()==(std::size_t)(element-sub.c_str()) - && boost::starts_with(sub,prevSub)) - { - ss << element; - continue; - } - - prevSub.clear(); - - if(element && element[0]) - prevSub = sub.substr(0,element-sub.c_str()); - - if(count > 1) - ss << ' '; - ss << sub; - } - if(count) { - if(count>3) - ss << "..."; - ss << ')'; - } - list << QString::fromUtf8(ss.str().c_str()); - - }else if(obj->Label.getStrValue() != objName) { - list << QString::fromLatin1("%1 (%2)"). - arg(QString::fromUtf8(obj->Label.getValue()), - QString::fromLatin1(objName)); - } else { - list << QString::fromUtf8(obj->Label.getValue()); - } - } else { - // no object assigned - // the document name - if (c->getTypeId().isDerivedFrom(App::DocumentObject::getClassTypeId())) { - App::DocumentObject* obj = static_cast(c); - list << QString::fromLatin1(obj->getDocument()->getName()); - } - else { - list << QString::fromLatin1(""); - } - - // the internal object name - list << QString::fromLatin1("Null"); - - // the object label - std::string msg; - if(xlink && xlink->checkRestore(&msg)>1) - list << QString::fromUtf8(msg.c_str()); - else - list << QString::fromLatin1(""); - } - - // the name of this object - if (c->getTypeId().isDerivedFrom(App::DocumentObject::getClassTypeId())) { - App::DocumentObject* obj = static_cast(c); - list << QString::fromLatin1(obj->getNameInDocument()); - } - else - list << QString::fromLatin1("Null"); - - list << QString::fromLatin1(prop->getName()); - assert(list.size() == FC_XLINK_VALUE_INDEX); + auto propLink = Base::freecad_dynamic_cast(prop); + if(!propLink) + return QVariant(); - if(xlink) { - list << QString::fromUtf8(xlink->getSubName(false)); - auto cobj = dynamic_cast(c); - if(cobj && obj && cobj->getDocument()!=obj->getDocument()) - list << QString::fromLatin1(obj->getDocument()->getName()); - } + auto links = DlgPropertyLink::getLinksFromProperty(propLink); + if(links.empty()) + return QVariant(); - return QVariant(list); + return QVariant::fromValue(links); } void PropertyLinkItem::setValue(const QVariant& value) { - if (!value.canConvert(QVariant::StringList)) - return; - QStringList items = value.toStringList(); - if (items.size() > 1) { - QString d = items[0]; - QString o = items[1]; - QString data; - if ( o.isEmpty() ) - data = QString::fromLatin1("None"); - else if(isXLink && items.size()>FC_XLINK_VALUE_INDEX+1) { - QString doc; - if(items.size()>=FC_XLINK_VALUE_INDEX+2) - doc = items[FC_XLINK_VALUE_INDEX+1]; - else - doc = d; - data = QString::fromLatin1("(App.getDocument('%1').getObject('%2'),'%3')"). - arg(doc,o,items[FC_XLINK_VALUE_INDEX]); - }else - data = QString::fromLatin1("App.getDocument('%1').getObject('%2')").arg(d,o); - setPropertyValue(data); - } + auto links = qvariant_cast >(value); + setPropertyValue(DlgPropertyLink::linksToPython(links)); } QWidget* PropertyLinkItem::createEditor(QWidget* parent, const QObject* receiver, const char* method) const { - LinkLabel *ll = new LinkLabel(parent, isXLink); + if(propertyItems.empty()) + return 0; + LinkLabel *ll = new LinkLabel(parent, propertyItems.front()); ll->setAutoFillBackground(true); ll->setDisabled(isReadOnly()); - QObject::connect(ll, SIGNAL(linkChanged(const QStringList&)), receiver, method); + QObject::connect(ll, SIGNAL(linkChanged(const QVariant&)), receiver, method); return ll; } void PropertyLinkItem::setEditorData(QWidget *editor, const QVariant& data) const { - QStringList list = data.toStringList(); + (void)data; LinkLabel *ll = static_cast(editor); - ll->setPropertyLink(list); + return ll->updatePropertyLink(); } QVariant PropertyLinkItem::editorData(QWidget *editor) const { LinkLabel *ll = static_cast(editor); - return QVariant(ll->propertyLink()); + return ll->propertyLink(); } // -------------------------------------------------------------------- -LinkListLabel::LinkListLabel (QWidget * parent) : QWidget(parent) -{ - QHBoxLayout *layout = new QHBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(1); - - label = new QLabel(this); - label->setAutoFillBackground(true); - layout->addWidget(label); - - editButton = new QPushButton(QLatin1String("..."), this); - editButton->setToolTip(tr("Change the linked objects")); - layout->addWidget(editButton); - - // setLayout(layout); - connect(editButton, SIGNAL(clicked()), - this, SLOT(onEditClicked())); -} - -LinkListLabel::~LinkListLabel() -{ -} - -void LinkListLabel::setPropertyLinkList(const QVariantList& o) -{ - links = o; - if (links.isEmpty()) { - label->clear(); - } - else if (links.size() == 1) { - QStringList s = links.front().toStringList(); - label->setText(s[2]); - } - else { - QStringList obj; - for (QVariantList::iterator it = links.begin(); it != links.end(); ++it) - obj << it->toStringList()[2]; - label->setText(QString::fromLatin1("[%1]").arg(obj.join(QString::fromLatin1(", ")))); - } -} - -QVariantList LinkListLabel::propertyLinkList() const -{ - return links; -} - -void LinkListLabel::onEditClicked () -{ - QStringList list = links.front().toStringList(); - Gui::Dialog::DlgPropertyLink dlg(list, this); - dlg.setSelectionMode(QAbstractItemView::ExtendedSelection); - if (dlg.exec() == QDialog::Accepted) { - setPropertyLinkList(dlg.propertyLinkList()); - Q_EMIT linkChanged(links); - } -} - -void LinkListLabel::resizeEvent(QResizeEvent* e) -{ - editButton->setFixedWidth(e->size().height()); -} - - PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyLinkListItem) PropertyLinkListItem::PropertyLinkListItem() { } -QVariant PropertyLinkListItem::toString(const QVariant& prop) const -{ - QVariantList list = prop.toList(); - if (list.empty()) { - return QString(); - } - else if (list.size() == 1) { - QStringList item = list.front().toStringList(); - return QString::fromLatin1("%1").arg(item[2]); - } - else { - QStringList obj; - for (QVariantList::iterator it = list.begin(); it != list.end(); ++it) - obj << it->toStringList()[2]; - return QString::fromLatin1("[%1]").arg(obj.join(QString::fromLatin1(", "))); - } -} - -QVariant PropertyLinkListItem::value(const App::Property* prop) const -{ - assert(prop && prop->getTypeId().isDerivedFrom(App::PropertyLinkList::getClassTypeId())); - - const App::PropertyLinkList* prop_link = static_cast(prop); - App::PropertyContainer* c = prop_link->getContainer(); - - // the name of this object - QString objName; - if (c->getTypeId().isDerivedFrom(App::DocumentObject::getClassTypeId())) { - App::DocumentObject* obj = static_cast(c); - objName = QString::fromLatin1(obj->getNameInDocument()); - } - else { - objName = QString::fromLatin1("Null"); - } - - // each item is a list of five elements: - //[document name, internal name, label, internal name of container, property name] - // the variant list contains at least one item - std::vector obj = prop_link->getValues(); - QVariantList varList; - if (!obj.empty()) { - for (std::vector::iterator it = obj.begin(); it != obj.end(); ++it) { - QStringList list; - list << QString::fromLatin1((*it)->getDocument()->getName()); - list << QString::fromLatin1((*it)->getNameInDocument()); - list << QString::fromUtf8((*it)->Label.getValue()); - list << objName; - list << QString::fromLatin1(prop->getName()); - varList << list; - } - } - else { - QStringList list; - // no object assigned - // the document name - if (c->getTypeId().isDerivedFrom(App::DocumentObject::getClassTypeId())) { - App::DocumentObject* obj = static_cast(c); - list << QString::fromLatin1(obj->getDocument()->getName()); - } - else { - list << QString::fromLatin1(""); - } - - // the internal object name - list << QString::fromLatin1("Null"); - // the object label - list << QString::fromLatin1(""); - list << objName; - list << QString::fromLatin1(prop->getName()); - varList << list; - } - - return QVariant(varList); -} - -void PropertyLinkListItem::setValue(const QVariant& value) -{ - if (!value.canConvert(QVariant::List)) - return; - QVariantList items = value.toList(); - QStringList data; - for (QVariantList::iterator it = items.begin(); it != items.end(); ++it) { - QStringList list = it->toStringList(); - QString d = list[0]; - QString o = list[1]; - if (!o.isEmpty()) - data << QString::fromLatin1("App.getDocument('%1').getObject('%2')").arg(d, o); - } - if(data.size()==0) - setPropertyValue(QLatin1String("[]")); - else - setPropertyValue(QString::fromLatin1("[%1]").arg(data.join(QString::fromLatin1(", ")))); -} - -QWidget* PropertyLinkListItem::createEditor(QWidget* parent, const QObject* receiver, const char* method) const -{ - LinkListLabel *ll = new LinkListLabel(parent); - ll->setAutoFillBackground(true); - ll->setDisabled(isReadOnly()); - QObject::connect(ll, SIGNAL(linkChanged(const QVariantList&)), receiver, method); - return ll; -} - -void PropertyLinkListItem::setEditorData(QWidget *editor, const QVariant& data) const -{ - QVariantList list = data.toList(); - LinkListLabel *ll = static_cast(editor); - ll->setPropertyLinkList(list); -} - -QVariant PropertyLinkListItem::editorData(QWidget *editor) const -{ - LinkListLabel *ll = static_cast(editor); - return QVariant(ll->propertyLinkList()); -} - // -------------------------------------------------------------------- PropertyItemEditorFactory::PropertyItemEditorFactory() diff --git a/src/Gui/propertyeditor/PropertyItem.h b/src/Gui/propertyeditor/PropertyItem.h index 43c7754441d4..5027348908f5 100644 --- a/src/Gui/propertyeditor/PropertyItem.h +++ b/src/Gui/propertyeditor/PropertyItem.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -63,7 +64,12 @@ void _class_::init(void) { \ } namespace Gui { -namespace Dialog { class TaskPlacement; } + +namespace Dialog { +class TaskPlacement; +class DlgPropertyLink; +} + namespace PropertyEditor { class PropertyItem; @@ -945,25 +951,26 @@ class LinkSelection : public QObject Q_OBJECT public: - LinkSelection(const QStringList&); + LinkSelection(const App::SubObjectT &); ~LinkSelection(); public Q_SLOTS: void select(); private: - QStringList link; + App::SubObjectT link; }; + class LinkLabel : public QWidget { Q_OBJECT public: - LinkLabel (QWidget * parent = 0, bool xlink = false); + LinkLabel (QWidget * parent, const App::Property *prop); virtual ~LinkLabel(); - void setPropertyLink(const QStringList& o); - QStringList propertyLink() const; + void updatePropertyLink(); + QVariant propertyLink() const; protected: void resizeEvent(QResizeEvent*); @@ -971,15 +978,18 @@ class LinkLabel : public QWidget protected Q_SLOTS: void onLinkActivated(const QString&); void onEditClicked(); + void onLinkChanged(); Q_SIGNALS: - void linkChanged(const QStringList&); + void linkChanged(const QVariant&); private: QLabel* label; QPushButton* editButton; - QStringList link; - bool isXLink; + QVariant link; + App::DocumentObjectT objProp; + + Gui::Dialog::DlgPropertyLink* dlg; }; /** @@ -1003,54 +1013,17 @@ class GuiExport PropertyLinkItem: public PropertyItem protected: PropertyLinkItem(); - -private: - mutable bool isXLink; -}; - -class LinkListLabel : public QWidget -{ - Q_OBJECT - -public: - LinkListLabel (QWidget * parent = 0); - virtual ~LinkListLabel(); - void setPropertyLinkList(const QVariantList& o); - QVariantList propertyLinkList() const; - -protected: - void resizeEvent(QResizeEvent*); - -protected Q_SLOTS: - void onEditClicked(); - -Q_SIGNALS: - void linkChanged(const QVariantList&); - -private: - QLabel* label; - QPushButton* editButton; - QVariantList links; }; /** * Edit properties of link list type. * \author Werner Mayer */ -class GuiExport PropertyLinkListItem: public PropertyItem +class GuiExport PropertyLinkListItem: public PropertyLinkItem { Q_OBJECT PROPERTYITEM_HEADER - virtual QWidget* createEditor(QWidget* parent, const QObject* receiver, const char* method) const; - virtual void setEditorData(QWidget *editor, const QVariant& data) const; - virtual QVariant editorData(QWidget *editor) const; - -protected: - virtual QVariant toString(const QVariant&) const; - virtual QVariant value(const App::Property*) const; - virtual void setValue(const QVariant&); - protected: PropertyLinkListItem(); }; diff --git a/src/Main/CMakeLists.txt b/src/Main/CMakeLists.txt index 56033ad8bd60..7877085746b5 100644 --- a/src/Main/CMakeLists.txt +++ b/src/Main/CMakeLists.txt @@ -1,6 +1,10 @@ #add_defintions(-D_FC_GUI_ENABLED_) #add_defintions(-DFREECADMAINPY) +configure_file(freecad.rc.cmake ${CMAKE_CURRENT_BINARY_DIR}/freecad.rc) +configure_file(freecadCmd.rc.cmake ${CMAKE_CURRENT_BINARY_DIR}/freecadCmd.rc) +file(COPY icon.ico DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + if(BUILD_GUI) include_directories( ${Boost_INCLUDE_DIRS} @@ -24,7 +28,7 @@ endif(BUILD_GUI) if(BUILD_GUI) SET(FreeCAD_SRCS - freecad.rc + ${CMAKE_CURRENT_BINARY_DIR}/freecad.rc icon.ico MainGui.cpp ) @@ -71,8 +75,11 @@ endif(BUILD_GUI) ######################## FreeCADMainCmd ######################## SET(FreeCADMainCmd_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/freecadCmd.rc + icon.ico MainCmd.cpp ) + add_executable(FreeCADMainCmd ${FreeCADMainCmd_SRCS}) SET(FreeCADMainCmd_LIBS diff --git a/src/Main/freecad.rc b/src/Main/freecad.rc index a713a4163ef6..31c7a1a739bf 100644 --- a/src/Main/freecad.rc +++ b/src/Main/freecad.rc @@ -1,4 +1,35 @@ +///////////////////////////////////////////////////////////////////////////// +// For info about the file structrure see +// https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource +// and +// https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block + +// Icon +// // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_ICON1 ICON DISCARDABLE "icon.ico" +// File info for the FreeCAD.exe +// +1 VERSIONINFO +FILEVERSION ${PACKAGE_VERSION_MAJOR},${PACKAGE_VERSION_MINOR},${FREECAD_VERSION_PATCH},${PACKAGE_VERSION_PATCH} +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // 409 stands for US English + BEGIN + VALUE "CompanyName", "${PROJECT_NAME} Team" + VALUE "FileDescription", "${PROJECT_NAME} main executable" + VALUE "InternalName", "FreeCAD.exe" + VALUE "LegalCopyright", "Copyright (C) 2020" + VALUE "OriginalFilename", "FreeCAD.exe" + VALUE "ProductName", "${PROJECT_NAME}" + VALUE "${FREECAD_VERSION}.${PACKAGE_VERSION_SUFFIX}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 //US English, Unicode + END +END diff --git a/src/Main/freecad.rc.cmake b/src/Main/freecad.rc.cmake new file mode 100644 index 000000000000..231f93cc911b --- /dev/null +++ b/src/Main/freecad.rc.cmake @@ -0,0 +1,35 @@ +///////////////////////////////////////////////////////////////////////////// +// For info about the file structrure see +// https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource +// and +// https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block + +// Icon +// +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON DISCARDABLE "icon.ico" + +// File info for the FreeCAD.exe +// +1 VERSIONINFO +FILEVERSION ${PACKAGE_VERSION_MAJOR},${PACKAGE_VERSION_MINOR},${FREECAD_VERSION_PATCH},${PACKAGE_VERSION_PATCH} +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // 409 stands for US English + BEGIN + VALUE "CompanyName", "${PROJECT_NAME} Team" + VALUE "FileDescription", "${PROJECT_NAME} main executable" + VALUE "InternalName", "FreeCAD.exe" + VALUE "LegalCopyright", "Copyright (C) 2020" + VALUE "OriginalFilename", "FreeCAD.exe" + VALUE "ProductName", "${PROJECT_NAME}" + VALUE "ProductVersion", "${FREECAD_VERSION}.${FREECAD_VERSION_PATCH}${PACKAGE_VERSION_SUFFIX}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 //US English, Unicode + END +END diff --git a/src/Main/freecadCmd.rc.cmake b/src/Main/freecadCmd.rc.cmake new file mode 100644 index 000000000000..05aa2798e6f5 --- /dev/null +++ b/src/Main/freecadCmd.rc.cmake @@ -0,0 +1,35 @@ +///////////////////////////////////////////////////////////////////////////// +// For info about the file structrure see +// https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource +// and +// https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block + +// Icon +// +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON DISCARDABLE "icon.ico" + +// File info for the FreeCADCmd.exe +// +1 VERSIONINFO +FILEVERSION ${PACKAGE_VERSION_MAJOR},${PACKAGE_VERSION_MINOR},${FREECAD_VERSION_PATCH},${PACKAGE_VERSION_PATCH} +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // 409 stands for US English + BEGIN + VALUE "CompanyName", "${PROJECT_NAME} Team" + VALUE "FileDescription", "${PROJECT_NAME} command line executable" + VALUE "InternalName", "FreeCADCmd.exe" + VALUE "LegalCopyright", "Copyright (C) 2020" + VALUE "OriginalFilename", "FreeCADCmd.exe" + VALUE "ProductName", "${PROJECT_NAME}" + VALUE "ProductVersion", "${FREECAD_VERSION}.${FREECAD_VERSION_PATCH}${PACKAGE_VERSION_SUFFIX}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 //US English, Unicode + END +END diff --git a/src/Main/res/FreeCAD.exe.manifest b/src/Main/res/FreeCAD.exe.manifest deleted file mode 100644 index 0e06cc636bcc..000000000000 --- a/src/Main/res/FreeCAD.exe.manifest +++ /dev/null @@ -1,10 +0,0 @@ - - - - CAD Application - - - - - - \ No newline at end of file diff --git a/src/Main/res/FreeCADD.exe.manifest b/src/Main/res/FreeCADD.exe.manifest deleted file mode 100644 index 00655c44c97b..000000000000 --- a/src/Main/res/FreeCADD.exe.manifest +++ /dev/null @@ -1,22 +0,0 @@ - - - - CAD Application - - - - - - \ No newline at end of file diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index fe2526178c2e..80674ea79603 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -385,7 +385,11 @@ def add_macro(self, macro): from PySide import QtGui self.macros.append(macro) import AddonManager_rc - addonicon = QtGui.QIcon(":/icons/" + macro.name.replace(" ","_") + "_macro_icon.svg") + path = ":/icons/" + macro.name.replace(" ","_") + "_macro_icon.svg" + if QtCore.QFile.exists(path): + addonicon = QtGui.QIcon(path) + else: + addonicon = QtGui.QIcon(":/icons/document-python.svg") if addonicon.isNull(): addonicon = QtGui.QIcon(":/icons/document-python.svg") if macro.is_installed(): diff --git a/src/Mod/Arch/InitGui.py b/src/Mod/Arch/InitGui.py index 26817684c536..cf178844da0f 100644 --- a/src/Mod/Arch/InitGui.py +++ b/src/Mod/Arch/InitGui.py @@ -1,142 +1,183 @@ -#*************************************************************************** -#* Copyright (c) 2011 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) * -#* 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 * -#* * -#*************************************************************************** - -class ArchWorkbench(Workbench): - "Arch workbench object" +"""Initialization of the Arch workbench (graphical interface).""" +# *************************************************************************** +# * Copyright (c) 2011 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) * +# * 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 * +# * * +# *************************************************************************** +import os +import FreeCAD +import FreeCADGui + + +class ArchWorkbench(FreeCADGui.Workbench): + """The Arch workbench definition.""" + def __init__(self): - self.__class__.Icon = FreeCAD.getResourceDir() + "Mod/Arch/Resources/icons/ArchWorkbench.svg" - self.__class__.MenuText = "Arch" - self.__class__.ToolTip = "Architecture workbench" + def QT_TRANSLATE_NOOP(context, text): + return text + + __dirname__ = os.path.join(FreeCAD.getResourceDir(), "Mod", "Arch") + _tooltip = ("The Arch workbench is used to model " + "architectural components, and entire buildings") + self.__class__.Icon = os.path.join(__dirname__, + "Resources", "icons", + "ArchWorkbench.svg") + self.__class__.MenuText = QT_TRANSLATE_NOOP("Arch", "Arch") + self.__class__.ToolTip = QT_TRANSLATE_NOOP("Arch", _tooltip) def Initialize(self): - import DraftTools,DraftGui,Arch_rc,Arch,Draft_rc - from DraftTools import translate - - # arch tools - self.archtools = ["Arch_Wall","Arch_Structure","Arch_Rebar","Arch_BuildingPart", - "Arch_Project", "Arch_Site", "Arch_Building", "Arch_Floor", "Arch_Reference", - "Arch_Window","Arch_Roof","Arch_AxisTools", - "Arch_SectionPlane","Arch_Space","Arch_Stairs", - "Arch_PanelTools","Arch_Equipment", - "Arch_Frame", "Arch_Fence", "Arch_MaterialTools","Arch_Schedule","Arch_PipeTools", - "Arch_CutPlane", "Arch_CutLine", - "Arch_Add","Arch_Remove","Arch_Survey"] - self.utilities = ["Arch_Component","Arch_CloneComponent","Arch_SplitMesh","Arch_MeshToShape", - "Arch_SelectNonSolidMeshes","Arch_RemoveShape", - "Arch_CloseHoles","Arch_MergeWalls","Arch_Check", - "Arch_ToggleIfcBrepFlag","Arch_3Views", - "Arch_IfcSpreadsheet","Arch_ToggleSubs"] - - # try to locate the Rebar addon + """When the workbench is first loaded.""" + + def QT_TRANSLATE_NOOP(context, text): + return text + + import Draft_rc + import DraftTools + import DraftGui + from draftguitools import gui_circulararray + from draftguitools import gui_polararray + import Arch_rc + import Arch + + # Set up command lists + self.archtools = ["Arch_Wall", "Arch_Structure", "Arch_Rebar", + "Arch_BuildingPart", + "Arch_Project", "Arch_Site", "Arch_Building", + "Arch_Floor", "Arch_Reference", + "Arch_Window", "Arch_Roof", "Arch_AxisTools", + "Arch_SectionPlane", "Arch_Space", "Arch_Stairs", + "Arch_PanelTools", "Arch_Equipment", + "Arch_Frame", "Arch_Fence", "Arch_MaterialTools", + "Arch_Schedule", "Arch_PipeTools", + "Arch_CutPlane", "Arch_CutLine", + "Arch_Add", "Arch_Remove", "Arch_Survey"] + self.utilities = ["Arch_Component", "Arch_CloneComponent", + "Arch_SplitMesh", "Arch_MeshToShape", + "Arch_SelectNonSolidMeshes", "Arch_RemoveShape", + "Arch_CloseHoles", "Arch_MergeWalls", "Arch_Check", + "Arch_ToggleIfcBrepFlag", "Arch_3Views", + "Arch_IfcSpreadsheet", "Arch_ToggleSubs"] + + # Add the rebar tools from the Reinforcement addon, if available try: import RebarTools - except: + except Exception: pass else: class RebarGroupCommand: def GetCommands(self): - return tuple(RebarTools.RebarCommands+["Arch_Rebar"]) + return tuple(RebarTools.RebarCommands + ["Arch_Rebar"]) + def GetResources(self): - return { 'MenuText': 'Rebar tools', - 'ToolTip': 'Rebar tools' - } + _tooltip = ("Create various types of rebars, " + "including U-shaped, L-shaped, and stirrup") + return {'MenuText': QT_TRANSLATE_NOOP("Arch", 'Rebar tools'), + 'ToolTip': QT_TRANSLATE_NOOP("Arch", _tooltip)} + def IsActive(self): return not FreeCAD.ActiveDocument is None FreeCADGui.addCommand('Arch_RebarTools', RebarGroupCommand()) self.archtools[2] = "Arch_RebarTools" + # Set up Draft command lists + import draftutils.init_tools as it + self.draft_drawing_commands = it.get_draft_drawing_commands() + self.draft_annotation_commands = it.get_draft_annotation_commands() + self.draft_modification_commands = it.get_draft_modification_commands() + self.draft_context_commands = it.get_draft_context_commands() + self.draft_line_commands = it.get_draft_line_commands() + self.draft_utility_commands = it.get_draft_utility_commands() + + # Set up toolbars + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Arch tools"), self.archtools) + self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft creation tools"), self.draft_drawing_commands) + self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft annotation tools"), self.draft_annotation_commands) + self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft modification tools"), self.draft_modification_commands) + + # Set up menus + self.appendMenu([QT_TRANSLATE_NOOP("arch", "&Arch"), + QT_TRANSLATE_NOOP("arch", "Utilities")], + self.utilities) + self.appendMenu(QT_TRANSLATE_NOOP("arch", "&Arch"), self.archtools) - # draft tools - self.drafttools = ["Draft_Line","Draft_Wire","Draft_Circle","Draft_Arc","Draft_Ellipse", - "Draft_Polygon","Draft_Rectangle", "Draft_Text", - "Draft_Dimension", "Draft_BSpline","Draft_Point", - "Draft_Facebinder","Draft_BezCurve","Draft_Label"] - self.draftmodtools = ["Draft_Move","Draft_Rotate","Draft_Offset", - "Draft_Trimex", "Draft_Upgrade", "Draft_Downgrade", "Draft_Scale", - "Draft_Shape2DView","Draft_Draft2Sketch","Draft_Array", - "Draft_Clone","Draft_Edit"] - self.draftextratools = ["Draft_WireToBSpline","Draft_AddPoint","Draft_DelPoint","Draft_ShapeString", - "Draft_PathArray","Draft_Mirror","Draft_Stretch"] - self.draftcontexttools = ["Draft_ApplyStyle","Draft_ToggleDisplayMode","Draft_AddToGroup","Draft_AutoGroup", - "Draft_SelectGroup","Draft_SelectPlane", - "Draft_ShowSnapBar","Draft_ToggleGrid","Draft_UndoLine", - "Draft_FinishLine","Draft_CloseLine"] - self.draftutils = ["Draft_Layer","Draft_Heal","Draft_FlipDimension", - "Draft_ToggleConstructionMode","Draft_ToggleContinueMode","Draft_Edit", - "Draft_Slope","Draft_SetWorkingPlaneProxy","Draft_AddConstruction"] - self.snapList = ['Draft_Snap_Lock','Draft_Snap_Midpoint','Draft_Snap_Perpendicular', - 'Draft_Snap_Grid','Draft_Snap_Intersection','Draft_Snap_Parallel', - 'Draft_Snap_Endpoint','Draft_Snap_Angle','Draft_Snap_Center', - 'Draft_Snap_Extension','Draft_Snap_Near','Draft_Snap_Ortho','Draft_Snap_Special', - 'Draft_Snap_Dimensions','Draft_Snap_WorkingPlane'] - - def QT_TRANSLATE_NOOP(scope, text): return text - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","Arch tools"),self.archtools) - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","Draft tools"),self.drafttools) - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","Draft mod tools"),self.draftmodtools) - self.appendMenu([QT_TRANSLATE_NOOP("arch","&Arch"),QT_TRANSLATE_NOOP("arch","Utilities")],self.utilities) - self.appendMenu(QT_TRANSLATE_NOOP("arch","&Arch"),self.archtools) - self.appendMenu(QT_TRANSLATE_NOOP("arch","&Draft"),self.drafttools+self.draftmodtools+self.draftextratools) - self.appendMenu([QT_TRANSLATE_NOOP("arch","&Draft"),QT_TRANSLATE_NOOP("arch","Utilities")],self.draftutils+self.draftcontexttools) - self.appendMenu([QT_TRANSLATE_NOOP("arch","&Draft"),QT_TRANSLATE_NOOP("arch","Snapping")],self.snapList) + self.appendMenu([QT_TRANSLATE_NOOP("arch", "&Draft"), + QT_TRANSLATE_NOOP("arch", "Creation")], + self.draft_drawing_commands) + self.appendMenu([QT_TRANSLATE_NOOP("arch", "&Draft"), + QT_TRANSLATE_NOOP("arch", "Annotation")], + self.draft_annotation_commands) + self.appendMenu([QT_TRANSLATE_NOOP("arch", "&Draft"), + QT_TRANSLATE_NOOP("arch", "Modification")], + self.draft_modification_commands) + self.appendMenu([QT_TRANSLATE_NOOP("arch", "&Draft"), + QT_TRANSLATE_NOOP("arch", "Utilities")], + self.draft_utility_commands + + self.draft_context_commands) FreeCADGui.addIconPath(":/icons") FreeCADGui.addLanguagePath(":/translations") - if hasattr(FreeCADGui,"draftToolBar"): - if not hasattr(FreeCADGui.draftToolBar,"loadedArchPreferences"): - FreeCADGui.addPreferencePage(":/ui/preferences-arch.ui","Arch") - FreeCADGui.addPreferencePage(":/ui/preferences-archdefaults.ui","Arch") + + # Set up preferences pages + if hasattr(FreeCADGui, "draftToolBar"): + if not hasattr(FreeCADGui.draftToolBar, "loadedArchPreferences"): + FreeCADGui.addPreferencePage(":/ui/preferences-arch.ui", QT_TRANSLATE_NOOP("Arch", "Arch")) + FreeCADGui.addPreferencePage(":/ui/preferences-archdefaults.ui", QT_TRANSLATE_NOOP("Arch", "Arch")) FreeCADGui.draftToolBar.loadedArchPreferences = True - if not hasattr(FreeCADGui.draftToolBar,"loadedPreferences"): - FreeCADGui.addPreferencePage(":/ui/preferences-draft.ui","Draft") - FreeCADGui.addPreferencePage(":/ui/preferences-draftsnap.ui","Draft") - FreeCADGui.addPreferencePage(":/ui/preferences-draftvisual.ui","Draft") - FreeCADGui.addPreferencePage(":/ui/preferences-drafttexts.ui","Draft") + if not hasattr(FreeCADGui.draftToolBar, "loadedPreferences"): + FreeCADGui.addPreferencePage(":/ui/preferences-draft.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) + FreeCADGui.addPreferencePage(":/ui/preferences-draftsnap.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) + FreeCADGui.addPreferencePage(":/ui/preferences-draftvisual.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) + FreeCADGui.addPreferencePage(":/ui/preferences-drafttexts.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.draftToolBar.loadedPreferences = True - Log ('Loading Arch module... done\n') + + FreeCAD.Console.PrintLog('Loading Arch workbench, done.\n') def Activated(self): - if hasattr(FreeCADGui,"draftToolBar"): + """When entering the workbench.""" + if hasattr(FreeCADGui, "draftToolBar"): FreeCADGui.draftToolBar.Activated() - if hasattr(FreeCADGui,"Snapper"): + if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.show() - Log("Arch workbench activated\n") - + FreeCAD.Console.PrintLog("Arch workbench activated.\n") + def Deactivated(self): - if hasattr(FreeCADGui,"draftToolBar"): + """When leaving the workbench.""" + if hasattr(FreeCADGui, "draftToolBar"): FreeCADGui.draftToolBar.Deactivated() - if hasattr(FreeCADGui,"Snapper"): + if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.hide() - Log("Arch workbench deactivated\n") + FreeCAD.Console.PrintLog("Arch workbench deactivated.\n") def ContextMenu(self, recipient): - self.appendContextMenu("Utilities",self.draftcontexttools) + """Define an optional custom context menu.""" + self.appendContextMenu("Utilities", self.draft_context_commands) - def GetClassName(self): + def GetClassName(self): + """Type of workbench.""" return "Gui::PythonWorkbench" + FreeCADGui.addWorkbench(ArchWorkbench) -# File format pref pages are independent and can be loaded at startup +# Preference pages for importing and exporting various file formats +# are independent of the loading of the workbench and can be loaded at startup import Arch_rc -FreeCADGui.addPreferencePage(":/ui/preferences-ifc.ui","Import-Export") -FreeCADGui.addPreferencePage(":/ui/preferences-dae.ui","Import-Export") +from PySide.QtCore import QT_TRANSLATE_NOOP +FreeCADGui.addPreferencePage(":/ui/preferences-ifc.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) +FreeCADGui.addPreferencePage(":/ui/preferences-dae.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) -FreeCAD.__unit_test__ += [ "TestArch" ] +FreeCAD.__unit_test__ += ["TestArch"] diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 9677028d28ab..febc7082d6f1 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -4172,7 +4172,7 @@ def getDefaultDisplayMode(self): return ["2D","3D"][getParam("dimstyle",0)] def getIcon(self): - return ":/icons/Draft_Dimension_Tree.svg" + return ":/icons/Draft_DimensionAngular.svg" def __getstate__(self): return self.Object.ViewObject.DisplayMode diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 4a1aaa56968f..0156b55822a0 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -24,6 +24,7 @@ icons/Draft_DelPoint.svg icons/Draft_Dimension.svg icons/Draft_Dimension_Tree.svg + icons/Draft_DimensionAngular.svg icons/Draft_Dot.svg icons/Draft_Downgrade.svg icons/Draft_Draft.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_DimensionAngular.svg b/src/Mod/Draft/Resources/icons/Draft_DimensionAngular.svg new file mode 100644 index 000000000000..265ef551d30b --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_DimensionAngular.svg @@ -0,0 +1,422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Wed Nov 13 19:25:01 2013 -0200 + + + [Yorik van Havre] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_ + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + + + triangle + arrow + curved + + + Two triangles, one pointing left, the other right, with a curved arrow below them pointing from the left to the right and slightly upwards + + + + diff --git a/src/Mod/Draft/draftutils/messages.py b/src/Mod/Draft/draftutils/messages.py old mode 100755 new mode 100644 diff --git a/src/Mod/Fem/App/FemVTKTools.cpp b/src/Mod/Fem/App/FemVTKTools.cpp index a6a513ea71fd..50e62ff4517b 100644 --- a/src/Mod/Fem/App/FemVTKTools.cpp +++ b/src/Mod/Fem/App/FemVTKTools.cpp @@ -694,7 +694,7 @@ std::map _getFreeCADMechResultScalarProperties() { // resFCScalProp["PrincipalMax"] = "Major Principal Stress"; // can be plotted in Paraview as THE MAJOR PRINCIPAL STRESS MAGNITUDE // resFCScalProp["PrincipalMed"] = "Intermediate Principal Stress"; // can be plotted in Paraview as THE INTERMEDIATE PRINCIPAL STRESS MAGNITUDE // resFCScalProp["PrincipalMin"] = "Minor Principal Stress"; // can be plotted in Paraview as THE MINOR PRINCIPAL STRESS MAGNITUDE - resFCScalProp["StressValues"] = "von Mises Stress"; + resFCScalProp["vonMises"] = "von Mises Stress"; resFCScalProp["Temperature"] = "Temperature"; resFCScalProp["MohrCoulomb"] = "MohrCoulomb"; resFCScalProp["ReinforcementRatio_x"] = "ReinforcementRatio_x"; diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index bede746db338..b3b75d8f3605 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -218,6 +218,7 @@ SET(FemTools_SRCS femtools/__init__.py femtools/ccxtools.py femtools/femutils.py + femtools/tokrules.py ) SET(FemObjectsScripts_SRCS diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index a6686c9af0d9..c1cb70c6a56e 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -418,6 +418,7 @@ SET(FemGuiPythonUI_SRCS Resources/ui/MeshGroup.ui Resources/ui/MeshGroupXDMFExport.ui Resources/ui/MeshRegion.ui + Resources/ui/ResultHints.ui Resources/ui/ResultShow.ui Resources/ui/SolverCalculix.ui ) diff --git a/src/Mod/Fem/Gui/DlgSettingsFemExportAbaqus.ui b/src/Mod/Fem/Gui/DlgSettingsFemExportAbaqus.ui index 55ed5e5eb7dc..01451d91bc99 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemExportAbaqus.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemExportAbaqus.ui @@ -82,10 +82,10 @@ not belonging to faces and faces not belonging to volumes. element parameter: All: all elements, highest: highest elements only, FEM: FEM elements only (only edges not belonging to faces and faces not belonging to volumes) - AbaqusElementChoice + AbaqusElementChoice - Mod/Fem/Abaqus + Mod/Fem/Abaqus diff --git a/src/Mod/Fem/Gui/DlgSettingsFemInOutVtk.ui b/src/Mod/Fem/Gui/DlgSettingsFemInOutVtk.ui index 29dd63a32c15..9aa94ee72596 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemInOutVtk.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemInOutVtk.ui @@ -69,10 +69,10 @@ exported from FreeCAD. 0 - ImportObject + ImportObject - Mod/Fem/InOutVtk + Mod/Fem/InOutVtk diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index fd4bd285accc..6bf8909ae77b 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -124,6 +124,7 @@ ui/MeshGroup.ui ui/MeshGroupXDMFExport.ui ui/MeshRegion.ui + ui/ResultHints.ui ui/ResultShow.ui ui/SolverCalculix.ui diff --git a/src/Mod/Fem/Gui/Resources/ui/ResultHints.ui b/src/Mod/Fem/Gui/Resources/ui/ResultHints.ui new file mode 100644 index 000000000000..a4ad8b6f00d0 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/ResultHints.ui @@ -0,0 +1,251 @@ + + + ShowDisplacement + + + + 0 + 0 + 451 + 768 + + + + Hints user defined equations + + + + + + + + Available result types: + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + 1 + + + displacement: x, y, z + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + -1 + + + Qt::TextSelectableByMouse + + + + + + + mass flow rate: MF + + + Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + network pressure: NP + + + Qt::TextSelectableByMouse + + + + + + + von Mises stress: vM + + + Qt::TextSelectableByMouse + + + + + + + temperature: T + + + Qt::TextSelectableByMouse + + + + + + + min. principal stress vector: s1x, s1y, s1z + + + Qt::TextSelectableByMouse + + + + + + + principal stresses: P1, P2, P3 + + + Qt::TextSelectableByMouse + + + + + + + reinforcement ratio: rx, ry, rz + + + Qt::TextSelectableByMouse + + + + + + + equivalent plastic strain: Peeq + + + Qt::TextSelectableByMouse + + + + + + + med. principal stress vector: s2x, s2y, s2z + + + Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + 1 + + + stress: sxx, syy, szz, sxy, sxz, syz + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + -1 + + + Qt::TextSelectableByMouse + + + + + + + strain: exx, eyy, ezz, exy, exz, eyz + + + Qt::TextSelectableByMouse + + + + + + + Mohr Coulomb: mc + + + Qt::TextSelectableByMouse + + + + + + + max. principal stress vector: s3x, s3y, s3z + + + Qt::TextSelectableByMouse + + + + + + + + + + + + Qt::Vertical + + + + 20 + 240 + + + + + + + + + + + diff --git a/src/Mod/Fem/Gui/Resources/ui/ResultShow.ui b/src/Mod/Fem/Gui/Resources/ui/ResultShow.ui index 765fe657ef07..987ff49db67d 100644 --- a/src/Mod/Fem/Gui/Resources/ui/ResultShow.ui +++ b/src/Mod/Fem/Gui/Resources/ui/ResultShow.ui @@ -314,181 +314,6 @@ p, li { white-space: pre-wrap; } - - - - - - min. principal stress vector: s1x, s1y, s1z - - - - - - - med. principal stress vector: s2x, s2y, s2z - - - - - - - principal stresses: P1, P2, P3 - - - - - - - equivalent plastic strain: Peeq - - - - - - - reinforcement ratio: rx, ry, rz - - - - - - - <html><head/><body><p><span style=" text-decoration: underline;">Available result types:</span></p></body></html> - - - - - - - strain: exx, eyy, ezz, exy, exz, eyz - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - 1 - - - stress: sxx, syy, szz, sxy, sxz, syz - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - false - - - -1 - - - Qt::NoTextInteraction - - - - - - - von Mises stress: Von - - - - - - - mass flow rate: MF - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - 1 - - - displacement: x, y, z - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - false - - - -1 - - - Qt::NoTextInteraction - - - - - - - - 0 - 0 - - - - network pressure: NP - - - - - - - temperature: T - - - - - - - Mohr Coulomb: mc - - - - - - - max. principal stress vector: s3x, s3y, s3z - - - - - diff --git a/src/Mod/Fem/InitGui.py b/src/Mod/Fem/InitGui.py index 8a0456ae0ff8..8826812643ca 100644 --- a/src/Mod/Fem/InitGui.py +++ b/src/Mod/Fem/InitGui.py @@ -48,6 +48,7 @@ def Initialize(self): import femcommands.commands def GetClassName(self): + # see https://forum.freecadweb.org/viewtopic.php?f=10&t=43300 return "FemGui::Workbench" diff --git a/src/Mod/Fem/femguiobjects/_TaskPanelFemSolverControl.py b/src/Mod/Fem/femguiobjects/_TaskPanelFemSolverControl.py index 080c0ece237e..25fc74a7fc25 100644 --- a/src/Mod/Fem/femguiobjects/_TaskPanelFemSolverControl.py +++ b/src/Mod/Fem/femguiobjects/_TaskPanelFemSolverControl.py @@ -37,8 +37,9 @@ _UPDATE_INTERVAL = 50 _REPORT_TITLE = "Run Report" _REPORT_ERR = ( - "Failed to run. Please try again after all" - "of the following errors are resolved.") + "Failed to run. Please try again after all " + "of the following errors are resolved." +) class ControlTaskPanel(QtCore.QObject): diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintElectrostaticPotential.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintElectrostaticPotential.py index 43e5dfb57669..c65860e1e01d 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintElectrostaticPotential.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintElectrostaticPotential.py @@ -68,7 +68,7 @@ def __init__(self, obj): FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/ElectrostaticPotential.ui") self._initParamWidget() self.form = [self._refWidget, self._paramWidget] - analysis = femutils.findAnalysisOfMember(obj) + analysis = obj.getParentGroup() self._mesh = femutils.get_single_member(analysis, "Fem::FemMeshObject") self._part = None if self._mesh is not None: diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintFlowVelocity.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintFlowVelocity.py index 3adcda1d325b..23f6a2688339 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintFlowVelocity.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintFlowVelocity.py @@ -69,7 +69,7 @@ def __init__(self, obj): ) self._initParamWidget() self.form = [self._refWidget, self._paramWidget] - analysis = femutils.findAnalysisOfMember(obj) + analysis = obj.getParentGroup() self._mesh = femutils.get_single_member(analysis, "Fem::FemMeshObject") self._part = None if self._mesh is not None: diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintInitialFlowVelocity.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintInitialFlowVelocity.py index d3b11a82c68b..f871659efde9 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintInitialFlowVelocity.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemConstraintInitialFlowVelocity.py @@ -65,7 +65,7 @@ def __init__(self, obj): FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/InitialFlowVelocity.ui") self._initParamWidget() self.form = [self._paramWidget] - analysis = femutils.findAnalysisOfMember(obj) + analysis = obj.getParentGroup() self._mesh = femutils.get_single_member(analysis, "Fem::FemMeshObject") self._part = None if self._mesh is not None: diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemMeshGmsh.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemMeshGmsh.py index 4ecaea84c825..9de35883d14c 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemMeshGmsh.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemMeshGmsh.py @@ -403,10 +403,12 @@ def run_gmsh(self): .format(sys.exc_info()[0]) ) if error: + FreeCAD.Console.PrintMessage("Gmsh had warnings ...\n") FreeCAD.Console.PrintMessage("{}\n".format(error)) self.console_log("Gmsh had warnings ...") self.console_log(error, "#FF0000") else: + FreeCAD.Console.PrintMessage("Clean run of Gmsh\n") self.console_log("Clean run of Gmsh") self.console_log("Gmsh done!") self.form.l_time.setText("Time: {0:4.1f}: ".format(time.time() - self.Start)) diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py index 27e81b677bad..527f4ef4294f 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py @@ -128,9 +128,11 @@ def __init__(self, obj): # in view provider checks: Mesh, active analysis and # if Mesh and result are in active analysis - self.form = FreeCADGui.PySideUic.loadUi( - FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/ResultShow.ui" - ) + ui_path = FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/" + self.result_widget = FreeCADGui.PySideUic.loadUi(ui_path + "ResultShow.ui") + self.info_widget = FreeCADGui.PySideUic.loadUi(ui_path + "ResultHints.ui") + self.form = [self.result_widget, self.info_widget] + self.fem_prefs = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/Fem/General" ) @@ -142,93 +144,97 @@ def __init__(self, obj): # result type radio buttons # TODO: move to combo box, to be independent from result types and result types count QtCore.QObject.connect( - self.form.rb_none, QtCore.SIGNAL("toggled(bool)"), + self.result_widget.rb_none, QtCore.SIGNAL("toggled(bool)"), self.none_selected ) QtCore.QObject.connect( - self.form.rb_abs_displacement, + self.result_widget.rb_abs_displacement, QtCore.SIGNAL("toggled(bool)"), self.abs_displacement_selected ) QtCore.QObject.connect( - self.form.rb_x_displacement, + self.result_widget.rb_x_displacement, QtCore.SIGNAL("toggled(bool)"), self.x_displacement_selected ) QtCore.QObject.connect( - self.form.rb_y_displacement, + self.result_widget.rb_y_displacement, QtCore.SIGNAL("toggled(bool)"), self.y_displacement_selected ) QtCore.QObject.connect( - self.form.rb_z_displacement, + self.result_widget.rb_z_displacement, QtCore.SIGNAL("toggled(bool)"), self.z_displacement_selected ) QtCore.QObject.connect( - self.form.rb_temperature, + self.result_widget.rb_temperature, QtCore.SIGNAL("toggled(bool)"), self.temperature_selected ) QtCore.QObject.connect( - self.form.rb_vm_stress, + self.result_widget.rb_vm_stress, QtCore.SIGNAL("toggled(bool)"), self.vm_stress_selected ) QtCore.QObject.connect( - self.form.rb_maxprin, + self.result_widget.rb_maxprin, QtCore.SIGNAL("toggled(bool)"), self.max_prin_selected ) QtCore.QObject.connect( - self.form.rb_minprin, + self.result_widget.rb_minprin, QtCore.SIGNAL("toggled(bool)"), self.min_prin_selected ) QtCore.QObject.connect( - self.form.rb_max_shear_stress, + self.result_widget.rb_max_shear_stress, QtCore.SIGNAL("toggled(bool)"), self.max_shear_selected ) QtCore.QObject.connect( - self.form.rb_massflowrate, + self.result_widget.rb_massflowrate, QtCore.SIGNAL("toggled(bool)"), self.massflowrate_selected ) QtCore.QObject.connect( - self.form.rb_networkpressure, + self.result_widget.rb_networkpressure, QtCore.SIGNAL("toggled(bool)"), self.networkpressure_selected ) QtCore.QObject.connect( - self.form.rb_peeq, + self.result_widget.rb_peeq, QtCore.SIGNAL("toggled(bool)"), self.peeq_selected ) # displacement QtCore.QObject.connect( - self.form.cb_show_displacement, + self.result_widget.cb_show_displacement, QtCore.SIGNAL("clicked(bool)"), self.show_displacement ) QtCore.QObject.connect( - self.form.hsb_displacement_factor, + self.result_widget.hsb_displacement_factor, QtCore.SIGNAL("valueChanged(int)"), self.hsb_disp_factor_changed ) - self.form.sb_displacement_factor.valueChanged.connect(self.sb_disp_factor_changed) - self.form.sb_displacement_factor_max.valueChanged.connect(self.sb_disp_factor_max_changed) + self.result_widget.sb_displacement_factor.valueChanged.connect( + self.sb_disp_factor_changed + ) + self.result_widget.sb_displacement_factor_max.valueChanged.connect( + self.sb_disp_factor_max_changed + ) # user defined equation QtCore.QObject.connect( - self.form.user_def_eq, + self.result_widget.user_def_eq, QtCore.SIGNAL("textchanged()"), self.user_defined_text ) QtCore.QObject.connect( - self.form.calculate, + self.result_widget.calculate, QtCore.SIGNAL("clicked()"), self.calculate ) @@ -240,62 +246,62 @@ def __init__(self, obj): self.restore_initial_result_dialog() # initialize scale factor for show displacement scale_factor = get_displacement_scale_factor(self.result_obj) - self.form.sb_displacement_factor_max.setValue(10. * scale_factor) - self.form.sb_displacement_factor.setValue(scale_factor) + self.result_widget.sb_displacement_factor_max.setValue(10. * scale_factor) + self.result_widget.sb_displacement_factor.setValue(scale_factor) def restore_result_dialog(self): try: rt = FreeCAD.FEM_dialog["results_type"] if rt == "None": - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) elif rt == "Uabs": - self.form.rb_abs_displacement.setChecked(True) + self.result_widget.rb_abs_displacement.setChecked(True) self.abs_displacement_selected(True) elif rt == "U1": - self.form.rb_x_displacement.setChecked(True) + self.result_widget.rb_x_displacement.setChecked(True) self.x_displacement_selected(True) elif rt == "U2": - self.form.rb_y_displacement.setChecked(True) + self.result_widget.rb_y_displacement.setChecked(True) self.y_displacement_selected(True) elif rt == "U3": - self.form.rb_z_displacement.setChecked(True) + self.result_widget.rb_z_displacement.setChecked(True) self.z_displacement_selected(True) elif rt == "Temp": - self.form.rb_temperature.setChecked(True) + self.result_widget.rb_temperature.setChecked(True) self.temperature_selected(True) elif rt == "Sabs": - self.form.rb_vm_stress.setChecked(True) + self.result_widget.rb_vm_stress.setChecked(True) self.vm_stress_selected(True) elif rt == "MaxPrin": - self.form.rb_maxprin.setChecked(True) + self.result_widget.rb_maxprin.setChecked(True) self.max_prin_selected(True) elif rt == "MinPrin": - self.form.rb_minprin.setChecked(True) + self.result_widget.rb_minprin.setChecked(True) self.min_prin_selected(True) elif rt == "MaxShear": - self.form.rb_max_shear_stress.setChecked(True) + self.result_widget.rb_max_shear_stress.setChecked(True) self.max_shear_selected(True) elif rt == "MFlow": - self.form.rb_massflowrate.setChecked(True) + self.result_widget.rb_massflowrate.setChecked(True) self.massflowrate_selected(True) elif rt == "NPress": - self.form.rb_networkpressure.setChecked(True) + self.result_widget.rb_networkpressure.setChecked(True) self.networkpressure_selected(True) elif rt == "Peeq": - self.form.rb_peeq.setChecked(True) + self.result_widget.rb_peeq.setChecked(True) self.peeq_selected(True) sd = FreeCAD.FEM_dialog["show_disp"] - self.form.cb_show_displacement.setChecked(sd) + self.result_widget.cb_show_displacement.setChecked(sd) self.show_displacement(sd) df = FreeCAD.FEM_dialog["disp_factor"] dfm = FreeCAD.FEM_dialog["disp_factor_max"] - # self.form.hsb_displacement_factor.setMaximum(dfm) - # self.form.hsb_displacement_factor.setValue(df) - self.form.sb_displacement_factor_max.setValue(dfm) - self.form.sb_displacement_factor.setValue(df) + # self.result_widget.hsb_displacement_factor.setMaximum(dfm) + # self.result_widget.hsb_displacement_factor.setValue(df) + self.result_widget.sb_displacement_factor_max.setValue(dfm) + self.result_widget.sb_displacement_factor.setValue(df) except: self.restore_initial_result_dialog() @@ -315,7 +321,7 @@ def restore_initial_result_dialog(self): "disp_factor": 0., "disp_factor_max": 100. } - self.form.sb_displacement_factor_max.setValue(100.) # init non standard values + self.result_widget.sb_displacement_factor_max.setValue(100.) # init non standard values def getStandardButtons(self): return int(QtGui.QDialogButtonBox.Close) @@ -339,7 +345,7 @@ def abs_displacement_selected(self, state): if len(self.result_obj.DisplacementLengths) > 0: self.result_selected("Uabs", self.result_obj.DisplacementLengths, "mm") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def x_displacement_selected(self, state): @@ -349,7 +355,7 @@ def x_displacement_selected(self, state): ) self.result_selected("U1", res_disp_u1, "mm") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def y_displacement_selected(self, state): @@ -359,7 +365,7 @@ def y_displacement_selected(self, state): ) self.result_selected("U2", res_disp_u2, "mm") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def z_displacement_selected(self, state): @@ -369,68 +375,68 @@ def z_displacement_selected(self, state): ) self.result_selected("U3", res_disp_u3, "mm") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def vm_stress_selected(self, state): - if len(self.result_obj.StressValues) > 0: - self.result_selected("Sabs", self.result_obj.StressValues, "MPa") + if len(self.result_obj.vonMises) > 0: + self.result_selected("Sabs", self.result_obj.vonMises, "MPa") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def max_shear_selected(self, state): if len(self.result_obj.MaxShear) > 0: self.result_selected("MaxShear", self.result_obj.MaxShear, "MPa") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def max_prin_selected(self, state): if len(self.result_obj.PrincipalMax) > 0: self.result_selected("MaxPrin", self.result_obj.PrincipalMax, "MPa") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def temperature_selected(self, state): if len(self.result_obj.Temperature) > 0: self.result_selected("Temp", self.result_obj.Temperature, "K") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def massflowrate_selected(self, state): if len(self.result_obj.MassFlowRate) > 0: self.result_selected("MFlow", self.result_obj.MassFlowRate, "kg/s") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def networkpressure_selected(self, state): if len(self.result_obj.NetworkPressure) > 0: self.result_selected("NPress", self.result_obj.NetworkPressure, "MPa") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def min_prin_selected(self, state): if len(self.result_obj.PrincipalMin) > 0: self.result_selected("MinPrin", self.result_obj.PrincipalMin, "MPa") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def peeq_selected(self, state): if len(self.result_obj.Peeq) > 0: self.result_selected("Peeq", self.result_obj.Peeq, "") else: - self.form.rb_none.setChecked(True) + self.result_widget.rb_none.setChecked(True) self.none_selected(True) def user_defined_text(self, equation): FreeCAD.FEM_dialog["results_type"] = "user" - self.form.user_def_eq.toPlainText() + self.result_widget.user_def_eq.toPlainText() def calculate(self): @@ -439,7 +445,7 @@ def calculate(self): P1 = np.array(self.result_obj.PrincipalMax) P2 = np.array(self.result_obj.PrincipalMed) P3 = np.array(self.result_obj.PrincipalMin) - Von = np.array(self.result_obj.StressValues) + vM = np.array(self.result_obj.vonMises) Peeq = np.array(self.result_obj.Peeq) T = np.array(self.result_obj.Temperature) MF = np.array(self.result_obj.MassFlowRate) @@ -488,8 +494,28 @@ def calculate(self): FreeCAD.FEM_dialog["results_type"] = "None" self.update() self.restore_result_dialog() - userdefined_eq = self.form.user_def_eq.toPlainText() # Get equation to be used - UserDefinedFormula = eval(userdefined_eq).tolist() + userdefined_eq = self.result_widget.user_def_eq.toPlainText() # Get equation to be used + + from ply import lex + from ply import yacc + import femtools.tokrules as tokrules + identifiers = [ + 'x', 'y', 'z', 'T', 'vM', 'Peeq', 'P1', 'P2', 'P3', + 'sxx', 'syy', 'szz', 'sxy', 'sxz', 'syz', + 'exx', 'eyy', 'ezz', 'exy', 'exz', 'eyz', + 'MF', 'NP', 'rx', 'ry', 'rz', 'mc', + 's1x', 's1y', 's1z', 's2x', 's2y', 's2z', 's3x', 's3y', 's3z' + ] + tokrules.names = {} + for i in identifiers: + tokrules.names[i] = locals()[i] + + lexer = lex.lex(module=tokrules) + yacc.parse(input="UserDefinedFormula={0}".format(userdefined_eq), lexer=lexer) + UserDefinedFormula = tokrules.names["UserDefinedFormula"].tolist() + tokrules.names = {} + # UserDefinedFormula = eval(userdefined_eq).tolist() + if UserDefinedFormula: self.result_obj.UserDefined = UserDefinedFormula minm = min(UserDefinedFormula) @@ -498,7 +524,7 @@ def calculate(self): self.update_colors_stats(UserDefinedFormula, "", minm, avg, maxm) # Dummy use of the variables to get around flake8 error - del x, y, z, T, Von, Peeq, P1, P2, P3 + del x, y, z, T, vM, Peeq, P1, P2, P3 del sxx, syy, szz, sxy, sxz, syz del exx, eyy, ezz, exy, exz, eyz del MF, NP, rx, ry, rz, mc @@ -526,17 +552,17 @@ def update_colors_stats(self, res_values, res_unit, minm, avg, maxm): QtGui.QApplication.restoreOverrideCursor() def set_result_stats(self, unit, minm, avg, maxm): - self.form.le_min.setProperty("unit", unit) - self.form.le_min.setProperty("rawText", "{:.6} {}".format(minm, unit)) - self.form.le_avg.setProperty("unit", unit) - self.form.le_avg.setProperty("rawText", "{:.6} {}".format(avg, unit)) - self.form.le_max.setProperty("unit", unit) - self.form.le_max.setProperty("rawText", "{:.6} {}".format(maxm, unit)) + self.result_widget.le_min.setProperty("unit", unit) + self.result_widget.le_min.setProperty("rawText", "{:.6} {}".format(minm, unit)) + self.result_widget.le_avg.setProperty("unit", unit) + self.result_widget.le_avg.setProperty("rawText", "{:.6} {}".format(avg, unit)) + self.result_widget.le_max.setProperty("unit", unit) + self.result_widget.le_max.setProperty("rawText", "{:.6} {}".format(maxm, unit)) def update_displacement(self, factor=None): if factor is None: if FreeCAD.FEM_dialog["show_disp"]: - factor = self.form.sb_displacement_factor.value() + factor = self.result_widget.sb_displacement_factor.value() else: factor = 0.0 self.mesh_obj.ViewObject.applyDisplacement(factor) @@ -557,31 +583,33 @@ def show_displacement(self, checked): QtGui.QApplication.restoreOverrideCursor() def hsb_disp_factor_changed(self, value): - self.form.sb_displacement_factor.setValue( - value / 100. * self.form.sb_displacement_factor_max.value() + self.result_widget.sb_displacement_factor.setValue( + value / 100. * self.result_widget.sb_displacement_factor_max.value() ) self.update_displacement() def sb_disp_factor_max_changed(self, value): FreeCAD.FEM_dialog["disp_factor_max"] = value - if value < self.form.sb_displacement_factor.value(): - self.form.sb_displacement_factor.setValue(value) + if value < self.result_widget.sb_displacement_factor.value(): + self.result_widget.sb_displacement_factor.setValue(value) if value == 0.: - self.form.hsb_displacement_factor.setValue(0) + self.result_widget.hsb_displacement_factor.setValue(0) else: - self.form.hsb_displacement_factor.setValue( - round(self.form.sb_displacement_factor.value() / value * 100.) + self.result_widget.hsb_displacement_factor.setValue( + round(self.result_widget.sb_displacement_factor.value() / value * 100.) ) def sb_disp_factor_changed(self, value): FreeCAD.FEM_dialog["disp_factor"] = value - if value > self.form.sb_displacement_factor_max.value(): - self.form.sb_displacement_factor.setValue(self.form.sb_displacement_factor_max.value()) - if self.form.sb_displacement_factor_max.value() == 0.: - self.form.hsb_displacement_factor.setValue(0.) + if value > self.result_widget.sb_displacement_factor_max.value(): + self.result_widget.sb_displacement_factor.setValue( + self.result_widget.sb_displacement_factor_max.value() + ) + if self.result_widget.sb_displacement_factor_max.value() == 0.: + self.result_widget.hsb_displacement_factor.setValue(0.) else: - self.form.hsb_displacement_factor.setValue( - round(value / self.form.sb_displacement_factor_max.value() * 100.) + self.result_widget.hsb_displacement_factor.setValue( + round(value / self.result_widget.sb_displacement_factor_max.value() * 100.) ) def disable_empty_result_buttons(self): @@ -590,7 +618,7 @@ def disable_empty_result_buttons(self): DisplacementLengths --> rb_abs_displacement DisplacementVectors --> rb_x_displacement, rb_y_displacement, rb_z_displacement Temperature --> rb_temperature - StressValues --> rb_vm_stress + vonMises --> rb_vm_stress PrincipalMax --> rb_maxprin PrincipalMin --> rb_minprin MaxShear --> rb_max_shear_stress @@ -598,27 +626,27 @@ def disable_empty_result_buttons(self): NetworkPressure --> rb_networkpressure Peeq --> rb_peeq""" if len(self.result_obj.DisplacementLengths) == 0: - self.form.rb_abs_displacement.setEnabled(0) + self.result_widget.rb_abs_displacement.setEnabled(0) if len(self.result_obj.DisplacementVectors) == 0: - self.form.rb_x_displacement.setEnabled(0) - self.form.rb_y_displacement.setEnabled(0) - self.form.rb_z_displacement.setEnabled(0) + self.result_widget.rb_x_displacement.setEnabled(0) + self.result_widget.rb_y_displacement.setEnabled(0) + self.result_widget.rb_z_displacement.setEnabled(0) if len(self.result_obj.Temperature) == 0: - self.form.rb_temperature.setEnabled(0) - if len(self.result_obj.StressValues) == 0: - self.form.rb_vm_stress.setEnabled(0) + self.result_widget.rb_temperature.setEnabled(0) + if len(self.result_obj.vonMises) == 0: + self.result_widget.rb_vm_stress.setEnabled(0) if len(self.result_obj.PrincipalMax) == 0: - self.form.rb_maxprin.setEnabled(0) + self.result_widget.rb_maxprin.setEnabled(0) if len(self.result_obj.PrincipalMin) == 0: - self.form.rb_minprin.setEnabled(0) + self.result_widget.rb_minprin.setEnabled(0) if len(self.result_obj.MaxShear) == 0: - self.form.rb_max_shear_stress.setEnabled(0) + self.result_widget.rb_max_shear_stress.setEnabled(0) if len(self.result_obj.MassFlowRate) == 0: - self.form.rb_massflowrate.setEnabled(0) + self.result_widget.rb_massflowrate.setEnabled(0) if len(self.result_obj.NetworkPressure) == 0: - self.form.rb_networkpressure.setEnabled(0) + self.result_widget.rb_networkpressure.setEnabled(0) if len(self.result_obj.Peeq) == 0: - self.form.rb_peeq.setEnabled(0) + self.result_widget.rb_peeq.setEnabled(0) def update(self): self.reset_result_mesh() diff --git a/src/Mod/Fem/feminout/importCcxFrdResults.py b/src/Mod/Fem/feminout/importCcxFrdResults.py index b8f91417d3e2..e1311824d106 100644 --- a/src/Mod/Fem/feminout/importCcxFrdResults.py +++ b/src/Mod/Fem/feminout/importCcxFrdResults.py @@ -151,7 +151,7 @@ def importFrd( # fill DisplacementLengths res_obj = restools.add_disp_apps(res_obj) - # fill StressValues + # fill vonMises res_obj = restools.add_von_mises(res_obj) if res_obj.getParentGroup(): has_reinforced_mat = False diff --git a/src/Mod/Fem/femmesh/gmshtools.py b/src/Mod/Fem/femmesh/gmshtools.py index 796166da86c3..2c9d1320447b 100644 --- a/src/Mod/Fem/femmesh/gmshtools.py +++ b/src/Mod/Fem/femmesh/gmshtools.py @@ -828,13 +828,25 @@ def run_gmsh_with_geo(self): error = "Error executing: {}\n".format(" ".join(comandlist)) Console.PrintError(error) self.error = True - return error + + # workaround + # filter useless gmsh warning in the regard of unknown element MSH type 15 + # https://forum.freecadweb.org/viewtopic.php?f=18&t=33946 + useless_warning = ( + "Warning : Unknown element type for UNV export " + "(MSH type 15) - output file might be invalid" + ) + new_err = error.replace(useless_warning, "") + # remove empty lines, https://stackoverflow.com/a/1140967 + new_err = "".join([s for s in new_err.splitlines(True) if s.strip("\r\n")]) + + return new_err def read_and_set_new_mesh(self): if not self.error: fem_mesh = Fem.read(self.temp_file_mesh) self.mesh_obj.FemMesh = fem_mesh - Console.PrintMessage(" The Part should have a pretty new FEM mesh!\n") + Console.PrintMessage(" New mesh was added to to the mesh object.\n") else: Console.PrintError("No mesh was created.\n") diff --git a/src/Mod/Fem/femobjects/_FemResultMechanical.py b/src/Mod/Fem/femobjects/_FemResultMechanical.py index bbc30b612d4d..5c09deebfe3d 100644 --- a/src/Mod/Fem/femobjects/_FemResultMechanical.py +++ b/src/Mod/Fem/femobjects/_FemResultMechanical.py @@ -139,9 +139,9 @@ def __init__(self, obj): ) obj.addProperty( "App::PropertyFloatList", - "StressValues", + "vonMises", "NodeData", - "", + "List of von Mises equivalent stresses", True ) obj.addProperty( @@ -301,6 +301,20 @@ def execute(self, obj): def onChanged(self, obj, prop): return + def onDocumentRestored(self, obj): + # migrate old result objects, because property "StressValues" + # was renamed to "vonMises" in commit 8b68ab7 + if hasattr(obj, "StressValues") is True: + obj.addProperty( + "App::PropertyFloatList", + "vonMises", + "NodeData", + "List of von Mises equivalent stresses", + True + ) + obj.vonMises = obj.StressValues + obj.removeProperty("StressValues") + def __getstate__(self): return self.Type diff --git a/src/Mod/Fem/femresult/resulttools.py b/src/Mod/Fem/femresult/resulttools.py index 98f21421b3ba..050e1aebec99 100644 --- a/src/Mod/Fem/femresult/resulttools.py +++ b/src/Mod/Fem/femresult/resulttools.py @@ -124,7 +124,7 @@ def show_result(resultobj, result_type="Sabs", limit=None): return if resultobj: if result_type == "Sabs": - values = resultobj.StressValues + values = resultobj.vonMises elif result_type == "Uabs": values = resultobj.DisplacementLengths # TODO: the result object does have more result types to show, implement them @@ -282,10 +282,10 @@ def fill_femresult_stats(res_obj): a_min = min(res_obj.DisplacementLengths) a_avg = sum(res_obj.DisplacementLengths) / no_of_values a_max = max(res_obj.DisplacementLengths) - if res_obj.StressValues: - s_min = min(res_obj.StressValues) - s_avg = sum(res_obj.StressValues) / no_of_values - s_max = max(res_obj.StressValues) + if res_obj.vonMises: + s_min = min(res_obj.vonMises) + s_avg = sum(res_obj.vonMises) / no_of_values + s_max = max(res_obj.vonMises) if res_obj.PrincipalMax: p1_min = min(res_obj.PrincipalMax) p1_avg = sum(res_obj.PrincipalMax) / no_of_values @@ -382,8 +382,8 @@ def add_von_mises(res_obj): ) for Sxx, Syy, Szz, Sxy, Sxz, Syz in iterator: mstress.append(calculate_von_mises((Sxx, Syy, Szz, Sxy, Sxz, Syz))) - res_obj.StressValues = mstress - FreeCAD.Console.PrintLog("Added StressValues (von Mises).\n") + res_obj.vonMises = mstress + FreeCAD.Console.PrintLog("Added von Mises stress.\n") return res_obj diff --git a/src/Mod/Fem/femsolver/calculix/tasks.py b/src/Mod/Fem/femsolver/calculix/tasks.py index 224312fc7f74..37a0a3375baf 100644 --- a/src/Mod/Fem/femsolver/calculix/tasks.py +++ b/src/Mod/Fem/femsolver/calculix/tasks.py @@ -40,9 +40,6 @@ from .. import settings from . import writer -if FreeCAD.GuiUp: - from PySide import QtGui - _inputFileName = None @@ -64,7 +61,7 @@ def run(self): w = writer.FemInputWriterCcx( self.analysis, self.solver, - c.mesh, + femutils.get_mesh_to_solve(self.analysis)[0], # pre check has been done already c.materials_linear, c.materials_nonlinear, c.constraints_fixed, @@ -170,19 +167,6 @@ class _Container(object): def __init__(self, analysis): self.analysis = analysis - # get mesh - mesh, message = femutils.get_mesh_to_solve(self.analysis) - if mesh is not None: - self.mesh = mesh - else: - if FreeCAD.GuiUp: - QtGui.QMessageBox.critical( - None, - "Missing prerequisite", - message - ) - raise Exception(message + "\n") - # get member # materials std_mats = self.get_several_member( diff --git a/src/Mod/Fem/femsolver/calculix/writer.py b/src/Mod/Fem/femsolver/calculix/writer.py index 1613a7b352c7..754a630a487f 100644 --- a/src/Mod/Fem/femsolver/calculix/writer.py +++ b/src/Mod/Fem/femsolver/calculix/writer.py @@ -1620,7 +1620,7 @@ def get_ccx_elsets_multiple_mat_multiple_shell(self): {"long": shellth_obj.Name, "short": shellth_data["ShortName"]} ] ccx_elset = {} - ccx_elset["ccx_elset"] = ccx_elset + ccx_elset["ccx_elset"] = elset_data ccx_elset["ccx_elset_name"] = get_ccx_elset_name_standard(names) ccx_elset["mat_obj_name"] = mat_obj.Name ccx_elset["ccx_mat_name"] = mat_obj.Material["Name"] diff --git a/src/Mod/Fem/femsolver/elmer/equations/equation.py b/src/Mod/Fem/femsolver/elmer/equations/equation.py index b78c818fc445..c40ddf6f6f03 100644 --- a/src/Mod/Fem/femsolver/elmer/equations/equation.py +++ b/src/Mod/Fem/femsolver/elmer/equations/equation.py @@ -75,7 +75,7 @@ def __init__(self, obj): self.form = self._refWidget else: self.form = [self.refWidget, propWidget] - analysis = femutils.findAnalysisOfMember(obj) + analysis = obj.getParentGroup() self._mesh = femutils.get_single_member(analysis, "Fem::FemMeshObject") self._part = self._mesh.Part if self._mesh is not None else None self._partVisible = None diff --git a/src/Mod/Fem/femsolver/elmer/writer.py b/src/Mod/Fem/femsolver/elmer/writer.py index 0713325517c5..66e67781febb 100644 --- a/src/Mod/Fem/femsolver/elmer/writer.py +++ b/src/Mod/Fem/femsolver/elmer/writer.py @@ -93,7 +93,7 @@ def getConstant(name, dimension): class Writer(object): def __init__(self, solver, directory, testmode=False): - self.analysis = femutils.findAnalysisOfMember(solver) + self.analysis = solver.getParentGroup() self.solver = solver self.directory = directory self.testmode = testmode @@ -353,7 +353,8 @@ def _handleElectrostaticBndConditions(self): for obj in self._getMember("Fem::ConstraintElectrostaticPotential"): if obj.References: for name in obj.References[0][1]: - if obj.Potential: + if hasattr(obj, "Potential"): + # https://forum.freecadweb.org/viewtopic.php?f=18&t=41488&start=10#p369454 potential = getFromUi(obj.Potential, "V", "M*L^2/(T^3 * I)") self._boundary(name, "Potential", potential) if obj.PotentialConstant: diff --git a/src/Mod/Fem/femsolver/run.py b/src/Mod/Fem/femsolver/run.py index fd0f898c167d..32a44e7fd511 100644 --- a/src/Mod/Fem/femsolver/run.py +++ b/src/Mod/Fem/femsolver/run.py @@ -146,6 +146,17 @@ def run_fem_solver(solver, working_dir=None): machine.target = RESULTS machine.start() machine.join() # wait for the machine to finish. + if machine.failed is True: + App.Console.PrintError("Machine failed to run.\n") + from .report import displayLog + displayLog(machine.report) + if App.GuiUp: + error_message = ( + "Failed to run. Please try again after all " + "of the following errors are resolved." + ) + from .report import display + display(machine.report, "Run Report", error_message) def getMachine(solver, path=None): @@ -288,7 +299,7 @@ def __init__(self): @property def analysis(self): - return femutils.findAnalysisOfMember(self.solver) + return self.solver.getParentGroup() class Machine(BaseTask): @@ -517,7 +528,7 @@ def _checkEquation(self, obj): _machines[o].reset() def _checkSolver(self, obj): - analysis = femutils.findAnalysisOfMember(obj) + analysis = obj.getParentGroup() for m in iter(_machines.values()): if analysis == m.analysis and obj == m.solver: m.reset() @@ -535,7 +546,7 @@ def _checkAnalysis(self, obj): def _checkModel(self, obj): if self._partOfModel(obj): - analysis = femutils.findAnalysisOfMember(obj) + analysis = obj.getParentGroup() if analysis is not None: self._resetAll(analysis) diff --git a/src/Mod/Fem/femsolver/z88/tasks.py b/src/Mod/Fem/femsolver/z88/tasks.py index ec89842ffce0..cb064a4c6c37 100644 --- a/src/Mod/Fem/femsolver/z88/tasks.py +++ b/src/Mod/Fem/femsolver/z88/tasks.py @@ -38,9 +38,6 @@ from .. import settings from . import writer -if FreeCAD.GuiUp: - from PySide import QtGui - class Check(run.Check): @@ -58,7 +55,7 @@ def run(self): w = writer.FemInputWriterZ88( self.analysis, self.solver, - c.mesh, + femutils.get_mesh_to_solve(self.analysis)[0], # pre check has been done already c.materials_linear, c.materials_nonlinear, c.constraints_fixed, @@ -155,19 +152,6 @@ class _Container(object): def __init__(self, analysis): self.analysis = analysis - # get mesh - mesh, message = femutils.get_mesh_to_solve(self.analysis) - if mesh is not None: - self.mesh = mesh - else: - if FreeCAD.GuiUp: - QtGui.QMessageBox.critical( - None, - "Missing prerequisite", - message - ) - raise Exception(message + "\n") - # get member, empty lists are not supported by z88 # materials self.materials_linear = self.get_several_member( diff --git a/src/Mod/Fem/femtools/femutils.py b/src/Mod/Fem/femtools/femutils.py index 1fa99717d56f..b6545a9434bb 100644 --- a/src/Mod/Fem/femtools/femutils.py +++ b/src/Mod/Fem/femtools/femutils.py @@ -66,35 +66,6 @@ def createObject(doc, name, proxy, viewProxy=None): return obj -def findAnalysisOfMember(member): - """ Find Analysis the *member* belongs to. - - :param member: a document object - - :returns: - If a analysis that contains *member* can be found a reference is returned. - If no such object exists in the document of *member*, ``None`` is returned. - """ - if member is None: - raise ValueError("Member must not be None") - for obj in member.Document.Objects: - if obj.isDerivedFrom("Fem::FemAnalysis"): - if member in obj.Group: - return obj - if _searchGroups(member, obj.Group): - return obj - return None - - -def _searchGroups(member, objs): - for o in objs: - if o == member: - return True - if hasattr(o, "Group"): - return _searchGroups(member, o.Group) - return False - - def get_member(analysis, t): """ Return list of all members of *analysis* of type *t*. diff --git a/src/Mod/Fem/femtools/tokrules.py b/src/Mod/Fem/femtools/tokrules.py new file mode 100644 index 000000000000..3a7ba6a1f35e --- /dev/null +++ b/src/Mod/Fem/femtools/tokrules.py @@ -0,0 +1,154 @@ +# *************************************************************************** +# * Copyright (c) 2020 Werner Mayer * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FEM Utilities" +__author__ = "Werner Mayer" +__url__ = "http://www.freecadweb.org" + +tokens = ( + 'NAME', + 'FLOAT', + 'INT', + 'PLUS', + 'MINUS', + 'TIMES', + 'DIVIDE', + 'EQUALS', + 'LPAREN', + 'RPAREN', + 'COMMENT', +) + +# Tokens + +t_PLUS = r'\+' +t_MINUS = r'-' +t_TIMES = r'\*' +t_DIVIDE = r'/' +t_EQUALS = r'=' +t_LPAREN = r'\(' +t_RPAREN = r'\)' +t_NAME = r'[a-zA-Z_][a-zA-Z0-9_]*' + + +def t_FLOAT(t): + r'\d+\.(\d+)?([eE][-+]?\d+)?' + t.value = float(t.value) + return t + + +def t_INT(t): + r'\d+' + t.value = int(t.value) + return t + + +# Ignored characters +t_ignore = " \t" + + +def t_COMMENT(t): + r'\#.*' + pass + + +def t_newline(t): + r'\n+' + t.lexer.lineno += t.value.count("\n") + + +def t_error(t): + print("Illegal character '%s'" % t.value[0]) + t.lexer.skip(1) + + +# Build the lexer +import ply.lex as lex +lex.lex() + + +# Precedence rules for the arithmetic operators +precedence = ( + ('left', 'PLUS', 'MINUS'), + ('left', 'TIMES', 'DIVIDE'), + ('right', 'UMINUS'), +) + +# dictionary of names (for storing variables) +names = {} + + +def p_statement_assign(p): + 'statement : NAME EQUALS expression' + names[p[1]] = p[3] + + +def p_statement_expr(p): + 'statement : expression' + print(p[1]) + + +def p_expression_binop(p): + '''expression : expression PLUS expression + | expression MINUS expression + | expression TIMES expression + | expression DIVIDE expression''' + if p[2] == '+' : p[0] = p[1] + p[3] + elif p[2] == '-': p[0] = p[1] - p[3] + elif p[2] == '*': p[0] = p[1] * p[3] + elif p[2] == '/': p[0] = p[1] / p[3] + + +def p_expression_uminus(p): + 'expression : MINUS expression %prec UMINUS' + p[0] = -p[2] + + +def p_expression_group(p): + 'expression : LPAREN expression RPAREN' + p[0] = p[2] + + +def p_expression_float(p): + 'expression : FLOAT' + p[0] = p[1] + + +def p_expression_int(p): + 'expression : INT' + p[0] = p[1] + + +def p_expression_name(p): + 'expression : NAME' + try: + p[0] = names[p[1]] + except LookupError: + print("Undefined name '%s'" % p[1]) + p[0] = 0 + + +def p_error(p): + print("Syntax error at '%s'" % p.value) + + +import ply.yacc as yacc +yacc.yacc() diff --git a/src/Mod/Inspection/App/InspectionFeature.cpp b/src/Mod/Inspection/App/InspectionFeature.cpp index f25ca00e839c..e23119c926bc 100644 --- a/src/Mod/Inspection/App/InspectionFeature.cpp +++ b/src/Mod/Inspection/App/InspectionFeature.cpp @@ -22,6 +22,7 @@ #include "PreCompiled.h" +#include #include #include #include diff --git a/src/Mod/OpenSCAD/InitGui.py b/src/Mod/OpenSCAD/InitGui.py index 0a5d4db5f49a..9df219ed085a 100644 --- a/src/Mod/OpenSCAD/InitGui.py +++ b/src/Mod/OpenSCAD/InitGui.py @@ -86,7 +86,6 @@ def QT_TRANSLATE_NOOP(scope, text): FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addPreferencePage(":/ui/openscadprefs-base.ui","OpenSCAD") def GetClassName(self): - #return "OpenSCADGui::Workbench" return "Gui::PythonWorkbench" diff --git a/src/Mod/Part/App/BRepOffsetAPI_MakeOffsetFix.cpp b/src/Mod/Part/App/BRepOffsetAPI_MakeOffsetFix.cpp index f657693ba964..81c8f19301e4 100644 --- a/src/Mod/Part/App/BRepOffsetAPI_MakeOffsetFix.cpp +++ b/src/Mod/Part/App/BRepOffsetAPI_MakeOffsetFix.cpp @@ -159,6 +159,11 @@ const TopoDS_Shape& BRepOffsetAPI_MakeOffsetFix::Shape() { if (myResult.IsNull()) { TopoDS_Shape result = mkOffset.Shape(); + if (result.IsNull()) { + myResult = result; + return myResult; + } + if (result.ShapeType() == TopAbs_WIRE) { MakeWire(result); } diff --git a/src/Mod/Part/App/BSplineCurvePyImp.cpp b/src/Mod/Part/App/BSplineCurvePyImp.cpp index d3b02b6c0877..f07ce26f1d6b 100644 --- a/src/Mod/Part/App/BSplineCurvePyImp.cpp +++ b/src/Mod/Part/App/BSplineCurvePyImp.cpp @@ -1241,7 +1241,15 @@ PyObject* BSplineCurvePy::buildFromPolesMultsKnots(PyObject *args, PyObject *key occmults.SetValue(occmults.Length(), degree+1); sum_of_mults = occmults.Length()+2*degree; } - else { sum_of_mults = occmults.Length()-1;} + else { + sum_of_mults = occmults.Length()-1; + } + } + // check multiplicity of inner knots + for (Standard_Integer i=2; i < occmults.Length(); i++) { + if (occmults(i) > degree) { + Standard_Failure::Raise("multiplicity of inner knot higher than degree"); + } } if (knots != Py_None) { //knots are given Py::Sequence knotssq(knots); diff --git a/src/Mod/Part/App/BSplineSurfacePyImp.cpp b/src/Mod/Part/App/BSplineSurfacePyImp.cpp index 44a96456c18c..33ae6d96a71e 100644 --- a/src/Mod/Part/App/BSplineSurfacePyImp.cpp +++ b/src/Mod/Part/App/BSplineSurfacePyImp.cpp @@ -1514,6 +1514,18 @@ PyObject* BSplineSurfacePy::buildFromPolesMultsKnots(PyObject *args, PyObject *k (PyObject_Not(vperiodic) && sum_of_vmults - vdegree -1 != lv)) { Standard_Failure::Raise("number of poles and sum of mults mismatch"); } + // check multiplicity of inner knots + for (Standard_Integer i=2; i < occumults.Length(); i++) { + if (occumults(i) > udegree) { + Standard_Failure::Raise("multiplicity of inner knot higher than degree"); + } + } + for (Standard_Integer i=2; i < occvmults.Length(); i++) { + if (occvmults(i) > vdegree) { + Standard_Failure::Raise("multiplicity of inner knot higher than degree"); + } + } + Handle(Geom_BSplineSurface) spline = new Geom_BSplineSurface(occpoles,occweights, occuknots,occvknots,occumults,occvmults,udegree,vdegree, PyObject_IsTrue(uperiodic) ? Standard_True : Standard_False, diff --git a/src/Mod/Part/App/GeometryCurvePyImp.cpp b/src/Mod/Part/App/GeometryCurvePyImp.cpp index 3541932f127a..91e88967a35e 100644 --- a/src/Mod/Part/App/GeometryCurvePyImp.cpp +++ b/src/Mod/Part/App/GeometryCurvePyImp.cpp @@ -54,6 +54,7 @@ # include # include # include +# include #endif #include @@ -563,6 +564,21 @@ PyObject* GeometryCurvePy::intersect2d(PyObject *args) tuple.setItem(1, Py::Float(pt.Y())); list.append(tuple); } + if (intCC.NbSegments() > 0) { + // See also Curve2dPy::intersectCC() that uses Geom2dAPI_ExtremaCurveCurve + const Geom2dInt_GInter& gInter = intCC.Intersector(); + for (int i=1; i <= gInter.NbSegments(); i++) { + const IntRes2d_IntersectionSegment& segm = gInter.Segment(i); + if (segm.HasFirstPoint()) { + const IntRes2d_IntersectionPoint& fp = segm.FirstPoint(); + gp_Pnt2d pt = fp.Value(); + Py::Tuple tuple(2); + tuple.setItem(0, Py::Float(pt.X())); + tuple.setItem(1, Py::Float(pt.Y())); + list.append(tuple); + } + } + } return Py::new_reference_to(list); } catch (Standard_Failure& e) { diff --git a/src/Mod/Part/App/Part2DObject.cpp b/src/Mod/Part/App/Part2DObject.cpp index 45bfeb71be33..597503823e31 100644 --- a/src/Mod/Part/App/Part2DObject.cpp +++ b/src/Mod/Part/App/Part2DObject.cpp @@ -37,6 +37,7 @@ # include # include # include +# include #endif #ifndef M_PI @@ -174,6 +175,21 @@ bool Part2DObject::seekTrimPoints(const std::vector &geomlist, for (int i=1; i <= Intersector.NbPoints(); i++) points.push_back(Intersector.Point(i)); + if (Intersector.NbSegments() > 0) { + const Geom2dInt_GInter& gInter = Intersector.Intersector(); + for (int i=1; i <= gInter.NbSegments(); i++) { + const IntRes2d_IntersectionSegment& segm = gInter.Segment(i); + if (segm.HasFirstPoint()) { + const IntRes2d_IntersectionPoint& fp = segm.FirstPoint(); + points.push_back(fp.Value()); + } + if (segm.HasLastPoint()) { + const IntRes2d_IntersectionPoint& fp = segm.LastPoint(); + points.push_back(fp.Value()); + } + } + } + for (auto p : points) { // get the parameter of the intersection point on the primary curve Projector.Init(p, primaryCurve); diff --git a/src/Mod/Part/App/TopoShapeWirePy.xml b/src/Mod/Part/App/TopoShapeWirePy.xml index 86d63f483700..96c37f4318b1 100644 --- a/src/Mod/Part/App/TopoShapeWirePy.xml +++ b/src/Mod/Part/App/TopoShapeWirePy.xml @@ -49,7 +49,7 @@ Make a loft defined by a list of profiles along a wire. Transition can be 0 (default), 1 (right corners) or 2 (rounded corners). - + Approximate B-Spline-curve from this wire diff --git a/src/Mod/Part/App/TopoShapeWirePyImp.cpp b/src/Mod/Part/App/TopoShapeWirePyImp.cpp index 8063b9e245df..5522896ec84b 100644 --- a/src/Mod/Part/App/TopoShapeWirePyImp.cpp +++ b/src/Mod/Part/App/TopoShapeWirePyImp.cpp @@ -326,12 +326,14 @@ PyObject* TopoShapeWirePy::makeHomogenousWires(PyObject *args) } } -PyObject* TopoShapeWirePy::approximate(PyObject *args) +PyObject* TopoShapeWirePy::approximate(PyObject *args, PyObject *kwds) { double tol2d = gp::Resolution(); double tol3d = 0.0001; int maxseg=10, maxdeg=3; - if (!PyArg_ParseTuple(args, "ddii",&tol2d,&tol3d,&maxseg,&maxdeg)) + + static char* kwds_approx[] = {"Tol2d","Tol3d","MaxSegments","MaxDegree",NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ddii", kwds_approx, &tol2d, &tol3d, &maxseg, &maxdeg)) return 0; try { BRepAdaptor_CompCurve adapt(TopoDS::Wire(getTopoShapePtr()->getShape())); diff --git a/src/Mod/Part/Gui/DlgImportExportStep.ui b/src/Mod/Part/Gui/DlgImportExportStep.ui index 110feff33da4..915713a7d2d1 100644 --- a/src/Mod/Part/Gui/DlgImportExportStep.ui +++ b/src/Mod/Part/Gui/DlgImportExportStep.ui @@ -55,10 +55,10 @@ Export invisible objects - ExportHiddenObject + ExportHiddenObject - Mod/Import + Mod/Import @@ -114,10 +114,10 @@ Use legacy exporter - Mod/Import + Mod/Import - ExportLegacy + ExportLegacy @@ -133,10 +133,10 @@ it inside the Placement property. Export single object placement - ExportKeepPlacement + ExportKeepPlacement - Mod/Import + Mod/Import @@ -233,10 +233,10 @@ during file reading (slower but higher details). Use LinkGroup - UseLinkGroup + UseLinkGroup - Mod/Import + Mod/Import @@ -249,10 +249,10 @@ during file reading (slower but higher details). Import invisible objects - ImportHiddenObject + ImportHiddenObject - Mod/Import + Mod/Import @@ -265,10 +265,10 @@ during file reading (slower but higher details). Reduce number of objects - ReduceObjects + ReduceObjects - Mod/Import + Mod/Import @@ -281,10 +281,10 @@ during file reading (slower but higher details). Expand compound shape - ExpandCompound + ExpandCompound - Mod/Import + Mod/Import @@ -297,10 +297,10 @@ during file reading (slower but higher details). Show progress bar when importing - ShowProgress + ShowProgress - Mod/Import + Mod/Import @@ -313,10 +313,10 @@ during file reading (slower but higher details). Ignore instance names - UseBaseName + UseBaseName - Mod/Import + Mod/Import @@ -338,10 +338,10 @@ during file reading (slower but higher details). - ImportMode + ImportMode - Mod/Import + Mod/Import diff --git a/src/Mod/Part/Gui/TaskSweep.cpp b/src/Mod/Part/Gui/TaskSweep.cpp index 28bc3c00f72d..f3f4fd0903d4 100644 --- a/src/Mod/Part/Gui/TaskSweep.cpp +++ b/src/Mod/Part/Gui/TaskSweep.cpp @@ -84,7 +84,7 @@ class SweepWidget::Private bool allow(App::Document* /*pDoc*/, App::DocumentObject*pObj, const char*sSubName) { if (pObj->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { - if (!sSubName) { + if (!sSubName || sSubName[0] == '\0') { // If selecting again the same edge the passed sub-element is empty. If the whole // shape is an edge or wire we can use it completely. const TopoDS_Shape& shape = static_cast(pObj)->Shape.getValue(); diff --git a/src/Mod/PartDesign/App/AppPartDesign.cpp b/src/Mod/PartDesign/App/AppPartDesign.cpp index 26abc6f76bcb..3cae7878ddf9 100644 --- a/src/Mod/PartDesign/App/AppPartDesign.cpp +++ b/src/Mod/PartDesign/App/AppPartDesign.cpp @@ -88,11 +88,11 @@ PyMOD_INIT_FUNC(_PartDesign) PartDesign::Feature ::init(); PartDesign::FeaturePython ::init(); PartDesign::Solid ::init(); - PartDesign::DressUp ::init(); PartDesign::FeatureAddSub ::init(); PartDesign::FeatureAddSubPython ::init(); PartDesign::FeatureAdditivePython ::init(); PartDesign::FeatureSubtractivePython ::init(); + PartDesign::DressUp ::init(); PartDesign::ProfileBased ::init(); PartDesign::Transformed ::init(); PartDesign::Mirrored ::init(); diff --git a/src/Mod/PartDesign/App/FeatureDressUp.cpp b/src/Mod/PartDesign/App/FeatureDressUp.cpp index b658318d2733..5bafab6420a3 100644 --- a/src/Mod/PartDesign/App/FeatureDressUp.cpp +++ b/src/Mod/PartDesign/App/FeatureDressUp.cpp @@ -33,6 +33,7 @@ #include "FeatureDressUp.h" +#include #include @@ -42,12 +43,17 @@ using namespace PartDesign; namespace PartDesign { -PROPERTY_SOURCE(PartDesign::DressUp, PartDesign::Feature) +PROPERTY_SOURCE(PartDesign::DressUp, PartDesign::FeatureAddSub) DressUp::DressUp() { ADD_PROPERTY(Base,(0)); Placement.setStatus(App::Property::ReadOnly, true); + + ADD_PROPERTY_TYPE(SupportTransform,(true),"Base", App::Prop_None, + "Enable support for transformed patterns"); + + addSubType = Additive; } short DressUp::mustExecute() const @@ -170,6 +176,32 @@ void DressUp::onChanged(const App::Property* prop) if (BaseFeature.getValue() && Base.getValue() != BaseFeature.getValue()) { BaseFeature.setValue (Base.getValue()); } + } else if (prop == &Shape || prop == &SupportTransform) { + if (!isRestoring() && !getDocument()->isPerformingTransaction()) { + Part::TopoShape s; + auto base = Base::freecad_dynamic_cast(getBaseObject(true)); + if(!base) { + addSubType = Additive; + if(!SupportTransform.getValue()) + s = getBaseShape(); + else + s = Shape.getShape(); + } else if (!SupportTransform.getValue()) { + addSubType = base->getAddSubType(); + s = base->AddSubShape.getShape(); + } else { + addSubType = base->getAddSubType(); + auto baseBase = base->getBaseObject(true); + if(!baseBase) { + s = Shape.getShape(); + addSubType = Additive; + } else if (addSubType == Additive) + s = Shape.getShape().cut(base->getBaseTopoShape().getShape()); + else + s = base->getBaseTopoShape().cut(Shape.getValue()); + } + AddSubShape.setValue(s); + } } Feature::onChanged(prop); diff --git a/src/Mod/PartDesign/App/FeatureDressUp.h b/src/Mod/PartDesign/App/FeatureDressUp.h index 520dc24a71c2..9e4f3650aaf1 100644 --- a/src/Mod/PartDesign/App/FeatureDressUp.h +++ b/src/Mod/PartDesign/App/FeatureDressUp.h @@ -25,12 +25,12 @@ #define PARTDESIGN_DressUp_H #include -#include "Feature.h" +#include "FeatureAddSub.h" namespace PartDesign { -class PartDesignExport DressUp : public PartDesign::Feature +class PartDesignExport DressUp : public PartDesign::FeatureAddSub { PROPERTY_HEADER(PartDesign::DressUp); @@ -43,6 +43,7 @@ class PartDesignExport DressUp : public PartDesign::Feature * But for consistency if BaseFeature is nonzero this links to the same body as it. */ App::PropertyLinkSub Base; + App::PropertyBool SupportTransform; short mustExecute() const; /// updates the Placement property from the Placement of the BaseFeature diff --git a/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp b/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp index 105f70440c16..056aa99434e1 100644 --- a/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskBooleanParameters.cpp @@ -82,7 +82,11 @@ TaskBooleanParameters::TaskBooleanParameters(ViewProviderBoolean *BooleanView,QW // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetBodies->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onBodyDeleted())); ui->listWidgetBodies->setContextMenuPolicy(Qt::ActionsContextMenu); diff --git a/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp b/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp index e7b25fa001b8..615c24a02113 100644 --- a/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskChamferParameters.cpp @@ -25,6 +25,8 @@ #ifndef _PreComp_ # include +# include +# include #endif #include "ui_TaskChamferParameters.h" @@ -50,7 +52,7 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskChamferParameters */ -TaskChamferParameters::TaskChamferParameters(ViewProviderDressUp *DressUpView,QWidget *parent) +TaskChamferParameters::TaskChamferParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, true, true, parent) { // we need a separate container widget to add all controls to @@ -77,33 +79,74 @@ TaskChamferParameters::TaskChamferParameters(ViewProviderDressUp *DressUpView,QW QMetaObject::connectSlotsByName(this); connect(ui->chamferDistance, SIGNAL(valueChanged(double)), - this, SLOT(onLengthChanged(double))); + this, SLOT(onLengthChanged(double))); connect(ui->buttonRefAdd, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefAdd(bool))); + this, SLOT(onButtonRefAdd(bool))); connect(ui->buttonRefRemove, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefRemove(bool))); + this, SLOT(onButtonRefRemove(bool))); // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetReferences->addAction(action); + // if there is only one item, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } connect(action, SIGNAL(triggered()), this, SLOT(onRefDeleted())); ui->listWidgetReferences->setContextMenuPolicy(Qt::ActionsContextMenu); + + connect(ui->listWidgetReferences, SIGNAL(itemClicked(QListWidgetItem*)), + this, SLOT(setSelection(QListWidgetItem*))); + connect(ui->listWidgetReferences, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(doubleClicked(QListWidgetItem*))); } void TaskChamferParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { + // executed when the user selected something in the CAD object + // adds/deletes the selection accordingly + if (selectionMode == none) return; if (msg.Type == Gui::SelectionChanges::AddSelection) { if (referenceSelected(msg)) { - if (selectionMode == refAdd) + QAction *action = ui->listWidgetReferences->actions().at(0); // we have only one action + if (selectionMode == refAdd) { ui->listWidgetReferences->addItem(QString::fromStdString(msg.pSubName)); - else + // it might be the second one so we can enable the context menu + if (ui->listWidgetReferences->count() > 1) { + action->setEnabled(true); + action->setStatusTip(QString()); + ui->buttonRefRemove->setEnabled(true); + ui->buttonRefRemove->setToolTip(tr("Click button to enter selection mode,\nclick again to end selection")); + } + } + else { removeItemFromListWidget(ui->listWidgetReferences, msg.pSubName); - clearButtons(none); - exitSelectionMode(); + // remove its selection too + Gui::Selection().clearSelection(); + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + // we must also end the selection mode + exitSelectionMode(); + clearButtons(none); + } + } + // highlight existing references for possible further selections + DressUpView->highlightReferences(true); } } } @@ -117,14 +160,47 @@ void TaskChamferParameters::clearButtons(const selectionModes notThis) void TaskChamferParameters::onRefDeleted(void) { - PartDesign::Chamfer* pcChamfer = static_cast(DressUpView->getObject()); - App::DocumentObject* base = pcChamfer->Base.getValue(); - std::vector refs = pcChamfer->Base.getSubValues(); - refs.erase(refs.begin() + ui->listWidgetReferences->currentRow()); - setupTransaction(); - pcChamfer->Base.setValue(base, refs); - ui->listWidgetReferences->model()->removeRow(ui->listWidgetReferences->currentRow()); - pcChamfer->getDocument()->recomputeFeature(pcChamfer); + // assure we we are not in selection mode + exitSelectionMode(); + clearButtons(none); + // delete any selections since the reference(s) might be highlighted + Gui::Selection().clearSelection(); + DressUpView->highlightReferences(false); + + // get the list of items to be deleted + QList selectedList = ui->listWidgetReferences->selectedItems(); + + // if all items are selected, we must stop because one must be kept to avoid that the feature gets broken + if (selectedList.count() == ui->listWidgetReferences->model()->rowCount()) { + QMessageBox::warning(this, tr("Selection error"), tr("At least one item must be kept.")); + return; + } + + // delete the selection backwards to assure the list index keeps valid for the deletion + for (int i = selectedList.count() - 1; i > -1; i--) { + // get the fillet object + PartDesign::Chamfer* pcChamfer = static_cast(DressUpView->getObject()); + App::DocumentObject* base = pcChamfer->Base.getValue(); + // get all fillet references + std::vector refs = pcChamfer->Base.getSubValues(); + // the ref index is the same as the listWidgetReferences index + // so we can erase using the row number of the element to be deleted + int rowNumber = ui->listWidgetReferences->row(selectedList.at(i)); + refs.erase(refs.begin() + rowNumber); + setupTransaction(); + pcChamfer->Base.setValue(base, refs); + ui->listWidgetReferences->model()->removeRow(rowNumber); + pcChamfer->getDocument()->recomputeFeature(pcChamfer); + } + + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + QAction *action = ui->listWidgetReferences->actions().at(0); // we have only one action + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } } void TaskChamferParameters::onLengthChanged(double len) @@ -142,7 +218,9 @@ double TaskChamferParameters::getLength(void) const TaskChamferParameters::~TaskChamferParameters() { + Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); + delete ui; } diff --git a/src/Mod/PartDesign/Gui/TaskChamferParameters.ui b/src/Mod/PartDesign/Gui/TaskChamferParameters.ui index 29a603a8f207..f35e2ce402c3 100644 --- a/src/Mod/PartDesign/Gui/TaskChamferParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskChamferParameters.ui @@ -1,65 +1,81 @@ - - - PartDesignGui::TaskChamferParameters - - - - 0 - 0 - 182 - 185 - - - - Form - - - - - - - - Add ref - - - true - - - - - - - Remove ref - - - true - - - - - - - - - - - - - - - Size: - - - - - - - - Gui::QuantitySpinBox - QWidget -
Gui/QuantitySpinBox.h
-
-
- - -
+ + + PartDesignGui::TaskChamferParameters + + + + 0 + 0 + 182 + 185 + + + + Form + + + + + + + + Click button to enter selection mode, +click again to end selection + + + Add + + + true + + + + + + + Click button to enter selection mode, +click again to end selection + + + Remove + + + true + + + + + + + + + - select an item to highlight it +- double-click on an item to see the chamfers + + + QAbstractItemView::ExtendedSelection + + + + + + + + + + Size: + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + +
diff --git a/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp b/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp index 71f27eb6513a..c32af9e2aae1 100644 --- a/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskDraftParameters.cpp @@ -27,6 +27,8 @@ #ifndef _PreComp_ # include # include +# include +# include #endif #include "ui_TaskDraftParameters.h" @@ -51,7 +53,7 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskDraftParameters */ -TaskDraftParameters::TaskDraftParameters(ViewProviderDressUp *DressUpView,QWidget *parent) +TaskDraftParameters::TaskDraftParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, false, true, parent) { // we need a separate container widget to add all controls to @@ -85,25 +87,41 @@ TaskDraftParameters::TaskDraftParameters(ViewProviderDressUp *DressUpView,QWidge QMetaObject::connectSlotsByName(this); connect(ui->draftAngle, SIGNAL(valueChanged(double)), - this, SLOT(onAngleChanged(double))); + this, SLOT(onAngleChanged(double))); connect(ui->checkReverse, SIGNAL(toggled(bool)), - this, SLOT(onReversedChanged(bool))); + this, SLOT(onReversedChanged(bool))); connect(ui->buttonRefAdd, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefAdd(bool))); + this, SLOT(onButtonRefAdd(bool))); connect(ui->buttonRefRemove, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefRemove(bool))); + this, SLOT(onButtonRefRemove(bool))); connect(ui->buttonPlane, SIGNAL(toggled(bool)), - this, SLOT(onButtonPlane(bool))); + this, SLOT(onButtonPlane(bool))); connect(ui->buttonLine, SIGNAL(toggled(bool)), - this, SLOT(onButtonLine(bool))); + this, SLOT(onButtonLine(bool))); // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetReferences->addAction(action); + // if there is only one item, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } connect(action, SIGNAL(triggered()), this, SLOT(onRefDeleted())); ui->listWidgetReferences->setContextMenuPolicy(Qt::ActionsContextMenu); + connect(ui->listWidgetReferences, SIGNAL(itemClicked(QListWidgetItem*)), + this, SLOT(setSelection(QListWidgetItem*))); + connect(ui->listWidgetReferences, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(doubleClicked(QListWidgetItem*))); + App::DocumentObject* ref = pcDraft->NeutralPlane.getValue(); strings = pcDraft->NeutralPlane.getSubValues(); ui->linePlane->setText(getRefStr(ref, strings)); @@ -115,17 +133,42 @@ TaskDraftParameters::TaskDraftParameters(ViewProviderDressUp *DressUpView,QWidge void TaskDraftParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { + // executed when the user selected something in the CAD object + // adds/deletes the selection accordingly + if (selectionMode == none) return; if (msg.Type == Gui::SelectionChanges::AddSelection) { if (referenceSelected(msg)) { - if (selectionMode == refAdd) + QAction *action = ui->listWidgetReferences->actions().at(0); // we have only one action + if (selectionMode == refAdd) { ui->listWidgetReferences->addItem(QString::fromStdString(msg.pSubName)); - else + // it might be the second one so we can enable the context menu + if (ui->listWidgetReferences->count() > 1) { + action->setEnabled(true); + action->setStatusTip(QString()); + ui->buttonRefRemove->setEnabled(true); + ui->buttonRefRemove->setToolTip(tr("Click button to enter selection mode,\nclick again to end selection")); + } + } + else { removeItemFromListWidget(ui->listWidgetReferences, msg.pSubName); - clearButtons(none); - exitSelectionMode(); + // remove its selection too + Gui::Selection().clearSelection(); + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + // we must also end the selection mode + exitSelectionMode(); + clearButtons(none); + } + } + // highlight existing references for possible further selections + DressUpView->highlightReferences(true); } else if (selectionMode == plane) { PartDesign::Draft* pcDraft = static_cast(DressUpView->getObject()); std::vector planes; @@ -138,8 +181,8 @@ void TaskDraftParameters::onSelectionChanged(const Gui::SelectionChanges& msg) ui->linePlane->setText(getRefStr(selObj, planes)); pcDraft->getDocument()->recomputeFeature(pcDraft); - clearButtons(none); - exitSelectionMode(); + // highlight existing references for possible further selections + DressUpView->highlightReferences(true); } else if (selectionMode == line) { PartDesign::Draft* pcDraft = static_cast(DressUpView->getObject()); std::vector edges; @@ -152,8 +195,8 @@ void TaskDraftParameters::onSelectionChanged(const Gui::SelectionChanges& msg) ui->lineLine->setText(getRefStr(selObj, edges)); pcDraft->getDocument()->recomputeFeature(pcDraft); - clearButtons(none); - exitSelectionMode(); + // highlight existing references for possible further selections + DressUpView->highlightReferences(true); } } } @@ -191,14 +234,47 @@ void TaskDraftParameters::onButtonLine(bool checked) void TaskDraftParameters::onRefDeleted(void) { - PartDesign::Draft* pcDraft = static_cast(DressUpView->getObject()); - App::DocumentObject* base = pcDraft->Base.getValue(); - std::vector faces = pcDraft->Base.getSubValues(); - faces.erase(faces.begin() + ui->listWidgetReferences->currentRow()); - setupTransaction(); - pcDraft->Base.setValue(base, faces); - ui->listWidgetReferences->model()->removeRow(ui->listWidgetReferences->currentRow()); - pcDraft->getDocument()->recomputeFeature(pcDraft); + // assure we we are not in selection mode + exitSelectionMode(); + clearButtons(none); + // delete any selections since the reference(s) might be highlighted + Gui::Selection().clearSelection(); + DressUpView->highlightReferences(false); + + // get the list of items to be deleted + QList selectedList = ui->listWidgetReferences->selectedItems(); + + // if all items are selected, we must stop because one must be kept to avoid that the feature gets broken + if (selectedList.count() == ui->listWidgetReferences->model()->rowCount()) { + QMessageBox::warning(this, tr("Selection error"), tr("At least one item must be kept.")); + return; + } + + // delete the selection backwards to assure the list index keeps valid for the deletion + for (int i = selectedList.count() - 1; i > -1; i--) { + // get the fillet object + PartDesign::Draft* pcDraft = static_cast(DressUpView->getObject()); + App::DocumentObject* base = pcDraft->Base.getValue(); + // get all fillet references + std::vector refs = pcDraft->Base.getSubValues(); + // the ref index is the same as the listWidgetReferences index + // so we can erase using the row number of the element to be deleted + int rowNumber = ui->listWidgetReferences->row(selectedList.at(i)); + refs.erase(refs.begin() + rowNumber); + setupTransaction(); + pcDraft->Base.setValue(base, refs); + ui->listWidgetReferences->model()->removeRow(rowNumber); + pcDraft->getDocument()->recomputeFeature(pcDraft); + } + + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + QAction *action = ui->listWidgetReferences->actions().at(0); // we have only one action + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } } void TaskDraftParameters::getPlane(App::DocumentObject*& obj, std::vector& sub) const @@ -248,7 +324,9 @@ bool TaskDraftParameters::getReversed(void) const TaskDraftParameters::~TaskDraftParameters() { + Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); + delete ui; } diff --git a/src/Mod/PartDesign/Gui/TaskDraftParameters.ui b/src/Mod/PartDesign/Gui/TaskDraftParameters.ui index 32b5fa4accd7..9bb7cc073c37 100644 --- a/src/Mod/PartDesign/Gui/TaskDraftParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskDraftParameters.ui @@ -1,128 +1,144 @@ - - - PartDesignGui::TaskDraftParameters - - - - 0 - 0 - 257 - 285 - - - - Form - - - - - - - - Add face - - - true - - - - - - - Remove face - - - true - - - - - - - - - - - - - - Draft angle - - - - - - - deg - - - 0.000000000000000 - - - 89.999999999999986 - - - 0.100000000000000 - - - 1.500000000000000 - - - - - - - - - - - Neutral plane - - - true - - - - - - - - - - - - - - Pull direction - - - true - - - - - - - - - - - - Reverse pull direction - - - - - checkReverse - listWidgetReferences - - - - Gui::QuantitySpinBox - QWidget -
Gui/QuantitySpinBox.h
-
-
- - -
+ + + PartDesignGui::TaskDraftParameters + + + + 0 + 0 + 257 + 285 + + + + Form + + + + + + + + Click button to enter selection mode, +click again to end selection + + + Add face + + + true + + + + + + + Click button to enter selection mode, +click again to end selection + + + Remove face + + + true + + + + + + + + + - select an item to highlight it +- double-click on an item to see the drafts + + + QAbstractItemView::ExtendedSelection + + + + + + + + + Draft angle + + + + + + + deg + + + 0.000000000000000 + + + 89.999999999999986 + + + 0.100000000000000 + + + 1.500000000000000 + + + + + + + + + + + Neutral plane + + + true + + + + + + + + + + + + + + Pull direction + + + true + + + + + + + + + + + + Reverse pull direction + + + + + checkReverse + listWidgetReferences + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + +
diff --git a/src/Mod/PartDesign/Gui/TaskDressUpParameters.cpp b/src/Mod/PartDesign/Gui/TaskDressUpParameters.cpp index 761e4ec35789..c821edea854b 100644 --- a/src/Mod/PartDesign/Gui/TaskDressUpParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskDressUpParameters.cpp @@ -25,7 +25,10 @@ #include "PreCompiled.h" #ifndef _PreComp_ +# include +# include # include +# include #endif #include @@ -41,6 +44,7 @@ #include #include #include +#include #include #include @@ -131,6 +135,9 @@ void TaskDressUpParameters::onButtonRefAdd(bool checked) Gui::Selection().clearSelection(); Gui::Selection().addSelectionGate(new ReferenceSelection(this->getBase(), allowEdges, allowFaces, false)); DressUpView->highlightReferences(true); + } else { + exitSelectionMode(); + DressUpView->highlightReferences(false); } } @@ -144,6 +151,64 @@ void TaskDressUpParameters::onButtonRefRemove(const bool checked) Gui::Selection().addSelectionGate(new ReferenceSelection(this->getBase(), allowEdges, allowFaces, false)); DressUpView->highlightReferences(true); } + else { + exitSelectionMode(); + DressUpView->highlightReferences(false); + } +} + +void TaskDressUpParameters::doubleClicked(QListWidgetItem* item) { + // executed when the user double-clicks on any item in the list + // shows the fillets as they are -> useful to switch out of selection mode + + Q_UNUSED(item) + wasDoubleClicked = true; + + // assure we are not in selection mode + exitSelectionMode(); + clearButtons(none); + + // assure the fillets are shown + showObject(); + // remove any highlights andd selections + DressUpView->highlightReferences(false); + Gui::Selection().clearSelection(); + + // enable next possible single-click event after double-click time passed + QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(itemClickedTimeout())); +} + +void TaskDressUpParameters::setSelection(QListWidgetItem* current) { + // executed when the user selected an item in the list (but double-clicked it) + // highlights the currently selected item + + if (!wasDoubleClicked) { + // we treat it as single-click event once the QApplication double-click time is passed + QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(itemClickedTimeout())); + + // name of the item + std::string subName = current->text().toStdString(); + // get the document name + std::string docName = DressUpView->getObject()->getDocument()->getName(); + // get the name of the body we are in + Part::BodyBase* body = PartDesign::Body::findBodyOf(DressUpView->getObject()); + std::string objName = body->getNameInDocument(); + + // hide fillet to see the original edge + // (a fillet creates new edges so that the original one is not available) + hideObject(); + // highlight all objects in the list + DressUpView->highlightReferences(true); + // clear existing selection because only the current item is highlighted, not all selected ones to keep the overview + Gui::Selection().clearSelection(); + // highligh the selected item + Gui::Selection().addSelection(docName.c_str(), objName.c_str(), subName.c_str(), 0, 0, 0); + } +} + +void TaskDressUpParameters::itemClickedTimeout() { + // executed after double-click time passed + wasDoubleClicked = false; } const std::vector TaskDressUpParameters::getReferences() const @@ -176,10 +241,11 @@ void TaskDressUpParameters::hideObject() void TaskDressUpParameters::showObject() { - DressUpView->getObject()->Visibility.setValue(true); App::DocumentObject* base = getBase(); - if (base) + if (base) { + DressUpView->getObject()->Visibility.setValue(true); base->Visibility.setValue(false); + } } Part::Feature* TaskDressUpParameters::getBase(void) const diff --git a/src/Mod/PartDesign/Gui/TaskDressUpParameters.h b/src/Mod/PartDesign/Gui/TaskDressUpParameters.h index aaecc506d900..0f8696218bb9 100644 --- a/src/Mod/PartDesign/Gui/TaskDressUpParameters.h +++ b/src/Mod/PartDesign/Gui/TaskDressUpParameters.h @@ -32,6 +32,7 @@ #include "ViewProviderDressUp.h" class QListWidget; +class QListWidgetItem; namespace Part { class Feature; @@ -60,11 +61,15 @@ class TaskDressUpParameters : public Gui::TaskView::TaskBox, public Gui::Selecti protected Q_SLOTS: void onButtonRefAdd(const bool checked); void onButtonRefRemove(const bool checked); - virtual void onRefDeleted(void)=0; + void doubleClicked(QListWidgetItem* item); + void setSelection(QListWidgetItem* current); + void itemClickedTimeout(); + virtual void onRefDeleted(void) = 0; protected: void exitSelectionMode(); bool referenceSelected(const Gui::SelectionChanges& msg); + bool wasDoubleClicked = false; protected: enum selectionModes { none, refAdd, refRemove, plane, line }; diff --git a/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp b/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp index 100265b80eac..93da8b44a0aa 100644 --- a/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskFilletParameters.cpp @@ -25,6 +25,9 @@ #ifndef _PreComp_ # include +# include +# include +# include #endif #include "ui_TaskFilletParameters.h" @@ -49,7 +52,7 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskFilletParameters */ -TaskFilletParameters::TaskFilletParameters(ViewProviderDressUp *DressUpView,QWidget *parent) +TaskFilletParameters::TaskFilletParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, true, true, parent) { // we need a separate container widget to add all controls to @@ -77,33 +80,73 @@ TaskFilletParameters::TaskFilletParameters(ViewProviderDressUp *DressUpView,QWid QMetaObject::connectSlotsByName(this); connect(ui->filletRadius, SIGNAL(valueChanged(double)), - this, SLOT(onLengthChanged(double))); + this, SLOT(onLengthChanged(double))); connect(ui->buttonRefAdd, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefAdd(bool))); + this, SLOT(onButtonRefAdd(bool))); connect(ui->buttonRefRemove, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefRemove(bool))); + this, SLOT(onButtonRefRemove(bool))); // Create context menu - QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); - ui->listWidgetReferences->addAction(action); - connect(action, SIGNAL(triggered()), this, SLOT(onRefDeleted())); + deleteAction = new QAction(tr("Remove"), this); + deleteAction->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + deleteAction->setShortcutVisibleInContextMenu(true); +#endif + ui->listWidgetReferences->addAction(deleteAction); + // if there is only one item, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + deleteAction->setEnabled(false); + deleteAction->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } + connect(deleteAction, SIGNAL(triggered()), this, SLOT(onRefDeleted())); ui->listWidgetReferences->setContextMenuPolicy(Qt::ActionsContextMenu); + + connect(ui->listWidgetReferences, SIGNAL(itemClicked(QListWidgetItem*)), + this, SLOT(setSelection(QListWidgetItem*))); + connect(ui->listWidgetReferences, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(doubleClicked(QListWidgetItem*))); } void TaskFilletParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { + // executed when the user selected something in the CAD object + // adds/deletes the selection accordingly + if (selectionMode == none) return; if (msg.Type == Gui::SelectionChanges::AddSelection) { if (referenceSelected(msg)) { - if (selectionMode == refAdd) + if (selectionMode == refAdd) { ui->listWidgetReferences->addItem(QString::fromStdString(msg.pSubName)); - else + // it might be the second one so we can enable the context menu + if (ui->listWidgetReferences->count() > 1) { + deleteAction->setEnabled(true); + deleteAction->setStatusTip(QString()); + ui->buttonRefRemove->setEnabled(true); + ui->buttonRefRemove->setToolTip(tr("Click button to enter selection mode,\nclick again to end selection")); + } + } + else { removeItemFromListWidget(ui->listWidgetReferences, msg.pSubName); - clearButtons(none); - exitSelectionMode(); + // remove its selection too + Gui::Selection().clearSelection(); + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + deleteAction->setEnabled(false); + deleteAction->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + // we must also end the selection mode + exitSelectionMode(); + clearButtons(none); + } + } + // highlight existing references for possible further selections + DressUpView->highlightReferences(true); } } } @@ -117,14 +160,46 @@ void TaskFilletParameters::clearButtons(const selectionModes notThis) void TaskFilletParameters::onRefDeleted(void) { - PartDesign::Fillet* pcFillet = static_cast(DressUpView->getObject()); - App::DocumentObject* base = pcFillet->Base.getValue(); - std::vector refs = pcFillet->Base.getSubValues(); - refs.erase(refs.begin() + ui->listWidgetReferences->currentRow()); - setupTransaction(); - pcFillet->Base.setValue(base, refs); - ui->listWidgetReferences->model()->removeRow(ui->listWidgetReferences->currentRow()); - pcFillet->getDocument()->recomputeFeature(pcFillet); + // assure we we are not in selection mode + exitSelectionMode(); + clearButtons(none); + // delete any selections since the reference(s) might be highlighted + Gui::Selection().clearSelection(); + DressUpView->highlightReferences(false); + + // get the list of items to be deleted + QList selectedList = ui->listWidgetReferences->selectedItems(); + + // if all items are selected, we must stop because one must be kept to avoid that the feature gets broken + if (selectedList.count() == ui->listWidgetReferences->model()->rowCount()){ + QMessageBox::warning(this, tr("Selection error"), tr("At least one item must be kept.")); + return; + } + + // delete the selection backwards to assure the list index keeps valid for the deletion + for (int i = selectedList.count()-1; i > -1; i--) { + // get the fillet object + PartDesign::Fillet* pcFillet = static_cast(DressUpView->getObject()); + App::DocumentObject* base = pcFillet->Base.getValue(); + // get all fillet references + std::vector refs = pcFillet->Base.getSubValues(); + // the ref index is the same as the listWidgetReferences index + // so we can erase using the row number of the element to be deleted + int rowNumber = ui->listWidgetReferences->row(selectedList.at(i)); + refs.erase(refs.begin() + rowNumber); + setupTransaction(); + pcFillet->Base.setValue(base, refs); + ui->listWidgetReferences->model()->removeRow(rowNumber); + pcFillet->getDocument()->recomputeFeature(pcFillet); + } + + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + deleteAction->setEnabled(false); + deleteAction->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } } void TaskFilletParameters::onLengthChanged(double len) @@ -143,10 +218,35 @@ double TaskFilletParameters::getLength(void) const TaskFilletParameters::~TaskFilletParameters() { + Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); + delete ui; } +bool TaskFilletParameters::event(QEvent *e) +{ + if (e && e->type() == QEvent::ShortcutOverride) { + QKeyEvent * kevent = static_cast(e); + if (kevent->modifiers() == Qt::NoModifier) { + if (kevent->key() == Qt::Key_Delete) { + kevent->accept(); + return true; + } + } + } + else if (e && e->type() == QEvent::KeyPress) { + QKeyEvent * kevent = static_cast(e); + if (kevent->key() == Qt::Key_Delete) { + if (deleteAction->isEnabled()) + deleteAction->trigger(); + return true; + } + } + + return TaskDressUpParameters::event(e); +} + void TaskFilletParameters::changeEvent(QEvent *e) { TaskBox::changeEvent(e); diff --git a/src/Mod/PartDesign/Gui/TaskFilletParameters.h b/src/Mod/PartDesign/Gui/TaskFilletParameters.h index 2b6966cf4711..0865bb6dd259 100644 --- a/src/Mod/PartDesign/Gui/TaskFilletParameters.h +++ b/src/Mod/PartDesign/Gui/TaskFilletParameters.h @@ -48,10 +48,12 @@ private Q_SLOTS: protected: double getLength(void) const; virtual void clearButtons(const selectionModes notThis); + bool event(QEvent *e); void changeEvent(QEvent *e); virtual void onSelectionChanged(const Gui::SelectionChanges& msg); private: + QAction* deleteAction; Ui_TaskFilletParameters* ui; }; diff --git a/src/Mod/PartDesign/Gui/TaskFilletParameters.ui b/src/Mod/PartDesign/Gui/TaskFilletParameters.ui index b6803af459b8..e8018c3a0827 100644 --- a/src/Mod/PartDesign/Gui/TaskFilletParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskFilletParameters.ui @@ -1,69 +1,85 @@ - - - PartDesignGui::TaskFilletParameters - - - - 0 - 0 - 208 - 164 - - - - Form - - - - - - - - Add ref - - - true - - - - - - - Remove ref - - - true - - - - - - - - - - - - - - Radius: - - - - - - - - - - - - - Gui::QuantitySpinBox - QWidget -
Gui/QuantitySpinBox.h
-
-
- - -
+ + + PartDesignGui::TaskFilletParameters + + + + 0 + 0 + 208 + 164 + + + + Form + + + + + + + + Click button to enter selection mode, +click again to end selection + + + Add + + + true + + + + + + + Click button to enter selection mode, +click again to end selection + + + Remove + + + true + + + + + + + + + - select an item to highlight it +- double-click on an item to see the fillets + + + QAbstractItemView::ExtendedSelection + + + + + + + + + Radius: + + + + + + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + +
diff --git a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp index 2771f83a8dd8..2ac64e9a78d4 100644 --- a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.cpp @@ -113,7 +113,11 @@ void TaskLinearPatternParameters::setupUI() // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetFeatures->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onFeatureDeleted())); ui->listWidgetFeatures->setContextMenuPolicy(Qt::ActionsContextMenu); @@ -227,12 +231,30 @@ void TaskLinearPatternParameters::kickUpdateViewTimer() const updateViewTimer->start(); } +void TaskLinearPatternParameters::addObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + QString objectName = QString::fromLatin1(obj->getNameInDocument()); + + QListWidgetItem* item = new QListWidgetItem(); + item->setText(label); + item->setData(Qt::UserRole, objectName); + ui->listWidgetFeatures->addItem(item); +} + +void TaskLinearPatternParameters::removeObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + removeItemFromListWidget(ui->listWidgetFeatures, label); +} + void TaskLinearPatternParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { - if (selectionMode!=none && msg.Type == Gui::SelectionChanges::AddSelection) { + if (selectionMode != none && msg.Type == Gui::SelectionChanges::AddSelection) { if (originalSelected(msg)) { exitSelectionMode(); - } else { + } + else if (selectionMode == reference) { // TODO check if this works correctly (2015-09-01, Fat-Zer) exitSelectionMode(); std::vector directions; diff --git a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h index b717a797360a..b492c965cd83 100644 --- a/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h +++ b/src/Mod/PartDesign/Gui/TaskLinearPatternParameters.h @@ -69,6 +69,8 @@ private Q_SLOTS: virtual void onFeatureDeleted(void); protected: + virtual void addObject(App::DocumentObject*); + virtual void removeObject(App::DocumentObject*); virtual void changeEvent(QEvent *e); virtual void onSelectionChanged(const Gui::SelectionChanges& msg); virtual void clearButtons(); diff --git a/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp b/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp index 3a5052252746..9b8f16498f33 100644 --- a/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskMirroredParameters.cpp @@ -110,7 +110,11 @@ void TaskMirroredParameters::setupUI() // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetFeatures->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onFeatureDeleted())); ui->listWidgetFeatures->setContextMenuPolicy(Qt::ActionsContextMenu); @@ -180,6 +184,23 @@ void TaskMirroredParameters::updateUI() blockUpdate = false; } +void TaskMirroredParameters::addObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + QString objectName = QString::fromLatin1(obj->getNameInDocument()); + + QListWidgetItem* item = new QListWidgetItem(); + item->setText(label); + item->setData(Qt::UserRole, objectName); + ui->listWidgetFeatures->addItem(item); +} + +void TaskMirroredParameters::removeObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + removeItemFromListWidget(ui->listWidgetFeatures, label); +} + void TaskMirroredParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { if (selectionMode!=none && msg.Type == Gui::SelectionChanges::AddSelection) { diff --git a/src/Mod/PartDesign/Gui/TaskMirroredParameters.h b/src/Mod/PartDesign/Gui/TaskMirroredParameters.h index afc66cabde64..906c0d45164e 100644 --- a/src/Mod/PartDesign/Gui/TaskMirroredParameters.h +++ b/src/Mod/PartDesign/Gui/TaskMirroredParameters.h @@ -67,6 +67,8 @@ private Q_SLOTS: virtual void onFeatureDeleted(void); protected: + virtual void addObject(App::DocumentObject*); + virtual void removeObject(App::DocumentObject*); virtual void changeEvent(QEvent *e); virtual void onSelectionChanged(const Gui::SelectionChanges& msg); virtual void clearButtons(); diff --git a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp index 96007d4c967e..d13d8a1d07c0 100644 --- a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.cpp @@ -75,7 +75,11 @@ TaskMultiTransformParameters::TaskMultiTransformParameters(ViewProviderTransform // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetFeatures->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onFeatureDeleted())); ui->listWidgetFeatures->setContextMenuPolicy(Qt::ActionsContextMenu); @@ -156,6 +160,23 @@ TaskMultiTransformParameters::TaskMultiTransformParameters(ViewProviderTransform // --------------------- } +void TaskMultiTransformParameters::addObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + QString objectName = QString::fromLatin1(obj->getNameInDocument()); + + QListWidgetItem* item = new QListWidgetItem(); + item->setText(label); + item->setData(Qt::UserRole, objectName); + ui->listWidgetFeatures->addItem(item); +} + +void TaskMultiTransformParameters::removeObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + removeItemFromListWidget(ui->listWidgetFeatures, label); +} + void TaskMultiTransformParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { if (originalSelected(msg)) { diff --git a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h index b86d24e9a24d..f309247fb109 100644 --- a/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h +++ b/src/Mod/PartDesign/Gui/TaskMultiTransformParameters.h @@ -87,6 +87,8 @@ private Q_SLOTS: virtual void slotDeletedObject(const Gui::ViewProviderDocumentObject& Obj); protected: + virtual void addObject(App::DocumentObject*); + virtual void removeObject(App::DocumentObject*); virtual void changeEvent(QEvent *e); virtual void onSelectionChanged(const Gui::SelectionChanges& msg); virtual void clearButtons(); diff --git a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp index bf922556f33c..7cd65f43e2e1 100644 --- a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.cpp @@ -111,7 +111,11 @@ void TaskPolarPatternParameters::setupUI() // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetFeatures->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onFeatureDeleted())); ui->listWidgetFeatures->setContextMenuPolicy(Qt::ActionsContextMenu); @@ -222,6 +226,23 @@ void TaskPolarPatternParameters::kickUpdateViewTimer() const updateViewTimer->start(); } +void TaskPolarPatternParameters::addObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + QString objectName = QString::fromLatin1(obj->getNameInDocument()); + + QListWidgetItem* item = new QListWidgetItem(); + item->setText(label); + item->setData(Qt::UserRole, objectName); + ui->listWidgetFeatures->addItem(item); +} + +void TaskPolarPatternParameters::removeObject(App::DocumentObject* obj) +{ + QString label = QString::fromUtf8(obj->Label.getValue()); + removeItemFromListWidget(ui->listWidgetFeatures, label); +} + void TaskPolarPatternParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { if (selectionMode!=none && msg.Type == Gui::SelectionChanges::AddSelection) { diff --git a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h index 63c519cb8eda..55bda7e23a2c 100644 --- a/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h +++ b/src/Mod/PartDesign/Gui/TaskPolarPatternParameters.h @@ -68,6 +68,8 @@ private Q_SLOTS: virtual void onFeatureDeleted(void); protected: + virtual void addObject(App::DocumentObject*); + virtual void removeObject(App::DocumentObject*); virtual void changeEvent(QEvent *e); virtual void onSelectionChanged(const Gui::SelectionChanges& msg); virtual void clearButtons(); diff --git a/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp b/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp index c91295397baa..d0f39c5d3734 100644 --- a/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskScaledParameters.cpp @@ -97,7 +97,11 @@ void TaskScaledParameters::setupUI() // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetFeatures->addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(onFeatureDeleted())); ui->listWidgetFeatures->setContextMenuPolicy(Qt::ActionsContextMenu); diff --git a/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp b/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp index 02bb13e86a3a..496a64e54edd 100644 --- a/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskThicknessParameters.cpp @@ -25,6 +25,8 @@ #ifndef _PreComp_ # include +# include +# include #endif #include "ui_TaskThicknessParameters.h" @@ -49,7 +51,7 @@ using namespace Gui; /* TRANSLATOR PartDesignGui::TaskThicknessParameters */ -TaskThicknessParameters::TaskThicknessParameters(ViewProviderDressUp *DressUpView,QWidget *parent) +TaskThicknessParameters::TaskThicknessParameters(ViewProviderDressUp *DressUpView, QWidget *parent) : TaskDressUpParameters(DressUpView, false, true, parent) { // we need a separate container widget to add all controls to @@ -85,27 +87,43 @@ TaskThicknessParameters::TaskThicknessParameters(ViewProviderDressUp *DressUpVie QMetaObject::connectSlotsByName(this); connect(ui->Value, SIGNAL(valueChanged(double)), - this, SLOT(onValueChanged(double))); + this, SLOT(onValueChanged(double))); connect(ui->checkReverse, SIGNAL(toggled(bool)), - this, SLOT(onReversedChanged(bool))); + this, SLOT(onReversedChanged(bool))); connect(ui->checkIntersection, SIGNAL(toggled(bool)), - this, SLOT(onIntersectionChanged(bool))); + this, SLOT(onIntersectionChanged(bool))); connect(ui->buttonRefAdd, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefAdd(bool))); + this, SLOT(onButtonRefAdd(bool))); connect(ui->buttonRefRemove, SIGNAL(toggled(bool)), - this, SLOT(onButtonRefRemove(bool))); + this, SLOT(onButtonRefRemove(bool))); connect(ui->modeComboBox, SIGNAL(currentIndexChanged(int)), - this, SLOT(onModeChanged(int))); + this, SLOT(onModeChanged(int))); connect(ui->joinComboBox, SIGNAL(currentIndexChanged(int)), - this, SLOT(onJoinTypeChanged(int))); + this, SLOT(onJoinTypeChanged(int))); // Create context menu QAction* action = new QAction(tr("Remove"), this); - action->setShortcut(QString::fromLatin1("Del")); + action->setShortcut(QKeySequence::Delete); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // display shortcut behind the context menu entry + action->setShortcutVisibleInContextMenu(true); +#endif ui->listWidgetReferences->addAction(action); + // if there is only one item, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } connect(action, SIGNAL(triggered()), this, SLOT(onRefDeleted())); ui->listWidgetReferences->setContextMenuPolicy(Qt::ActionsContextMenu); + connect(ui->listWidgetReferences, SIGNAL(itemClicked(QListWidgetItem*)), + this, SLOT(setSelection(QListWidgetItem*))); + connect(ui->listWidgetReferences, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(doubleClicked(QListWidgetItem*))); + int mode = pcThickness->Mode.getValue(); ui->modeComboBox->setCurrentIndex(mode); @@ -115,17 +133,42 @@ TaskThicknessParameters::TaskThicknessParameters(ViewProviderDressUp *DressUpVie void TaskThicknessParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { + // executed when the user selected something in the CAD object + // adds/deletes the selection accordingly + if (selectionMode == none) return; if (msg.Type == Gui::SelectionChanges::AddSelection) { if (referenceSelected(msg)) { - if (selectionMode == refAdd) + QAction *action = ui->listWidgetReferences->actions().at(0); // we have only one action + if (selectionMode == refAdd) { ui->listWidgetReferences->addItem(QString::fromStdString(msg.pSubName)); - else + // it might be the second one so we can enable the context menu + if (ui->listWidgetReferences->count() > 1) { + action->setEnabled(true); + action->setStatusTip(QString()); + ui->buttonRefRemove->setEnabled(true); + ui->buttonRefRemove->setToolTip(tr("Click button to enter selection mode,\nclick again to end selection")); + } + } + else { removeItemFromListWidget(ui->listWidgetReferences, msg.pSubName); - clearButtons(none); - exitSelectionMode(); + // remove its selection too + Gui::Selection().clearSelection(); + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + // we must also end the selection mode + exitSelectionMode(); + clearButtons(none); + } + } + // highlight existing references for possible further selections + DressUpView->highlightReferences(true); } } } @@ -139,16 +182,47 @@ void TaskThicknessParameters::clearButtons(const selectionModes notThis) void TaskThicknessParameters::onRefDeleted(void) { - PartDesign::Thickness* pcThickness = static_cast(DressUpView->getObject()); - App::DocumentObject* base = pcThickness->Base.getValue(); - std::vector faces = pcThickness->Base.getSubValues(); - faces.erase(faces.begin() + ui->listWidgetReferences->currentRow()); - setupTransaction(); - pcThickness->Base.setValue(base, faces); - ui->listWidgetReferences->model()->removeRow(ui->listWidgetReferences->currentRow()); - pcThickness->getDocument()->recomputeFeature(pcThickness); - clearButtons(none); + // assure we we are not in selection mode exitSelectionMode(); + clearButtons(none); + // delete any selections since the reference(s) might be highlighted + Gui::Selection().clearSelection(); + DressUpView->highlightReferences(false); + + // get the list of items to be deleted + QList selectedList = ui->listWidgetReferences->selectedItems(); + + // if all items are selected, we must stop because one must be kept to avoid that the feature gets broken + if (selectedList.count() == ui->listWidgetReferences->model()->rowCount()) { + QMessageBox::warning(this, tr("Selection error"), tr("At least one item must be kept.")); + return; + } + + // delete the selection backwards to assure the list index keeps valid for the deletion + for (int i = selectedList.count() - 1; i > -1; i--) { + // get the fillet object + PartDesign::Thickness* pcThickness = static_cast(DressUpView->getObject()); + App::DocumentObject* base = pcThickness->Base.getValue(); + // get all fillet references + std::vector refs = pcThickness->Base.getSubValues(); + // the ref index is the same as the listWidgetReferences index + // so we can erase using the row number of the element to be deleted + int rowNumber = ui->listWidgetReferences->row(selectedList.at(i)); + refs.erase(refs.begin() + rowNumber); + setupTransaction(); + pcThickness->Base.setValue(base, refs); + ui->listWidgetReferences->model()->removeRow(rowNumber); + pcThickness->getDocument()->recomputeFeature(pcThickness); + } + + // if there is only one item left, it cannot be deleted + if (ui->listWidgetReferences->count() == 1) { + QAction *action = ui->listWidgetReferences->actions().at(0); // we have only one action + action->setEnabled(false); + action->setStatusTip(tr("There must be at least one item")); + ui->buttonRefRemove->setEnabled(false); + ui->buttonRefRemove->setToolTip(tr("There must be at least one item")); + } } void TaskThicknessParameters::onValueChanged(double angle) @@ -222,7 +296,9 @@ int TaskThicknessParameters::getMode(void) const { TaskThicknessParameters::~TaskThicknessParameters() { + Gui::Selection().clearSelection(); Gui::Selection().rmvSelectionGate(); + delete ui; } diff --git a/src/Mod/PartDesign/Gui/TaskThicknessParameters.ui b/src/Mod/PartDesign/Gui/TaskThicknessParameters.ui index e4812e09f8e1..ec49cc53a5df 100644 --- a/src/Mod/PartDesign/Gui/TaskThicknessParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskThicknessParameters.ui @@ -1,159 +1,175 @@ - - - PartDesignGui::TaskThicknessParameters - - - - 0 - 0 - 321 - 509 - - - - Form - - - - - - - - Add face - - - true - - - - - - - Remove face - - - true - - - - - - - - - - - - - - Thickness - - - - - - - Qt::TabFocus - - - mm - - - 0.000000000000000 - - - 999999999.000000000000000 - - - 0.100000000000000 - - - 1.000000000000000 - - - - - - - Mode - - - - - - - Join Type - - - - - - - - Skin - - - - - Pipe - - - - - Recto Verso - - - - - - - - - Arc - - - - - Intersection - - - - - - - - - - Intersection - - - - - - - Make thickness inwards - - - - - - - - Gui::QuantitySpinBox - QWidget -
Gui/QuantitySpinBox.h
-
-
- - Value - modeComboBox - joinComboBox - checkIntersection - checkReverse - buttonRefAdd - buttonRefRemove - listWidgetReferences - - - -
+ + + PartDesignGui::TaskThicknessParameters + + + + 0 + 0 + 321 + 509 + + + + Form + + + + + + + + Click button to enter selection mode, +click again to end selection + + + Add face + + + true + + + + + + + Click button to enter selection mode, +click again to end selection + + + Remove face + + + true + + + + + + + + + - select an item to highlight it +- double-click on an item to see the features + + + QAbstractItemView::MultiSelection + + + + + + + + + Thickness + + + + + + + Qt::TabFocus + + + mm + + + 0.000000000000000 + + + 999999999.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + Mode + + + + + + + Join Type + + + + + + + + Skin + + + + + Pipe + + + + + Recto Verso + + + + + + + + + Arc + + + + + Intersection + + + + + + + + + + Intersection + + + + + + + Make thickness inwards + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + Value + modeComboBox + joinComboBox + checkIntersection + checkReverse + buttonRefAdd + buttonRefRemove + listWidgetReferences + + + +
diff --git a/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp b/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp index 3ab316d3495f..a5e413ae51fd 100644 --- a/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskTransformedParameters.cpp @@ -116,6 +116,14 @@ int TaskTransformedParameters::getUpdateViewTimeout() const return 500; } +void TaskTransformedParameters::addObject(App::DocumentObject*) +{ +} + +void TaskTransformedParameters::removeObject(App::DocumentObject*) +{ +} + bool TaskTransformedParameters::originalSelected(const Gui::SelectionChanges& msg) { if (msg.Type == Gui::SelectionChanges::AddSelection && ( @@ -132,15 +140,21 @@ bool TaskTransformedParameters::originalSelected(const Gui::SelectionChanges& ms std::vector originals = pcTransformed->Originals.getValues(); std::vector::iterator o = std::find(originals.begin(), originals.end(), selectedObject); if (selectionMode == addFeature) { - if (o == originals.end()) + if (o == originals.end()) { originals.push_back(selectedObject); - else + addObject(selectedObject); + } + else { return false; // duplicate selection + } } else { - if (o != originals.end()) + if (o != originals.end()) { originals.erase(o); - else + removeObject(selectedObject); + } + else { return false; + } } setupTransaction(); pcTransformed->Originals.setValues(originals); diff --git a/src/Mod/PartDesign/Gui/TaskTransformedParameters.h b/src/Mod/PartDesign/Gui/TaskTransformedParameters.h index 9d9b35eb83ce..c93fa0503427 100644 --- a/src/Mod/PartDesign/Gui/TaskTransformedParameters.h +++ b/src/Mod/PartDesign/Gui/TaskTransformedParameters.h @@ -190,6 +190,8 @@ protected Q_SLOTS: void checkVisibility(); protected: + virtual void addObject(App::DocumentObject*); + virtual void removeObject(App::DocumentObject*); /** Notifies when the object is about to be removed. */ virtual void slotDeletedObject(const Gui::ViewProviderDocumentObject& Obj); virtual void changeEvent(QEvent *e) = 0; diff --git a/src/Mod/Path/Gui/DlgSettingsPathColor.ui b/src/Mod/Path/Gui/DlgSettingsPathColor.ui index 7477b0932c3b..fe18cf2843c4 100644 --- a/src/Mod/Path/Gui/DlgSettingsPathColor.ui +++ b/src/Mod/Path/Gui/DlgSettingsPathColor.ui @@ -406,10 +406,10 @@ - DefaultTaskPanelLayout + DefaultTaskPanelLayout - Mod/Path + Mod/Path diff --git a/src/Mod/Path/Gui/ViewProviderPath.cpp b/src/Mod/Path/Gui/ViewProviderPath.cpp index abcb92a99a4b..1b36e7f2f39a 100644 --- a/src/Mod/Path/Gui/ViewProviderPath.cpp +++ b/src/Mod/Path/Gui/ViewProviderPath.cpp @@ -97,22 +97,26 @@ class PathSelectionObserver: public Gui::SelectionObserver { setArrow(); return; } - if((msg.Type!=Gui::SelectionChanges::SetPreselect + if(msg.Type!=Gui::SelectionChanges::SetPreselect && msg.Type!=Gui::SelectionChanges::MovePreselect) - || !msg.pOriginalMsg || !msg.pSubObject || !msg.pParentObject) + return; + auto obj = msg.Object.getObject(); + if(!obj) + return; + Base::Matrix4D mat; + auto sobj = obj->getSubObject(msg.pSubName,0,&mat); + if(!sobj) return; Base::Matrix4D linkMat; - auto sobj = msg.pSubObject->getLinkedObject(true,&linkMat,false); + auto linked = sobj->getLinkedObject(true,&linkMat,false); auto vp = Base::freecad_dynamic_cast( - Application::Instance->getViewProvider(sobj)); + Application::Instance->getViewProvider(linked)); if(!vp) { setArrow(); return; } if(vp->pt0Index >= 0) { - Base::Matrix4D mat; - msg.pParentObject->getSubObject(msg.pSubName,0,&mat); mat *= linkMat; mat.inverse(); Base::Vector3d pt = mat*Base::Vector3d(msg.x,msg.y,msg.z); diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index 95e36c395ad8..1a7a4f6f536a 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -1,958 +1,904 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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 * -# * * -# *************************************************************************** - -import FreeCAD -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathOp as PathOp -import PathScripts.PathUtils as PathUtils -import PathScripts.PathGeom as PathGeom -import Draft -import math - -# from PathScripts.PathUtils import waiting_effects -from PySide import QtCore -if FreeCAD.GuiUp: - import FreeCADGui - - -__title__ = "Base class for PathArea based operations." -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Base class and properties for Path.Area based operations." -__contributors__ = "russ4262 (Russell Johnson)" -__createdDate__ = "2017" -__scriptVersion__ = "2m testing" -__lastModified__ = "2019-07-20 13:29 CST" - -LOGLEVEL = PathLog.Level.INFO -PathLog.setLevel(LOGLEVEL, PathLog.thisModule()) - -if LOGLEVEL is PathLog.Level.DEBUG: - PathLog.trackModule() - -# Qt translation handling - - -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -class ObjectOp(PathOp.ObjectOp): - '''Base class for all Path.Area based operations. - Provides standard features including debugging properties AreaParams, - PathParams and removalshape, all hidden. - The main reason for existence is to implement the standard interface - to Path.Area so subclasses only have to provide the shapes for the - operations.''' - - # These are static while document is open, if it contains a 3D Surface Op - initOpFinalDepth = None - initOpStartDepth = None - initWithRotation = False - defValsSet = False - docRestored = False - - def opFeatures(self, obj): - '''opFeatures(obj) ... returns the base features supported by all Path.Area based operations. - The standard feature list is OR'ed with the return value of areaOpFeatures(). - Do not overwrite, implement areaOpFeatures(obj) instead.''' - # return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj) | PathOp.FeatureRotation - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj) | PathOp.FeatureCoolant - - def areaOpFeatures(self, obj): - '''areaOpFeatures(obj) ... overwrite to add operation specific features. - Can safely be overwritten by subclasses.''' - # pylint: disable=unused-argument - return 0 - - def initOperation(self, obj): - '''initOperation(obj) ... sets up standard Path.Area properties and calls initAreaOp(). - Do not overwrite, overwrite initAreaOp(obj) instead.''' - PathLog.track() - - # Debugging - obj.addProperty("App::PropertyString", "AreaParams", "Path") - obj.setEditorMode('AreaParams', 2) # hide - obj.addProperty("App::PropertyString", "PathParams", "Path") - obj.setEditorMode('PathParams', 2) # hide - obj.addProperty("Part::PropertyPartShape", "removalshape", "Path") - obj.setEditorMode('removalshape', 2) # hide - # obj.Proxy = self - - self.setupAdditionalProperties(obj) - self.initAreaOp(obj) - - def setupAdditionalProperties(self, obj): - if not hasattr(obj, 'EnableRotation'): - obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) - obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] - - def initAreaOp(self, obj): - '''initAreaOp(obj) ... overwrite if the receiver class needs initialisation. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def areaOpShapeForDepths(self, obj, job): - '''areaOpShapeForDepths(obj) ... returns the shape used to make an initial calculation for the depths being used. - The default implementation returns the job's Base.Shape''' - if job: - if job.Stock: - PathLog.debug("job=%s base=%s shape=%s" % (job, job.Stock, job.Stock.Shape)) - return job.Stock.Shape - else: - PathLog.warning(translate("PathAreaOp", "job %s has no Base.") % job.Label) - else: - PathLog.warning(translate("PathAreaOp", "no job for op %s found.") % obj.Label) - return None - - def areaOpOnChanged(self, obj, prop): - '''areaOpOnChanged(obj, porp) ... overwrite to process operation specific changes to properties. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def opOnChanged(self, obj, prop): - '''opOnChanged(obj, prop) ... base implementation of the notification framework - do not overwrite. - The base implementation takes a stab at determining Heights and Depths if the operations's Base - changes. - Do not overwrite, overwrite areaOpOnChanged(obj, prop) instead.''' - # PathLog.track(obj.Label, prop) - if prop in ['AreaParams', 'PathParams', 'removalshape']: - obj.setEditorMode(prop, 2) - - if prop == 'Base' and len(obj.Base) == 1: - (base, sub) = obj.Base[0] - bb = base.Shape.BoundBox # parent boundbox - subobj = base.Shape.getElement(sub[0]) - fbb = subobj.BoundBox # feature boundbox - - if hasattr(obj, 'Side'): - if bb.XLength == fbb.XLength and bb.YLength == fbb.YLength: - obj.Side = "Outside" - else: - obj.Side = "Inside" - - self.areaOpOnChanged(obj, prop) - - def opOnDocumentRestored(self, obj): - for prop in ['AreaParams', 'PathParams', 'removalshape']: - if hasattr(obj, prop): - obj.setEditorMode(prop, 2) - - self.initOpFinalDepth = obj.OpFinalDepth.Value - self.initOpStartDepth = obj.OpStartDepth.Value - self.docRestored = True - - self.setupAdditionalProperties(obj) - self.areaOpOnDocumentRestored(obj) - - def areaOpOnDocumentRestored(self, obj): - '''areaOpOnDocumentRestored(obj) ... overwrite to fully restore receiver''' - pass # pylint: disable=unnecessary-pass - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj) ... base implementation, do not overwrite. - The base implementation sets the depths and heights based on the - areaOpShapeForDepths() return value. - Do not overwrite, overwrite areaOpSetDefaultValues(obj, job) instead.''' - PathLog.debug("opSetDefaultValues(%s, %s)" % (obj.Label, job.Label)) - - # Initial setting for EnableRotation is taken from Job settings/SetupSheet - # User may override on per-operation basis as needed. - if hasattr(job.SetupSheet, 'SetupEnableRotation'): - obj.EnableRotation = job.SetupSheet.SetupEnableRotation - else: - obj.EnableRotation = 'Off' - PathLog.debug("opSetDefaultValues(): Enable Rotation: {}".format(obj.EnableRotation)) - - if PathOp.FeatureDepths & self.opFeatures(obj): - try: - shape = self.areaOpShapeForDepths(obj, job) - except Exception as ee: # pylint: disable=broad-except - PathLog.error(ee) - shape = None - - # Set initial start and final depths - if shape is None: - PathLog.debug("shape is None") - startDepth = 1.0 - finalDepth = 0.0 - else: - bb = job.Stock.Shape.BoundBox - startDepth = bb.ZMax - finalDepth = bb.ZMin - - # Adjust start and final depths if rotation is enabled - if obj.EnableRotation != 'Off': - self.initWithRotation = True - self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init - # Calculate rotational distances/radii - opHeights = self.opDetermineRotationRadii(obj) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfset)] - (xRotRad, yRotRad, zRotRad) = opHeights[0] # pylint: disable=unused-variable - PathLog.debug("opHeights[0]: " + str(opHeights[0])) - PathLog.debug("opHeights[1]: " + str(opHeights[1])) - - if obj.EnableRotation == 'A(x)': - startDepth = xRotRad - if obj.EnableRotation == 'B(y)': - startDepth = yRotRad - else: - startDepth = max(xRotRad, yRotRad) - finalDepth = -1 * startDepth - - # Manage operation start and final depths - if self.docRestored is True: # This op is NOT the first in the Operations list - PathLog.debug("Doc restored") - obj.FinalDepth.Value = obj.OpFinalDepth.Value - obj.StartDepth.Value = obj.OpStartDepth.Value - else: - PathLog.debug("New operation") - obj.StartDepth.Value = startDepth - obj.FinalDepth.Value = finalDepth - obj.OpStartDepth.Value = startDepth - obj.OpFinalDepth.Value = finalDepth - - if obj.EnableRotation != 'Off': - if self.initOpFinalDepth is None: - self.initOpFinalDepth = finalDepth - PathLog.debug("Saved self.initOpFinalDepth") - if self.initOpStartDepth is None: - self.initOpStartDepth = startDepth - PathLog.debug("Saved self.initOpStartDepth") - self.defValsSet = True - PathLog.debug("Default OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value)) - PathLog.debug("Default Depths are Start: {}, and Final: {}".format(startDepth, finalDepth)) - - self.areaOpSetDefaultValues(obj, job) - - def areaOpSetDefaultValues(self, obj, job): - '''areaOpSetDefaultValues(obj, job) ... overwrite to set initial values of operation specific properties. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def _buildPathArea(self, obj, baseobject, isHole, start, getsim): - '''_buildPathArea(obj, baseobject, isHole, start, getsim) ... internal function.''' - # pylint: disable=unused-argument - PathLog.track() - area = Path.Area() - area.setPlane(PathUtils.makeWorkplane(baseobject)) - area.add(baseobject) - - areaParams = self.areaOpAreaParams(obj, isHole) # pylint: disable=assignment-from-no-return - - heights = [i for i in self.depthparams] - PathLog.debug('depths: {}'.format(heights)) - area.setParams(**areaParams) - obj.AreaParams = str(area.getParams()) - - PathLog.debug("Area with params: {}".format(area.getParams())) - - sections = area.makeSections(mode=0, project=self.areaOpUseProjection(obj), heights=heights) - PathLog.debug("sections = %s" % sections) - shapelist = [sec.getShape() for sec in sections] - PathLog.debug("shapelist = %s" % shapelist) - - pathParams = self.areaOpPathParams(obj, isHole) # pylint: disable=assignment-from-no-return - pathParams['shapes'] = shapelist - pathParams['feedrate'] = self.horizFeed - pathParams['feedrate_v'] = self.vertFeed - pathParams['verbose'] = True - pathParams['resume_height'] = obj.SafeHeight.Value - pathParams['retraction'] = obj.ClearanceHeight.Value - pathParams['return_end'] = True - # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers - pathParams['preamble'] = False - - if not self.areaOpRetractTool(obj): - pathParams['threshold'] = 2.001 * self.radius - - if self.endVector is not None: - pathParams['start'] = self.endVector - elif PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: - pathParams['start'] = obj.StartPoint - - obj.PathParams = str({key: value for key, value in pathParams.items() if key != 'shapes'}) - PathLog.debug("Path with params: {}".format(obj.PathParams)) - - (pp, end_vector) = Path.fromShapes(**pathParams) - PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) - self.endVector = end_vector # pylint: disable=attribute-defined-outside-init - - simobj = None - if getsim: - areaParams['Thicken'] = True - areaParams['ToolRadius'] = self.radius - self.radius * .005 - area.setParams(**areaParams) - sec = area.makeSections(mode=0, project=False, heights=heights)[-1].getShape() - simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax)) - - return pp, simobj - - def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ - '''opExecute(obj, getsim=False) ... implementation of Path.Area ops. - determines the parameters for _buildPathArea(). - Do not overwrite, implement - areaOpAreaParams(obj, isHole) ... op specific area param dictionary - areaOpPathParams(obj, isHole) ... op specific path param dictionary - areaOpShapes(obj) ... the shape for path area to process - areaOpUseProjection(obj) ... return true if operation can use projection - instead.''' - PathLog.track() - - # Instantiate class variables for operation reference - self.endVector = None # pylint: disable=attribute-defined-outside-init - self.rotateFlag = False # pylint: disable=attribute-defined-outside-init - self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init - self.cloneNames = [] # pylint: disable=attribute-defined-outside-init - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init - self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init - self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones - - # Import OpFinalDepth from pre-existing operation for recompute() scenarios - if self.defValsSet is True: - PathLog.debug("self.defValsSet is True.") - if self.initOpStartDepth is not None: - if self.initOpStartDepth != obj.OpStartDepth.Value: - obj.OpStartDepth.Value = self.initOpStartDepth - obj.StartDepth.Value = self.initOpStartDepth - - if self.initOpFinalDepth is not None: - if self.initOpFinalDepth != obj.OpFinalDepth.Value: - obj.OpFinalDepth.Value = self.initOpFinalDepth - obj.FinalDepth.Value = self.initOpFinalDepth - self.defValsSet = False - - if obj.EnableRotation != 'Off': - # Calculate operation heights based upon rotation radii - opHeights = self.opDetermineRotationRadii(obj) - (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init - (self.clrOfset, self.safOfst) = opHeights[1] # pylint: disable=attribute-defined-outside-init - - # Set clearance and safe heights based upon rotation radii - if obj.EnableRotation == 'A(x)': - strDep = self.xRotRad - elif obj.EnableRotation == 'B(y)': - strDep = self.yRotRad - else: - strDep = max(self.xRotRad, self.yRotRad) - finDep = -1 * strDep - - obj.ClearanceHeight.Value = strDep + self.clrOfset - obj.SafeHeight.Value = strDep + self.safOfst - - if self.initWithRotation is False: - if obj.FinalDepth.Value == obj.OpFinalDepth.Value: - obj.FinalDepth.Value = finDep - if obj.StartDepth.Value == obj.OpStartDepth.Value: - obj.StartDepth.Value = strDep - - # Create visual axes when debugging. - if PathLog.getLevel(PathLog.thisModule()) == 4: - self.visualAxis() - else: - strDep = obj.StartDepth.Value - finDep = obj.FinalDepth.Value - - # Set axial feed rates based upon horizontal feed rates - safeCircum = 2 * math.pi * obj.SafeHeight.Value - self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init - self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init - - # Initiate depthparams and calculate operation heights for rotational operation - finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 - self.depthparams = PathUtils.depth_params( # pylint: disable=attribute-defined-outside-init - clearance_height=obj.ClearanceHeight.Value, - safe_height=obj.SafeHeight.Value, - start_depth=obj.StartDepth.Value, - step_down=obj.StepDown.Value, - z_finish_step=finish_step, - final_depth=obj.FinalDepth.Value, - user_depths=None) - - # Set start point - if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: - start = obj.StartPoint - else: - start = None - - aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return - - # Adjust tuples length received from other PathWB tools/operations beside PathPocketShape - shapes = [] - for shp in aOS: - if len(shp) == 2: - (fc, iH) = shp - # fc, iH, sub, angle, axis, strtDep, finDep - tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) - else: - shapes.append(shp) - - if len(shapes) > 1: - jobs = [{ - 'x': s[0].BoundBox.XMax, - 'y': s[0].BoundBox.YMax, - 'shape': s - } for s in shapes] - - jobs = PathUtils.sort_jobs(jobs, ['x', 'y']) - - shapes = [j['shape'] for j in jobs] - - sims = [] - numShapes = len(shapes) - - for ns in range(0, numShapes): - (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable - if ns < numShapes - 1: - nextAxis = shapes[ns + 1][4] - else: - nextAxis = 'L' - - finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 - self.depthparams = PathUtils.depth_params( # pylint: disable=attribute-defined-outside-init - clearance_height=obj.ClearanceHeight.Value, - safe_height=obj.SafeHeight.Value, - start_depth=strDep, # obj.StartDepth.Value, - step_down=obj.StepDown.Value, - z_finish_step=finish_step, - final_depth=finDep, # obj.FinalDepth.Value, - user_depths=None) - - try: - (pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim) - except Exception as e: # pylint: disable=broad-except - FreeCAD.Console.PrintError(e) - FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.") - else: - ppCmds = pp.Commands - if obj.EnableRotation != 'Off' and self.rotateFlag is True: - # Rotate model to index for cut - if axis == 'X': - axisOfRot = 'A' - elif axis == 'Y': - axisOfRot = 'B' - # Reverse angle temporarily to match model. Error in FreeCAD render of B axis rotations - if obj.B_AxisErrorOverride is True: - angle = -1 * angle - elif axis == 'Z': - axisOfRot = 'C' - else: - axisOfRot = 'A' - # Rotate Model to correct angle - ppCmds.insert(0, Path.Command('G1', {axisOfRot: angle, 'F': self.axialFeed})) - ppCmds.insert(0, Path.Command('N100', {})) - - # Raise cutter to safe depth and return index to starting position - ppCmds.append(Path.Command('N200', {})) - ppCmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - if axis != nextAxis: - ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid})) - # Eif - - # Save gcode commands to object command list - self.commandlist.extend(ppCmds) - sims.append(sim) - # Eif - - if self.areaOpRetractTool(obj): - self.endVector = None # pylint: disable=attribute-defined-outside-init - - # Raise cutter to safe height and rotate back to original orientation - if self.rotateFlag is True: - self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - self.commandlist.append(Path.Command('G0', {'A': 0.0, 'F': self.axialRapid})) - self.commandlist.append(Path.Command('G0', {'B': 0.0, 'F': self.axialRapid})) - - self.useTempJobClones('Delete') # Delete temp job clone group and contents - self.guiMessage('title', None, show=True) # Process GUI messages to user - for ton in self.tempObjectNames: # remove temporary objects by name - FreeCAD.ActiveDocument.removeObject(ton) - PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n") - return sims - - def areaOpRetractTool(self, obj): - '''areaOpRetractTool(obj) ... return False to keep the tool at current level between shapes. Default is True.''' - # pylint: disable=unused-argument - return True - - def areaOpAreaParams(self, obj, isHole): - '''areaOpAreaParams(obj, isHole) ... return operation specific area parameters in a dictionary. - Note that the resulting parameters are stored in the property AreaParams. - Must be overwritten by subclasses.''' - # pylint: disable=unused-argument - pass # pylint: disable=unnecessary-pass - - def areaOpPathParams(self, obj, isHole): - '''areaOpPathParams(obj, isHole) ... return operation specific path parameters in a dictionary. - Note that the resulting parameters are stored in the property PathParams. - Must be overwritten by subclasses.''' - # pylint: disable=unused-argument - pass # pylint: disable=unnecessary-pass - - def areaOpShapes(self, obj): - '''areaOpShapes(obj) ... return all shapes to be processed by Path.Area for this op. - Must be overwritten by subclasses.''' - # pylint: disable=unused-argument - pass # pylint: disable=unnecessary-pass - - def areaOpUseProjection(self, obj): - '''areaOpUseProcjection(obj) ... return True if the operation can use procjection, defaults to False. - Can safely be overwritten by subclasses.''' - # pylint: disable=unused-argument - return False - - # Rotation-related methods - def opDetermineRotationRadii(self, obj): - '''opDetermineRotationRadii(obj) - Determine rotational radii for 4th-axis rotations, for clearance/safe heights ''' - - parentJob = PathUtils.findParentJob(obj) - # bb = parentJob.Stock.Shape.BoundBox - xlim = 0.0 - ylim = 0.0 - # zlim = 0.0 - - # Determine boundbox radius based upon xzy limits data - if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax): - zlim = self.stockBB.ZMin - else: - zlim = self.stockBB.ZMax - - if obj.EnableRotation != 'B(y)': - # Rotation is around X-axis, cutter moves along same axis - if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax): - ylim = self.stockBB.YMin - else: - ylim = self.stockBB.YMax - - if obj.EnableRotation != 'A(x)': - # Rotation is around Y-axis, cutter moves along same axis - if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax): - xlim = self.stockBB.XMin - else: - xlim = self.stockBB.XMax - - xRotRad = math.sqrt(ylim**2 + zlim**2) - yRotRad = math.sqrt(xlim**2 + zlim**2) - zRotRad = math.sqrt(xlim**2 + ylim**2) - - clrOfst = parentJob.SetupSheet.ClearanceHeightOffset.Value - safOfst = parentJob.SetupSheet.SafeHeightOffset.Value - - return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)] - - def faceRotationAnalysis(self, obj, norm, surf): - '''faceRotationAnalysis(obj, norm, surf) - Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) ''' - PathLog.track() - - praInfo = "faceRotationAnalysis()" - rtn = True - orientation = 'X' - angle = 500.0 - precision = 6 - - for i in range(0, 13): - if PathGeom.Tolerance * (i * 10) == 1.0: - precision = i - break - - def roundRoughValues(precision, val): - # Convert VALxe-15 numbers to zero - if PathGeom.isRoughly(0.0, val) is True: - return 0.0 - # Convert VAL.99999999 to next integer - elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: - return round(val) - else: - return round(val, precision) - - nX = roundRoughValues(precision, norm.x) - nY = roundRoughValues(precision, norm.y) - nZ = roundRoughValues(precision, norm.z) - praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ) - - saX = roundRoughValues(precision, surf.x) - saY = roundRoughValues(precision, surf.y) - saZ = roundRoughValues(precision, surf.z) - praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ) - - # Determine rotation needed and current orientation - if saX == 0.0: - if saY == 0.0: - orientation = "Z" - if saZ == 1.0: - angle = 0.0 - elif saZ == -1.0: - angle = -180.0 - else: - praInfo += "_else_X" + str(saZ) - elif saY == 1.0: - orientation = "Y" - angle = 90.0 - elif saY == -1.0: - orientation = "Y" - angle = -90.0 - else: - if saZ != 0.0: - angle = math.degrees(math.atan(saY / saZ)) - orientation = "Y" - elif saY == 0.0: - if saZ == 0.0: - orientation = "X" - if saX == 1.0: - angle = -90.0 - elif saX == -1.0: - angle = 90.0 - else: - praInfo += "_else_X" + str(saX) - else: - orientation = "X" - ratio = saX / saZ - angle = math.degrees(math.atan(ratio)) - if ratio < 0.0: - praInfo += " NEG-ratio" - # angle -= 90 - else: - praInfo += " POS-ratio" - angle = -1 * angle - if saX < 0.0: - angle = angle + 180.0 - elif saZ == 0.0: - # if saY != 0.0: - angle = math.degrees(math.atan(saX / saY)) - orientation = "Y" - - if saX + nX == 0.0: - angle = -1 * angle - if saY + nY == 0.0: - angle = -1 * angle - if saZ + nZ == 0.0: - angle = -1 * angle - - if saY == -1.0 or saY == 1.0: - if nX != 0.0: - angle = -1 * angle - - # Enforce enabled rotation in settings - praInfo += "\n -Initial orientation: {}".format(orientation) - if orientation == 'Y': - axis = 'X' - if obj.EnableRotation == 'B(y)': # Required axis disabled - if angle == 180.0 or angle == -180.0: - axis = 'Y' - else: - rtn = False - elif orientation == 'X': - axis = 'Y' - if obj.EnableRotation == 'A(x)': # Required axis disabled - if angle == 180.0 or angle == -180.0: - axis = 'X' - else: - rtn = False - elif orientation == 'Z': - axis = 'X' - - if math.fabs(angle) == 0.0: - angle = 0.0 - rtn = False - - if angle == 500.0: - angle = 0.0 - rtn = False - - if rtn is False: - if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True: - if obj.EnableRotation == 'B(y)': - axis = 'Y' - rtn = True - - if rtn is True: - self.rotateFlag = True # pylint: disable=attribute-defined-outside-init - if obj.ReverseDirection is True: - if angle < 180.0: - angle = angle + 180.0 - else: - angle = angle - 180.0 - angle = round(angle, precision) - - praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis) - if rtn is True: - praInfo += "\n - ... rotation triggered" - else: - praInfo += "\n - ... NO rotation triggered" - - PathLog.debug("\n" + str(praInfo)) - - return (rtn, angle, axis, praInfo) - - def guiMessage(self, title, msg, show=False): - '''guiMessage(title, msg, show=False) - Handle op related GUI messages to user''' - if msg is not None: - self.guiMsgs.append((title, msg)) - if show is True: - if len(self.guiMsgs) > 0: - if FreeCAD.GuiUp: - from PySide.QtGui import QMessageBox - for entry in self.guiMsgs: - (title, msg) = entry - QMessageBox.warning(None, title, msg) - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - return True - else: - for entry in self.guiMsgs: - (title, msg) = entry - PathLog.warning("{}:: {}".format(title, msg)) - self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init - return True - return False - - def visualAxis(self): - '''visualAxis() - Create visual X & Y axis for use in orientation of rotational operations - Triggered only for PathLog.debug''' - - if not FreeCAD.ActiveDocument.getObject('xAxCyl'): - xAx = 'xAxCyl' - yAx = 'yAxCyl' - # zAx = 'zAxCyl' - FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "visualAxis") - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject('visualAxis').Visibility = False - vaGrp = FreeCAD.ActiveDocument.getObject("visualAxis") - - FreeCAD.ActiveDocument.addObject("Part::Cylinder", xAx) - cyl = FreeCAD.ActiveDocument.getObject(xAx) - cyl.Label = xAx - cyl.Radius = self.xRotRad - cyl.Height = 0.01 - cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90)) - cyl.purgeTouched() - if FreeCAD.GuiUp: - cylGui = FreeCADGui.ActiveDocument.getObject(xAx) - cylGui.ShapeColor = (0.667, 0.000, 0.000) - cylGui.Transparency = 85 - cylGui.Visibility = False - vaGrp.addObject(cyl) - - FreeCAD.ActiveDocument.addObject("Part::Cylinder", yAx) - cyl = FreeCAD.ActiveDocument.getObject(yAx) - cyl.Label = yAx - cyl.Radius = self.yRotRad - cyl.Height = 0.01 - cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90)) - cyl.purgeTouched() - if FreeCAD.GuiUp: - cylGui = FreeCADGui.ActiveDocument.getObject(yAx) - cylGui.ShapeColor = (0.000, 0.667, 0.000) - cylGui.Transparency = 85 - cylGui.Visibility = False - vaGrp.addObject(cyl) - - def useTempJobClones(self, cloneName): - '''useTempJobClones(cloneName) - Manage use of temporary model clones for rotational operation calculations. - Clones are stored in 'rotJobClones' group.''' - if FreeCAD.ActiveDocument.getObject('rotJobClones'): - if cloneName == 'Start': - if PathLog.getLevel(PathLog.thisModule()) < 4: - for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: - FreeCAD.ActiveDocument.removeObject(cln.Name) - elif cloneName == 'Delete': - if PathLog.getLevel(PathLog.thisModule()) < 4: - for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: - FreeCAD.ActiveDocument.removeObject(cln.Name) - FreeCAD.ActiveDocument.removeObject('rotJobClones') - else: - FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "rotJobClones") - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False - - if cloneName != 'Start' and cloneName != 'Delete': - FreeCAD.ActiveDocument.getObject('rotJobClones').addObject(FreeCAD.ActiveDocument.getObject(cloneName)) - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(cloneName).Visibility = False - - def cloneBaseAndStock(self, obj, base, angle, axis, subCount): - '''cloneBaseAndStock(obj, base, angle, axis, subCount) - Method called to create a temporary clone of the base and parent Job stock. - Clones are destroyed after usage for calculations related to rotational operations.''' - # Create a temporary clone and stock of model for rotational use. - rndAng = round(angle, 8) - if rndAng < 0.0: # neg sign is converted to underscore in clone name creation. - tag = axis + '_' + axis + '_' + str(math.fabs(rndAng)).replace('.', '_') - else: - tag = axis + str(rndAng).replace('.', '_') - clnNm = obj.Name + '_base_' + '_' + str(subCount) + '_' + tag - stckClnNm = obj.Name + '_stock_' + '_' + str(subCount) + '_' + tag - if clnNm not in self.cloneNames: - self.cloneNames.append(clnNm) - self.cloneNames.append(stckClnNm) - if FreeCAD.ActiveDocument.getObject(clnNm): - FreeCAD.ActiveDocument.getObject(clnNm).Shape = base.Shape - else: - FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape - self.useTempJobClones(clnNm) - if FreeCAD.ActiveDocument.getObject(stckClnNm): - FreeCAD.ActiveDocument.getObject(stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape - else: - FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape - self.useTempJobClones(stckClnNm) - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90 - FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000) - clnBase = FreeCAD.ActiveDocument.getObject(clnNm) - clnStock = FreeCAD.ActiveDocument.getObject(stckClnNm) - tag = base.Name + '_' + tag - return (clnBase, clnStock, tag) - - def getFaceNormAndSurf(self, face): - '''getFaceNormAndSurf(face) - Return face.normalAt(0,0) or face.normal(0,0) and face.Surface.Axis vectors - ''' - norm = FreeCAD.Vector(0.0, 0.0, 0.0) - surf = FreeCAD.Vector(0.0, 0.0, 0.0) - - if hasattr(face, 'normalAt'): - n = face.normalAt(0, 0) - elif hasattr(face, 'normal'): - n = face.normal(0, 0) - if hasattr(face.Surface, 'Axis'): - s = face.Surface.Axis - else: - s = n - norm.x = n.x - norm.y = n.y - norm.z = n.z - surf.x = s.x - surf.y = s.y - surf.z = s.z - return (norm, surf) - - def applyRotationalAnalysis(self, obj, base, angle, axis, subCount): - '''applyRotationalAnalysis(obj, base, angle, axis, subCount) - Create temp clone and stock and apply rotation to both. - Return new rotated clones - ''' - if axis == 'X': - vect = FreeCAD.Vector(1, 0, 0) - elif axis == 'Y': - vect = FreeCAD.Vector(0, 1, 0) - - if obj.InverseAngle is True: - angle = -1 * angle - if math.fabs(angle) == 0.0: - angle = 0.0 - - # Create a temporary clone of model for rotational use. - (clnBase, clnStock, tag) = self.cloneBaseAndStock(obj, base, angle, axis, subCount) - - # Rotate base to such that Surface.Axis of pocket bottom is Z=1 - clnBase = Draft.rotate(clnBase, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnStock = Draft.rotate(clnStock, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - - clnBase.purgeTouched() - clnStock.purgeTouched() - return (clnBase, angle, clnStock, tag) - - def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle): - '''applyInverseAngle(obj, clnBase, clnStock, axis, angle) - Apply rotations to incoming base and stock objects.''' - if axis == 'X': - vect = FreeCAD.Vector(1, 0, 0) - elif axis == 'Y': - vect = FreeCAD.Vector(0, 1, 0) - # Rotate base to inverse of original angle - clnBase = Draft.rotate(clnBase, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnStock = Draft.rotate(clnStock, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) - clnBase.purgeTouched() - clnStock.purgeTouched() - # Update property and angle values - obj.InverseAngle = True - obj.AttemptInverseAngle = False - angle = -1 * angle - - PathLog.info(translate("Path", "Rotated to inverse angle.")) - return (clnBase, clnStock, angle) - - def calculateStartFinalDepths(self, obj, shape, stock): - '''calculateStartFinalDepths(obj, shape, stock) - Calculate correct start and final depths for the shape(face) object provided.''' - finDep = max(obj.FinalDepth.Value, shape.BoundBox.ZMin) - stockTop = stock.Shape.BoundBox.ZMax - if obj.EnableRotation == 'Off': - strDep = obj.StartDepth.Value - if strDep <= finDep: - strDep = stockTop - else: - strDep = min(obj.StartDepth.Value, stockTop) - if strDep <= finDep: - strDep = stockTop # self.strDep - msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.") - PathLog.error(msg) - return (strDep, finDep) - - def sortTuplesByIndex(self, TupleList, tagIdx): - '''sortTuplesByIndex(TupleList, tagIdx) - sort list of tuples based on tag index provided - return (TagList, GroupList) - ''' - # Separate elements, regroup by orientation (axis_angle combination) - TagList = ['X34.2'] - GroupList = [[(2.3, 3.4, 'X')]] - for tup in TupleList: - if tup[tagIdx] in TagList: - # Determine index of found string - i = 0 - for orn in TagList: - if orn == tup[4]: - break - i += 1 - GroupList[i].append(tup) - else: - TagList.append(tup[4]) # add orientation entry - GroupList.append([tup]) # add orientation entry - # Remove temp elements - TagList.pop(0) - GroupList.pop(0) - return (TagList, GroupList) - - def warnDisabledAxis(self, obj, axis, sub=''): - '''warnDisabledAxis(self, obj, axis) - Provide user feedback if required axis is disabled''' - if axis == 'X' and obj.EnableRotation == 'B(y)': - msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " - msg += translate('Path', "Selected feature(s) require 'Enable Rotation: A(x)' for access.") - PathLog.warning(msg) - return True - elif axis == 'Y' and obj.EnableRotation == 'A(x)': - msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " - msg += translate('Path', "Selected feature(s) require 'Enable Rotation: B(y)' for access.") - PathLog.warning(msg) - return True - else: - return False +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** + +import FreeCAD +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathOp as PathOp +import PathScripts.PathUtils as PathUtils +import PathScripts.PathGeom as PathGeom +import Draft +import math + +# from PathScripts.PathUtils import waiting_effects +from PySide import QtCore +if FreeCAD.GuiUp: + import FreeCADGui + + +__title__ = "Base class for PathArea based operations." +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Base class and properties for Path.Area based operations." +__contributors__ = "russ4262 (Russell Johnson)" +__createdDate__ = "2017" +__scriptVersion__ = "2p" +__lastModified__ = "2020-02-13 17:11 CST" + +LOGLEVEL = PathLog.Level.INFO +PathLog.setLevel(LOGLEVEL, PathLog.thisModule()) + +if LOGLEVEL is PathLog.Level.DEBUG: + PathLog.trackModule() + +# Qt translation handling + + +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class ObjectOp(PathOp.ObjectOp): + '''Base class for all Path.Area based operations. + Provides standard features including debugging properties AreaParams, + PathParams and removalshape, all hidden. + The main reason for existence is to implement the standard interface + to Path.Area so subclasses only have to provide the shapes for the + operations.''' + + def opFeatures(self, obj): + '''opFeatures(obj) ... returns the base features supported by all Path.Area based operations. + The standard feature list is OR'ed with the return value of areaOpFeatures(). + Do not overwrite, implement areaOpFeatures(obj) instead.''' + # return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj) | PathOp.FeatureRotation + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj) | PathOp.FeatureCoolant + + def areaOpFeatures(self, obj): + '''areaOpFeatures(obj) ... overwrite to add operation specific features. + Can safely be overwritten by subclasses.''' + # pylint: disable=unused-argument + return 0 + + def initOperation(self, obj): + '''initOperation(obj) ... sets up standard Path.Area properties and calls initAreaOp(). + Do not overwrite, overwrite initAreaOp(obj) instead.''' + PathLog.track() + + # These are static while document is open, if it contains a 3D Surface Op + self.initWithRotation = False + + # Debugging + obj.addProperty("App::PropertyString", "AreaParams", "Path") + obj.setEditorMode('AreaParams', 2) # hide + obj.addProperty("App::PropertyString", "PathParams", "Path") + obj.setEditorMode('PathParams', 2) # hide + obj.addProperty("Part::PropertyPartShape", "removalshape", "Path") + obj.setEditorMode('removalshape', 2) # hide + # obj.Proxy = self + + self.setupAdditionalProperties(obj) + self.initAreaOp(obj) + + def setupAdditionalProperties(self, obj): + if not hasattr(obj, 'EnableRotation'): + obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) + obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] + + def initAreaOp(self, obj): + '''initAreaOp(obj) ... overwrite if the receiver class needs initialisation. + Can safely be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def areaOpShapeForDepths(self, obj, job): + '''areaOpShapeForDepths(obj) ... returns the shape used to make an initial calculation for the depths being used. + The default implementation returns the job's Base.Shape''' + if job: + if job.Stock: + PathLog.debug("job=%s base=%s shape=%s" % (job, job.Stock, job.Stock.Shape)) + return job.Stock.Shape + else: + PathLog.warning(translate("PathAreaOp", "job %s has no Base.") % job.Label) + else: + PathLog.warning(translate("PathAreaOp", "no job for op %s found.") % obj.Label) + return None + + def areaOpOnChanged(self, obj, prop): + '''areaOpOnChanged(obj, porp) ... overwrite to process operation specific changes to properties. + Can safely be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def opOnChanged(self, obj, prop): + '''opOnChanged(obj, prop) ... base implementation of the notification framework - do not overwrite. + The base implementation takes a stab at determining Heights and Depths if the operations's Base + changes. + Do not overwrite, overwrite areaOpOnChanged(obj, prop) instead.''' + # PathLog.track(obj.Label, prop) + if prop in ['AreaParams', 'PathParams', 'removalshape']: + obj.setEditorMode(prop, 2) + + if prop == 'Base' and len(obj.Base) == 1: + (base, sub) = obj.Base[0] + bb = base.Shape.BoundBox # parent boundbox + subobj = base.Shape.getElement(sub[0]) + fbb = subobj.BoundBox # feature boundbox + + if hasattr(obj, 'Side'): + if bb.XLength == fbb.XLength and bb.YLength == fbb.YLength: + obj.Side = "Outside" + else: + obj.Side = "Inside" + + self.areaOpOnChanged(obj, prop) + + def opOnDocumentRestored(self, obj): + for prop in ['AreaParams', 'PathParams', 'removalshape']: + if hasattr(obj, prop): + obj.setEditorMode(prop, 2) + + self.setupAdditionalProperties(obj) + self.areaOpOnDocumentRestored(obj) + + def areaOpOnDocumentRestored(self, obj): + '''areaOpOnDocumentRestored(obj) ... overwrite to fully restore receiver''' + pass # pylint: disable=unnecessary-pass + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj) ... base implementation, do not overwrite. + The base implementation sets the depths and heights based on the + areaOpShapeForDepths() return value. + Do not overwrite, overwrite areaOpSetDefaultValues(obj, job) instead.''' + PathLog.debug("opSetDefaultValues(%s, %s)" % (obj.Label, job.Label)) + + # Initial setting for EnableRotation is taken from Job settings/SetupSheet + # User may override on per-operation basis as needed. + if hasattr(job.SetupSheet, 'SetupEnableRotation'): + obj.EnableRotation = job.SetupSheet.SetupEnableRotation + else: + obj.EnableRotation = 'Off' + PathLog.debug("opSetDefaultValues(): Enable Rotation: {}".format(obj.EnableRotation)) + + if PathOp.FeatureDepths & self.opFeatures(obj): + try: + shape = self.areaOpShapeForDepths(obj, job) + except Exception as ee: # pylint: disable=broad-except + PathLog.error(ee) + shape = None + + # Set initial start and final depths + if shape is None: + PathLog.debug("shape is None") + startDepth = 1.0 + finalDepth = 0.0 + else: + bb = job.Stock.Shape.BoundBox + startDepth = bb.ZMax + finalDepth = bb.ZMin + + # Adjust start and final depths if rotation is enabled + if obj.EnableRotation != 'Off': + self.initWithRotation = True + self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init + # Calculate rotational distances/radii + opHeights = self.opDetermineRotationRadii(obj) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfset)] + (xRotRad, yRotRad, zRotRad) = opHeights[0] # pylint: disable=unused-variable + PathLog.debug("opHeights[0]: " + str(opHeights[0])) + PathLog.debug("opHeights[1]: " + str(opHeights[1])) + + if obj.EnableRotation == 'A(x)': + startDepth = xRotRad + if obj.EnableRotation == 'B(y)': + startDepth = yRotRad + else: + startDepth = max(xRotRad, yRotRad) + finalDepth = -1 * startDepth + + obj.StartDepth.Value = startDepth + obj.FinalDepth.Value = finalDepth + obj.OpStartDepth.Value = startDepth + obj.OpFinalDepth.Value = finalDepth + + PathLog.debug("Default OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value)) + PathLog.debug("Default Depths are Start: {}, and Final: {}".format(startDepth, finalDepth)) + + self.areaOpSetDefaultValues(obj, job) + + def areaOpSetDefaultValues(self, obj, job): + '''areaOpSetDefaultValues(obj, job) ... overwrite to set initial values of operation specific properties. + Can safely be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def _buildPathArea(self, obj, baseobject, isHole, start, getsim): + '''_buildPathArea(obj, baseobject, isHole, start, getsim) ... internal function.''' + # pylint: disable=unused-argument + PathLog.track() + area = Path.Area() + area.setPlane(PathUtils.makeWorkplane(baseobject)) + area.add(baseobject) + + areaParams = self.areaOpAreaParams(obj, isHole) # pylint: disable=assignment-from-no-return + + heights = [i for i in self.depthparams] + PathLog.debug('depths: {}'.format(heights)) + area.setParams(**areaParams) + obj.AreaParams = str(area.getParams()) + + PathLog.debug("Area with params: {}".format(area.getParams())) + + sections = area.makeSections(mode=0, project=self.areaOpUseProjection(obj), heights=heights) + PathLog.debug("sections = %s" % sections) + shapelist = [sec.getShape() for sec in sections] + PathLog.debug("shapelist = %s" % shapelist) + + pathParams = self.areaOpPathParams(obj, isHole) # pylint: disable=assignment-from-no-return + pathParams['shapes'] = shapelist + pathParams['feedrate'] = self.horizFeed + pathParams['feedrate_v'] = self.vertFeed + pathParams['verbose'] = True + pathParams['resume_height'] = obj.SafeHeight.Value + pathParams['retraction'] = obj.ClearanceHeight.Value + pathParams['return_end'] = True + # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers + pathParams['preamble'] = False + + if not self.areaOpRetractTool(obj): + pathParams['threshold'] = 2.001 * self.radius + + if self.endVector is not None: + pathParams['start'] = self.endVector + elif PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: + pathParams['start'] = obj.StartPoint + + obj.PathParams = str({key: value for key, value in pathParams.items() if key != 'shapes'}) + PathLog.debug("Path with params: {}".format(obj.PathParams)) + + (pp, end_vector) = Path.fromShapes(**pathParams) + PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) + self.endVector = end_vector # pylint: disable=attribute-defined-outside-init + + simobj = None + if getsim: + areaParams['Thicken'] = True + areaParams['ToolRadius'] = self.radius - self.radius * .005 + area.setParams(**areaParams) + sec = area.makeSections(mode=0, project=False, heights=heights)[-1].getShape() + simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax)) + + return pp, simobj + + def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ + '''opExecute(obj, getsim=False) ... implementation of Path.Area ops. + determines the parameters for _buildPathArea(). + Do not overwrite, implement + areaOpAreaParams(obj, isHole) ... op specific area param dictionary + areaOpPathParams(obj, isHole) ... op specific path param dictionary + areaOpShapes(obj) ... the shape for path area to process + areaOpUseProjection(obj) ... return true if operation can use projection + instead.''' + PathLog.track() + + # Instantiate class variables for operation reference + self.endVector = None # pylint: disable=attribute-defined-outside-init + self.rotateFlag = False # pylint: disable=attribute-defined-outside-init + self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init + self.cloneNames = [] # pylint: disable=attribute-defined-outside-init + self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init + self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init + self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init + self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones + + if obj.EnableRotation != 'Off': + # Calculate operation heights based upon rotation radii + opHeights = self.opDetermineRotationRadii(obj) + (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init + (self.clrOfset, self.safOfst) = opHeights[1] # pylint: disable=attribute-defined-outside-init + + # Set clearance and safe heights based upon rotation radii + if obj.EnableRotation == 'A(x)': + strDep = self.xRotRad + elif obj.EnableRotation == 'B(y)': + strDep = self.yRotRad + else: + strDep = max(self.xRotRad, self.yRotRad) + finDep = -1 * strDep + + obj.ClearanceHeight.Value = strDep + self.clrOfset + obj.SafeHeight.Value = strDep + self.safOfst + + #if self.initWithRotation is False: + # if obj.FinalDepth.Value == obj.OpFinalDepth.Value: + # obj.FinalDepth.Value = finDep + # if obj.StartDepth.Value == obj.OpStartDepth.Value: + # obj.StartDepth.Value = strDep + + # Create visual axes when debugging. + if PathLog.getLevel(PathLog.thisModule()) == 4: + self.visualAxis() + else: + strDep = obj.StartDepth.Value + finDep = obj.FinalDepth.Value + + # Set axial feed rates based upon horizontal feed rates + safeCircum = 2 * math.pi * obj.SafeHeight.Value + self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init + self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init + + # Initiate depthparams and calculate operation heights for rotational operation + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + self.depthparams = PathUtils.depth_params( # pylint: disable=attribute-defined-outside-init + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=obj.StartDepth.Value, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=obj.FinalDepth.Value, + user_depths=None) + + # Set start point + if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: + start = obj.StartPoint + else: + start = None + + aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return + + # Adjust tuples length received from other PathWB tools/operations beside PathPocketShape + shapes = [] + for shp in aOS: + if len(shp) == 2: + (fc, iH) = shp + # fc, iH, sub, angle, axis, strtDep, finDep + tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + else: + shapes.append(shp) + + if len(shapes) > 1: + jobs = [{ + 'x': s[0].BoundBox.XMax, + 'y': s[0].BoundBox.YMax, + 'shape': s + } for s in shapes] + + jobs = PathUtils.sort_jobs(jobs, ['x', 'y']) + + shapes = [j['shape'] for j in jobs] + + sims = [] + numShapes = len(shapes) + + for ns in range(0, numShapes): + (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable + if ns < numShapes - 1: + nextAxis = shapes[ns + 1][4] + else: + nextAxis = 'L' + + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + self.depthparams = PathUtils.depth_params( # pylint: disable=attribute-defined-outside-init + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=strDep, # obj.StartDepth.Value, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=finDep, # obj.FinalDepth.Value, + user_depths=None) + + try: + (pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim) + except Exception as e: # pylint: disable=broad-except + FreeCAD.Console.PrintError(e) + FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.") + else: + ppCmds = pp.Commands + if obj.EnableRotation != 'Off' and self.rotateFlag is True: + # Rotate model to index for cut + if axis == 'X': + axisOfRot = 'A' + elif axis == 'Y': + axisOfRot = 'B' + # Reverse angle temporarily to match model. Error in FreeCAD render of B axis rotations + if obj.B_AxisErrorOverride is True: + angle = -1 * angle + elif axis == 'Z': + axisOfRot = 'C' + else: + axisOfRot = 'A' + # Rotate Model to correct angle + ppCmds.insert(0, Path.Command('G1', {axisOfRot: angle, 'F': self.axialFeed})) + ppCmds.insert(0, Path.Command('N100', {})) + + # Raise cutter to safe depth and return index to starting position + ppCmds.append(Path.Command('N200', {})) + ppCmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if axis != nextAxis: + ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid})) + # Eif + + # Save gcode commands to object command list + self.commandlist.extend(ppCmds) + sims.append(sim) + # Eif + + if self.areaOpRetractTool(obj): + self.endVector = None # pylint: disable=attribute-defined-outside-init + + # Raise cutter to safe height and rotate back to original orientation + if self.rotateFlag is True: + self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + self.commandlist.append(Path.Command('G0', {'A': 0.0, 'F': self.axialRapid})) + self.commandlist.append(Path.Command('G0', {'B': 0.0, 'F': self.axialRapid})) + + self.useTempJobClones('Delete') # Delete temp job clone group and contents + self.guiMessage('title', None, show=True) # Process GUI messages to user + for ton in self.tempObjectNames: # remove temporary objects by name + FreeCAD.ActiveDocument.removeObject(ton) + PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n") + return sims + + def areaOpRetractTool(self, obj): + '''areaOpRetractTool(obj) ... return False to keep the tool at current level between shapes. Default is True.''' + # pylint: disable=unused-argument + return True + + def areaOpAreaParams(self, obj, isHole): + '''areaOpAreaParams(obj, isHole) ... return operation specific area parameters in a dictionary. + Note that the resulting parameters are stored in the property AreaParams. + Must be overwritten by subclasses.''' + # pylint: disable=unused-argument + pass # pylint: disable=unnecessary-pass + + def areaOpPathParams(self, obj, isHole): + '''areaOpPathParams(obj, isHole) ... return operation specific path parameters in a dictionary. + Note that the resulting parameters are stored in the property PathParams. + Must be overwritten by subclasses.''' + # pylint: disable=unused-argument + pass # pylint: disable=unnecessary-pass + + def areaOpShapes(self, obj): + '''areaOpShapes(obj) ... return all shapes to be processed by Path.Area for this op. + Must be overwritten by subclasses.''' + # pylint: disable=unused-argument + pass # pylint: disable=unnecessary-pass + + def areaOpUseProjection(self, obj): + '''areaOpUseProcjection(obj) ... return True if the operation can use procjection, defaults to False. + Can safely be overwritten by subclasses.''' + # pylint: disable=unused-argument + return False + + # Rotation-related methods + def opDetermineRotationRadii(self, obj): + '''opDetermineRotationRadii(obj) + Determine rotational radii for 4th-axis rotations, for clearance/safe heights ''' + + parentJob = PathUtils.findParentJob(obj) + # bb = parentJob.Stock.Shape.BoundBox + xlim = 0.0 + ylim = 0.0 + # zlim = 0.0 + + # Determine boundbox radius based upon xzy limits data + if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax): + zlim = self.stockBB.ZMin + else: + zlim = self.stockBB.ZMax + + if obj.EnableRotation != 'B(y)': + # Rotation is around X-axis, cutter moves along same axis + if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax): + ylim = self.stockBB.YMin + else: + ylim = self.stockBB.YMax + + if obj.EnableRotation != 'A(x)': + # Rotation is around Y-axis, cutter moves along same axis + if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax): + xlim = self.stockBB.XMin + else: + xlim = self.stockBB.XMax + + xRotRad = math.sqrt(ylim**2 + zlim**2) + yRotRad = math.sqrt(xlim**2 + zlim**2) + zRotRad = math.sqrt(xlim**2 + ylim**2) + + clrOfst = parentJob.SetupSheet.ClearanceHeightOffset.Value + safOfst = parentJob.SetupSheet.SafeHeightOffset.Value + + return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)] + + def faceRotationAnalysis(self, obj, norm, surf): + '''faceRotationAnalysis(obj, norm, surf) + Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) ''' + PathLog.track() + + praInfo = "faceRotationAnalysis()" + rtn = True + orientation = 'X' + angle = 500.0 + precision = 6 + + for i in range(0, 13): + if PathGeom.Tolerance * (i * 10) == 1.0: + precision = i + break + + def roundRoughValues(precision, val): + # Convert VALxe-15 numbers to zero + if PathGeom.isRoughly(0.0, val) is True: + return 0.0 + # Convert VAL.99999999 to next integer + elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: + return round(val) + else: + return round(val, precision) + + nX = roundRoughValues(precision, norm.x) + nY = roundRoughValues(precision, norm.y) + nZ = roundRoughValues(precision, norm.z) + praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ) + + saX = roundRoughValues(precision, surf.x) + saY = roundRoughValues(precision, surf.y) + saZ = roundRoughValues(precision, surf.z) + praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ) + + # Determine rotation needed and current orientation + if saX == 0.0: + if saY == 0.0: + orientation = "Z" + if saZ == 1.0: + angle = 0.0 + elif saZ == -1.0: + angle = -180.0 + else: + praInfo += "_else_X" + str(saZ) + elif saY == 1.0: + orientation = "Y" + angle = 90.0 + elif saY == -1.0: + orientation = "Y" + angle = -90.0 + else: + if saZ != 0.0: + angle = math.degrees(math.atan(saY / saZ)) + orientation = "Y" + elif saY == 0.0: + if saZ == 0.0: + orientation = "X" + if saX == 1.0: + angle = -90.0 + elif saX == -1.0: + angle = 90.0 + else: + praInfo += "_else_X" + str(saX) + else: + orientation = "X" + ratio = saX / saZ + angle = math.degrees(math.atan(ratio)) + if ratio < 0.0: + praInfo += " NEG-ratio" + # angle -= 90 + else: + praInfo += " POS-ratio" + angle = -1 * angle + if saX < 0.0: + angle = angle + 180.0 + elif saZ == 0.0: + # if saY != 0.0: + angle = math.degrees(math.atan(saX / saY)) + orientation = "Y" + + if saX + nX == 0.0: + angle = -1 * angle + if saY + nY == 0.0: + angle = -1 * angle + if saZ + nZ == 0.0: + angle = -1 * angle + + if saY == -1.0 or saY == 1.0: + if nX != 0.0: + angle = -1 * angle + + # Enforce enabled rotation in settings + praInfo += "\n -Initial orientation: {}".format(orientation) + if orientation == 'Y': + axis = 'X' + if obj.EnableRotation == 'B(y)': # Required axis disabled + if angle == 180.0 or angle == -180.0: + axis = 'Y' + else: + rtn = False + elif orientation == 'X': + axis = 'Y' + if obj.EnableRotation == 'A(x)': # Required axis disabled + if angle == 180.0 or angle == -180.0: + axis = 'X' + else: + rtn = False + elif orientation == 'Z': + axis = 'X' + + if math.fabs(angle) == 0.0: + angle = 0.0 + rtn = False + + if angle == 500.0: + angle = 0.0 + rtn = False + + if rtn is False: + if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True: + if obj.EnableRotation == 'B(y)': + axis = 'Y' + rtn = True + + if rtn is True: + self.rotateFlag = True # pylint: disable=attribute-defined-outside-init + if obj.ReverseDirection is True: + if angle < 180.0: + angle = angle + 180.0 + else: + angle = angle - 180.0 + angle = round(angle, precision) + + praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis) + if rtn is True: + praInfo += "\n - ... rotation triggered" + else: + praInfo += "\n - ... NO rotation triggered" + + PathLog.debug("\n" + str(praInfo)) + + return (rtn, angle, axis, praInfo) + + def guiMessage(self, title, msg, show=False): + '''guiMessage(title, msg, show=False) + Handle op related GUI messages to user''' + if msg is not None: + self.guiMsgs.append((title, msg)) + if show is True: + if len(self.guiMsgs) > 0: + if FreeCAD.GuiUp: + from PySide.QtGui import QMessageBox + for entry in self.guiMsgs: + (title, msg) = entry + QMessageBox.warning(None, title, msg) + self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init + return True + else: + for entry in self.guiMsgs: + (title, msg) = entry + PathLog.warning("{}:: {}".format(title, msg)) + self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init + return True + return False + + def visualAxis(self): + '''visualAxis() + Create visual X & Y axis for use in orientation of rotational operations + Triggered only for PathLog.debug''' + + if not FreeCAD.ActiveDocument.getObject('xAxCyl'): + xAx = 'xAxCyl' + yAx = 'yAxCyl' + # zAx = 'zAxCyl' + FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "visualAxis") + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject('visualAxis').Visibility = False + vaGrp = FreeCAD.ActiveDocument.getObject("visualAxis") + + FreeCAD.ActiveDocument.addObject("Part::Cylinder", xAx) + cyl = FreeCAD.ActiveDocument.getObject(xAx) + cyl.Label = xAx + cyl.Radius = self.xRotRad + cyl.Height = 0.01 + cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90)) + cyl.purgeTouched() + if FreeCAD.GuiUp: + cylGui = FreeCADGui.ActiveDocument.getObject(xAx) + cylGui.ShapeColor = (0.667, 0.000, 0.000) + cylGui.Transparency = 85 + cylGui.Visibility = False + vaGrp.addObject(cyl) + + FreeCAD.ActiveDocument.addObject("Part::Cylinder", yAx) + cyl = FreeCAD.ActiveDocument.getObject(yAx) + cyl.Label = yAx + cyl.Radius = self.yRotRad + cyl.Height = 0.01 + cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90)) + cyl.purgeTouched() + if FreeCAD.GuiUp: + cylGui = FreeCADGui.ActiveDocument.getObject(yAx) + cylGui.ShapeColor = (0.000, 0.667, 0.000) + cylGui.Transparency = 85 + cylGui.Visibility = False + vaGrp.addObject(cyl) + + def useTempJobClones(self, cloneName): + '''useTempJobClones(cloneName) + Manage use of temporary model clones for rotational operation calculations. + Clones are stored in 'rotJobClones' group.''' + if FreeCAD.ActiveDocument.getObject('rotJobClones'): + if cloneName == 'Start': + if PathLog.getLevel(PathLog.thisModule()) < 4: + for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: + FreeCAD.ActiveDocument.removeObject(cln.Name) + elif cloneName == 'Delete': + if PathLog.getLevel(PathLog.thisModule()) < 4: + for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: + FreeCAD.ActiveDocument.removeObject(cln.Name) + FreeCAD.ActiveDocument.removeObject('rotJobClones') + else: + FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "rotJobClones") + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False + + if cloneName != 'Start' and cloneName != 'Delete': + FreeCAD.ActiveDocument.getObject('rotJobClones').addObject(FreeCAD.ActiveDocument.getObject(cloneName)) + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(cloneName).Visibility = False + + def cloneBaseAndStock(self, obj, base, angle, axis, subCount): + '''cloneBaseAndStock(obj, base, angle, axis, subCount) + Method called to create a temporary clone of the base and parent Job stock. + Clones are destroyed after usage for calculations related to rotational operations.''' + # Create a temporary clone and stock of model for rotational use. + rndAng = round(angle, 8) + if rndAng < 0.0: # neg sign is converted to underscore in clone name creation. + tag = axis + '_' + axis + '_' + str(math.fabs(rndAng)).replace('.', '_') + else: + tag = axis + str(rndAng).replace('.', '_') + clnNm = obj.Name + '_base_' + '_' + str(subCount) + '_' + tag + stckClnNm = obj.Name + '_stock_' + '_' + str(subCount) + '_' + tag + if clnNm not in self.cloneNames: + self.cloneNames.append(clnNm) + self.cloneNames.append(stckClnNm) + if FreeCAD.ActiveDocument.getObject(clnNm): + FreeCAD.ActiveDocument.getObject(clnNm).Shape = base.Shape + else: + FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape + self.useTempJobClones(clnNm) + if FreeCAD.ActiveDocument.getObject(stckClnNm): + FreeCAD.ActiveDocument.getObject(stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape + else: + FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape + self.useTempJobClones(stckClnNm) + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90 + FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000) + clnBase = FreeCAD.ActiveDocument.getObject(clnNm) + clnStock = FreeCAD.ActiveDocument.getObject(stckClnNm) + tag = base.Name + '_' + tag + return (clnBase, clnStock, tag) + + def getFaceNormAndSurf(self, face): + '''getFaceNormAndSurf(face) + Return face.normalAt(0,0) or face.normal(0,0) and face.Surface.Axis vectors + ''' + norm = FreeCAD.Vector(0.0, 0.0, 0.0) + surf = FreeCAD.Vector(0.0, 0.0, 0.0) + + if hasattr(face, 'normalAt'): + n = face.normalAt(0, 0) + elif hasattr(face, 'normal'): + n = face.normal(0, 0) + if hasattr(face.Surface, 'Axis'): + s = face.Surface.Axis + else: + s = n + norm.x = n.x + norm.y = n.y + norm.z = n.z + surf.x = s.x + surf.y = s.y + surf.z = s.z + return (norm, surf) + + def applyRotationalAnalysis(self, obj, base, angle, axis, subCount): + '''applyRotationalAnalysis(obj, base, angle, axis, subCount) + Create temp clone and stock and apply rotation to both. + Return new rotated clones + ''' + if axis == 'X': + vect = FreeCAD.Vector(1, 0, 0) + elif axis == 'Y': + vect = FreeCAD.Vector(0, 1, 0) + + if obj.InverseAngle is True: + angle = -1 * angle + if math.fabs(angle) == 0.0: + angle = 0.0 + + # Create a temporary clone of model for rotational use. + (clnBase, clnStock, tag) = self.cloneBaseAndStock(obj, base, angle, axis, subCount) + + # Rotate base to such that Surface.Axis of pocket bottom is Z=1 + clnBase = Draft.rotate(clnBase, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) + clnStock = Draft.rotate(clnStock, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) + + clnBase.purgeTouched() + clnStock.purgeTouched() + return (clnBase, angle, clnStock, tag) + + def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle): + '''applyInverseAngle(obj, clnBase, clnStock, axis, angle) + Apply rotations to incoming base and stock objects.''' + if axis == 'X': + vect = FreeCAD.Vector(1, 0, 0) + elif axis == 'Y': + vect = FreeCAD.Vector(0, 1, 0) + # Rotate base to inverse of original angle + clnBase = Draft.rotate(clnBase, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) + clnStock = Draft.rotate(clnStock, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False) + clnBase.purgeTouched() + clnStock.purgeTouched() + # Update property and angle values + obj.InverseAngle = True + obj.AttemptInverseAngle = False + angle = -1 * angle + + PathLog.info(translate("Path", "Rotated to inverse angle.")) + return (clnBase, clnStock, angle) + + def sortTuplesByIndex(self, TupleList, tagIdx): + '''sortTuplesByIndex(TupleList, tagIdx) + sort list of tuples based on tag index provided + return (TagList, GroupList) + ''' + # Separate elements, regroup by orientation (axis_angle combination) + TagList = ['X34.2'] + GroupList = [[(2.3, 3.4, 'X')]] + for tup in TupleList: + if tup[tagIdx] in TagList: + # Determine index of found string + i = 0 + for orn in TagList: + if orn == tup[4]: + break + i += 1 + GroupList[i].append(tup) + else: + TagList.append(tup[4]) # add orientation entry + GroupList.append([tup]) # add orientation entry + # Remove temp elements + TagList.pop(0) + GroupList.pop(0) + return (TagList, GroupList) + + def warnDisabledAxis(self, obj, axis, sub=''): + '''warnDisabledAxis(self, obj, axis) + Provide user feedback if required axis is disabled''' + if axis == 'X' and obj.EnableRotation == 'B(y)': + msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " + msg += translate('Path', "Selected feature(s) require 'Enable Rotation: A(x)' for access.") + PathLog.warning(msg) + return True + elif axis == 'Y' and obj.EnableRotation == 'A(x)': + msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " " + msg += translate('Path', "Selected feature(s) require 'Enable Rotation: B(y)' for access.") + PathLog.warning(msg) + return True + else: + return False diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 0f4153937840..986c79d539d6 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -50,8 +50,8 @@ __doc__ = "Base class an implementation for operations on circular holes." __contributors__ = "russ4262 (Russell Johnson)" __created__ = "2017" -__scriptVersion__ = "1e testing" -__lastModified__ = "2019-07-26 14:15 CST" +__scriptVersion__ = "2b" +__lastModified__ = "2020-02-13 17:11 CST" # Qt translation handling @@ -60,7 +60,7 @@ def translate(context, text, disambig=None): PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) class ObjectOp(PathOp.ObjectOp): @@ -75,7 +75,7 @@ class ObjectOp(PathOp.ObjectOp): def opFeatures(self, obj): '''opFeatures(obj) ... calls circularHoleFeatures(obj) and ORs in the standard features required for processing circular holes. Do not overwrite, implement circularHoleFeatures(obj) instead''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj) | PathOp.FeatureCoolant + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj) | PathOp.FeatureCoolant def circularHoleFeatures(self, obj): '''circularHoleFeatures(obj) ... overwrite to add operations specific features. @@ -136,15 +136,14 @@ def holeDiameter(self, obj, base, sub): if shape.ShapeType == 'Edge' and type(shape.Curve) == Part.Circle: return shape.Curve.Radius * 2 - + if shape.ShapeType == 'Face': for i in range(len(shape.Edges)): - if (type(shape.Edges[i].Curve) == Part.Circle and - shape.Edges[i].Curve.Radius * 2 < shape.BoundBox.XLength*1.1 and + if (type(shape.Edges[i].Curve) == Part.Circle and + shape.Edges[i].Curve.Radius * 2 < shape.BoundBox.XLength*1.1 and shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9): return shape.Edges[i].Curve.Radius * 2 - - + # for all other shapes the diameter is just the dimension in X. This may be inaccurate as the BoundBox is calculated on the tessellated geometry PathLog.warning(translate("Path", "Hole diameter may be inaccurate due to tessellation on face. Consider selecting hole edge.")) return shape.BoundBox.XLength @@ -207,7 +206,6 @@ def opExecute(self, obj): self.safeHeight = obj.SafeHeight.Value # pylint: disable=attribute-defined-outside-init self.axialFeed = 0.0 # pylint: disable=attribute-defined-outside-init self.axialRapid = 0.0 # pylint: disable=attribute-defined-outside-init - trgtDep = None def haveLocations(self, obj): if PathOp.FeatureLocations & self.opFeatures(obj): @@ -324,38 +322,29 @@ def haveLocations(self, obj): pos = self.holePosition(obj, base, sub) if pos: # Default is treat selection as 'Face' shape - finDep = base.Shape.getElement(sub).BoundBox.ZMin + holeBtm = base.Shape.getElement(sub).BoundBox.ZMin if base.Shape.getElement(sub).ShapeType == 'Edge': - msg = translate("Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm".format(sub, round(finDep, 4))) + " " + msg = translate("Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm".format(sub, round(holeBtm, 4))) + " " msg += translate("Path", "Always select the bottom edge of the hole when using an edge.") PathLog.warning(msg) - # If user has not adjusted Final Depth value, attempt to determine from sub - trgtDep = obj.FinalDepth.Value - if obj.OpFinalDepth.Value == obj.FinalDepth.Value: - trgtDep = finDep - if obj.FinalDepth.Value < finDep: - msg = translate("Path", "Final Depth setting is below the hole bottom for {}.".format(sub)) + # Warn user if Final Depth set lower than bottom of hole + if finDep < holeBtm: + msg = translate("Path", "Final Depth setting is below the hole bottom for {}.".format(sub)) + ' ' + msg += translate("Path", "{} depth is calculated at {} mm".format(sub, round(holeBtm, 4))) PathLog.warning(msg) holes.append({'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub), - 'angle': angle, 'axis': axis, 'trgtDep': trgtDep, + 'angle': angle, 'axis': axis, 'trgtDep': finDep, 'stkTop': stock.Shape.BoundBox.ZMax}) if haveLocations(self, obj): for location in obj.Locations: - # holes.append({'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'finDep': obj.FinalDepth.Value}) - trgtDep = obj.FinalDepth.Value + # holes.append({'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'holeBtm': obj.FinalDepth.Value}) holes.append({'x': location.x, 'y': location.y, 'r': 0, - 'angle': 0.0, 'axis': 'X', 'trgtDep': trgtDep, + 'angle': 0.0, 'axis': 'X', 'trgtDep': finDep, 'stkTop': PathUtils.findParentJob(obj).stock.Shape.BoundBox.ZMax}) - # If all holes based upon edges, set post-operation Final Depth to highest edge height - if obj.OpFinalDepth.Value == obj.FinalDepth.Value: - if len(holes) == 1: - PathLog.info(translate('Path', "Single-hole operation. Saving Final Depth determined from hole base.")) - obj.FinalDepth.Value = trgtDep - if len(holes) > 0: self.circularHoleExecute(obj, holes) # circularHoleExecute() located in PathDrilling.py @@ -822,23 +811,6 @@ def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle): angle = -1 * angle return (clnBase, clnStock, angle) - def calculateStartFinalDepths(self, obj, shape, stock): - '''calculateStartFinalDepths(obj, shape, stock) - Calculate correct start and final depths for the shape(face) object provided.''' - finDep = max(obj.FinalDepth.Value, shape.BoundBox.ZMin) - stockTop = stock.Shape.BoundBox.ZMax - if obj.EnableRotation == 'Off': - strDep = obj.StartDepth.Value - if strDep <= finDep: - strDep = stockTop - else: - strDep = min(obj.StartDepth.Value, stockTop) - if strDep <= finDep: - strDep = stockTop - msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.") - PathLog.error(msg) - return (strDep, finDep) - def sortTuplesByIndex(self, TupleList, tagIdx): '''sortTuplesByIndex(TupleList, tagIdx) sort list of tuples based on tag index provided diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py index 4e77c0001e46..6aa404a12bfc 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py @@ -52,6 +52,8 @@ class TaskPanelHoleGeometryPage(PathOpGui.TaskPanelBaseGeometryPage): DataObject = QtCore.Qt.ItemDataRole.UserRole + 1 DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 2 + InitBase = False + def getForm(self): '''getForm() ... load and return page''' return FreeCADGui.PySideUic.loadUi(":/panels/PageBaseHoleGeometryEdit.ui") diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index ef324a7dad2c..4a792322d350 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -1017,7 +1017,7 @@ def setupUi(self): if self.deleteOnReject and PathOp.FeatureBaseGeometry & self.obj.Proxy.opFeatures(self.obj): sel = FreeCADGui.Selection.getSelectionEx() for page in self.featurePages: - if hasattr(page, 'addBase'): + if getattr(page, 'InitBase', True) and hasattr(page, 'addBase'): page.clearBase() page.addBaseGeometry(sel) diff --git a/src/Mod/Path/PathScripts/PathPocket.py b/src/Mod/Path/PathScripts/PathPocket.py index 239faa4ff2b4..355178ff528d 100644 --- a/src/Mod/Path/PathScripts/PathPocket.py +++ b/src/Mod/Path/PathScripts/PathPocket.py @@ -37,11 +37,11 @@ __doc__ = "Class and implementation of the 3D Pocket operation." __contributors__ = "russ4262 (Russell Johnson)" __created__ = "2014" -__scriptVersion__ = "2g testing" -__lastModified__ = "2019-07-20 22:02 CST" +__scriptVersion__ = "2e" +__lastModified__ = "2020-02-13 17:22 CST" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) # Qt translation handling @@ -102,8 +102,11 @@ def areaOpShapes(self, obj): allSubsFaceType = False if allSubsFaceType is True and obj.HandleMultipleFeatures == 'Collectively': + (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) + if obj.FinalDepth.Value < fzmin: + PathLog.warning(translate('PathPocket', 'Final depth set below ZMin of face(s) selected.')) + ''' if obj.OpFinalDepth == obj.FinalDepth: - (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) obj.FinalDepth.Value = fzmin finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 self.depthparams = PathUtils.depth_params( @@ -115,6 +118,7 @@ def areaOpShapes(self, obj): final_depth=fzmin, user_depths=None) PathLog.info("Updated obj.FinalDepth.Value and self.depthparams to zmin: {}".format(fzmin)) + ''' if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True: pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups) @@ -148,8 +152,9 @@ def areaOpShapes(self, obj): PathLog.debug("processing the whole job base object") strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value - recomputeDepthparams = False + # recomputeDepthparams = False for base in self.model: + ''' if obj.OpFinalDepth == obj.FinalDepth: if base.Shape.BoundBox.ZMin < obj.FinalDepth.Value: obj.FinalDepth.Value = base.Shape.BoundBox.ZMin @@ -173,11 +178,13 @@ def areaOpShapes(self, obj): final_depth=obj.FinalDepth.Value, user_depths=None) recomputeDepthparams = False + ''' if obj.ProcessStockArea is True: job = PathUtils.findParentJob(obj) - finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + ''' + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 depthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, @@ -187,6 +194,8 @@ def areaOpShapes(self, obj): final_depth=base.Shape.BoundBox.ZMin, user_depths=None) stockEnvShape = PathUtils.getEnvelope(job.Stock.Shape, subshape=None, depthparams=depthparams) + ''' + stockEnvShape = PathUtils.getEnvelope(job.Stock.Shape, subshape=None, depthparams=self.depthparams) obj.removalshape = stockEnvShape.cut(base.Shape) obj.removalshape.tessellate(0.1) @@ -225,7 +234,6 @@ def calculateAdaptivePocket(self, obj, base, subObjTups): tryNonPlanar = False isHighFacePlanar = True isLowFacePlanar = True - faceType = 0 for (sub, face) in subObjTups: Faces.append(face) diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py index 5739ceb8bb5f..f89a6138705e 100644 --- a/src/Mod/Path/PathScripts/PathPocketShape.py +++ b/src/Mod/Path/PathScripts/PathPocketShape.py @@ -40,11 +40,11 @@ __doc__ = "Class and implementation of shape based Pocket operation." __contributors__ = "russ4262 (Russell Johnson)" __created__ = "2017" -__scriptVersion__ = "2i usable" -__lastModified__ = "2019-09-07 08:32 CST" +__scriptVersion__ = "2i" +__lastModified__ = "2020-02-13 17:01 CST" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) # Qt translation handling @@ -65,11 +65,12 @@ def endPoints(edgeOrWire): unique.append(p) return unique pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter) - plast = edgeOrWire.valueAt(edgeOrWire.LastParameter) + plast = edgeOrWire.valueAt(edgeOrWire.LastParameter) if PathGeom.pointsCoincide(pfirst, plast): return None return [pfirst, plast] + def includesPoint(p, pts): '''includesPoint(p, pts) ... answer True if the collection of pts includes the point p''' for pt in pts: @@ -83,7 +84,7 @@ def selectOffsetWire(feature, wires): closest = None for w in wires: dist = feature.distToShape(w)[0] - if closest is None or dist > closest[0]: # pylint: disable=unsubscriptable-object + if closest is None or dist > closest[0]: # pylint: disable=unsubscriptable-object closest = (dist, w) if closest is not None: return closest[1] @@ -116,6 +117,7 @@ def extendWire(feature, wire, length): return Part.Wire(edges) return None + class Extension(object): DirectionNormal = 0 DirectionX = 1 @@ -163,7 +165,7 @@ def _getEdges(self): return [self.obj.Shape.getElement(sub) for sub in self._getEdgeNames()] def _getDirectedNormal(self, p0, normal): - poffPlus = p0 + 0.01 * normal + poffPlus = p0 + 0.01 * normal poffMinus = p0 - 0.01 * normal if not self.obj.Shape.isInside(poffPlus, 0.005, True): return normal @@ -281,7 +283,6 @@ def areaOpShapes(self, obj): baseSubsTuples = [] subCount = 0 allTuples = [] - finalDepths = [] def planarFaceFromExtrusionEdges(face, trans): useFace = 'useFaceName' @@ -383,7 +384,7 @@ def clasifySub(self, bs, sub): if obj.Base: PathLog.debug('Processing... obj.Base') - self.removalshapes = [] # pylint: disable=attribute-defined-outside-init + self.removalshapes = [] # pylint: disable=attribute-defined-outside-init # ---------------------------------------------------------------------- if obj.EnableRotation == 'Off': stock = PathUtils.findParentJob(obj).Stock @@ -401,14 +402,14 @@ def clasifySub(self, bs, sub): PathLog.info("Common Surface.Axis or normalAt() value found for loop faces.") rtn = False subCount += 1 - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.info("angle: {}; axis: {}".format(angle, axis)) if rtn is True: faceNums = "" for f in subsList: faceNums += '_' + f.replace('Face', '') - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable + (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable # Verify faces are correctly oriented - InverseAngle might be necessary PathLog.debug("Checking if faces are oriented correctly after rotation...") @@ -458,7 +459,7 @@ def clasifySub(self, bs, sub): PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub))) (norm, surf) = self.getFaceNormAndSurf(face) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable if rtn is True: faceNum = sub.replace('Face', '') @@ -466,7 +467,7 @@ def clasifySub(self, bs, sub): # Verify faces are correctly oriented - InverseAngle might be necessary faceIA = clnBase.Shape.getElement(sub) (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + (rtn, praAngle, praAxis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable if rtn is True: PathLog.debug("Face not aligned after initial rotation.") if obj.AttemptInverseAngle is True and obj.InverseAngle is False: @@ -492,8 +493,8 @@ def clasifySub(self, bs, sub): PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub))) for o in baseSubsTuples: - self.horiz = [] # pylint: disable=attribute-defined-outside-init - self.vert = [] # pylint: disable=attribute-defined-outside-init + self.horiz = [] # pylint: disable=attribute-defined-outside-init + self.vert = [] # pylint: disable=attribute-defined-outside-init subBase = o[0] subsList = o[1] angle = o[2] @@ -545,7 +546,7 @@ def clasifySub(self, bs, sub): # move all horizontal faces to FinalDepth for f in self.horiz: - finDep = max(obj.FinalDepth.Value, f.BoundBox.ZMin) + finDep = obj.FinalDepth.Value # max(obj.FinalDepth.Value, f.BoundBox.ZMin) f.translate(FreeCAD.Vector(0, 0, finDep - f.BoundBox.ZMin)) # check all faces and see if they are touching/overlapping and combine those into a compound @@ -560,20 +561,17 @@ def clasifySub(self, bs, sub): else: self.horizontal.append(shape) + # extrude all faces up to StartDepth and those are the removal shapes + sD = obj.StartDepth.Value + fD = obj.FinalDepth.Value + extent = FreeCAD.Vector(0, 0, sD - fD) for face in self.horizontal: - # extrude all faces up to StartDepth and those are the removal shapes - (strDep, finDep) = self.calculateStartFinalDepths(obj, face, stock) - finalDepths.append(finDep) - extent = FreeCAD.Vector(0, 0, strDep - finDep) - self.removalshapes.append((face.removeSplitter().extrude(extent), False, 'pathPocketShape', angle, axis, strDep, finDep)) - PathLog.debug("Extent depths are str: {}, and fin: {}".format(strDep, finDep)) + self.removalshapes.append((face.removeSplitter().extrude(extent), + False, 'pathPocketShape', angle, axis, sD, fD)) + PathLog.debug("Extent depths are str: {}, and fin: {}".format(sD, fD)) # Efor face + # Efor - # Adjust obj.FinalDepth.Value as needed. - if len(finalDepths) > 0: - finalDep = min(finalDepths) - if subCount == 1: - obj.FinalDepth.Value = finalDep else: # process the job base object as a whole PathLog.debug(translate("Path", 'Processing model as a whole ...')) diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index 7f6c17c0acd6..9baa5704580e 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -1485,7 +1485,7 @@ void ViewProviderSketch::onSelectionChanged(const Gui::SelectionChanges& msg) // are we in edit? if (edit) { // ignore external object - if(msg.ObjName.size() && msg.DocName.size() && msg.DocName!=getObject()->getDocument()->getName()) + if(msg.Object.getObjectName().size() && msg.Object.getDocument()!=getObject()->getDocument()) return; bool handled=false; diff --git a/src/Mod/TechDraw/App/DrawProjGroup.cpp b/src/Mod/TechDraw/App/DrawProjGroup.cpp index 7225bdf4d931..9d1e1a8e8de6 100644 --- a/src/Mod/TechDraw/App/DrawProjGroup.cpp +++ b/src/Mod/TechDraw/App/DrawProjGroup.cpp @@ -60,7 +60,8 @@ const char* DrawProjGroup::ProjectionTypeEnums[] = {"Default", PROPERTY_SOURCE(TechDraw::DrawProjGroup, TechDraw::DrawViewCollection) -DrawProjGroup::DrawProjGroup(void) +DrawProjGroup::DrawProjGroup(void) : + m_lockScale(false) { static const char *group = "Base"; static const char *agroup = "Distribute"; @@ -97,20 +98,12 @@ void DrawProjGroup::onChanged(const App::Property* prop) TechDraw::DrawPage *page = getPage(); if (!isRestoring() && page) { if (prop == &Source) { -// std::vector sourceObjs = Source.getValues(); -// if (!sourceObjs.empty()) { -// if (!hasAnchor()) { -// // if we have a Source, but no Anchor, make an anchor -// Anchor.setValue(addProjection("Front")); //<<<<< semi-loop here! -// //add projection marks object as changed -> onChanged, but anchor value isn't set -// Anchor.purgeTouched(); //don't need to mark this -// } -// } else { -// //Source has been changed to null! Why? What to do? -// } + //nothing in particular } if (prop == &Scale) { - updateChildrenScale(); + if (!m_lockScale) { + updateChildrenScale(); + } } if (prop == &ProjectionType) { @@ -128,11 +121,7 @@ void DrawProjGroup::onChanged(const App::Property* prop) if (prop == &ScaleType) { double newScale = getScale(); if (ScaleType.isValue("Automatic")) { - //Recalculate scale if Group is too big or too small! - newScale = calculateAutomaticScale(); - if(std::abs(getScale() - newScale) > FLT_EPSILON) { - Scale.setValue(newScale); - } + //Nothing in particular } else if (ScaleType.isValue("Page")) { newScale = page->Scale.getValue(); if(std::abs(getScale() - newScale) > FLT_EPSILON) { @@ -176,8 +165,18 @@ App::DocumentObjectExecReturn *DrawProjGroup::execute(void) return DrawViewCollection::execute(); } - autoPositionChildren(); + if (ScaleType.isValue("Automatic")) { + if (!checkFit()) { + double newScale = autoScale(); + m_lockScale = true; + Scale.setValue(newScale); + Scale.purgeTouched(); + updateChildrenScale(); + m_lockScale = false; + } + } + autoPositionChildren(); return DrawViewCollection::execute(); } @@ -219,6 +218,7 @@ Base::BoundBox3d DrawProjGroup::getBoundingBox() const bb.ScaleX(1. / part->getScale()); bb.ScaleY(1. / part->getScale()); + bb.ScaleZ(1. / part->getScale()); // X and Y of dependent views are relative to the anchorView if (part != anchorView) { @@ -237,6 +237,7 @@ TechDraw::DrawPage * DrawProjGroup::getPage(void) const return findParentPage(); } +// obs? replaced by autoscale? // Function provided by Joe Dowsett, 2014 double DrawProjGroup::calculateAutomaticScale() const { @@ -248,15 +249,14 @@ double DrawProjGroup::calculateAutomaticScale() const arrangeViewPointers(viewPtrs); double width, height; - minimumBbViews(viewPtrs, width, height); //get 1:1 bbxs - // if Page.keepUpdated is false, and DrawViews have never been executed, - // bb's will be 0x0 and this routine will return 0!!! - // if we return 1.0, AutoScale will sort itself out once bb's are non-zero. + minimumBbViews(viewPtrs, width, height); //get SCALED boxes! + // if Page.keepUpdated is false, and DrawViews have never been executed, + // bb's will be 0x0 and this routine will return 0!!! + // if we return 1.0, AutoScale will sort itself out once bb's are non-zero. double bbFudge = 1.2; width *= bbFudge; height *= bbFudge; - // C++ Standard says casting bool to int gives 0 or 1 int numVertSpaces = (viewPtrs[0] || viewPtrs[3] || viewPtrs[7]) + (viewPtrs[2] || viewPtrs[5] || viewPtrs[9]) + @@ -284,26 +284,36 @@ double DrawProjGroup::calculateAutomaticScale() const return result; } +//returns the (scaled) bounding rectangle of all the views. QRectF DrawProjGroup::getRect() const //this is current rect, not potential rect { +// Base::Console().Message("DPG::getRect - views: %d\n", Views.getValues().size()); DrawProjGroupItem *viewPtrs[10]; arrangeViewPointers(viewPtrs); double width, height; - minimumBbViews(viewPtrs, width, height); // w,h of just the views at 1:1 scale + minimumBbViews(viewPtrs, width, height); //this is scaled! double xSpace = spacingX.getValue() * 3.0 * std::max(1.0,getScale()); double ySpace = spacingY.getValue() * 2.0 * std::max(1.0,getScale()); - double rectW = getScale() * width + xSpace; //scale the 1:1 w,h and add whitespace - double rectH = getScale() * height + ySpace; + double rectW = 0.0; + double rectH = 0.0; + if ( !(DrawUtil::fpCompare(width, 0.0) && + DrawUtil::fpCompare(height, 0.0)) ) { + rectW = width + xSpace; + rectH = height + ySpace; + } + double fudge = 1.3; //make rect a little big to make sure it fits + rectW *= fudge; + rectH *= fudge; return QRectF(0,0,rectW,rectH); } -//find area consumed by Views only in 1:1 Scale +//find area consumed by Views only in current scale void DrawProjGroup::minimumBbViews(DrawProjGroupItem *viewPtrs[10], double &width, double &height) const { // Get bounding boxes in object scale Base::BoundBox3d bboxes[10]; - makeViewBbs(viewPtrs, bboxes, true); + makeViewBbs(viewPtrs, bboxes, true); //true => scaled //TODO: note that TLF/TRF/BLF,BRF extend a bit farther than a strict row/col arrangement would suggest. //get widest view in each row/column @@ -607,6 +617,7 @@ gp_Dir DrawProjGroup::vec2dir(Base::Vector3d v) return result; } +//this can be improved. this implementation positions views too far apart. Base::Vector3d DrawProjGroup::getXYPosition(const char *viewTypeCStr) { Base::Vector3d result(0.0,0.0,0.0); @@ -635,14 +646,14 @@ Base::Vector3d DrawProjGroup::getXYPosition(const char *viewTypeCStr) // Calculate bounding boxes for each displayed view Base::BoundBox3d bboxes[10]; - makeViewBbs(viewPtrs, bboxes); + makeViewBbs(viewPtrs, bboxes); //scaled - double xSpacing = spacingX.getValue(); //in mm/scale - double ySpacing = spacingY.getValue(); //in mm/scale + double xSpacing = spacingX.getValue(); //in mm, no scale + double ySpacing = spacingY.getValue(); //in mm, no scale double bigRow = 0.0; double bigCol = 0.0; - for (auto& b: bboxes) { + for (auto& b: bboxes) { //space based on width/height of biggest view if (!b.IsValid()) { continue; } @@ -659,6 +670,7 @@ Base::Vector3d DrawProjGroup::getXYPosition(const char *viewTypeCStr) bigCol = std::max(bigCol,bigRow); bigRow = bigCol; } + //TODO: find biggest for each row/column and adjust calculation to use bigCol[i], bigRow[j] ????? if (viewPtrs[0] && //iso bboxes[0].IsValid()) { @@ -791,7 +803,7 @@ int DrawProjGroup::getViewIndex(const char *viewTypeCStr) const void DrawProjGroup::arrangeViewPointers(DrawProjGroupItem *viewPtrs[10]) const { for (int i=0; i<10; ++i) { - viewPtrs[i] = NULL; + viewPtrs[i] = nullptr; } // Determine layout - should be either "First Angle" or "Third Angle" @@ -871,7 +883,9 @@ void DrawProjGroup::makeViewBbs(DrawProjGroupItem *viewPtrs[10], Base::BoundBox3d bboxes[10], bool documentScale) const { - for (int i = 0; i < 10; ++i) + Base::BoundBox3d empty(Base::Vector3d(0.0, 0.0, 0.0), 0.0); + for (int i = 0; i < 10; ++i) { + bboxes[i] = empty; if (viewPtrs[i]) { bboxes[i] = viewPtrs[i]->getBoundingBox(); if (!documentScale) { @@ -880,12 +894,8 @@ void DrawProjGroup::makeViewBbs(DrawProjGroupItem *viewPtrs[10], bboxes[i].ScaleY(scale); bboxes[i].ScaleZ(scale); } - } else { - // BoundBox3d defaults to length=(FLOAT_MAX + -FLOAT_MAX) - bboxes[i].ScaleX(0); - bboxes[i].ScaleY(0); - bboxes[i].ScaleZ(0); } + } } void DrawProjGroup::recomputeChildren(void) @@ -928,6 +938,7 @@ void DrawProjGroup::updateChildrenScale(void) throw Base::TypeError("Error: projection in DPG list is not a DPGI!"); } else if(view->Scale.getValue()!=Scale.getValue()) { view->Scale.setValue(Scale.getValue()); + view->recomputeFeature(); } } } @@ -982,34 +993,6 @@ void DrawProjGroup::updateChildrenEnforce(void) } } -/*! - * check if ProjectionGroup fits on Page - */ -bool DrawProjGroup::checkFit(TechDraw::DrawPage* p) const -{ - bool result = true; - - QRectF viewBox = getRect(); - double fudge = 1.1; - double maxWidth = viewBox.width() * fudge; - double maxHeight = viewBox.height() * fudge; - - if ( (maxWidth > p->getPageWidth()) || - (maxHeight > p->getPageHeight()) ) { - result = false; - } - - if (ScaleType.isValue("Automatic")) { //expand if too small - double magnifyLimit = 0.60; - if ( (maxWidth < p->getPageWidth() * magnifyLimit) && - (maxHeight < p->getPageHeight() * magnifyLimit) ) { - result = false; - } - } - return result; -} - - App::Enumeration DrawProjGroup::usedProjectionType(void) { //TODO: Would've been nice to have an Enumeration(const PropertyEnumeration &) constructor diff --git a/src/Mod/TechDraw/App/DrawProjGroup.h b/src/Mod/TechDraw/App/DrawProjGroup.h index 011c8731aa48..23d5367104b4 100644 --- a/src/Mod/TechDraw/App/DrawProjGroup.h +++ b/src/Mod/TechDraw/App/DrawProjGroup.h @@ -69,7 +69,6 @@ class TechDrawExport DrawProjGroup : public TechDraw::DrawViewCollection Base::BoundBox3d getBoundingBox() const; double calculateAutomaticScale() const; virtual QRectF getRect(void) const override; - virtual bool checkFit(TechDraw::DrawPage* p) const override; /// Check if container has a view of a specific type bool hasProjection(const char *viewProjType) const; @@ -175,7 +174,8 @@ class TechDrawExport DrawProjGroup : public TechDraw::DrawViewCollection gp_Dir vec2dir(Base::Vector3d v); virtual void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property * prop) override; - + + bool m_lockScale; }; } //namespace TechDraw diff --git a/src/Mod/TechDraw/App/DrawProjGroupItem.h b/src/Mod/TechDraw/App/DrawProjGroupItem.h index d8a7cc0447d1..7ecbe1aec947 100644 --- a/src/Mod/TechDraw/App/DrawProjGroupItem.h +++ b/src/Mod/TechDraw/App/DrawProjGroupItem.h @@ -85,6 +85,9 @@ class TechDrawExport DrawProjGroupItem : public TechDraw::DrawViewPart void autoPosition(void); bool isAnchor(void) const; + //DPGI always fits on page since DPG handles scaling + virtual bool checkFit(void) const override { return true; } + virtual bool checkFit(DrawPage*) const override { return true; } protected: void onChanged(const App::Property* prop) override; diff --git a/src/Mod/TechDraw/App/DrawView.cpp b/src/Mod/TechDraw/App/DrawView.cpp index f23caf129305..39f8407ff0cd 100644 --- a/src/Mod/TechDraw/App/DrawView.cpp +++ b/src/Mod/TechDraw/App/DrawView.cpp @@ -262,30 +262,60 @@ DrawViewClip* DrawView::getClipGroup(void) return result; } +double DrawView::autoScale(void) const +{ + auto page = findParentPage(); + double w = page->getPageWidth(); + double h = page->getPageHeight(); + return autoScale(w,h); +} -double DrawView::autoScale(double w, double h) const +//compare 1:1 rect of view to pagesize(pw,h) +double DrawView::autoScale(double pw, double ph) const { - double fudgeFactor = 0.90; - QRectF viewBox = getRect(); +// Base::Console().Message("DV::autoScale(Page: %.3f, %.3f) - %s\n", pw, ph, getNameInDocument()); + double fudgeFactor = 1.0; //make it a bit smaller just in case. + QRectF viewBox = getRect(); //getRect is scaled (ie current actual size) + if (!viewBox.isValid()) { + return 1.0; + } //have to unscale rect to determine new scale double vbw = viewBox.width()/getScale(); double vbh = viewBox.height()/getScale(); - double xScale = w/vbw; - double yScale = h/vbh; - //TODO: find a standard scale that's close? 1:2, 1:10, 1:100...? Logic in TaskProjGroup - double newScale = fudgeFactor * std::min(xScale,yScale); - newScale = DrawUtil::sensibleScale(newScale); - return newScale; + double xScale = pw/vbw; // > 1 page bigger than figure + double yScale = ph/vbh; // < 1 page is smaller than figure + double newScale = std::min(xScale,yScale) * fudgeFactor; + double sensibleScale = DrawUtil::sensibleScale(newScale); + return sensibleScale; +} + +bool DrawView::checkFit(void) const +{ + auto page = findParentPage(); + return checkFit(page); } -//!check if View fits on Page +//!check if View is too big for page +//should check if unscaled rect is too big for page bool DrawView::checkFit(TechDraw::DrawPage* p) const { bool result = true; - QRectF viewBox = getRect(); - if ( (viewBox.width() > p->getPageWidth()) || - (viewBox.height() > p->getPageHeight()) ) { - result = false; + double fudge = 1.1; + + double width = 0.0; + double height = 0.0; + QRectF viewBox = getRect(); //rect is scaled + if (!viewBox.isValid()) { + result = true; + } else { + width = viewBox.width() / getScale(); //unscaled rect w x h + height = viewBox.height() / getScale(); + width *= fudge; + height *= fudge; + if ( (width > p->getPageWidth()) || + (height > p->getPageHeight()) ) { + result = false; + } } return result; } diff --git a/src/Mod/TechDraw/App/DrawView.h b/src/Mod/TechDraw/App/DrawView.h index 31d0cfd22be7..d7f3c0838eec 100644 --- a/src/Mod/TechDraw/App/DrawView.h +++ b/src/Mod/TechDraw/App/DrawView.h @@ -83,7 +83,9 @@ class TechDrawExport DrawView : public App::DocumentObject virtual DrawPage* findParentPage() const; virtual QRectF getRect() const; //must be overridden by derived class + virtual double autoScale(void) const; virtual double autoScale(double w, double h) const; + virtual bool checkFit(void) const; virtual bool checkFit(DrawPage*) const; virtual void setPosition(double x, double y, bool force = false); bool keepUpdated(void); @@ -106,8 +108,6 @@ class TechDrawExport DrawView : public App::DocumentObject int prefScaleType(void); double prefScale(void); - - private: static const char* ScaleTypeEnums[]; static App::PropertyFloatConstraint::Constraints scaleRange; diff --git a/src/Mod/TechDraw/App/DrawViewArch.cpp b/src/Mod/TechDraw/App/DrawViewArch.cpp index 8fd53be71adb..ef471ac7f265 100644 --- a/src/Mod/TechDraw/App/DrawViewArch.cpp +++ b/src/Mod/TechDraw/App/DrawViewArch.cpp @@ -103,6 +103,14 @@ App::DocumentObjectExecReturn *DrawViewArch::execute(void) } App::DocumentObject* sourceObj = Source.getValue(); + //if (sourceObj is not ArchSection) return + App::Property* proxy = sourceObj->getPropertyByName("Proxy"); + if (proxy == nullptr) { + Base::Console().Error("DVA::execute - %s is not an ArchSection\n", sourceObj->Label.getValue()); + //this is definitely not an ArchSection + return DrawView::execute(); + } + if (sourceObj) { std::string svgFrag; std::string svgHead = getSVGHead(); diff --git a/src/Mod/TechDraw/App/DrawViewDetail.cpp b/src/Mod/TechDraw/App/DrawViewDetail.cpp index f6467daf5767..45032d4410ba 100644 --- a/src/Mod/TechDraw/App/DrawViewDetail.cpp +++ b/src/Mod/TechDraw/App/DrawViewDetail.cpp @@ -225,6 +225,29 @@ App::DocumentObjectExecReturn *DrawViewDetail::execute(void) //unblock } + detailExec(shape, dvp, dvs); + + //second pass if required + if (ScaleType.isValue("Automatic")) { + if (!checkFit()) { + double newScale = autoScale(); + Scale.setValue(newScale); + Scale.purgeTouched(); + if (geometryObject != nullptr) { + delete geometryObject; + geometryObject = nullptr; + detailExec(shape, dvp, dvs); + } + } + } + dvp->requestPaint(); //to refresh detail highlight! + return DrawView::execute(); +} + +void DrawViewDetail::detailExec(TopoDS_Shape shape, + DrawViewPart* dvp, + DrawViewSection* dvs) +{ Base::Vector3d anchor = AnchorPoint.getValue(); //this is a 2D point (in unrotated coords) Base::Vector3d dirDetail = dvp->Direction.getValue(); @@ -269,7 +292,7 @@ App::DocumentObjectExecReturn *DrawViewDetail::execute(void) TopoDS_Face aProjFace = mkFace.Face(); if(aProjFace.IsNull()) { Base::Console().Warning("DVD::execute - %s - failed to create tool base face\n", getNameInDocument()); - return DrawView::execute(); + return; } Base::Vector3d extrudeVec = dirDetail * extrudeLength; @@ -279,11 +302,11 @@ App::DocumentObjectExecReturn *DrawViewDetail::execute(void) BRepAlgoAPI_Common mkCommon(copyShape,tool); if (!mkCommon.IsDone()) { Base::Console().Warning("DVD::execute - %s - detail cut operation failed (1)\n", getNameInDocument()); - return DrawView::execute(); + return; } if (mkCommon.Shape().IsNull()) { Base::Console().Warning("DVD::execute - %s - detail cut operation failed (2)\n", getNameInDocument()); - return DrawView::execute(); + return; } //Did we get a solid? @@ -310,7 +333,7 @@ App::DocumentObjectExecReturn *DrawViewDetail::execute(void) } dvp->requestPaint(); Base::Console().Warning("DVD::execute - %s - detail area contains no geometry\n", getNameInDocument()); - return DrawView::execute(); + return; } //for debugging show compound instead of common @@ -358,7 +381,7 @@ App::DocumentObjectExecReturn *DrawViewDetail::execute(void) } catch (Standard_Failure& e4) { Base::Console().Log("LOG - DVD::execute - extractFaces failed for %s - %s **\n",getNameInDocument(),e4.GetMessageString()); - return new App::DocumentObjectExecReturn(e4.GetMessageString()); + return; } } @@ -366,18 +389,12 @@ App::DocumentObjectExecReturn *DrawViewDetail::execute(void) } catch (Standard_Failure& e1) { Base::Console().Message("LOG - DVD::execute - failed to create detail %s - %s **\n",getNameInDocument(),e1.GetMessageString()); - - return new App::DocumentObjectExecReturn(e1.GetMessageString()); + return; } addCosmeticVertexesToGeom(); addCosmeticEdgesToGeom(); addCenterLinesToGeom(); - - requestPaint(); - dvp->requestPaint(); //to refresh detail highlight! - - return DrawView::execute(); } double DrawViewDetail::getFudgeRadius() diff --git a/src/Mod/TechDraw/App/DrawViewDetail.h b/src/Mod/TechDraw/App/DrawViewDetail.h index 1ed657798210..496bfa7c6572 100644 --- a/src/Mod/TechDraw/App/DrawViewDetail.h +++ b/src/Mod/TechDraw/App/DrawViewDetail.h @@ -69,7 +69,9 @@ class TechDrawExport DrawViewDetail : public DrawViewPart return "TechDrawGui::ViewProviderViewPart"; } -public: + void detailExec(TopoDS_Shape s, + DrawViewPart* baseView, + DrawViewSection* sectionAlias); double getFudgeRadius(void); protected: diff --git a/src/Mod/TechDraw/App/DrawViewPart.cpp b/src/Mod/TechDraw/App/DrawViewPart.cpp index 209d8cba5ae3..ac572df4e721 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.cpp +++ b/src/Mod/TechDraw/App/DrawViewPart.cpp @@ -167,6 +167,8 @@ DrawViewPart::DrawViewPart(void) : geometryObject = nullptr; getRunControl(); + //initialize bbox to non-garbage + bbox = Base::BoundBox3d(Base::Vector3d(0.0, 0.0, 0.0), 0.0); } DrawViewPart::~DrawViewPart() @@ -254,33 +256,32 @@ App::DocumentObjectExecReturn *DrawViewPart::execute(void) XDirection.purgeTouched(); //don't trigger updates! //unblock } - -// m_saveShape = shape; - geometryObject = makeGeometryForShape(shape); - -#if MOD_TECHDRAW_HANDLE_FACES auto start = std::chrono::high_resolution_clock::now(); - if (handleFaces() && !geometryObject->usePolygonHLR()) { - try { - extractFaces(); - } - catch (Standard_Failure& e4) { - Base::Console().Log("LOG - DVP::execute - extractFaces failed for %s - %s **\n",getNameInDocument(),e4.GetMessageString()); - return new App::DocumentObjectExecReturn(e4.GetMessageString()); + + m_saveShape = shape; + partExec(shape); + + //second pass if required + if (ScaleType.isValue("Automatic")) { + if (!checkFit()) { + double newScale = autoScale(); + Scale.setValue(newScale); + Scale.purgeTouched(); + if (geometryObject != nullptr) { + delete geometryObject; + geometryObject = nullptr; + partExec(shape); + } } } - addCosmeticVertexesToGeom(); - addCosmeticEdgesToGeom(); - addCenterLinesToGeom(); - auto end = std::chrono::high_resolution_clock::now(); auto diff = end - start; double diffOut = std::chrono::duration (diff).count(); Base::Console().Log("TIMING - %s DVP spent: %.3f millisecs handling Faces\n", getNameInDocument(),diffOut); -#endif //#if MOD_TECHDRAW_HANDLE_FACES +//#endif //#if MOD_TECHDRAW_HANDLE_FACES // Base::Console().Message("DVP::execute - exits\n"); return DrawView::execute(); } @@ -318,6 +319,28 @@ void DrawViewPart::onChanged(const App::Property* prop) //TODO: when scale changes, any Dimensions for this View sb recalculated. DVD should pick this up subject to topological naming issues. } +void DrawViewPart::partExec(TopoDS_Shape shape) +{ +// Base::Console().Message("DVP::partExec()\n"); + geometryObject = makeGeometryForShape(shape); + +#if MOD_TECHDRAW_HANDLE_FACES +// auto start = std::chrono::high_resolution_clock::now(); + if (handleFaces() && !geometryObject->usePolygonHLR()) { + try { + extractFaces(); + } + catch (Standard_Failure& e4) { + Base::Console().Log("LOG - DVP::partExec - extractFaces failed for %s - %s **\n",getNameInDocument(),e4.GetMessageString()); + } + } +#endif //#if MOD_TECHDRAW_HANDLE_FACES + + addCosmeticVertexesToGeom(); + addCosmeticEdgesToGeom(); + addCenterLinesToGeom(); +} + GeometryObject* DrawViewPart::makeGeometryForShape(TopoDS_Shape shape) { // Base::Console().Message("DVP::makeGeometryforShape() - %s\n", Label.getValue()); @@ -725,15 +748,10 @@ double DrawViewPart::getBoxY(void) const QRectF DrawViewPart::getRect() const { +// Base::Console().Message("DVP::getRect() - %s\n", getNameInDocument()); double x = getBoxX(); double y = getBoxY(); - QRectF result; - if (std::isinf(x) || std::isinf(y)) { - //geometry isn't created yet. return an arbitrary rect. - result = QRectF(0.0,0.0,100.0,100.0); - } else { - result = QRectF(0.0,0.0,getBoxX(),getBoxY()); //this is from GO and is already scaled - } + QRectF result(0.0, 0.0, x, y); return result; } diff --git a/src/Mod/TechDraw/App/DrawViewPart.h b/src/Mod/TechDraw/App/DrawViewPart.h index 995f49f90453..bc06e41efcec 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.h +++ b/src/Mod/TechDraw/App/DrawViewPart.h @@ -201,6 +201,7 @@ class TechDrawExport DrawViewPart : public DrawView, public CosmeticExtension virtual TechDraw::GeometryObject* buildGeometryObject(TopoDS_Shape shape, gp_Ax2 viewAxis); //const?? virtual TechDraw::GeometryObject* makeGeometryForShape(TopoDS_Shape shape); //const?? + void partExec(TopoDS_Shape shape); void extractFaces(); diff --git a/src/Mod/TechDraw/App/DrawViewSection.cpp b/src/Mod/TechDraw/App/DrawViewSection.cpp index 589c2b81f84f..0e88be3dbccf 100644 --- a/src/Mod/TechDraw/App/DrawViewSection.cpp +++ b/src/Mod/TechDraw/App/DrawViewSection.cpp @@ -195,14 +195,20 @@ void DrawViewSection::onChanged(const App::Property* prop) if ((prop == &FileHatchPattern) && (doc != nullptr) ) { if (!FileHatchPattern.isEmpty()) { - replaceSvgIncluded(FileHatchPattern.getValue()); + Base::FileInfo fi(FileHatchPattern.getValue()); + if (fi.isReadable()) { + replaceSvgIncluded(FileHatchPattern.getValue()); + } } } if ( (prop == &FileGeomPattern) && (doc != nullptr) ) { if (!FileGeomPattern.isEmpty()) { - replacePatIncluded(FileGeomPattern.getValue()); + Base::FileInfo fi(FileGeomPattern.getValue()); + if (fi.isReadable()) { + replacePatIncluded(FileGeomPattern.getValue()); + } } } } @@ -213,19 +219,24 @@ void DrawViewSection::onChanged(const App::Property* prop) std::string fileSpec = FileGeomPattern.getValue(); Base::FileInfo fi(fileSpec); std::string ext = fi.extension(); - if ( (ext == "pat") || - (ext == "PAT") ) { - if ((!fileSpec.empty()) && - (!NameGeomPattern.isEmpty())) { - std::vector specs = - DrawGeomHatch::getDecodedSpecsFromFile(fileSpec, - NameGeomPattern.getValue()); - m_lineSets.clear(); - for (auto& hl: specs) { - //hl.dump("hl from section"); - LineSet ls; - ls.setPATLineSpec(hl); - m_lineSets.push_back(ls); + if (!fi.isReadable()) { + Base::Console().Message("%s can not read hatch file: %s\n", getNameInDocument(), fileSpec.c_str()); + Base::Console().Message("%s using included hatch file.\n", getNameInDocument()); + } else { + if ( (ext == "pat") || + (ext == "PAT") ) { + if ((!fileSpec.empty()) && + (!NameGeomPattern.isEmpty())) { + std::vector specs = + DrawGeomHatch::getDecodedSpecsFromFile(fileSpec, + NameGeomPattern.getValue()); + m_lineSets.clear(); + for (auto& hl: specs) { + //hl.dump("hl from section"); + LineSet ls; + ls.setPATLineSpec(hl); + m_lineSets.push_back(ls); + } } } } @@ -308,6 +319,29 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) //unblock } + sectionExec(baseShape); + + //second pass if required + if (ScaleType.isValue("Automatic")) { + if (!checkFit()) { + double newScale = autoScale(); + Scale.setValue(newScale); + Scale.purgeTouched(); + if (geometryObject != nullptr) { + delete geometryObject; + geometryObject = nullptr; + sectionExec(baseShape); + } + } + } + + + dvp->requestPaint(); //to refresh section line + return DrawView::execute(); +} + +void DrawViewSection::sectionExec(TopoDS_Shape baseShape) +{ //is SectionOrigin valid? Bnd_Box centerBox; BRepBndLib::Add(baseShape, centerBox); @@ -326,7 +360,8 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) BRepBuilderAPI_MakeFace mkFace(pln, -dMax,dMax,-dMax,dMax); TopoDS_Face aProjFace = mkFace.Face(); if(aProjFace.IsNull()) { - return new App::DocumentObjectExecReturn("DrawViewSection - Projected face is NULL"); + Base::Console().Warning("DVS: Section face is NULL in %s\n",getNameInDocument()); + return; } gp_Vec extrudeDir = dMax * gp_Vec(gpNormal); TopoDS_Shape prism = BRepPrimAPI_MakePrism(aProjFace, extrudeDir, false, true).Shape(); @@ -337,7 +372,8 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) BRepAlgoAPI_Cut mkCut(myShape, prism); if (!mkCut.IsDone()) { - return new App::DocumentObjectExecReturn("Section cut has failed"); + Base::Console().Warning("DVS: Section cut has failed in %s\n",getNameInDocument()); + return; } TopoDS_Shape rawShape = mkCut.Shape(); @@ -353,7 +389,7 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) testBox.SetGap(0.0); if (testBox.IsVoid()) { //prism & input don't intersect. rawShape is garbage, don't bother. Base::Console().Warning("DVS::execute - prism & input don't intersect - %s\n", Label.getValue()); - return DrawView::execute(); + return; } gp_Ax2 viewAxis; @@ -389,7 +425,7 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) catch (Standard_Failure& e1) { Base::Console().Warning("DVS::execute - failed to build base shape %s - %s **\n", getNameInDocument(),e1.GetMessageString()); - return DrawView::execute(); + return; } try { @@ -451,15 +487,12 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) catch (Standard_Failure& e2) { Base::Console().Warning("DVS::execute - failed to build section faces for %s - %s **\n", getNameInDocument(),e2.GetMessageString()); - return DrawView::execute(); + return; } addCosmeticVertexesToGeom(); addCosmeticEdgesToGeom(); addCenterLinesToGeom(); - - dvp->requestPaint(); //to refresh section line - return DrawView::execute(); } gp_Pln DrawViewSection::getSectionPlane() const diff --git a/src/Mod/TechDraw/App/DrawViewSection.h b/src/Mod/TechDraw/App/DrawViewSection.h index fdc9c2ebfb31..5664803d1bdd 100644 --- a/src/Mod/TechDraw/App/DrawViewSection.h +++ b/src/Mod/TechDraw/App/DrawViewSection.h @@ -90,6 +90,8 @@ class TechDrawExport DrawViewSection : public DrawViewPart virtual void unsetupObject() override; virtual short mustExecute() const override; + void sectionExec(TopoDS_Shape s); + std::vector getFaceGeometry(); void setCSFromBase(const std::string sectionName); diff --git a/src/Mod/TechDraw/Gui/Command.cpp b/src/Mod/TechDraw/Gui/Command.cpp index ec5c57d3a48f..6434468041ea 100644 --- a/src/Mod/TechDraw/Gui/Command.cpp +++ b/src/Mod/TechDraw/Gui/Command.cpp @@ -30,7 +30,9 @@ #include +#include #include +#include #include #include #include @@ -1153,6 +1155,43 @@ void CmdTechDrawArchView::activated(int iMsg) QObject::tr("Select exactly one object.")); return; } + //if the docObj doesn't have a Proxy property, it definitely isn't an ArchSection + App::DocumentObject* frontObj = objects.front(); + App::Property* proxy = frontObj->getPropertyByName("Proxy"); + if (proxy == nullptr) { + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), + QObject::tr("Selected object is not ArchSection.")); + return; + } + App::PropertyPythonObject* proxyPy = dynamic_cast(proxy); + Py::Object proxyObj = proxyPy->getValue(); + std::stringstream ss; + bool proceed = false; + if (proxyPy != nullptr) { + Base::PyGILStateLocker lock; + try { + if (proxyObj.hasAttr("__module__")) { + Py::String mod(proxyObj.getAttr("__module__")); + ss << (std::string)mod; + } + if (ss.str() == "ArchSectionPlane") { + proceed = true; + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + proceed = false; + } + } else { + proceed = false; + } + + if (!proceed) { + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), + QObject::tr("Selected object is not ArchSection.")); + return; + } std::string PageName = page->getNameInDocument(); diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui index 2ee018e38b3f..689c6881ac08 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui @@ -23,7 +23,7 @@ - TechDraw General + General @@ -216,7 +216,7 @@ for ProjectionGroups Normal line color - + 0 0 @@ -286,7 +286,7 @@ for ProjectionGroups Preselection color - + 255 255 @@ -318,7 +318,7 @@ for ProjectionGroups Section face color - + 225 225 @@ -350,7 +350,7 @@ for ProjectionGroups Selected item color - + 28 173 @@ -382,7 +382,7 @@ for ProjectionGroups Section face hatch color - + 0 0 @@ -414,7 +414,7 @@ for ProjectionGroups Window background color - + 80 80 @@ -446,7 +446,7 @@ for ProjectionGroups Geometric hatch color - + 0 0 @@ -473,7 +473,7 @@ for ProjectionGroups Color of Dimension lines and text. - + 0 0 @@ -565,7 +565,7 @@ for ProjectionGroups Face color - + 255 255 @@ -597,7 +597,7 @@ for ProjectionGroups Default color for annotations - + 0 0 @@ -733,7 +733,7 @@ for ProjectionGroups - + 0 @@ -743,7 +743,7 @@ for ProjectionGroups Label size - + 8.000000000000000 @@ -793,7 +793,7 @@ for ProjectionGroups - + 0 @@ -844,7 +844,7 @@ for ProjectionGroups - + 0 @@ -884,7 +884,7 @@ for ProjectionGroups - + 0 @@ -921,7 +921,7 @@ for ProjectionGroups - + 0 @@ -958,7 +958,7 @@ for ProjectionGroups - + 0 @@ -998,7 +998,7 @@ for ProjectionGroups - + 0 diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui index 1ba0b9d8f7c9..e8cde08fcb43 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui @@ -23,7 +23,7 @@ - TechDraw Scale + Scale diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui index 1c44ccf4ab98..d8d7bfaafbc6 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui @@ -17,7 +17,7 @@ - TechDraw Dimensions + Dimensions @@ -284,7 +284,7 @@ - + 0 @@ -294,7 +294,7 @@ Dimension font size - + 4.000000000000000 @@ -306,7 +306,7 @@ - + 0 @@ -316,7 +316,7 @@ Dimension arrowhead size - + 5.000000000000000 @@ -581,7 +581,7 @@ - Solid + Continuous @@ -726,7 +726,7 @@ - Solid + Continuous @@ -1068,14 +1068,14 @@ Style for hidden lines.
- SectionLineStyle + HiddenLine - Mod/TechDraw/Standards + Mod/TechDraw/General - Solid + Continuous diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui index 6d1470267edd..ea800f1244ab 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui @@ -11,7 +11,7 @@ - TechDraw Advanced + Advanced @@ -65,7 +65,7 @@ - Max Svg Hatch Tiles + Max SVG Hatch Tiles @@ -121,7 +121,9 @@ - Limit on number of 64x64 Svg tiles used to hatch a single face + Limit of 64x64 pixel SVG tiles used to hatch a single face. +For large scalings you might get an error about to many SVG tiles. +Then you need to increase the tile limit. 1 @@ -277,7 +279,8 @@ can be a performance penalty in complex models. - The maximum number of hatch line segments to use when hatching a face with a PAT pattern + Maximum hatch line segments to use +when hatching a face with a PAT pattern 1 diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui index c142a770ad53..056654249b6e 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui @@ -7,7 +7,7 @@ 0 0 441 - 333 + 354 @@ -23,7 +23,7 @@ - TechDraw HLR Parameters + HLR Parameters diff --git a/src/Mod/TechDraw/Gui/MDIViewPage.cpp b/src/Mod/TechDraw/Gui/MDIViewPage.cpp index 368bc5b68ce9..dab64929afe7 100644 --- a/src/Mod/TechDraw/Gui/MDIViewPage.cpp +++ b/src/Mod/TechDraw/Gui/MDIViewPage.cpp @@ -1020,7 +1020,7 @@ void MDIViewPage::clearSceneSelection() { // Base::Console().Message("MDIVP::clearSceneSelection()\n"); blockSelection(true); - m_sceneSelected.clear(); + qgSceneSelected.clear(); std::vector views = m_view->getViews(); @@ -1100,35 +1100,35 @@ void MDIViewPage::sceneSelectionManager() QList sceneSel = m_view->scene()->selectedItems(); if (sceneSel.isEmpty()) { - m_sceneSelected.clear(); //TODO: need to signal somebody? Tree? handled elsewhere + qgSceneSelected.clear(); //TODO: need to signal somebody? Tree? handled elsewhere //clearSelection return; } - if (m_sceneSelected.isEmpty() && + if (qgSceneSelected.isEmpty() && !sceneSel.isEmpty()) { - m_sceneSelected.push_back(sceneSel.front()); + qgSceneSelected.push_back(sceneSel.front()); return; } - //add to m_sceneSelected anything that is in q_sceneSel + //add to qgSceneSelected anything that is in q_sceneSel for (auto qts: sceneSel) { bool found = false; - for (auto ms: m_sceneSelected) { + for (auto ms: qgSceneSelected) { if ( qts == ms ) { found = true; break; } } if (!found) { - m_sceneSelected.push_back(qts); + qgSceneSelected.push_back(qts); break; } } - //remove items from m_sceneSelected that are not in q_sceneSel + //remove items from qgSceneSelected that are not in q_sceneSel QList m_new; - for (auto m: m_sceneSelected) { + for (auto m: qgSceneSelected) { for (auto q: sceneSel) { if (m == q) { m_new.push_back(m); @@ -1136,7 +1136,7 @@ void MDIViewPage::sceneSelectionManager() } } } - m_sceneSelected = m_new; + qgSceneSelected = m_new; } //! update Tree Selection from QGraphicsScene selection @@ -1154,7 +1154,7 @@ void MDIViewPage::sceneSelectionChanged() std::vector treeSel = Gui::Selection().getSelectionEx(); // QList sceneSel = m_view->scene()->selectedItems(); - QList sceneSel = m_sceneSelected; + QList sceneSel = qgSceneSelected; //check if really need to change selection bool sameSel = compareSelections(treeSel,sceneSel); @@ -1173,7 +1173,7 @@ void MDIViewPage::setTreeToSceneSelect(void) blockSelection(true); Gui::Selection().clearSelection(); // QList sceneSel = m_view->scene()->selectedItems(); //"no particular order"!!! - QList sceneSel = m_sceneSelected; + QList sceneSel = qgSceneSelected; for (QList::iterator it = sceneSel.begin(); it != sceneSel.end(); ++it) { QGIView *itemView = dynamic_cast(*it); if(itemView == 0) { diff --git a/src/Mod/TechDraw/Gui/MDIViewPage.h b/src/Mod/TechDraw/Gui/MDIViewPage.h index 375a59346a2a..c9ce3794b5ff 100644 --- a/src/Mod/TechDraw/Gui/MDIViewPage.h +++ b/src/Mod/TechDraw/Gui/MDIViewPage.h @@ -163,7 +163,7 @@ public Q_SLOTS: QPrinter::PaperSize m_paperSize; ViewProviderPage *m_vpPage; - QList m_sceneSelected; + QList qgSceneSelected; QList deleteItems; }; diff --git a/src/Mod/TechDraw/Gui/QGIEdge.cpp b/src/Mod/TechDraw/Gui/QGIEdge.cpp index d1bbce8ec546..b1e6b76d69de 100644 --- a/src/Mod/TechDraw/Gui/QGIEdge.cpp +++ b/src/Mod/TechDraw/Gui/QGIEdge.cpp @@ -91,7 +91,9 @@ Qt::PenStyle QGIEdge::getHiddenStyle() { Base::Reference hGrp = App::GetApplication().GetUserParameter().GetGroup("BaseApp")-> GetGroup("Preferences")->GetGroup("Mod/TechDraw/General"); - Qt::PenStyle hidStyle = static_cast (hGrp->GetInt("HiddenLine",1)); + //Qt::PenStyle - NoPen, Solid, Dashed, ... + //Preferences::General - Solid, Dashed + Qt::PenStyle hidStyle = static_cast (hGrp->GetInt("HiddenLine",0) + 1); return hidStyle; } diff --git a/src/Mod/TechDraw/Gui/QGIRichAnno.cpp b/src/Mod/TechDraw/Gui/QGIRichAnno.cpp index b54ad551faec..9e4dba95752c 100644 --- a/src/Mod/TechDraw/Gui/QGIRichAnno.cpp +++ b/src/Mod/TechDraw/Gui/QGIRichAnno.cpp @@ -38,8 +38,11 @@ #include #include #include +#include #include #include +#include + # include #endif @@ -49,6 +52,7 @@ #include #include #include +#include #include #include @@ -231,28 +235,54 @@ void QGIRichAnno::setTextItem() m_text->setTextWidth(Rez::guiX(annoFeat->MaxWidth.getValue())); m_text->setHtml(outHtml); +// setLineSpacing(50); //this has no effect on the display?! +// m_text->update(); + + if (annoFeat->ShowFrame.getValue()) { + QRectF r = m_text->boundingRect().adjusted(1,1,-1,-1); + m_rect->setPen(rectPen()); + m_rect->setBrush(Qt::NoBrush); + m_rect->setRect(r); + m_rect->show(); + } else { + m_rect->hide(); + } } else { - //TODO: fix line spacing. common solutions (style sheet, - // QTextBlock::setLineHeight(150, QTextBlockFormat::ProportionalHeight)) don't help + // don't force line wrap & strip formatting that doesn't export well! double realWidth = m_text->boundingRect().width(); m_text->setTextWidth(realWidth); - m_text->setHtml(inHtml); - } - if (annoFeat->ShowFrame.getValue()) { - QRectF r = m_text->boundingRect().adjusted(1,1,-1,-1); - m_rect->setPen(rectPen()); - m_rect->setBrush(Qt::NoBrush); - m_rect->setRect(r); - m_rect->show(); - } else { + QFont f = prefFont(); + double ptSize = prefPointSize(); + f.setPointSizeF(ptSize); + m_text->setFont(f); + + QString plainText = QTextDocumentFragment::fromHtml( inHtml ).toPlainText(); + m_text->setPlainText(plainText); + setLineSpacing(100); //this doesn't appear in the generated Svg, but does space the lines! m_rect->hide(); + m_rect->update(); } m_text->centerAt(0.0, 0.0); m_rect->centerAt(0.0, 0.0); } +void QGIRichAnno::setLineSpacing(int lineSpacing) +{ + //this line spacing should be px, but seems to be %? in any event, it does + //space out the lines. + QTextBlock block = m_text->document()->begin(); + for (; block.isValid(); block = block.next()) { + QTextCursor tc = QTextCursor(block); + QTextBlockFormat fmt = block.blockFormat(); +// fmt.setTopMargin(lineSpacing); //no effect??? + fmt.setBottomMargin(lineSpacing); //spaces out the lines! + tc.setBlockFormat(fmt); +// } + } +} + //void QGIRichAnno::drawBorder() //{ //////Leaders have no border! @@ -310,4 +340,30 @@ QPen QGIRichAnno::rectPen() const return pen; } +QFont QGIRichAnno::prefFont(void) +{ + Base::Reference hGrp = App::GetApplication().GetUserParameter() + .GetGroup("BaseApp")->GetGroup("Preferences")->GetGroup("Mod/TechDraw/Labels"); + std::string fontName = hGrp->GetASCII("LabelFont", "osifont"); + QString family = Base::Tools::fromStdString(fontName); + QFont result; + result.setFamily(family); + return result; +} + +double QGIRichAnno::prefPointSize(void) +{ +// Base::Console().Message("QGIRA::prefPointSize()\n"); + Base::Reference hGrp = App::GetApplication().GetUserParameter() + .GetGroup("BaseApp")->GetGroup("Preferences")->GetGroup("Mod/TechDraw/Dimensions"); + double fontSize = hGrp->GetFloat("FontSize", 5.0); // this is mm, not pts! + + //this conversion is only approximate. the factor changes for different fonts. +// double mmToPts = 2.83; //theoretical value + double mmToPts = 2.00; //practical value. seems to be reasonable for common fonts. + + double ptsSize = round(fontSize * mmToPts); + return ptsSize; +} + #include diff --git a/src/Mod/TechDraw/Gui/QGIRichAnno.h b/src/Mod/TechDraw/Gui/QGIRichAnno.h index 9d074338c293..cc96b1a18e43 100644 --- a/src/Mod/TechDraw/Gui/QGIRichAnno.h +++ b/src/Mod/TechDraw/Gui/QGIRichAnno.h @@ -94,11 +94,11 @@ public Q_SLOTS: virtual void draw() override; virtual QVariant itemChange( GraphicsItemChange change, const QVariant &value ) override; + void setLineSpacing(int lineSpacing); + double prefPointSize(void); + QFont prefFont(void); bool m_isExporting; - -protected: -/* QGMText* m_text;*/ QGCustomText* m_text; bool m_hasHover; QGCustomRect* m_rect; diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.ui b/src/Mod/TechDraw/Gui/TaskCenterLine.ui index 577737563fe8..cf7bcd1d72ad 100644 --- a/src/Mod/TechDraw/Gui/TaskCenterLine.ui +++ b/src/Mod/TechDraw/Gui/TaskCenterLine.ui @@ -256,7 +256,7 @@ - Solid + Continuous diff --git a/src/Mod/TechDraw/Gui/TaskLeaderLine.cpp b/src/Mod/TechDraw/Gui/TaskLeaderLine.cpp index 3f2d464e0c83..212829448623 100644 --- a/src/Mod/TechDraw/Gui/TaskLeaderLine.cpp +++ b/src/Mod/TechDraw/Gui/TaskLeaderLine.cpp @@ -86,15 +86,37 @@ TaskLeaderLine::TaskLeaderLine(TechDrawGui::ViewProviderLeader* leadVP) : Base::Console().Error("TaskLeaderLine - bad parameters. Can not proceed.\n"); return; } - ui->setupUi(this); - + m_lineFeat = m_lineVP->getFeature(); + m_basePage = m_lineFeat->findParentPage(); + if ( m_basePage == nullptr ) { + Base::Console().Error("TaskRichAnno - bad parameters (2). Can not proceed.\n"); + return; + } App::DocumentObject* obj = m_lineFeat->LeaderParent.getValue(); - if ( obj->isDerivedFrom(TechDraw::DrawView::getClassTypeId()) ) { - m_baseFeat = static_cast(m_lineFeat->LeaderParent.getValue()); + if (obj != nullptr) { + if (obj->isDerivedFrom(TechDraw::DrawView::getClassTypeId()) ) { + m_baseFeat = static_cast(m_lineFeat->LeaderParent.getValue()); + } + } + + Gui::Document* activeGui = Gui::Application::Instance->getDocument(m_basePage->getDocument()); + Gui::ViewProvider* vp = activeGui->getViewProvider(m_basePage); + ViewProviderPage* vpp = static_cast(vp); + + m_qgParent = nullptr; + m_haveMdi = true; + m_mdi = vpp->getMDIViewPage(); + if (m_mdi != nullptr) { + m_scene = m_mdi->m_scene; + m_view = m_mdi->getQGVPage(); + if (m_baseFeat != nullptr) { + m_qgParent = m_view->findQViewForDocObj(m_baseFeat); + } + } else { + m_haveMdi = false; } - m_basePage = m_lineFeat->findParentPage(); //TODO: when/if leaders are allowed to be parented to Page, check for m_baseFeat will be removed if ( (m_lineFeat == nullptr) || @@ -104,11 +126,9 @@ TaskLeaderLine::TaskLeaderLine(TechDrawGui::ViewProviderLeader* leadVP) : return; } - setUiEdit(); + ui->setupUi(this); - m_mdi = m_lineVP->getMDIViewPage(); - m_scene = m_mdi->m_scene; - m_view = m_mdi->getQGVPage(); + setUiEdit(); m_attachPoint = Rez::guiX(Base::Vector3d(m_lineFeat->X.getValue(), -m_lineFeat->Y.getValue(), @@ -123,7 +143,9 @@ TaskLeaderLine::TaskLeaderLine(TechDrawGui::ViewProviderLeader* leadVP) : saveState(); m_trackerMode = QGTracker::TrackerMode::Line; - m_saveContextPolicy = m_mdi->contextMenuPolicy(); + if (m_haveMdi) { + m_saveContextPolicy = m_mdi->contextMenuPolicy(); + } } //ctor for creation @@ -148,14 +170,25 @@ TaskLeaderLine::TaskLeaderLine(TechDraw::DrawView* baseFeat, return; } - ui->setupUi(this); - Gui::Document* activeGui = Gui::Application::Instance->getDocument(m_basePage->getDocument()); Gui::ViewProvider* vp = activeGui->getViewProvider(m_basePage); ViewProviderPage* vpp = static_cast(vp); +// vpp->show(); + + m_qgParent = nullptr; + m_haveMdi = true; m_mdi = vpp->getMDIViewPage(); - m_scene = m_mdi->m_scene; - m_view = m_mdi->getQGVPage(); + if (m_mdi != nullptr) { + m_scene = m_mdi->m_scene; + m_view = m_mdi->getQGVPage(); + if (baseFeat != nullptr) { + m_qgParent = m_view->findQViewForDocObj(baseFeat); + } + } else { + m_haveMdi = false; + } + + ui->setupUi(this); setUiPrimary(); @@ -166,7 +199,9 @@ TaskLeaderLine::TaskLeaderLine(TechDraw::DrawView* baseFeat, ui->pbCancelEdit->setEnabled(false); m_trackerMode = QGTracker::TrackerMode::Line; - m_saveContextPolicy = m_mdi->contextMenuPolicy(); + if (m_haveMdi) { + m_saveContextPolicy = m_mdi->contextMenuPolicy(); + } } @@ -217,9 +252,16 @@ void TaskLeaderLine::setUiPrimary() std::string baseName = m_baseFeat->getNameInDocument(); ui->leBaseView->setText(Base::Tools::fromStdString(baseName)); } + ui->pbTracker->setText(QString::fromUtf8("Pick points")); - ui->pbTracker->setEnabled(true); - ui->pbCancelEdit->setEnabled(true); + if (m_haveMdi) { + ui->pbTracker->setEnabled(true); + ui->pbCancelEdit->setEnabled(true); + } else { + ui->pbTracker->setEnabled(false); + ui->pbCancelEdit->setEnabled(false); + } + int aSize = getPrefArrowStyle() + 1; ui->cboxStartSym->setCurrentIndex(aSize); @@ -251,8 +293,13 @@ void TaskLeaderLine::setUiEdit() ui->cboxStartSym->setCurrentIndex(m_lineFeat->StartSymbol.getValue() + 1); ui->cboxEndSym->setCurrentIndex(m_lineFeat->EndSymbol.getValue() + 1); ui->pbTracker->setText(QString::fromUtf8("Edit points")); - ui->pbTracker->setEnabled(true); - ui->pbCancelEdit->setEnabled(true); + if (m_haveMdi) { + ui->pbTracker->setEnabled(true); + ui->pbCancelEdit->setEnabled(true); + } else { + ui->pbTracker->setEnabled(false); + ui->pbCancelEdit->setEnabled(false); + } } if (m_lineVP != nullptr) { @@ -383,6 +430,11 @@ void TaskLeaderLine::onTrackerClicked(bool b) Q_UNUSED(b); // Base::Console().Message("TTL::onTrackerClicked() m_pbTrackerState: %d\n", // m_pbTrackerState); + if (!m_haveMdi) { + Base::Console().Message("TLL::onTrackerClicked - no Mdi, no Tracker!\n"); + return; + } + if ( (m_pbTrackerState == TRACKERSAVE) && (getCreateMode()) ){ if (m_tracker != nullptr) { @@ -479,6 +531,9 @@ void TaskLeaderLine::onTrackerClicked(bool b) void TaskLeaderLine::startTracker(void) { // Base::Console().Message("TTL::startTracker()\n"); + if (!m_haveMdi) { + return; + } if (m_trackerMode == QGTracker::TrackerMode::None) { return; } @@ -548,6 +603,9 @@ void TaskLeaderLine::onTrackerFinished(std::vector pts, QGIView* qgPare void TaskLeaderLine::removeTracker(void) { // Base::Console().Message("TTL::removeTracker()\n"); + if (!m_haveMdi) { + return; + } if ( (m_tracker != nullptr) && (m_tracker->scene() != nullptr) ) { m_scene->removeItem(m_tracker); @@ -590,6 +648,9 @@ QGIView* TaskLeaderLine::findParentQGIV() void TaskLeaderLine::setEditCursor(QCursor c) { + if (!m_haveMdi) { + return; + } if (m_baseFeat != nullptr) { QGIView* qgivBase = m_view->findQViewForDocObj(m_baseFeat); qgivBase->setCursor(c); @@ -711,7 +772,9 @@ bool TaskLeaderLine::accept() Gui::Command::doCommand(Gui::Command::Gui,"Gui.ActiveDocument.resetEdit()"); - m_mdi->setContextMenuPolicy(m_saveContextPolicy); + if (m_haveMdi) { + m_mdi->setContextMenuPolicy(m_saveContextPolicy); + } return true; } diff --git a/src/Mod/TechDraw/Gui/TaskLeaderLine.h b/src/Mod/TechDraw/Gui/TaskLeaderLine.h index 2c96d635b8ac..991ff0b0fab0 100644 --- a/src/Mod/TechDraw/Gui/TaskLeaderLine.h +++ b/src/Mod/TechDraw/Gui/TaskLeaderLine.h @@ -158,6 +158,8 @@ protected Q_SLOTS: std::vector m_savePoints; double m_saveX; double m_saveY; + + bool m_haveMdi; }; class TaskDlgLeaderLine : public Gui::TaskView::TaskDialog diff --git a/src/Mod/TechDraw/Gui/TaskLeaderLine.ui b/src/Mod/TechDraw/Gui/TaskLeaderLine.ui index 03ce155b8c05..2081c176df94 100644 --- a/src/Mod/TechDraw/Gui/TaskLeaderLine.ui +++ b/src/Mod/TechDraw/Gui/TaskLeaderLine.ui @@ -370,7 +370,7 @@ - Solid + Continuous diff --git a/src/Mod/TechDraw/Gui/TaskLineDecor.ui b/src/Mod/TechDraw/Gui/TaskLineDecor.ui index f6f46bad4254..46f111cf4263 100644 --- a/src/Mod/TechDraw/Gui/TaskLineDecor.ui +++ b/src/Mod/TechDraw/Gui/TaskLineDecor.ui @@ -192,7 +192,7 @@ - Solid + Continuous diff --git a/src/Mod/TechDraw/Gui/TaskRichAnno.cpp b/src/Mod/TechDraw/Gui/TaskRichAnno.cpp index 1f5eae0f55a6..f42c0e0d95fe 100644 --- a/src/Mod/TechDraw/Gui/TaskRichAnno.cpp +++ b/src/Mod/TechDraw/Gui/TaskRichAnno.cpp @@ -83,15 +83,21 @@ TaskRichAnno::TaskRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP) : m_textDialog(nullptr), m_rte(nullptr) { +// Base::Console().Message("TRA::TRA() - edit\n"); if (m_annoVP == nullptr) { //should be caught in CMD caller Base::Console().Error("TaskRichAnno - bad parameters. Can not proceed.\n"); return; } - ui->setupUi(this); - + m_annoFeat = m_annoVP->getFeature(); + m_basePage = m_annoFeat->findParentPage(); + if ( m_basePage == nullptr ) { + Base::Console().Error("TaskRichAnno - bad parameters (2). Can not proceed.\n"); + return; + } + //m_baseFeat can be null App::DocumentObject* obj = m_annoFeat->AnnoParent.getValue(); if (obj != nullptr) { @@ -99,23 +105,28 @@ TaskRichAnno::TaskRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP) : m_baseFeat = static_cast(m_annoFeat->AnnoParent.getValue()); } } - m_basePage = m_annoFeat->findParentPage(); - if ( m_basePage == nullptr ) { - Base::Console().Error("TaskRichAnno - bad parameters (2). Can not proceed.\n"); - return; + + Gui::Document* activeGui = Gui::Application::Instance->getDocument(m_basePage->getDocument()); + Gui::ViewProvider* vp = activeGui->getViewProvider(m_basePage); + ViewProviderPage* dvp = static_cast(vp); + + m_mdi = dvp->getMDIViewPage(); + m_qgParent = nullptr; + m_haveMdi = true; + if (m_mdi != nullptr) { + m_view = m_mdi->getQGVPage(); + if (m_baseFeat != nullptr) { + m_qgParent = m_view->findQViewForDocObj(m_baseFeat); + } + } else { + m_haveMdi = false; } - setUiEdit(); -// m_title = QObject::tr("Rich text editor"); - m_mdi = m_annoVP->getMDIViewPage(); - m_scene = m_mdi->m_scene; - m_view = m_mdi->getQGVPage(); - if (m_baseFeat != nullptr) { - m_qgParent = m_view->findQViewForDocObj(m_baseFeat); - } - - m_saveContextPolicy = m_mdi->contextMenuPolicy(); + ui->setupUi(this); + + m_title = QObject::tr("Rich text editor"); + setUiEdit(); m_attachPoint = Rez::guiX(Base::Vector3d(m_annoFeat->X.getValue(), -m_annoFeat->Y.getValue(), @@ -139,27 +150,30 @@ TaskRichAnno::TaskRichAnno(TechDraw::DrawView* baseFeat, m_textDialog(nullptr), m_rte(nullptr) { +// Base::Console().Message("TRA::TRA() - create\n"); if (m_basePage == nullptr) { //should be caught in CMD caller Base::Console().Error("TaskRichAnno - bad parameters. Can not proceed.\n"); return; } - - ui->setupUi(this); - m_title = QObject::tr("Rich text creator"); - Gui::Document* activeGui = Gui::Application::Instance->getDocument(m_basePage->getDocument()); Gui::ViewProvider* vp = activeGui->getViewProvider(m_basePage); - ViewProviderPage* vpp = static_cast(vp); - m_mdi = vpp->getMDIViewPage(); - m_scene = m_mdi->m_scene; - m_view = m_mdi->getQGVPage(); - if (baseFeat != nullptr) { - m_qgParent = m_view->findQViewForDocObj(baseFeat); + ViewProviderPage* dvp = static_cast(vp); + + m_qgParent = nullptr; + m_haveMdi = true; + m_mdi = dvp->getMDIViewPage(); + if (m_mdi != nullptr) { + m_view = m_mdi->getQGVPage(); + if (baseFeat != nullptr) { + m_qgParent = m_view->findQViewForDocObj(baseFeat); + } + } else { + m_haveMdi = false; } - - m_saveContextPolicy = m_mdi->contextMenuPolicy(); + ui->setupUi(this); + m_title = QObject::tr("Rich text creator"); setUiPrimary(); @@ -346,10 +360,16 @@ void TaskRichAnno::createAnnoFeature() if (obj->isDerivedFrom(TechDraw::DrawRichAnno::getClassTypeId())) { m_annoFeat = static_cast(obj); commonFeatureUpdate(); - QPointF qTemp = calcTextStartPos(m_annoFeat->getScale()); - Base::Vector3d vTemp(qTemp.x(), qTemp.y()); - m_annoFeat->X.setValue(Rez::appX(vTemp.x)); - m_annoFeat->Y.setValue(Rez::appX(vTemp.y)); + if (m_haveMdi) { + QPointF qTemp = calcTextStartPos(m_annoFeat->getScale()); + Base::Vector3d vTemp(qTemp.x(), qTemp.y()); + m_annoFeat->X.setValue(Rez::appX(vTemp.x)); + m_annoFeat->Y.setValue(Rez::appX(vTemp.y)); + } else { + //if we don't have a mdi, we can't calculate start position, so just put it mid-page + m_annoFeat->X.setValue(m_basePage->getPageWidth()/2.0); + m_annoFeat->Y.setValue(m_basePage->getPageHeight()/2.0); + } } if (m_annoFeat != nullptr) { @@ -463,7 +483,7 @@ QPointF TaskRichAnno::calcTextStartPos(double scale) QPointF result(w,h); return result; } else { - Base::Console().Message("TRA::calcStartPos - no m_basePage\n"); + Base::Console().Message("TRA::calcStartPos - no m_basePage\n"); //shouldn't happen. caught elsewhere } } @@ -518,7 +538,7 @@ bool TaskRichAnno::accept() } else { createAnnoFeature(); } - m_mdi->setContextMenuPolicy(m_saveContextPolicy); +// m_mdi->setContextMenuPolicy(m_saveContextPolicy); Gui::Command::doCommand(Gui::Command::Gui,"Gui.ActiveDocument.resetEdit()"); return true; @@ -537,10 +557,6 @@ bool TaskRichAnno::reject() if (!doc) { return false; } - - if (m_mdi != nullptr) { - m_mdi->setContextMenuPolicy(m_saveContextPolicy); - } if (getCreateMode() && (m_annoFeat != nullptr) ) { removeFeature(); diff --git a/src/Mod/TechDraw/Gui/TaskRichAnno.h b/src/Mod/TechDraw/Gui/TaskRichAnno.h index cded4653d68e..a453a6d6ab34 100644 --- a/src/Mod/TechDraw/Gui/TaskRichAnno.h +++ b/src/Mod/TechDraw/Gui/TaskRichAnno.h @@ -107,7 +107,6 @@ protected Q_SLOTS: bool blockUpdate; MDIViewPage* m_mdi; - QGraphicsScene* m_scene; QGVPage* m_view; ViewProviderRichAnno* m_annoVP; TechDraw::DrawView* m_baseFeat; @@ -131,6 +130,7 @@ protected Q_SLOTS: QDialog* m_textDialog; MRichTextEdit* m_rte; QString m_title; + bool m_haveMdi; }; class TaskDlgRichAnno : public Gui::TaskView::TaskDialog diff --git a/src/Mod/TechDraw/Gui/TaskRichAnno.ui b/src/Mod/TechDraw/Gui/TaskRichAnno.ui index 003fc92c2f59..26ea6e7b47ce 100644 --- a/src/Mod/TechDraw/Gui/TaskRichAnno.ui +++ b/src/Mod/TechDraw/Gui/TaskRichAnno.ui @@ -201,7 +201,7 @@ - Solid + Continuous diff --git a/src/Mod/TechDraw/Gui/ViewProviderLeader.cpp b/src/Mod/TechDraw/Gui/ViewProviderLeader.cpp index ab73e140a0ab..f5b797c315bf 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderLeader.cpp +++ b/src/Mod/TechDraw/Gui/ViewProviderLeader.cpp @@ -27,6 +27,8 @@ #ifndef _PreComp_ #endif +#include + /// Here the FreeCAD includes sorted by Base,App,Gui...... #include #include @@ -96,7 +98,6 @@ bool ViewProviderLeader::setEdit(int ModNum) if (Gui::Control().activeDialog()) { //TaskPanel already open! return false; } - // clear the selection (convenience) Gui::Selection().clearSelection(); Gui::Control().showDialog(new TaskDlgLeaderLine(this)); return true; diff --git a/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp b/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp index 28e38d787dc0..55cc58da0a60 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp +++ b/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp @@ -94,7 +94,6 @@ bool ViewProviderRichAnno::setEdit(int ModNum) if (Gui::Control().activeDialog()) { //TaskPanel already open! return false; } - // clear the selection (convenience) Gui::Selection().clearSelection(); Gui::Control().showDialog(new TaskDlgRichAnno(this)); return true; diff --git a/src/Mod/TechDraw/Gui/mrichtextedit.cpp b/src/Mod/TechDraw/Gui/mrichtextedit.cpp index 5dca6ec84ae0..bc1c13ea3dab 100644 --- a/src/Mod/TechDraw/Gui/mrichtextedit.cpp +++ b/src/Mod/TechDraw/Gui/mrichtextedit.cpp @@ -222,7 +222,10 @@ MRichTextEdit::MRichTextEdit(QWidget *parent, QString textIn) : QWidget(parent) //set current font size to match inserted text at cursor pos QTextCharFormat fmt = cursor.charFormat(); double currSize = fmt.fontPointSize(); - int fSize = f_fontsize->findText(QString::number(currSize)); + int intSize = round(currSize); + QString qsSize = QString::number(intSize); + addFontSize(qsSize); + int fSize = f_fontsize->findText(qsSize); f_fontsize ->setCurrentIndex(fSize); } else { QTextCursor cursor = f_textedit->textCursor(); @@ -236,6 +239,7 @@ MRichTextEdit::MRichTextEdit(QWidget *parent, QString textIn) : QWidget(parent) f_fontsize->setCurrentIndex(f_fontsize->findText(getDefFontSize())); } + } @@ -816,7 +820,6 @@ void MRichTextEdit::addFontSize(QString fs) } f_fontsize->clear(); f_fontsize->addItems(newList); - size = newList.size(); } #include diff --git a/src/Mod/Test/Document.py b/src/Mod/Test/Document.py index 2b381ea67345..de64e6ac8df0 100644 --- a/src/Mod/Test/Document.py +++ b/src/Mod/Test/Document.py @@ -1504,6 +1504,16 @@ def slotFinishSaveDocument(self, obj, name): self.signal.append('DocFinishSave') self.parameter.append(obj) self.parameter2.append(name) + + def slotBeforeAddingDynamicExtension(self, obj, extension): + self.signal.append('ObjBeforeDynExt') + self.parameter.append(obj) + self.parameter2.append(extension) + + def slotAddedDynamicExtension(self, obj, extension): + self.signal.append('ObjDynExt') + self.parameter.append(obj) + self.parameter2.append(extension) class GuiObserver(): @@ -1777,6 +1787,18 @@ def testObject(self): self.failUnless(self.Obs.parameter2.pop() == 'Prop') self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + pyobj.addExtension("App::GroupExtensionPython", None) + self.failUnless(self.Obs.signal.pop() == 'ObjDynExt') + self.failUnless(self.Obs.parameter.pop() is pyobj) + self.failUnless(self.Obs.parameter2.pop() == 'App::GroupExtensionPython') + self.failUnless(self.Obs.signal.pop(0) == 'ObjBeforeDynExt') + self.failUnless(self.Obs.parameter.pop(0) is pyobj) + self.failUnless(self.Obs.parameter2.pop(0) == 'App::GroupExtensionPython') + #a proxy property was changed, hence those events are also in the signal list + self.Obs.signal = [] + self.Obs.parameter = [] + self.Obs.parameter2 = [] + FreeCAD.closeDocument(self.Doc1.Name) self.Obs.signal = [] self.Obs.parameter = [] @@ -1907,6 +1929,18 @@ def testGuiObserver(self): self.failUnless(self.GuiObs.parameter.pop(0) is obj.ViewObject) self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + obj.ViewObject.addExtension("Gui::ViewProviderGroupExtensionPython", None) + self.failUnless(self.Obs.signal.pop() == 'ObjDynExt') + self.failUnless(self.Obs.parameter.pop() is obj.ViewObject) + self.failUnless(self.Obs.parameter2.pop() == 'Gui::ViewProviderGroupExtensionPython') + self.failUnless(self.Obs.signal.pop() == 'ObjBeforeDynExt') + self.failUnless(self.Obs.parameter.pop() is obj.ViewObject) + self.failUnless(self.Obs.parameter2.pop() == 'Gui::ViewProviderGroupExtensionPython') + #a proxy property was changed, hence those events are also in the signal list (but of GUI observer) + self.GuiObs.signal = [] + self.GuiObs.parameter = [] + self.GuiObs.parameter2 = [] + vo = obj.ViewObject FreeCAD.ActiveDocument.removeObject(obj.Name) self.failUnless(self.Obs.signal.pop(0) == 'ObjDeleted')