diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml new file mode 100644 index 0000000..bec52c2 --- /dev/null +++ b/.github/workflows/autobuild.yml @@ -0,0 +1,140 @@ +name: Continuous Integration +on: + push: + branches: [ main ] + + pull_request: + branches: [ main ] + +jobs: + Build-on-Ubuntu-Qt5: + name: "Build on Ubuntu for Qt5" + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + aqtversion: '==3.1.*' + version: '5.15.2' + host: 'linux' + target: 'desktop' + arch: 'gcc_64' + tools: 'tools_ninja' + cache: true + + - name: Set up compiler cache + uses: chocobo1/setup-ccache-action@v1 + with: + ccache_options: | + max_size=100M + override_cache_key: ccache-linux-gcc-x86_64 + update_packager_index: false + + - name: Configure project + run: > + mkdir build && cd build && cmake -GNinja + -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/gcc + -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/g++ + -DCMAKE_MAKE_PROGRAM:FILEPATH=$IQTA_TOOLS/Ninja/ninja + .. + + - name: Build project + run: cd build && cmake --build . --target all -- -k0 + + - name: Test project + run: cd build && ctest --rerun-failed --output-on-failure + env: + QT_QPA_PLATFORM: offscreen + + Build-on-Ubuntu-Qt6: + name: "Build on Ubuntu for Qt6" + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + aqtversion: '==3.1.*' + version: '6.6.1' + host: 'linux' + target: 'desktop' + arch: 'gcc_64' + modules: 'qt3d' + tools: 'tools_ninja' + cache: true + + - name: Set up compiler cache + uses: chocobo1/setup-ccache-action@v1 + with: + ccache_options: | + max_size=100M + override_cache_key: ccache-linux-clang-x86_64 + update_packager_index: false + + - name: Configure project + run: > + mkdir build && cd build && cmake -GNinja + -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/clang-15 + -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/clang++-15 + -DCMAKE_MAKE_PROGRAM:FILEPATH=$IQTA_TOOLS/Ninja/ninja + .. + + - name: Build project + run: cd build && cmake --build . --target all -- -k0 + + - name: Test project + run: cd build && ctest --rerun-failed --output-on-failure + env: + QT_QPA_PLATFORM: offscreen + + Build-on-Windows-Qt6: + name: "Build on Windows for Qt6" + runs-on: windows-2022 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + aqtversion: '==3.1.*' + version: '6.6.1' + host: 'windows' + target: 'desktop' + arch: 'win64_mingw' + modules: 'qt3d' + tools: 'tools_mingw90 tools_ninja' + cache: true + + - name: Set up compiler cache + uses: chocobo1/setup-ccache-action@v1 + with: + ccache_options: | + max_size=100M + override_cache_key: ccache-windows-mingw64 + windows_compile_environment: msvc # guess "windows_package_manager: choco" would be a better name + + - name: Configure project + run: > + mkdir build && cd build && cmake -GNinja + -DCMAKE_C_COMPILER:FILEPATH=$env:IQTA_TOOLS/mingw1120_64/bin/gcc.exe + -DCMAKE_CXX_COMPILER:FILEPATH=$env:IQTA_TOOLS/mingw1120_64/bin/g++.exe + -DCMAKE_MAKE_PROGRAM:FILEPATH=$env:IQTA_TOOLS/Ninja/ninja.exe + .. + + - name: Build project + run: cd build && cmake --build . --target all -- -k0 + + - name: Test project + run: cd build && ctest --rerun-failed --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 012a0cd..cc85e29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.18) project(QtCSG VERSION 0.1 LANGUAGES CXX) enable_testing() @@ -10,8 +10,32 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050f00) -add_compile_options(-Werror=switch -Werror=unused) +list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + +option(QTCSG_IGNORE_ERRORS "Ignore errors when building CSG trees") + +if (QTCSG_IGNORE_ERRORS) + add_compile_definitions(QTCSG_IGNORE_ERRORS=1) +endif() + +add_compile_definitions( + QT_DISABLE_DEPRECATED_BEFORE=0x050f00 + QT_NO_CONTEXTLESS_CONNECT=1 +) + +add_compile_options( + $<$:-Werror=missing-declarations> + -Wall -Wextra + -Werror=parentheses + -Werror=sign-compare + -Werror=switch + -Werror=unused + -Werror=type-limits +) + +include(QtCSGDebug) +include(QtCSGEnableAlternateLinker) +qtcsg_enable_alternate_linker() # Find Qt5 or Qt6 set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) @@ -19,27 +43,30 @@ set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME) set(QT_MODULES Core Gui Test Widgets 3DCore 3DExtras 3DRender) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS ${QT_MODULES}) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${QT_MODULES}) - -foreach(module ${QT_MODULES}) - add_library(Qt::${module} ALIAS Qt${QT_VERSION_MAJOR}::${module}) -endforeach() - -# Very minimal backport of qt_add_executable for Qt -if (NOT COMMAND qt_add_executable) - function(qt_add_executable NAME) - if (ANDROID) - add_library(${NAME} SHARED ${ARGN}) - else() - add_executable(${NAME} ${ARGN}) - endif() - endfunction() -endif() +message(STATUS "Building for Qt ${QT_VERSION} from ${QT_DIR}") +set(Qt${QT_VERSION_MAJOR}_DIR "${QT_DIR}") + +find_package(Qt${QT_VERSION_MAJOR} ${QT_VERSION} REQUIRED COMPONENTS ${QT_MODULES}) + +include(QtCSGBackports) add_subdirectory(qtcsg) add_subdirectory(qt3dcsg) +add_subdirectory(assets) add_subdirectory(demo) add_subdirectory(tests) -add_custom_target(docs SOURCES README.md docs/demo.png) +add_custom_target( + docs SOURCES + LICENSE.md + LICENSE.spdx + licenses/BSD-3-Clause.md + licenses/GPL-3.0-or-later.md + README.md + docs/demo.png +) +add_custom_target( + github SOURCES + .github/workflows/autobuild.yml +) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f9daf2e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,18 @@ +# Licensing information + +Unless otherwise noted, the files in this project are provided under the +terms of the [GNU General Public License](licenses/GPL-3.0-or-later.md). + +As an exception to this, the following files are provided under the +terms of the [revised BSD 3-Clause License](licenses/BSD-3-Clause.md): + +- [demo/shaders/gl3/robustwireframe.frag](demo/shaders/gl3/robustwireframe.frag) +- [demo/shaders/gl3/robustwireframe.geom](demo/shaders/gl3/robustwireframe.geom) +- [demo/shaders/gl3/robustwireframe.vert](demo/shaders/gl3/robustwireframe.vert) + +As with any licensing, the license offered here is a starting point +for negotiations. Contact me if this offer doesn't work for you: + + Mathias Hasselmann + +PackageLicenseDeclared: GPL-3.0-or-later AND BSD-3-Clause diff --git a/LICENSE.spdx b/LICENSE.spdx new file mode 100644 index 0000000..2720c23 --- /dev/null +++ b/LICENSE.spdx @@ -0,0 +1,18 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: QtCSG +PackageOriginator: Person: Mathias Hasselmann +PackageHomePage: https://github.com/hasselmm/qtcsg.git +PackageLicenseDeclared: GPL-3.0-or-later AND BSD-3-Clause + +FileName: demo/shaders/gl3/robustwireframe.frag +FileContributor: Klaralvdalens Datakonsult AB (KDAB) +LicenseConcluded: BSD-3-Clause + +FileName: demo/shaders/gl3/robustwireframe.geom +FileContributor: Klaralvdalens Datakonsult AB (KDAB) +LicenseConcluded: BSD-3-Clause + +FileName: demo/shaders/gl3/robustwireframe.vert +FileContributor: Klaralvdalens Datakonsult AB (KDAB) +LicenseConcluded: BSD-3-Clause diff --git a/README.md b/README.md index 61efacc..c9d3d27 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ The code has been tested with Qt 5.15 and Qt 6.2. ## Legal Notice -QtCSG is released under the terms of the GNU General Public License v3.0 or later +QtCSG is provided under the terms of the +[GNU General Public License v3.0 or later](licenses/GPL-3.0-or-later.md) -Copyright Ⓒ 2022 Mathias Hasselmann - -SPDX-License-Identifier: GPL-3.0-or-later +See [LICENSE.md](LICENSE.md) for detailed licensing information. diff --git a/assets/CMakeLists.txt b/assets/CMakeLists.txt new file mode 100644 index 0000000..c1c594b --- /dev/null +++ b/assets/CMakeLists.txt @@ -0,0 +1 @@ +qt_add_library(QtCSGAssets OBJECT qtcsgassets.qrc) diff --git a/assets/cutoff/BG3-R12/body.off b/assets/cutoff/BG3-R12/body.off new file mode 100644 index 0000000..2548c85 --- /dev/null +++ b/assets/cutoff/BG3-R12/body.off @@ -0,0 +1,478 @@ +OFF +160 316 0 +-15.1376 6 44.4804 +-15.2753 5.98296 44.4623 +9.0977 6 47.6711 +9.23539 5.98296 47.6892 +-15.4224 5.93301 44.4429 +9.38246 5.93301 47.7086 +-15.5688 5.85355 44.4237 +9.52889 5.85355 47.7278 +-15.7046 5.75 44.4058 +9.66471 5.75 47.7457 +-15.9087 5.5 44.3789 +9.86882 5.5 47.7726 +-18.3874 1 44.0526 +12.3474 1 48.0989 +-18.3874 0 44.0526 +12.3474 0 48.0989 +9.0977 6 -47.6711 +9.23539 5.98296 -47.6892 +-15.1376 6 -44.4804 +-15.2753 5.98296 -44.4623 +9.38246 5.93301 -47.7086 +-15.4224 5.93301 -44.4429 +9.52889 5.85355 -47.7278 +-15.5688 5.85355 -44.4237 +9.66471 5.75 -47.7457 +-15.7046 5.75 -44.4058 +9.86882 5.5 -47.7726 +-15.9087 5.5 -44.3789 +12.3474 1 -48.0989 +-18.3874 1 -44.0526 +12.3474 0 -48.0989 +-18.3874 0 -44.0526 +-13.9869 6 34.635 +-14.125 5.98296 34.6209 +-14.2726 5.93301 34.6058 +-14.4195 5.85355 34.5908 +-14.5558 5.75 34.5769 +-14.7606 5.5 34.5559 +-17.2477 1 34.3018 +-17.2477 0 34.3018 +13.5918 0 37.4525 +13.5918 1 37.4525 +11.1047 5.5 37.1984 +10.8999 5.75 37.1775 +10.7637 5.85355 37.1636 +10.6167 5.93301 37.1486 +10.4691 5.98296 37.1335 +10.331 6 37.1194 +-13.1229 6 24.7602 +-13.2614 5.98296 24.7501 +-13.4094 5.93301 24.7394 +-13.5567 5.85355 24.7286 +-13.6933 5.75 24.7187 +-13.8987 5.5 24.7037 +-16.392 1 24.5221 +-16.392 0 24.5221 +14.526 0 26.7745 +14.526 1 26.7745 +12.0326 5.5 26.5928 +11.8273 5.75 26.5779 +11.6907 5.85355 26.5679 +11.5434 5.93301 26.5572 +11.3954 5.98296 26.5464 +11.2569 6 26.5363 +-12.5466 6 14.8645 +-12.6853 5.98296 14.8585 +-12.8335 5.93301 14.852 +-12.9811 5.85355 14.8455 +-13.1179 5.75 14.8396 +-13.3236 5.5 14.8306 +-15.8212 1 14.7215 +-15.8212 0 14.7215 +15.1493 0 16.0737 +15.1493 1 16.0737 +12.6517 5.5 15.9647 +12.446 5.75 15.9557 +12.3091 5.85355 15.9497 +12.1616 5.93301 15.9433 +12.0134 5.98296 15.9368 +11.8746 6 15.9308 +-12.2583 6 4.95624 +-12.3971 5.98296 4.95422 +-12.5454 5.93301 4.95206 +-12.6931 5.85355 4.94991 +-12.8301 5.75 4.94792 +-13.0359 5.5 4.94493 +-15.5357 1 4.90857 +-15.5357 0 4.90857 +15.461 0 5.35943 +15.461 1 5.35943 +12.9613 5.5 5.32307 +12.7554 5.75 5.32007 +12.6185 5.85355 5.31808 +12.4708 5.93301 5.31593 +12.3225 5.98296 5.31378 +12.1836 6 5.31176 +-12.2583 6 -4.95623 +-12.3971 5.98296 -4.95422 +-12.5454 5.93301 -4.95206 +-12.6931 5.85355 -4.94991 +-12.8301 5.75 -4.94792 +-13.0359 5.5 -4.94492 +-15.5357 1 -4.90856 +-15.5357 0 -4.90856 +15.461 0 -5.35942 +15.461 1 -5.35942 +12.9613 5.5 -5.32306 +12.7554 5.75 -5.32007 +12.6185 5.85355 -5.31808 +12.4708 5.93301 -5.31593 +12.3225 5.98296 -5.31377 +12.1836 6 -5.31175 +-12.5466 6 -14.8645 +-12.6853 5.98296 -14.8585 +-12.8335 5.93301 -14.852 +-12.9811 5.85355 -14.8455 +-13.1179 5.75 -14.8396 +-13.3236 5.5 -14.8306 +-15.8212 1 -14.7215 +-15.8212 0 -14.7215 +15.1493 0 -16.0737 +15.1493 1 -16.0737 +12.6517 5.5 -15.9647 +12.446 5.75 -15.9557 +12.3091 5.85355 -15.9497 +12.1616 5.93301 -15.9433 +12.0134 5.98296 -15.9368 +11.8746 6 -15.9308 +-13.1229 6 -24.7602 +-13.2614 5.98296 -24.7501 +-13.4094 5.93301 -24.7394 +-13.5567 5.85355 -24.7286 +-13.6933 5.75 -24.7187 +-13.8987 5.5 -24.7037 +-16.392 1 -24.5221 +-16.392 0 -24.5221 +14.526 0 -26.7745 +14.526 1 -26.7745 +12.0326 5.5 -26.5928 +11.8273 5.75 -26.5779 +11.6907 5.85355 -26.5679 +11.5434 5.93301 -26.5572 +11.3954 5.98296 -26.5464 +11.2569 6 -26.5363 +-13.9869 6 -34.635 +-14.125 5.98296 -34.6209 +-14.2726 5.93301 -34.6058 +-14.4195 5.85355 -34.5908 +-14.5558 5.75 -34.5768 +-14.7606 5.5 -34.5559 +-17.2477 1 -34.3018 +-17.2477 0 -34.3018 +13.5918 0 -37.4525 +13.5918 1 -37.4525 +11.1047 5.5 -37.1984 +10.8999 5.75 -37.1775 +10.7637 5.85355 -37.1636 +10.6167 5.93301 -37.1486 +10.4691 5.98296 -37.1335 +10.331 6 -37.1194 +3 0 1 2 +3 3 2 1 +3 1 4 3 +3 5 3 4 +3 4 6 5 +3 7 5 6 +3 6 8 7 +3 9 7 8 +3 8 10 9 +3 11 9 10 +3 10 12 11 +3 13 11 12 +3 12 14 13 +3 15 13 14 +3 16 17 18 +3 19 18 17 +3 17 20 19 +3 21 19 20 +3 20 22 21 +3 23 21 22 +3 22 24 23 +3 25 23 24 +3 24 26 25 +3 27 25 26 +3 26 28 27 +3 29 27 28 +3 28 30 29 +3 31 29 30 +3 0 32 33 +3 33 1 0 +3 1 33 34 +3 34 4 1 +3 4 34 35 +3 35 6 4 +3 6 35 36 +3 36 8 6 +3 8 36 37 +3 37 10 8 +3 10 37 38 +3 38 12 10 +3 12 38 39 +3 39 14 12 +3 14 39 40 +3 40 15 14 +3 15 40 41 +3 41 13 15 +3 13 41 42 +3 42 11 13 +3 11 42 43 +3 43 9 11 +3 9 43 44 +3 44 7 9 +3 7 44 45 +3 45 5 7 +3 5 45 46 +3 46 3 5 +3 3 46 47 +3 47 2 3 +3 2 47 32 +3 32 0 2 +3 32 48 49 +3 49 33 32 +3 33 49 50 +3 50 34 33 +3 34 50 51 +3 51 35 34 +3 35 51 52 +3 52 36 35 +3 36 52 53 +3 53 37 36 +3 37 53 54 +3 54 38 37 +3 38 54 55 +3 55 39 38 +3 39 55 56 +3 56 40 39 +3 40 56 57 +3 57 41 40 +3 41 57 58 +3 58 42 41 +3 42 58 59 +3 59 43 42 +3 43 59 60 +3 60 44 43 +3 44 60 61 +3 61 45 44 +3 45 61 62 +3 62 46 45 +3 46 62 63 +3 63 47 46 +3 47 63 48 +3 48 32 47 +3 48 64 65 +3 65 49 48 +3 49 65 66 +3 66 50 49 +3 50 66 67 +3 67 51 50 +3 51 67 68 +3 68 52 51 +3 52 68 69 +3 69 53 52 +3 53 69 70 +3 70 54 53 +3 54 70 71 +3 71 55 54 +3 55 71 72 +3 72 56 55 +3 56 72 73 +3 73 57 56 +3 57 73 74 +3 74 58 57 +3 58 74 75 +3 75 59 58 +3 59 75 76 +3 76 60 59 +3 60 76 77 +3 77 61 60 +3 61 77 78 +3 78 62 61 +3 62 78 79 +3 79 63 62 +3 63 79 64 +3 64 48 63 +3 64 80 81 +3 81 65 64 +3 65 81 82 +3 82 66 65 +3 66 82 83 +3 83 67 66 +3 67 83 84 +3 84 68 67 +3 68 84 85 +3 85 69 68 +3 69 85 86 +3 86 70 69 +3 70 86 87 +3 87 71 70 +3 71 87 88 +3 88 72 71 +3 72 88 89 +3 89 73 72 +3 73 89 90 +3 90 74 73 +3 74 90 91 +3 91 75 74 +3 75 91 92 +3 92 76 75 +3 76 92 93 +3 93 77 76 +3 77 93 94 +3 94 78 77 +3 78 94 95 +3 95 79 78 +3 79 95 80 +3 80 64 79 +3 80 96 97 +3 97 81 80 +3 81 97 98 +3 98 82 81 +3 82 98 99 +3 99 83 82 +3 83 99 100 +3 100 84 83 +3 84 100 101 +3 101 85 84 +3 85 101 102 +3 102 86 85 +3 86 102 103 +3 103 87 86 +3 87 103 104 +3 104 88 87 +3 88 104 105 +3 105 89 88 +3 89 105 106 +3 106 90 89 +3 90 106 107 +3 107 91 90 +3 91 107 108 +3 108 92 91 +3 92 108 109 +3 109 93 92 +3 93 109 110 +3 110 94 93 +3 94 110 111 +3 111 95 94 +3 95 111 96 +3 96 80 95 +3 96 112 113 +3 113 97 96 +3 97 113 114 +3 114 98 97 +3 98 114 115 +3 115 99 98 +3 99 115 116 +3 116 100 99 +3 100 116 117 +3 117 101 100 +3 101 117 118 +3 118 102 101 +3 102 118 119 +3 119 103 102 +3 103 119 120 +3 120 104 103 +3 104 120 121 +3 121 105 104 +3 105 121 122 +3 122 106 105 +3 106 122 123 +3 123 107 106 +3 107 123 124 +3 124 108 107 +3 108 124 125 +3 125 109 108 +3 109 125 126 +3 126 110 109 +3 110 126 127 +3 127 111 110 +3 111 127 112 +3 112 96 111 +3 112 128 129 +3 129 113 112 +3 113 129 130 +3 130 114 113 +3 114 130 131 +3 131 115 114 +3 115 131 132 +3 132 116 115 +3 116 132 133 +3 133 117 116 +3 117 133 134 +3 134 118 117 +3 118 134 135 +3 135 119 118 +3 119 135 136 +3 136 120 119 +3 120 136 137 +3 137 121 120 +3 121 137 138 +3 138 122 121 +3 122 138 139 +3 139 123 122 +3 123 139 140 +3 140 124 123 +3 124 140 141 +3 141 125 124 +3 125 141 142 +3 142 126 125 +3 126 142 143 +3 143 127 126 +3 127 143 128 +3 128 112 127 +3 128 144 145 +3 145 129 128 +3 129 145 146 +3 146 130 129 +3 130 146 147 +3 147 131 130 +3 131 147 148 +3 148 132 131 +3 132 148 149 +3 149 133 132 +3 133 149 150 +3 150 134 133 +3 134 150 151 +3 151 135 134 +3 135 151 152 +3 152 136 135 +3 136 152 153 +3 153 137 136 +3 137 153 154 +3 154 138 137 +3 138 154 155 +3 155 139 138 +3 139 155 156 +3 156 140 139 +3 140 156 157 +3 157 141 140 +3 141 157 158 +3 158 142 141 +3 142 158 159 +3 159 143 142 +3 143 159 144 +3 144 128 143 +3 144 18 19 +3 19 145 144 +3 145 19 21 +3 21 146 145 +3 146 21 23 +3 23 147 146 +3 147 23 25 +3 25 148 147 +3 148 25 27 +3 27 149 148 +3 149 27 29 +3 29 150 149 +3 150 29 31 +3 31 151 150 +3 151 31 30 +3 30 152 151 +3 152 30 28 +3 28 153 152 +3 153 28 26 +3 26 154 153 +3 154 26 24 +3 24 155 154 +3 155 24 22 +3 22 156 155 +3 156 22 20 +3 20 157 156 +3 157 20 17 +3 17 158 157 +3 158 17 16 +3 16 159 158 +3 159 16 18 +3 18 144 159 diff --git a/assets/cutoff/BG3-R12/left.off b/assets/cutoff/BG3-R12/left.off new file mode 100644 index 0000000..449816e --- /dev/null +++ b/assets/cutoff/BG3-R12/left.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-6.05279 -4 150.054 +70.4839 -4 -34.7216 +70.4839 6 -34.7216 +-6.05279 6 150.054 +-34.6931 -4 138.191 +-34.6931 6 138.191 +41.8436 6 -46.5848 +41.8436 -4 -46.5848 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BG3-R12/right.off b/assets/cutoff/BG3-R12/right.off new file mode 100644 index 0000000..f7b1dc8 --- /dev/null +++ b/assets/cutoff/BG3-R12/right.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-34.6931 -4 -138.191 +41.8437 -4 46.5848 +41.8437 6 46.5848 +-34.6931 6 -138.191 +-6.05281 -4 -150.054 +-6.05281 6 -150.054 +70.4839 6 34.7216 +70.4839 -4 34.7216 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BG5/body.off b/assets/cutoff/BG5/body.off new file mode 100644 index 0000000..832d471 --- /dev/null +++ b/assets/cutoff/BG5/body.off @@ -0,0 +1,94 @@ +OFF +32 60 0 +-12.2222 6 18.25 +-12.3611 5.98296 18.25 +12.2222 6 18.25 +12.3611 5.98296 18.25 +-12.5094 5.93301 18.25 +12.5094 5.93301 18.25 +-12.6571 5.85355 18.25 +12.6571 5.85355 18.25 +-12.7941 5.75 18.25 +12.7941 5.75 18.25 +-13 5.5 18.25 +13 5.5 18.25 +-15.5 1 18.25 +15.5 1 18.25 +-15.5 0 18.25 +15.5 0 18.25 +12.2222 6 -18.25 +12.3611 5.98296 -18.25 +-12.2222 6 -18.25 +-12.3611 5.98296 -18.25 +12.5094 5.93301 -18.25 +-12.5094 5.93301 -18.25 +12.6571 5.85355 -18.25 +-12.6571 5.85355 -18.25 +12.7941 5.75 -18.25 +-12.7941 5.75 -18.25 +13 5.5 -18.25 +-13 5.5 -18.25 +15.5 1 -18.25 +-15.5 1 -18.25 +15.5 0 -18.25 +-15.5 0 -18.25 +3 0 1 2 +3 3 2 1 +3 1 4 3 +3 5 3 4 +3 4 6 5 +3 7 5 6 +3 6 8 7 +3 9 7 8 +3 8 10 9 +3 11 9 10 +3 10 12 11 +3 13 11 12 +3 12 14 13 +3 15 13 14 +3 16 17 18 +3 19 18 17 +3 17 20 19 +3 21 19 20 +3 20 22 21 +3 23 21 22 +3 22 24 23 +3 25 23 24 +3 24 26 25 +3 27 25 26 +3 26 28 27 +3 29 27 28 +3 28 30 29 +3 31 29 30 +3 0 18 19 +3 19 1 0 +3 1 19 21 +3 21 4 1 +3 4 21 23 +3 23 6 4 +3 6 23 25 +3 25 8 6 +3 8 25 27 +3 27 10 8 +3 10 27 29 +3 29 12 10 +3 12 29 31 +3 31 14 12 +3 14 31 30 +3 30 15 14 +3 15 30 28 +3 28 13 15 +3 13 28 26 +3 26 11 13 +3 11 26 24 +3 24 9 11 +3 9 24 22 +3 22 7 9 +3 7 22 20 +3 20 5 7 +3 5 20 17 +3 17 3 5 +3 3 17 16 +3 16 2 3 +3 2 16 18 +3 18 0 2 diff --git a/assets/cutoff/BG5/left.off b/assets/cutoff/BG5/left.off new file mode 100644 index 0000000..4c1e7be --- /dev/null +++ b/assets/cutoff/BG5/left.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-10.565 -4 -121.735 +-62.3288 -4 71.4503 +-62.3288 6 71.4503 +-10.565 6 -121.735 +19.3787 -4 -113.711 +19.3787 6 -113.711 +-32.3852 6 79.4737 +-32.3852 -4 79.4737 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BG5/right.off b/assets/cutoff/BG5/right.off new file mode 100644 index 0000000..f4f1cdf --- /dev/null +++ b/assets/cutoff/BG5/right.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-19.3787 -4 -113.711 +32.3852 -4 79.4737 +32.3852 6 79.4737 +-19.3787 6 -113.711 +10.565 -4 -121.735 +10.565 6 -121.735 +62.3289 6 71.4503 +62.3289 -4 71.4503 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BR11-32/body.off b/assets/cutoff/BR11-32/body.off new file mode 100644 index 0000000..5d10b7b --- /dev/null +++ b/assets/cutoff/BR11-32/body.off @@ -0,0 +1,1006 @@ +OFF +336 668 0 +-25.2991 6 99.329 +-25.4333 5.98296 99.2931 +-1.68762 6 105.656 +-1.55348 5.98296 105.692 +-25.5766 5.93301 99.2547 +-1.41019 5.93301 105.73 +-25.7192 5.85355 99.2164 +-1.26752 5.85355 105.768 +-25.8516 5.75 99.181 +-1.1352 5.75 105.804 +-26.0504 5.5 99.1277 +-0.936342 5.5 105.857 +-28.4652 1 98.4806 +1.47847 1 106.504 +-28.4652 0 98.4806 +1.47847 0 106.504 +-1.68762 6 -105.656 +-1.55348 5.98296 -105.692 +-25.2991 6 -99.329 +-25.4333 5.98296 -99.2931 +-1.41019 5.93301 -105.73 +-25.5766 5.93301 -99.2547 +-1.26752 5.85355 -105.768 +-25.7192 5.85355 -99.2164 +-1.1352 5.75 -105.804 +-25.8516 5.75 -99.181 +-0.936342 5.5 -105.857 +-26.0504 5.5 -99.1277 +1.47847 1 -106.504 +-28.4652 1 -98.4806 +1.47847 0 -106.504 +-28.4652 0 -98.4806 +-22.826 6 89.5911 +-22.9611 5.98296 89.5587 +-23.1053 5.93301 89.5241 +-23.2489 5.85355 89.4896 +-23.3821 5.75 89.4576 +-23.5823 5.5 89.4096 +-26.0133 1 88.826 +-26.0133 0 88.826 +4.13021 0 96.0628 +4.13021 1 96.0628 +1.69928 5.5 95.4792 +1.49909 5.75 95.4311 +1.36589 5.85355 95.3991 +1.22227 5.93301 95.3646 +1.07803 5.98296 95.33 +0.942993 6 95.2976 +-20.6087 6 79.7919 +-20.7445 5.98296 79.763 +-20.8896 5.93301 79.7322 +-21.0341 5.85355 79.7015 +-21.1681 5.75 79.673 +-21.3695 5.5 79.6302 +-23.8148 1 79.1104 +-23.8148 0 79.1104 +6.50775 0 85.5557 +6.50775 1 85.5557 +4.06239 5.5 85.0359 +3.86101 5.75 84.9931 +3.72701 5.85355 84.9646 +3.58254 5.93301 84.9339 +3.43744 5.98296 84.903 +3.3016 6 84.8742 +-18.6486 6 69.9379 +-18.7852 5.98296 69.9126 +-18.931 5.93301 69.8856 +-19.0762 5.85355 69.8587 +-19.2109 5.75 69.8337 +-19.4134 5.5 69.7962 +-21.8715 1 69.3406 +-21.8715 0 69.3406 +8.60941 0 74.9899 +8.60941 1 74.9899 +6.15127 5.5 74.5343 +5.94884 5.75 74.4968 +5.81415 5.85355 74.4718 +5.66892 5.93301 74.4449 +5.52307 5.98296 74.4179 +5.38652 6 74.3926 +-16.9471 6 60.0361 +-17.0843 5.98296 60.0144 +-17.2308 5.93301 59.9911 +-17.3767 5.85355 59.968 +-17.512 5.75 59.9466 +-17.7153 5.5 59.9144 +-20.1846 1 59.5233 +-20.1846 0 59.5233 +10.4338 0 64.3728 +10.4338 1 64.3728 +7.96455 5.5 63.9817 +7.76121 5.75 63.9495 +7.62591 5.85355 63.9281 +7.48003 5.93301 63.905 +7.33351 5.98296 63.8818 +7.19635 6 63.86 +-15.5055 6 50.0931 +-15.6432 5.98296 50.0749 +-15.7902 5.93301 50.0556 +-15.9367 5.85355 50.0363 +-16.0725 5.75 50.0184 +-16.2766 5.5 49.9915 +-18.7552 1 49.6652 +-18.7552 0 49.6652 +11.9796 0 53.7115 +11.9796 1 53.7115 +9.50097 5.5 53.3852 +9.29685 5.75 53.3583 +9.16103 5.85355 53.3405 +9.0146 5.93301 53.3212 +8.86753 5.98296 53.3018 +8.72984 6 53.2837 +-14.3246 6 40.1157 +-14.4627 5.98296 40.1012 +-14.6102 5.93301 40.0857 +-14.7571 5.85355 40.0702 +-14.8933 5.75 40.0559 +-15.0981 5.5 40.0344 +-17.5844 1 39.7731 +-17.5844 0 39.7731 +13.2458 0 43.0135 +13.2458 1 43.0135 +10.7595 5.5 42.7521 +10.5547 5.75 42.7306 +10.4185 5.85355 42.7163 +10.2716 5.93301 42.7009 +10.1241 5.98296 42.6854 +9.98596 6 42.6708 +-13.4053 6 30.1109 +-13.5437 5.98296 30.1 +-13.6916 5.93301 30.0883 +-13.8389 5.85355 30.0767 +-13.9754 5.75 30.066 +-14.1807 5.5 30.0498 +-16.673 1 29.8537 +-16.673 0 29.8537 +14.2315 0 32.2859 +14.2315 1 32.2859 +11.7392 5.5 32.0898 +11.5339 5.75 32.0736 +11.3974 5.85355 32.0629 +11.2501 5.93301 32.0513 +11.1023 5.98296 32.0396 +10.9638 6 32.0287 +-12.7482 6 20.0854 +-12.8869 5.98296 20.0781 +-13.035 5.93301 20.0703 +-13.1825 5.85355 20.0626 +-13.3193 5.75 20.0554 +-13.5249 5.5 20.0447 +-16.0215 1 19.9138 +-16.0215 0 19.9138 +14.936 0 21.5362 +14.936 1 21.5362 +12.4395 5.5 21.4054 +12.2339 5.75 21.3946 +12.0971 5.85355 21.3875 +11.9496 5.93301 21.3797 +11.8014 5.98296 21.372 +11.6627 6 21.3647 +-12.3537 6 10.0461 +-12.4925 5.98296 10.0425 +-12.6408 5.93301 10.0386 +-12.7885 5.85355 10.0347 +-12.9254 5.75 10.0312 +-13.1312 5.5 10.0258 +-15.6304 1 9.96033 +-15.6304 0 9.96033 +15.359 0 10.7718 +15.359 1 10.7718 +12.8599 5.5 10.7064 +12.6541 5.75 10.701 +12.5171 5.85355 10.6974 +12.3695 5.93301 10.6935 +12.2212 5.98296 10.6896 +12.0824 6 10.686 +-12.2222 6 0 +-12.3611 5.98296 0 +-12.5094 5.93301 0 +-12.6571 5.85355 0 +-12.7941 5.75 0 +-13 5.5 0 +-15.5 1 0 +-15.5 0 0 +15.5 0 0 +15.5 1 0 +13 5.5 0 +12.7941 5.75 0 +12.6571 5.85355 0 +12.5094 5.93301 0 +12.3611 5.98296 0 +12.2222 6 0 +-12.3537 6 -10.0461 +-12.4925 5.98296 -10.0425 +-12.6408 5.93301 -10.0386 +-12.7885 5.85355 -10.0347 +-12.9254 5.75 -10.0312 +-13.1312 5.5 -10.0258 +-15.6304 1 -9.96033 +-15.6304 0 -9.96033 +15.359 0 -10.7718 +15.359 1 -10.7718 +12.8599 5.5 -10.7064 +12.6541 5.75 -10.701 +12.5171 5.85355 -10.6974 +12.3695 5.93301 -10.6935 +12.2212 5.98296 -10.6896 +12.0824 6 -10.686 +-12.7482 6 -20.0854 +-12.8869 5.98296 -20.0781 +-13.035 5.93301 -20.0703 +-13.1825 5.85355 -20.0626 +-13.3193 5.75 -20.0554 +-13.5249 5.5 -20.0447 +-16.0215 1 -19.9138 +-16.0215 0 -19.9138 +14.936 0 -21.5362 +14.936 1 -21.5362 +12.4395 5.5 -21.4054 +12.2339 5.75 -21.3946 +12.0971 5.85355 -21.3875 +11.9496 5.93301 -21.3797 +11.8014 5.98296 -21.372 +11.6627 6 -21.3647 +-13.4053 6 -30.1109 +-13.5437 5.98296 -30.1 +-13.6916 5.93301 -30.0883 +-13.8389 5.85355 -30.0767 +-13.9754 5.75 -30.066 +-14.1807 5.5 -30.0498 +-16.673 1 -29.8537 +-16.673 0 -29.8537 +14.2315 0 -32.2859 +14.2315 1 -32.2859 +11.7392 5.5 -32.0898 +11.5339 5.75 -32.0736 +11.3974 5.85355 -32.0629 +11.2501 5.93301 -32.0513 +11.1023 5.98296 -32.0396 +10.9638 6 -32.0287 +-14.3246 6 -40.1157 +-14.4627 5.98296 -40.1012 +-14.6102 5.93301 -40.0857 +-14.7571 5.85355 -40.0702 +-14.8933 5.75 -40.0559 +-15.0981 5.5 -40.0344 +-17.5844 1 -39.7731 +-17.5844 0 -39.7731 +13.2458 0 -43.0135 +13.2458 1 -43.0135 +10.7595 5.5 -42.7521 +10.5547 5.75 -42.7306 +10.4185 5.85355 -42.7163 +10.2716 5.93301 -42.7009 +10.1241 5.98296 -42.6854 +9.98596 6 -42.6708 +-15.5055 6 -50.0931 +-15.6432 5.98296 -50.0749 +-15.7902 5.93301 -50.0556 +-15.9367 5.85355 -50.0363 +-16.0725 5.75 -50.0184 +-16.2766 5.5 -49.9915 +-18.7552 1 -49.6652 +-18.7552 0 -49.6652 +11.9796 0 -53.7115 +11.9796 1 -53.7115 +9.50097 5.5 -53.3852 +9.29685 5.75 -53.3583 +9.16103 5.85355 -53.3405 +9.0146 5.93301 -53.3212 +8.86753 5.98296 -53.3018 +8.72984 6 -53.2837 +-16.9471 6 -60.0361 +-17.0843 5.98296 -60.0144 +-17.2308 5.93301 -59.9911 +-17.3767 5.85355 -59.968 +-17.512 5.75 -59.9466 +-17.7153 5.5 -59.9144 +-20.1846 1 -59.5233 +-20.1846 0 -59.5233 +10.4338 0 -64.3728 +10.4338 1 -64.3728 +7.96455 5.5 -63.9817 +7.76121 5.75 -63.9495 +7.62591 5.85355 -63.9281 +7.48003 5.93301 -63.905 +7.33351 5.98296 -63.8818 +7.19635 6 -63.86 +-18.6486 6 -69.9379 +-18.7852 5.98296 -69.9126 +-18.931 5.93301 -69.8856 +-19.0762 5.85355 -69.8587 +-19.2109 5.75 -69.8337 +-19.4134 5.5 -69.7962 +-21.8715 1 -69.3406 +-21.8715 0 -69.3406 +8.60941 0 -74.9899 +8.60941 1 -74.9899 +6.15127 5.5 -74.5343 +5.94884 5.75 -74.4968 +5.81415 5.85355 -74.4718 +5.66892 5.93301 -74.4449 +5.52307 5.98296 -74.4179 +5.38652 6 -74.3926 +-20.6087 6 -79.7919 +-20.7445 5.98296 -79.763 +-20.8896 5.93301 -79.7322 +-21.0341 5.85355 -79.7015 +-21.1681 5.75 -79.673 +-21.3695 5.5 -79.6302 +-23.8148 1 -79.1104 +-23.8148 0 -79.1104 +6.50775 0 -85.5557 +6.50775 1 -85.5557 +4.06239 5.5 -85.0359 +3.86101 5.75 -84.9931 +3.72701 5.85355 -84.9646 +3.58254 5.93301 -84.9339 +3.43744 5.98296 -84.903 +3.3016 6 -84.8742 +-22.826 6 -89.5911 +-22.9611 5.98296 -89.5587 +-23.1053 5.93301 -89.5241 +-23.2489 5.85355 -89.4896 +-23.3821 5.75 -89.4576 +-23.5823 5.5 -89.4096 +-26.0133 1 -88.826 +-26.0133 0 -88.826 +4.13021 0 -96.0628 +4.13021 1 -96.0628 +1.69928 5.5 -95.4792 +1.49909 5.75 -95.4311 +1.36589 5.85355 -95.3991 +1.22227 5.93301 -95.3646 +1.07803 5.98296 -95.33 +0.942993 6 -95.2976 +3 0 1 2 +3 3 2 1 +3 1 4 3 +3 5 3 4 +3 4 6 5 +3 7 5 6 +3 6 8 7 +3 9 7 8 +3 8 10 9 +3 11 9 10 +3 10 12 11 +3 13 11 12 +3 12 14 13 +3 15 13 14 +3 16 17 18 +3 19 18 17 +3 17 20 19 +3 21 19 20 +3 20 22 21 +3 23 21 22 +3 22 24 23 +3 25 23 24 +3 24 26 25 +3 27 25 26 +3 26 28 27 +3 29 27 28 +3 28 30 29 +3 31 29 30 +3 0 32 33 +3 33 1 0 +3 1 33 34 +3 34 4 1 +3 4 34 35 +3 35 6 4 +3 6 35 36 +3 36 8 6 +3 8 36 37 +3 37 10 8 +3 10 37 38 +3 38 12 10 +3 12 38 39 +3 39 14 12 +3 14 39 40 +3 40 15 14 +3 15 40 41 +3 41 13 15 +3 13 41 42 +3 42 11 13 +3 11 42 43 +3 43 9 11 +3 9 43 44 +3 44 7 9 +3 7 44 45 +3 45 5 7 +3 5 45 46 +3 46 3 5 +3 3 46 47 +3 47 2 3 +3 2 47 32 +3 32 0 2 +3 32 48 49 +3 49 33 32 +3 33 49 50 +3 50 34 33 +3 34 50 51 +3 51 35 34 +3 35 51 52 +3 52 36 35 +3 36 52 53 +3 53 37 36 +3 37 53 54 +3 54 38 37 +3 38 54 55 +3 55 39 38 +3 39 55 56 +3 56 40 39 +3 40 56 57 +3 57 41 40 +3 41 57 58 +3 58 42 41 +3 42 58 59 +3 59 43 42 +3 43 59 60 +3 60 44 43 +3 44 60 61 +3 61 45 44 +3 45 61 62 +3 62 46 45 +3 46 62 63 +3 63 47 46 +3 47 63 48 +3 48 32 47 +3 48 64 65 +3 65 49 48 +3 49 65 66 +3 66 50 49 +3 50 66 67 +3 67 51 50 +3 51 67 68 +3 68 52 51 +3 52 68 69 +3 69 53 52 +3 53 69 70 +3 70 54 53 +3 54 70 71 +3 71 55 54 +3 55 71 72 +3 72 56 55 +3 56 72 73 +3 73 57 56 +3 57 73 74 +3 74 58 57 +3 58 74 75 +3 75 59 58 +3 59 75 76 +3 76 60 59 +3 60 76 77 +3 77 61 60 +3 61 77 78 +3 78 62 61 +3 62 78 79 +3 79 63 62 +3 63 79 64 +3 64 48 63 +3 64 80 81 +3 81 65 64 +3 65 81 82 +3 82 66 65 +3 66 82 83 +3 83 67 66 +3 67 83 84 +3 84 68 67 +3 68 84 85 +3 85 69 68 +3 69 85 86 +3 86 70 69 +3 70 86 87 +3 87 71 70 +3 71 87 88 +3 88 72 71 +3 72 88 89 +3 89 73 72 +3 73 89 90 +3 90 74 73 +3 74 90 91 +3 91 75 74 +3 75 91 92 +3 92 76 75 +3 76 92 93 +3 93 77 76 +3 77 93 94 +3 94 78 77 +3 78 94 95 +3 95 79 78 +3 79 95 80 +3 80 64 79 +3 80 96 97 +3 97 81 80 +3 81 97 98 +3 98 82 81 +3 82 98 99 +3 99 83 82 +3 83 99 100 +3 100 84 83 +3 84 100 101 +3 101 85 84 +3 85 101 102 +3 102 86 85 +3 86 102 103 +3 103 87 86 +3 87 103 104 +3 104 88 87 +3 88 104 105 +3 105 89 88 +3 89 105 106 +3 106 90 89 +3 90 106 107 +3 107 91 90 +3 91 107 108 +3 108 92 91 +3 92 108 109 +3 109 93 92 +3 93 109 110 +3 110 94 93 +3 94 110 111 +3 111 95 94 +3 95 111 96 +3 96 80 95 +3 96 112 113 +3 113 97 96 +3 97 113 114 +3 114 98 97 +3 98 114 115 +3 115 99 98 +3 99 115 116 +3 116 100 99 +3 100 116 117 +3 117 101 100 +3 101 117 118 +3 118 102 101 +3 102 118 119 +3 119 103 102 +3 103 119 120 +3 120 104 103 +3 104 120 121 +3 121 105 104 +3 105 121 122 +3 122 106 105 +3 106 122 123 +3 123 107 106 +3 107 123 124 +3 124 108 107 +3 108 124 125 +3 125 109 108 +3 109 125 126 +3 126 110 109 +3 110 126 127 +3 127 111 110 +3 111 127 112 +3 112 96 111 +3 112 128 129 +3 129 113 112 +3 113 129 130 +3 130 114 113 +3 114 130 131 +3 131 115 114 +3 115 131 132 +3 132 116 115 +3 116 132 133 +3 133 117 116 +3 117 133 134 +3 134 118 117 +3 118 134 135 +3 135 119 118 +3 119 135 136 +3 136 120 119 +3 120 136 137 +3 137 121 120 +3 121 137 138 +3 138 122 121 +3 122 138 139 +3 139 123 122 +3 123 139 140 +3 140 124 123 +3 124 140 141 +3 141 125 124 +3 125 141 142 +3 142 126 125 +3 126 142 143 +3 143 127 126 +3 127 143 128 +3 128 112 127 +3 128 144 145 +3 145 129 128 +3 129 145 146 +3 146 130 129 +3 130 146 147 +3 147 131 130 +3 131 147 148 +3 148 132 131 +3 132 148 149 +3 149 133 132 +3 133 149 150 +3 150 134 133 +3 134 150 151 +3 151 135 134 +3 135 151 152 +3 152 136 135 +3 136 152 153 +3 153 137 136 +3 137 153 154 +3 154 138 137 +3 138 154 155 +3 155 139 138 +3 139 155 156 +3 156 140 139 +3 140 156 157 +3 157 141 140 +3 141 157 158 +3 158 142 141 +3 142 158 159 +3 159 143 142 +3 143 159 144 +3 144 128 143 +3 144 160 161 +3 161 145 144 +3 145 161 162 +3 162 146 145 +3 146 162 163 +3 163 147 146 +3 147 163 164 +3 164 148 147 +3 148 164 165 +3 165 149 148 +3 149 165 166 +3 166 150 149 +3 150 166 167 +3 167 151 150 +3 151 167 168 +3 168 152 151 +3 152 168 169 +3 169 153 152 +3 153 169 170 +3 170 154 153 +3 154 170 171 +3 171 155 154 +3 155 171 172 +3 172 156 155 +3 156 172 173 +3 173 157 156 +3 157 173 174 +3 174 158 157 +3 158 174 175 +3 175 159 158 +3 159 175 160 +3 160 144 159 +3 160 176 177 +3 177 161 160 +3 161 177 178 +3 178 162 161 +3 162 178 179 +3 179 163 162 +3 163 179 180 +3 180 164 163 +3 164 180 181 +3 181 165 164 +3 165 181 182 +3 182 166 165 +3 166 182 183 +3 183 167 166 +3 167 183 184 +3 184 168 167 +3 168 184 185 +3 185 169 168 +3 169 185 186 +3 186 170 169 +3 170 186 187 +3 187 171 170 +3 171 187 188 +3 188 172 171 +3 172 188 189 +3 189 173 172 +3 173 189 190 +3 190 174 173 +3 174 190 191 +3 191 175 174 +3 175 191 176 +3 176 160 175 +3 176 192 193 +3 193 177 176 +3 177 193 194 +3 194 178 177 +3 178 194 195 +3 195 179 178 +3 179 195 196 +3 196 180 179 +3 180 196 197 +3 197 181 180 +3 181 197 198 +3 198 182 181 +3 182 198 199 +3 199 183 182 +3 183 199 200 +3 200 184 183 +3 184 200 201 +3 201 185 184 +3 185 201 202 +3 202 186 185 +3 186 202 203 +3 203 187 186 +3 187 203 204 +3 204 188 187 +3 188 204 205 +3 205 189 188 +3 189 205 206 +3 206 190 189 +3 190 206 207 +3 207 191 190 +3 191 207 192 +3 192 176 191 +3 192 208 209 +3 209 193 192 +3 193 209 210 +3 210 194 193 +3 194 210 211 +3 211 195 194 +3 195 211 212 +3 212 196 195 +3 196 212 213 +3 213 197 196 +3 197 213 214 +3 214 198 197 +3 198 214 215 +3 215 199 198 +3 199 215 216 +3 216 200 199 +3 200 216 217 +3 217 201 200 +3 201 217 218 +3 218 202 201 +3 202 218 219 +3 219 203 202 +3 203 219 220 +3 220 204 203 +3 204 220 221 +3 221 205 204 +3 205 221 222 +3 222 206 205 +3 206 222 223 +3 223 207 206 +3 207 223 208 +3 208 192 207 +3 208 224 225 +3 225 209 208 +3 209 225 226 +3 226 210 209 +3 210 226 227 +3 227 211 210 +3 211 227 228 +3 228 212 211 +3 212 228 229 +3 229 213 212 +3 213 229 230 +3 230 214 213 +3 214 230 231 +3 231 215 214 +3 215 231 232 +3 232 216 215 +3 216 232 233 +3 233 217 216 +3 217 233 234 +3 234 218 217 +3 218 234 235 +3 235 219 218 +3 219 235 236 +3 236 220 219 +3 220 236 237 +3 237 221 220 +3 221 237 238 +3 238 222 221 +3 222 238 239 +3 239 223 222 +3 223 239 224 +3 224 208 223 +3 224 240 241 +3 241 225 224 +3 225 241 242 +3 242 226 225 +3 226 242 243 +3 243 227 226 +3 227 243 244 +3 244 228 227 +3 228 244 245 +3 245 229 228 +3 229 245 246 +3 246 230 229 +3 230 246 247 +3 247 231 230 +3 231 247 248 +3 248 232 231 +3 232 248 249 +3 249 233 232 +3 233 249 250 +3 250 234 233 +3 234 250 251 +3 251 235 234 +3 235 251 252 +3 252 236 235 +3 236 252 253 +3 253 237 236 +3 237 253 254 +3 254 238 237 +3 238 254 255 +3 255 239 238 +3 239 255 240 +3 240 224 239 +3 240 256 257 +3 257 241 240 +3 241 257 258 +3 258 242 241 +3 242 258 259 +3 259 243 242 +3 243 259 260 +3 260 244 243 +3 244 260 261 +3 261 245 244 +3 245 261 262 +3 262 246 245 +3 246 262 263 +3 263 247 246 +3 247 263 264 +3 264 248 247 +3 248 264 265 +3 265 249 248 +3 249 265 266 +3 266 250 249 +3 250 266 267 +3 267 251 250 +3 251 267 268 +3 268 252 251 +3 252 268 269 +3 269 253 252 +3 253 269 270 +3 270 254 253 +3 254 270 271 +3 271 255 254 +3 255 271 256 +3 256 240 255 +3 256 272 273 +3 273 257 256 +3 257 273 274 +3 274 258 257 +3 258 274 275 +3 275 259 258 +3 259 275 276 +3 276 260 259 +3 260 276 277 +3 277 261 260 +3 261 277 278 +3 278 262 261 +3 262 278 279 +3 279 263 262 +3 263 279 280 +3 280 264 263 +3 264 280 281 +3 281 265 264 +3 265 281 282 +3 282 266 265 +3 266 282 283 +3 283 267 266 +3 267 283 284 +3 284 268 267 +3 268 284 285 +3 285 269 268 +3 269 285 286 +3 286 270 269 +3 270 286 287 +3 287 271 270 +3 271 287 272 +3 272 256 271 +3 272 288 289 +3 289 273 272 +3 273 289 290 +3 290 274 273 +3 274 290 291 +3 291 275 274 +3 275 291 292 +3 292 276 275 +3 276 292 293 +3 293 277 276 +3 277 293 294 +3 294 278 277 +3 278 294 295 +3 295 279 278 +3 279 295 296 +3 296 280 279 +3 280 296 297 +3 297 281 280 +3 281 297 298 +3 298 282 281 +3 282 298 299 +3 299 283 282 +3 283 299 300 +3 300 284 283 +3 284 300 301 +3 301 285 284 +3 285 301 302 +3 302 286 285 +3 286 302 303 +3 303 287 286 +3 287 303 288 +3 288 272 287 +3 288 304 305 +3 305 289 288 +3 289 305 306 +3 306 290 289 +3 290 306 307 +3 307 291 290 +3 291 307 308 +3 308 292 291 +3 292 308 309 +3 309 293 292 +3 293 309 310 +3 310 294 293 +3 294 310 311 +3 311 295 294 +3 295 311 312 +3 312 296 295 +3 296 312 313 +3 313 297 296 +3 297 313 314 +3 314 298 297 +3 298 314 315 +3 315 299 298 +3 299 315 316 +3 316 300 299 +3 300 316 317 +3 317 301 300 +3 301 317 318 +3 318 302 301 +3 302 318 319 +3 319 303 302 +3 303 319 304 +3 304 288 303 +3 304 320 321 +3 321 305 304 +3 305 321 322 +3 322 306 305 +3 306 322 323 +3 323 307 306 +3 307 323 324 +3 324 308 307 +3 308 324 325 +3 325 309 308 +3 309 325 326 +3 326 310 309 +3 310 326 327 +3 327 311 310 +3 311 327 328 +3 328 312 311 +3 312 328 329 +3 329 313 312 +3 313 329 330 +3 330 314 313 +3 314 330 331 +3 331 315 314 +3 315 331 332 +3 332 316 315 +3 316 332 333 +3 333 317 316 +3 317 333 334 +3 334 318 317 +3 318 334 335 +3 335 319 318 +3 319 335 320 +3 320 304 319 +3 320 18 19 +3 19 321 320 +3 321 19 21 +3 21 322 321 +3 322 21 23 +3 23 323 322 +3 323 23 25 +3 25 324 323 +3 324 25 27 +3 27 325 324 +3 325 27 29 +3 29 326 325 +3 326 29 31 +3 31 327 326 +3 327 31 30 +3 30 328 327 +3 328 30 28 +3 28 329 328 +3 329 28 26 +3 26 330 329 +3 330 26 24 +3 24 331 330 +3 331 24 22 +3 22 332 331 +3 332 22 20 +3 20 333 332 +3 333 20 17 +3 17 334 333 +3 334 17 16 +3 16 335 334 +3 335 16 18 +3 18 320 335 diff --git a/assets/cutoff/BR11-32/left.off b/assets/cutoff/BR11-32/left.off new file mode 100644 index 0000000..64d6dce --- /dev/null +++ b/assets/cutoff/BR11-32/left.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-50.4823 -4 -199.717 +-50.4823 -4 0.283379 +-50.4823 6 0.283379 +-50.4823 6 -199.717 +-19.4823 -4 -199.717 +-19.4823 6 -199.717 +-19.4823 6 0.283379 +-19.4823 -4 0.283379 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BR11-32/right.off b/assets/cutoff/BR11-32/right.off new file mode 100644 index 0000000..40a9e3d --- /dev/null +++ b/assets/cutoff/BR11-32/right.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-19.4823 -4 199.717 +-19.4823 -4 -0.283379 +-19.4823 6 -0.283379 +-19.4823 6 199.717 +-50.4823 -4 199.717 +-50.4823 6 199.717 +-50.4823 6 -0.283379 +-50.4823 -4 -0.283379 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BR12-22/body.off b/assets/cutoff/BR12-22/body.off new file mode 100644 index 0000000..7f706e1 --- /dev/null +++ b/assets/cutoff/BR12-22/body.off @@ -0,0 +1,430 @@ +OFF +144 284 0 +-14.9537 6 41.6741 +-15.0914 5.98296 41.656 +9.28163 6 44.8648 +9.41932 5.98296 44.8829 +-15.2384 5.93301 41.6366 +9.56639 5.93301 44.9022 +-15.3849 5.85355 41.6173 +9.71282 5.85355 44.9215 +-15.5207 5.75 41.5995 +9.84864 5.75 44.9394 +-15.7248 5.5 41.5726 +10.0528 5.5 44.9663 +-18.2034 1 41.2463 +12.5314 1 45.2926 +-18.2034 0 41.2463 +12.5314 0 45.2926 +9.28163 6 -44.8648 +9.41932 5.98296 -44.8829 +-14.9537 6 -41.6741 +-15.0914 5.98296 -41.656 +9.56639 5.93301 -44.9022 +-15.2384 5.93301 -41.6366 +9.71282 5.85355 -44.9215 +-15.3849 5.85355 -41.6173 +9.84864 5.75 -44.9394 +-15.5207 5.75 -41.5995 +10.0528 5.5 -44.9663 +-15.7248 5.5 -41.5726 +12.5314 1 -45.2926 +-18.2034 1 -41.2463 +12.5314 0 -45.2926 +-18.2034 0 -41.2463 +-13.7596 6 31.2947 +-13.8978 5.98296 31.2811 +-14.0455 5.93301 31.2665 +-14.1924 5.85355 31.2521 +-14.3288 5.75 31.2386 +-14.5337 5.5 31.2185 +-17.0216 1 30.9734 +-17.0216 0 30.9734 +13.8291 0 34.0119 +13.8291 1 34.0119 +11.3411 5.5 33.7669 +11.1363 5.75 33.7467 +10.9999 5.85355 33.7333 +10.8529 5.93301 33.7188 +10.7053 5.98296 33.7043 +10.5671 6 33.6907 +-12.9058 6 20.8818 +-13.0444 5.98296 20.8727 +-13.1924 5.93301 20.863 +-13.3398 5.85355 20.8533 +-13.4765 5.75 20.8444 +-13.6819 5.5 20.8309 +-16.1766 1 20.6674 +-16.1766 0 20.6674 +14.7571 0 22.6949 +14.7571 1 22.6949 +12.2624 5.5 22.5314 +12.057 5.75 22.5179 +11.9203 5.85355 22.509 +11.7729 5.93301 22.4993 +11.6249 5.98296 22.4896 +11.4863 6 22.4805 +-12.3932 6 10.4465 +-12.532 5.98296 10.4419 +-12.6802 5.93301 10.4371 +-12.8278 5.85355 10.4322 +-12.9648 5.75 10.4278 +-13.1705 5.5 10.421 +-15.6692 1 10.3392 +-15.6692 0 10.3392 +15.3142 0 11.3535 +15.3142 1 11.3535 +12.8155 5.5 11.2717 +12.6098 5.75 11.265 +12.4729 5.85355 11.2605 +12.3252 5.93301 11.2557 +12.177 5.98296 11.2508 +12.0382 6 11.2463 +-12.2222 6 0 +-12.3611 5.98296 0 +-12.5094 5.93301 0 +-12.6571 5.85355 0 +-12.7941 5.75 0 +-13 5.5 0 +-15.5 1 0 +-15.5 0 0 +15.5 0 0 +15.5 1 0 +13 5.5 0 +12.7941 5.75 0 +12.6571 5.85355 0 +12.5094 5.93301 0 +12.3611 5.98296 0 +12.2222 6 0 +-12.3932 6 -10.4465 +-12.532 5.98296 -10.4419 +-12.6802 5.93301 -10.4371 +-12.8278 5.85355 -10.4322 +-12.9648 5.75 -10.4278 +-13.1705 5.5 -10.421 +-15.6692 1 -10.3392 +-15.6692 0 -10.3392 +15.3142 0 -11.3535 +15.3142 1 -11.3535 +12.8155 5.5 -11.2717 +12.6098 5.75 -11.265 +12.4729 5.85355 -11.2605 +12.3252 5.93301 -11.2557 +12.177 5.98296 -11.2508 +12.0382 6 -11.2463 +-12.9058 6 -20.8818 +-13.0444 5.98296 -20.8727 +-13.1924 5.93301 -20.863 +-13.3398 5.85355 -20.8533 +-13.4765 5.75 -20.8444 +-13.6819 5.5 -20.8309 +-16.1766 1 -20.6674 +-16.1766 0 -20.6674 +14.7571 0 -22.6949 +14.7571 1 -22.6949 +12.2624 5.5 -22.5314 +12.057 5.75 -22.5179 +11.9203 5.85355 -22.509 +11.7729 5.93301 -22.4993 +11.6249 5.98296 -22.4896 +11.4863 6 -22.4805 +-13.7596 6 -31.2947 +-13.8978 5.98296 -31.2811 +-14.0455 5.93301 -31.2665 +-14.1924 5.85355 -31.2521 +-14.3288 5.75 -31.2386 +-14.5337 5.5 -31.2185 +-17.0216 1 -30.9734 +-17.0216 0 -30.9734 +13.8291 0 -34.0119 +13.8291 1 -34.0119 +11.3411 5.5 -33.7669 +11.1363 5.75 -33.7467 +10.9999 5.85355 -33.7333 +10.8529 5.93301 -33.7188 +10.7053 5.98296 -33.7043 +10.5671 6 -33.6907 +3 0 1 2 +3 3 2 1 +3 1 4 3 +3 5 3 4 +3 4 6 5 +3 7 5 6 +3 6 8 7 +3 9 7 8 +3 8 10 9 +3 11 9 10 +3 10 12 11 +3 13 11 12 +3 12 14 13 +3 15 13 14 +3 16 17 18 +3 19 18 17 +3 17 20 19 +3 21 19 20 +3 20 22 21 +3 23 21 22 +3 22 24 23 +3 25 23 24 +3 24 26 25 +3 27 25 26 +3 26 28 27 +3 29 27 28 +3 28 30 29 +3 31 29 30 +3 0 32 33 +3 33 1 0 +3 1 33 34 +3 34 4 1 +3 4 34 35 +3 35 6 4 +3 6 35 36 +3 36 8 6 +3 8 36 37 +3 37 10 8 +3 10 37 38 +3 38 12 10 +3 12 38 39 +3 39 14 12 +3 14 39 40 +3 40 15 14 +3 15 40 41 +3 41 13 15 +3 13 41 42 +3 42 11 13 +3 11 42 43 +3 43 9 11 +3 9 43 44 +3 44 7 9 +3 7 44 45 +3 45 5 7 +3 5 45 46 +3 46 3 5 +3 3 46 47 +3 47 2 3 +3 2 47 32 +3 32 0 2 +3 32 48 49 +3 49 33 32 +3 33 49 50 +3 50 34 33 +3 34 50 51 +3 51 35 34 +3 35 51 52 +3 52 36 35 +3 36 52 53 +3 53 37 36 +3 37 53 54 +3 54 38 37 +3 38 54 55 +3 55 39 38 +3 39 55 56 +3 56 40 39 +3 40 56 57 +3 57 41 40 +3 41 57 58 +3 58 42 41 +3 42 58 59 +3 59 43 42 +3 43 59 60 +3 60 44 43 +3 44 60 61 +3 61 45 44 +3 45 61 62 +3 62 46 45 +3 46 62 63 +3 63 47 46 +3 47 63 48 +3 48 32 47 +3 48 64 65 +3 65 49 48 +3 49 65 66 +3 66 50 49 +3 50 66 67 +3 67 51 50 +3 51 67 68 +3 68 52 51 +3 52 68 69 +3 69 53 52 +3 53 69 70 +3 70 54 53 +3 54 70 71 +3 71 55 54 +3 55 71 72 +3 72 56 55 +3 56 72 73 +3 73 57 56 +3 57 73 74 +3 74 58 57 +3 58 74 75 +3 75 59 58 +3 59 75 76 +3 76 60 59 +3 60 76 77 +3 77 61 60 +3 61 77 78 +3 78 62 61 +3 62 78 79 +3 79 63 62 +3 63 79 64 +3 64 48 63 +3 64 80 81 +3 81 65 64 +3 65 81 82 +3 82 66 65 +3 66 82 83 +3 83 67 66 +3 67 83 84 +3 84 68 67 +3 68 84 85 +3 85 69 68 +3 69 85 86 +3 86 70 69 +3 70 86 87 +3 87 71 70 +3 71 87 88 +3 88 72 71 +3 72 88 89 +3 89 73 72 +3 73 89 90 +3 90 74 73 +3 74 90 91 +3 91 75 74 +3 75 91 92 +3 92 76 75 +3 76 92 93 +3 93 77 76 +3 77 93 94 +3 94 78 77 +3 78 94 95 +3 95 79 78 +3 79 95 80 +3 80 64 79 +3 80 96 97 +3 97 81 80 +3 81 97 98 +3 98 82 81 +3 82 98 99 +3 99 83 82 +3 83 99 100 +3 100 84 83 +3 84 100 101 +3 101 85 84 +3 85 101 102 +3 102 86 85 +3 86 102 103 +3 103 87 86 +3 87 103 104 +3 104 88 87 +3 88 104 105 +3 105 89 88 +3 89 105 106 +3 106 90 89 +3 90 106 107 +3 107 91 90 +3 91 107 108 +3 108 92 91 +3 92 108 109 +3 109 93 92 +3 93 109 110 +3 110 94 93 +3 94 110 111 +3 111 95 94 +3 95 111 96 +3 96 80 95 +3 96 112 113 +3 113 97 96 +3 97 113 114 +3 114 98 97 +3 98 114 115 +3 115 99 98 +3 99 115 116 +3 116 100 99 +3 100 116 117 +3 117 101 100 +3 101 117 118 +3 118 102 101 +3 102 118 119 +3 119 103 102 +3 103 119 120 +3 120 104 103 +3 104 120 121 +3 121 105 104 +3 105 121 122 +3 122 106 105 +3 106 122 123 +3 123 107 106 +3 107 123 124 +3 124 108 107 +3 108 124 125 +3 125 109 108 +3 109 125 126 +3 126 110 109 +3 110 126 127 +3 127 111 110 +3 111 127 112 +3 112 96 111 +3 112 128 129 +3 129 113 112 +3 113 129 130 +3 130 114 113 +3 114 130 131 +3 131 115 114 +3 115 131 132 +3 132 116 115 +3 116 132 133 +3 133 117 116 +3 117 133 134 +3 134 118 117 +3 118 134 135 +3 135 119 118 +3 119 135 136 +3 136 120 119 +3 120 136 137 +3 137 121 120 +3 121 137 138 +3 138 122 121 +3 122 138 139 +3 139 123 122 +3 123 139 140 +3 140 124 123 +3 124 140 141 +3 141 125 124 +3 125 141 142 +3 142 126 125 +3 126 142 143 +3 143 127 126 +3 127 143 128 +3 128 112 127 +3 128 18 19 +3 19 129 128 +3 129 19 21 +3 21 130 129 +3 130 21 23 +3 23 131 130 +3 131 23 25 +3 25 132 131 +3 132 25 27 +3 27 133 132 +3 133 27 29 +3 29 134 133 +3 134 29 31 +3 31 135 134 +3 135 31 30 +3 30 136 135 +3 136 30 28 +3 28 137 136 +3 137 28 26 +3 26 138 137 +3 138 26 24 +3 24 139 138 +3 139 24 22 +3 22 140 139 +3 140 22 20 +3 20 141 140 +3 141 20 17 +3 17 142 141 +3 142 17 16 +3 16 143 142 +3 143 16 18 +3 18 128 143 diff --git a/assets/cutoff/BR12-22/left.off b/assets/cutoff/BR12-22/left.off new file mode 100644 index 0000000..6585197 --- /dev/null +++ b/assets/cutoff/BR12-22/left.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-26.8182 -4 -144.49 +-52.9234 -4 53.799 +-52.9234 6 53.799 +-26.8182 6 -144.49 +3.91662 -4 -140.444 +3.91662 6 -140.444 +-22.1886 6 57.8453 +-22.1886 -4 57.8453 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BR12-22/right.off b/assets/cutoff/BR12-22/right.off new file mode 100644 index 0000000..56fac91 --- /dev/null +++ b/assets/cutoff/BR12-22/right.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +3.91664 -4 140.444 +-22.1886 -4 -57.8453 +-22.1886 6 -57.8453 +3.91664 6 140.444 +-26.8182 -4 144.49 +-26.8182 6 144.49 +-52.9234 6 -53.799 +-52.9234 -4 -53.799 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BR22/body.off b/assets/cutoff/BR22/body.off new file mode 100644 index 0000000..2548c85 --- /dev/null +++ b/assets/cutoff/BR22/body.off @@ -0,0 +1,478 @@ +OFF +160 316 0 +-15.1376 6 44.4804 +-15.2753 5.98296 44.4623 +9.0977 6 47.6711 +9.23539 5.98296 47.6892 +-15.4224 5.93301 44.4429 +9.38246 5.93301 47.7086 +-15.5688 5.85355 44.4237 +9.52889 5.85355 47.7278 +-15.7046 5.75 44.4058 +9.66471 5.75 47.7457 +-15.9087 5.5 44.3789 +9.86882 5.5 47.7726 +-18.3874 1 44.0526 +12.3474 1 48.0989 +-18.3874 0 44.0526 +12.3474 0 48.0989 +9.0977 6 -47.6711 +9.23539 5.98296 -47.6892 +-15.1376 6 -44.4804 +-15.2753 5.98296 -44.4623 +9.38246 5.93301 -47.7086 +-15.4224 5.93301 -44.4429 +9.52889 5.85355 -47.7278 +-15.5688 5.85355 -44.4237 +9.66471 5.75 -47.7457 +-15.7046 5.75 -44.4058 +9.86882 5.5 -47.7726 +-15.9087 5.5 -44.3789 +12.3474 1 -48.0989 +-18.3874 1 -44.0526 +12.3474 0 -48.0989 +-18.3874 0 -44.0526 +-13.9869 6 34.635 +-14.125 5.98296 34.6209 +-14.2726 5.93301 34.6058 +-14.4195 5.85355 34.5908 +-14.5558 5.75 34.5769 +-14.7606 5.5 34.5559 +-17.2477 1 34.3018 +-17.2477 0 34.3018 +13.5918 0 37.4525 +13.5918 1 37.4525 +11.1047 5.5 37.1984 +10.8999 5.75 37.1775 +10.7637 5.85355 37.1636 +10.6167 5.93301 37.1486 +10.4691 5.98296 37.1335 +10.331 6 37.1194 +-13.1229 6 24.7602 +-13.2614 5.98296 24.7501 +-13.4094 5.93301 24.7394 +-13.5567 5.85355 24.7286 +-13.6933 5.75 24.7187 +-13.8987 5.5 24.7037 +-16.392 1 24.5221 +-16.392 0 24.5221 +14.526 0 26.7745 +14.526 1 26.7745 +12.0326 5.5 26.5928 +11.8273 5.75 26.5779 +11.6907 5.85355 26.5679 +11.5434 5.93301 26.5572 +11.3954 5.98296 26.5464 +11.2569 6 26.5363 +-12.5466 6 14.8645 +-12.6853 5.98296 14.8585 +-12.8335 5.93301 14.852 +-12.9811 5.85355 14.8455 +-13.1179 5.75 14.8396 +-13.3236 5.5 14.8306 +-15.8212 1 14.7215 +-15.8212 0 14.7215 +15.1493 0 16.0737 +15.1493 1 16.0737 +12.6517 5.5 15.9647 +12.446 5.75 15.9557 +12.3091 5.85355 15.9497 +12.1616 5.93301 15.9433 +12.0134 5.98296 15.9368 +11.8746 6 15.9308 +-12.2583 6 4.95624 +-12.3971 5.98296 4.95422 +-12.5454 5.93301 4.95206 +-12.6931 5.85355 4.94991 +-12.8301 5.75 4.94792 +-13.0359 5.5 4.94493 +-15.5357 1 4.90857 +-15.5357 0 4.90857 +15.461 0 5.35943 +15.461 1 5.35943 +12.9613 5.5 5.32307 +12.7554 5.75 5.32007 +12.6185 5.85355 5.31808 +12.4708 5.93301 5.31593 +12.3225 5.98296 5.31378 +12.1836 6 5.31176 +-12.2583 6 -4.95623 +-12.3971 5.98296 -4.95422 +-12.5454 5.93301 -4.95206 +-12.6931 5.85355 -4.94991 +-12.8301 5.75 -4.94792 +-13.0359 5.5 -4.94492 +-15.5357 1 -4.90856 +-15.5357 0 -4.90856 +15.461 0 -5.35942 +15.461 1 -5.35942 +12.9613 5.5 -5.32306 +12.7554 5.75 -5.32007 +12.6185 5.85355 -5.31808 +12.4708 5.93301 -5.31593 +12.3225 5.98296 -5.31377 +12.1836 6 -5.31175 +-12.5466 6 -14.8645 +-12.6853 5.98296 -14.8585 +-12.8335 5.93301 -14.852 +-12.9811 5.85355 -14.8455 +-13.1179 5.75 -14.8396 +-13.3236 5.5 -14.8306 +-15.8212 1 -14.7215 +-15.8212 0 -14.7215 +15.1493 0 -16.0737 +15.1493 1 -16.0737 +12.6517 5.5 -15.9647 +12.446 5.75 -15.9557 +12.3091 5.85355 -15.9497 +12.1616 5.93301 -15.9433 +12.0134 5.98296 -15.9368 +11.8746 6 -15.9308 +-13.1229 6 -24.7602 +-13.2614 5.98296 -24.7501 +-13.4094 5.93301 -24.7394 +-13.5567 5.85355 -24.7286 +-13.6933 5.75 -24.7187 +-13.8987 5.5 -24.7037 +-16.392 1 -24.5221 +-16.392 0 -24.5221 +14.526 0 -26.7745 +14.526 1 -26.7745 +12.0326 5.5 -26.5928 +11.8273 5.75 -26.5779 +11.6907 5.85355 -26.5679 +11.5434 5.93301 -26.5572 +11.3954 5.98296 -26.5464 +11.2569 6 -26.5363 +-13.9869 6 -34.635 +-14.125 5.98296 -34.6209 +-14.2726 5.93301 -34.6058 +-14.4195 5.85355 -34.5908 +-14.5558 5.75 -34.5768 +-14.7606 5.5 -34.5559 +-17.2477 1 -34.3018 +-17.2477 0 -34.3018 +13.5918 0 -37.4525 +13.5918 1 -37.4525 +11.1047 5.5 -37.1984 +10.8999 5.75 -37.1775 +10.7637 5.85355 -37.1636 +10.6167 5.93301 -37.1486 +10.4691 5.98296 -37.1335 +10.331 6 -37.1194 +3 0 1 2 +3 3 2 1 +3 1 4 3 +3 5 3 4 +3 4 6 5 +3 7 5 6 +3 6 8 7 +3 9 7 8 +3 8 10 9 +3 11 9 10 +3 10 12 11 +3 13 11 12 +3 12 14 13 +3 15 13 14 +3 16 17 18 +3 19 18 17 +3 17 20 19 +3 21 19 20 +3 20 22 21 +3 23 21 22 +3 22 24 23 +3 25 23 24 +3 24 26 25 +3 27 25 26 +3 26 28 27 +3 29 27 28 +3 28 30 29 +3 31 29 30 +3 0 32 33 +3 33 1 0 +3 1 33 34 +3 34 4 1 +3 4 34 35 +3 35 6 4 +3 6 35 36 +3 36 8 6 +3 8 36 37 +3 37 10 8 +3 10 37 38 +3 38 12 10 +3 12 38 39 +3 39 14 12 +3 14 39 40 +3 40 15 14 +3 15 40 41 +3 41 13 15 +3 13 41 42 +3 42 11 13 +3 11 42 43 +3 43 9 11 +3 9 43 44 +3 44 7 9 +3 7 44 45 +3 45 5 7 +3 5 45 46 +3 46 3 5 +3 3 46 47 +3 47 2 3 +3 2 47 32 +3 32 0 2 +3 32 48 49 +3 49 33 32 +3 33 49 50 +3 50 34 33 +3 34 50 51 +3 51 35 34 +3 35 51 52 +3 52 36 35 +3 36 52 53 +3 53 37 36 +3 37 53 54 +3 54 38 37 +3 38 54 55 +3 55 39 38 +3 39 55 56 +3 56 40 39 +3 40 56 57 +3 57 41 40 +3 41 57 58 +3 58 42 41 +3 42 58 59 +3 59 43 42 +3 43 59 60 +3 60 44 43 +3 44 60 61 +3 61 45 44 +3 45 61 62 +3 62 46 45 +3 46 62 63 +3 63 47 46 +3 47 63 48 +3 48 32 47 +3 48 64 65 +3 65 49 48 +3 49 65 66 +3 66 50 49 +3 50 66 67 +3 67 51 50 +3 51 67 68 +3 68 52 51 +3 52 68 69 +3 69 53 52 +3 53 69 70 +3 70 54 53 +3 54 70 71 +3 71 55 54 +3 55 71 72 +3 72 56 55 +3 56 72 73 +3 73 57 56 +3 57 73 74 +3 74 58 57 +3 58 74 75 +3 75 59 58 +3 59 75 76 +3 76 60 59 +3 60 76 77 +3 77 61 60 +3 61 77 78 +3 78 62 61 +3 62 78 79 +3 79 63 62 +3 63 79 64 +3 64 48 63 +3 64 80 81 +3 81 65 64 +3 65 81 82 +3 82 66 65 +3 66 82 83 +3 83 67 66 +3 67 83 84 +3 84 68 67 +3 68 84 85 +3 85 69 68 +3 69 85 86 +3 86 70 69 +3 70 86 87 +3 87 71 70 +3 71 87 88 +3 88 72 71 +3 72 88 89 +3 89 73 72 +3 73 89 90 +3 90 74 73 +3 74 90 91 +3 91 75 74 +3 75 91 92 +3 92 76 75 +3 76 92 93 +3 93 77 76 +3 77 93 94 +3 94 78 77 +3 78 94 95 +3 95 79 78 +3 79 95 80 +3 80 64 79 +3 80 96 97 +3 97 81 80 +3 81 97 98 +3 98 82 81 +3 82 98 99 +3 99 83 82 +3 83 99 100 +3 100 84 83 +3 84 100 101 +3 101 85 84 +3 85 101 102 +3 102 86 85 +3 86 102 103 +3 103 87 86 +3 87 103 104 +3 104 88 87 +3 88 104 105 +3 105 89 88 +3 89 105 106 +3 106 90 89 +3 90 106 107 +3 107 91 90 +3 91 107 108 +3 108 92 91 +3 92 108 109 +3 109 93 92 +3 93 109 110 +3 110 94 93 +3 94 110 111 +3 111 95 94 +3 95 111 96 +3 96 80 95 +3 96 112 113 +3 113 97 96 +3 97 113 114 +3 114 98 97 +3 98 114 115 +3 115 99 98 +3 99 115 116 +3 116 100 99 +3 100 116 117 +3 117 101 100 +3 101 117 118 +3 118 102 101 +3 102 118 119 +3 119 103 102 +3 103 119 120 +3 120 104 103 +3 104 120 121 +3 121 105 104 +3 105 121 122 +3 122 106 105 +3 106 122 123 +3 123 107 106 +3 107 123 124 +3 124 108 107 +3 108 124 125 +3 125 109 108 +3 109 125 126 +3 126 110 109 +3 110 126 127 +3 127 111 110 +3 111 127 112 +3 112 96 111 +3 112 128 129 +3 129 113 112 +3 113 129 130 +3 130 114 113 +3 114 130 131 +3 131 115 114 +3 115 131 132 +3 132 116 115 +3 116 132 133 +3 133 117 116 +3 117 133 134 +3 134 118 117 +3 118 134 135 +3 135 119 118 +3 119 135 136 +3 136 120 119 +3 120 136 137 +3 137 121 120 +3 121 137 138 +3 138 122 121 +3 122 138 139 +3 139 123 122 +3 123 139 140 +3 140 124 123 +3 124 140 141 +3 141 125 124 +3 125 141 142 +3 142 126 125 +3 126 142 143 +3 143 127 126 +3 127 143 128 +3 128 112 127 +3 128 144 145 +3 145 129 128 +3 129 145 146 +3 146 130 129 +3 130 146 147 +3 147 131 130 +3 131 147 148 +3 148 132 131 +3 132 148 149 +3 149 133 132 +3 133 149 150 +3 150 134 133 +3 134 150 151 +3 151 135 134 +3 135 151 152 +3 152 136 135 +3 136 152 153 +3 153 137 136 +3 137 153 154 +3 154 138 137 +3 138 154 155 +3 155 139 138 +3 139 155 156 +3 156 140 139 +3 140 156 157 +3 157 141 140 +3 141 157 158 +3 158 142 141 +3 142 158 159 +3 159 143 142 +3 143 159 144 +3 144 128 143 +3 144 18 19 +3 19 145 144 +3 145 19 21 +3 21 146 145 +3 146 21 23 +3 23 147 146 +3 147 23 25 +3 25 148 147 +3 148 25 27 +3 27 149 148 +3 149 27 29 +3 29 150 149 +3 150 29 31 +3 31 151 150 +3 151 31 30 +3 30 152 151 +3 152 30 28 +3 28 153 152 +3 153 28 26 +3 26 154 153 +3 154 26 24 +3 24 155 154 +3 155 24 22 +3 22 156 155 +3 156 22 20 +3 20 157 156 +3 157 20 17 +3 17 158 157 +3 158 17 16 +3 16 159 158 +3 159 16 18 +3 18 144 159 diff --git a/assets/cutoff/BR22/left.off b/assets/cutoff/BR22/left.off new file mode 100644 index 0000000..449816e --- /dev/null +++ b/assets/cutoff/BR22/left.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-6.05279 -4 150.054 +70.4839 -4 -34.7216 +70.4839 6 -34.7216 +-6.05279 6 150.054 +-34.6931 -4 138.191 +-34.6931 6 138.191 +41.8436 6 -46.5848 +41.8436 -4 -46.5848 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/cutoff/BR22/right.off b/assets/cutoff/BR22/right.off new file mode 100644 index 0000000..f7b1dc8 --- /dev/null +++ b/assets/cutoff/BR22/right.off @@ -0,0 +1,16 @@ +OFF +8 6 0 +-34.6931 -4 -138.191 +41.8437 -4 46.5848 +41.8437 6 46.5848 +-34.6931 6 -138.191 +-6.05281 -4 -150.054 +-6.05281 6 -150.054 +70.4839 6 34.7216 +70.4839 -4 34.7216 +4 0 1 2 3 +4 4 5 6 7 +4 0 4 7 1 +4 3 2 6 5 +4 0 3 5 4 +4 1 7 6 2 diff --git a/assets/qtcsgassets.qrc b/assets/qtcsgassets.qrc new file mode 100644 index 0000000..f0079a0 --- /dev/null +++ b/assets/qtcsgassets.qrc @@ -0,0 +1,19 @@ + + + cutoff/BG3-R12/body.off + cutoff/BG3-R12/left.off + cutoff/BG3-R12/right.off + cutoff/BG5/body.off + cutoff/BG5/left.off + cutoff/BG5/right.off + cutoff/BR11-32/body.off + cutoff/BR11-32/left.off + cutoff/BR11-32/right.off + cutoff/BR12-22/body.off + cutoff/BR12-22/left.off + cutoff/BR12-22/right.off + cutoff/BR22/body.off + cutoff/BR22/left.off + cutoff/BR22/right.off + + diff --git a/cmake/QtCSGBackports.cmake b/cmake/QtCSGBackports.cmake new file mode 100644 index 0000000..ee85dc4 --- /dev/null +++ b/cmake/QtCSGBackports.cmake @@ -0,0 +1,30 @@ +if (NOT QT_VERSION STREQUAL Qt${QT_VERSION_MAJOR}_VERSION) + message(FATAL_ERROR "Unexpected version for Qt${QT_VERSION_MAJOR} package: ${Qt${QT_VERSION_MAJOR}_VERSION}") +endif() + +if (NOT QT_DIR STREQUAL Qt${QT_VERSION_MAJOR}_DIR) + message(FATAL_ERROR "Unexpected directory for Qt${QT_VERSION_MAJOR} package: ${Qt${QT_VERSION_MAJOR}_DIR}") +endif() + +foreach(module ${QT_MODULES}) + if (NOT TARGET Qt::${module}) + add_library(Qt::${module} ALIAS Qt${QT_VERSION_MAJOR}::${module}) + endif() +endforeach() + +# Very minimal backport of qt_add_executable for Qt +if (NOT COMMAND qt_add_executable) + function(qt_add_executable NAME) + if (ANDROID) + add_library(${NAME} SHARED ${ARGN}) + else() + add_executable(${NAME} ${ARGN}) + endif() + endfunction() +endif() + +if (NOT COMMAND qt_add_library) + function(qt_add_library NAME) + add_library(${NAME} ${ARGN}) + endfunction() +endif() diff --git a/cmake/QtCSGDebug.cmake b/cmake/QtCSGDebug.cmake new file mode 100644 index 0000000..b1eda43 --- /dev/null +++ b/cmake/QtCSGDebug.cmake @@ -0,0 +1,9 @@ +macro(qtcsg_show_variable variable) + message(STATUS "${variable}: ${${variable}}") +endmacro() + +macro(qtcsg_show_property target property) + get_target_property(${property} ${target} ${property}) + message(STATUS "${target} ${property}: ${${property}}") +endmacro() + diff --git a/cmake/QtCSGEnableAlternateLinker.cmake b/cmake/QtCSGEnableAlternateLinker.cmake new file mode 100644 index 0000000..52a1b7c --- /dev/null +++ b/cmake/QtCSGEnableAlternateLinker.cmake @@ -0,0 +1,16 @@ +include(CheckLinkerFlag) + +# Checks if alternate, faster linkers like mold, lld, or gold +# are supported by the compiler and enables the best one found. +function(qtcsg_enable_alternate_linker) + foreach(linker_name IN ITEMS mold lld gold) + string(TOUPPER QTCSG_HAVE_ALTERNATE_LINKER_${linker_name} linker_variable) + check_linker_flag(CXX -fuse-ld=${linker_name} ${linker_variable}) + + if (${linker_variable}) + message(STATUS "Using alternate linker: ${linker_name}") + add_link_options(-fuse-ld=${linker_name}) + break() + endif() + endforeach() +endfunction() diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 3152057..c3d4e78 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -1,5 +1,37 @@ +qt_add_library( + QtCSGAppSupport STATIC + qtcsgappsupport.cpp + qtcsgappsupport.h + orbitcameracontroller.cpp + orbitcameracontroller.h + wireframematerial.cpp + wireframematerial.h + wireframematerial.qrc +) + +target_link_libraries(QtCSGAppSupport PUBLIC Qt3DCSG Qt::3DExtras) + qt_add_executable(QtCSGDemo qtcsgdemo.cpp) -target_link_libraries(QtCSGDemo PRIVATE Qt3DCSG Qt::3DExtras Qt::Widgets) +target_link_libraries(QtCSGDemo PRIVATE QtCSGAppSupport Qt::Widgets) + +set_target_properties(QtCSGDemo PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER qtcsgdemo.taschenorakel.de + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +qt_add_executable(QtCSGInspector qtcsginspector.cpp) +target_link_libraries(QtCSGInspector PRIVATE QtCSGAppSupport QtCSGAssets Qt::Widgets) + +set_target_properties(QtCSGInspector PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER qtcsginspector.taschenorakel.de + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) #if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) ## Define target properties for Android with Qt 6 as: @@ -9,11 +41,3 @@ target_link_libraries(QtCSGDemo PRIVATE Qt3DCSG Qt::3DExtras Qt::Widgets) ## Define properties for Android with Qt 5 after find_package() calls as: ## set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") #endif() - -set_target_properties(QtCSGDemo PROPERTIES - MACOSX_BUNDLE_GUI_IDENTIFIER qtcsg.taschenorakel.de - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} - MACOSX_BUNDLE TRUE - WIN32_EXECUTABLE TRUE -) diff --git a/demo/orbitcameracontroller.cpp b/demo/orbitcameracontroller.cpp new file mode 100644 index 0000000..9fb56e1 --- /dev/null +++ b/demo/orbitcameracontroller.cpp @@ -0,0 +1,77 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "orbitcameracontroller.h" + +#include + +namespace QtCSG::Demo { + +void OrbitCameraController::moveCamera(const InputState &state, float dt) +{ + if (state.rightMouseButtonActive && state.leftMouseButtonActive) { + zoom(state.ryAxisValue * dt); + } else if (state.rightMouseButtonActive + || (state.leftMouseButtonActive && state.altKeyActive)) { + if (state.shiftKeyActive) { + translate(state, dt); + } else { + translate(state, dt * 2.5); + } + } else if (state.leftMouseButtonActive) { + orbit(state.rxAxisValue * dt, state.ryAxisValue * dt); + } + + if (state.altKeyActive) { + if (state.shiftKeyActive) { + translate(state, dt / 2.5); + } else { + translate(state, dt); + } + } else { + orbit(state.txAxisValue * dt, state.tyAxisValue * dt); + zoom(state.tzAxisValue * dt); + } +} + +void OrbitCameraController::orbit(float rx, float ry) +{ + camera()->panAboutViewCenter(rx * lookSpeed(), {0, 1, 0}); + camera()->tiltAboutViewCenter(ry * lookSpeed()); +} + +void OrbitCameraController::zoom(float dz) +{ + const auto camera = this->camera(); + const auto zoomDistance = (camera->viewCenter() - camera->position()).lengthSquared(); + + if (zoomDistance > zoomInLimit() * zoomInLimit()) { // Dolly up to limit + camera->translate({0, 0, linearSpeed() * dz}, camera->DontTranslateViewCenter); + } else { + camera->translate({0, 0, -0.5}, camera->DontTranslateViewCenter); + } +} + +void OrbitCameraController::translate(const InputState &state, float dt) +{ + const auto dx = qBound(-1, state.rxAxisValue + state.txAxisValue, 1); + const auto dy = qBound(-1, state.ryAxisValue + state.tyAxisValue, 1); + camera()->translate({dx * linearSpeed() * dt, dy * linearSpeed() * dt, 0}); +} + +} // namespace QtCSG::Demo diff --git a/demo/orbitcameracontroller.h b/demo/orbitcameracontroller.h new file mode 100644 index 0000000..d6bede8 --- /dev/null +++ b/demo/orbitcameracontroller.h @@ -0,0 +1,58 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef QT3DCSG_ORBITCAMERACONTROLLER_H +#define QT3DCSG_ORBITCAMERACONTROLLER_H + +#include + +namespace QtCSG::Demo { + +// a simple orbital camera controller for inspectiong the scene +// ------------------------------------------------------------------------------------------------- +// +// Mouse bindings +// +// - left button: orbits the objects +// - right button: moves the object quickly +// - right button + shift key: moves the object slowly +// - left button + right button: zooms the objects +// - left button + alt key: simulates right button +// +// Keyboard bindings +// - arrow keys (left, right, up, down): orbits the object +// - page up, page down: zooms the object +// - arrow keys plus alt key: moves the object quickly +// - arrow keys plus alt key and shift: moves the object slowly +// +class OrbitCameraController : public Qt3DExtras::QOrbitCameraController +{ +public: + using Qt3DExtras::QOrbitCameraController::QOrbitCameraController; + +private: + void moveCamera(const InputState &state, float dt) override; + + void orbit(float rx, float ry); + void zoom(float dz); + void translate(const InputState &state, float dt); +}; + +} // namespace QtCSG::Demo + +#endif // QT3DCSG_ORBITCAMERACONTROLLER_H diff --git a/demo/qtcsgappsupport.cpp b/demo/qtcsgappsupport.cpp new file mode 100644 index 0000000..549576a --- /dev/null +++ b/demo/qtcsgappsupport.cpp @@ -0,0 +1,23 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgappsupport.h" + +namespace QtCSG::Demo { + +} // namespace QtCSG::Demo diff --git a/demo/qtcsgappsupport.h b/demo/qtcsgappsupport.h new file mode 100644 index 0000000..db77cb0 --- /dev/null +++ b/demo/qtcsgappsupport.h @@ -0,0 +1,111 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef QT3DCSG_APPSUPPORT_H +#define QT3DCSG_APPSUPPORT_H + +#include +#include +#include + +namespace QtCSG::AppSupport { + +// some utility functions making it easier to deal with matrices and vectors +// ------------------------------------------------------------------------------------------------- + +[[nodiscard]] inline QPoint toPoint(QSize size) +{ + return {size.width(), size.height()}; +} + +// move static initialization of QApplication from main() into our Application class +// ------------------------------------------------------------------------------------------------- + +template +class StaticInit +{ +public: + StaticInit() { T::staticInit(); } +}; + +// combines multiple enum classes into one +// ------------------------------------------------------------------------------------------------- + +template +struct MultiEnum : public std::variant +{ + static constexpr bool EnumsArgumentsOnly = (std::is_enum_v &&...); + static_assert(EnumsArgumentsOnly); + + using std::variant::variant; + + constexpr bool operator==(const MultiEnum &) const = default; + + template + constexpr bool operator==(const T &v) const + { + if (!std::holds_alternative(*this)) + return false; + + return std::get(*this) == v; + } + + bool operator==(const QVariant &v) const + { + registerConverters(); + + if (QMetaType::canConvert(v.metaType(), QMetaType::fromType())) + return qvariant_cast(v) == *this; + + return false; + } + + operator QVariant() const + { + registerConverters(); + return QVariant::fromValue(*this); + } + + + operator const Alias &() const { return *static_cast(this); } + template constexpr operator T() const { return std::get(*this); } + + static bool registerConverters() + { + static const auto s_registered = (QMetaType::registerConverter() + && QMetaType::registerConverter() + && (QMetaType::registerConverter() && ...) + && (QMetaType::registerConverter() && ...) + && (QMetaType::registerConverter() && ...) + && (QMetaType::registerConverter() && ...)); + + return s_registered; + } +}; + +template +inline QDebug operator<<(QDebug debug, const MultiEnum &multiEnum) +{ + return std::visit([debug](auto v) { + return debug << v; + }, multiEnum); +} + +} // namespace QtCSG::AppSupport + +#endif // QT3DCSG_APPSUPPORT_H diff --git a/demo/qtcsgdemo.cpp b/demo/qtcsgdemo.cpp index 845b7a0..3e790a7 100644 --- a/demo/qtcsgdemo.cpp +++ b/demo/qtcsgdemo.cpp @@ -16,16 +16,21 @@ * * SPDX-License-Identifier: GPL-3.0-or-later */ +#include "qtcsgappsupport.h" +#include "orbitcameracontroller.h" +#include "wireframematerial.h" + #include +#include +#include + #include #include #include #include -#include #include -#include #include #include @@ -36,41 +41,61 @@ #include #include +#include +#include +#include #include +namespace QtCSG::Demo { namespace { -QPoint toPoint(QSize size) -{ - return {size.width(), size.height()}; -} +using Qt3DCore::QEntity; +using Qt3DRender::QGeometryRenderer; -template -QMatrix4x4 translation(Args... args) -{ - auto matrix = QMatrix4x4{}; - matrix.translate(args...); - return matrix; -} +const auto s_colors = std::array { + QRgb{0x66'23'54}, + QRgb{0x66'23'23}, + QRgb{0x66'54'23}, + QRgb{0x23'66'54}, + QRgb{0x23'23'66}, +}; -template -QMatrix4x4 scale(Args... args) -{ - auto matrix = QMatrix4x4{}; - matrix.scale(args...); - return matrix; -} +struct RenderingStyle { + float lineWidth; + float diffuseAlpha; + QColor specularColor; +}; + +const auto s_wireframeVisible = RenderingStyle{1.0f, 0.2f, QColor::fromRgbF(0.0, 0.0, 0.0, 0.0)}; +const auto s_wireframeHidden = RenderingStyle{0.0f, 1.0f, QColor::fromRgbF(0.95, 0.95, 0.95, 1.0)}; -template -class StaticInit +// convenience function to create Qt3D entities from geometry renderers +// ------------------------------------------------------------------------------------------------- +void createEntity(QGeometryRenderer *renderer, QVector3D position, QColor color, QEntity *parent) { -public: - StaticInit() { T::staticInit(); } -}; + const auto transform = new Qt3DCore::QTransform; + transform->setScale(1.5f); + transform->setRotation(QQuaternion::fromAxisAndAngle({1.0f, 0.0f, 0.0f}, 45.0f)); + transform->setTranslation(position); + + const auto material = new WireframeMaterial; + material->setFrontLineWidth(s_wireframeHidden.lineWidth); + material->setBackLineWidth(s_wireframeHidden.lineWidth); + material->setSpecular(s_wireframeHidden.specularColor); + color.setAlphaF(s_wireframeHidden.diffuseAlpha); + material->setDiffuse(std::move(color)); + + const auto entity = new QEntity{parent}; + entity->addComponent(renderer); + entity->addComponent(material); + entity->addComponent(transform); +} +// the demo application +// ------------------------------------------------------------------------------------------------- class Application - : private StaticInit - , public QApplication + : private AppSupport::StaticInit + , public QApplication { friend class StaticInit; @@ -81,63 +106,24 @@ class Application private: static void staticInit(); -}; - -int Application::run() -{ - using namespace Qt3DCore; - using namespace Qt3DExtras; - using namespace Qt3DRender; - - // 3D view - const auto view = new Qt3DWindow; - view->defaultFrameGraph()->setClearColor(QRgb{0x4d'4d'4f}); - - const auto container = QWidget::createWindowContainer(view); - const auto screenSize = view->screen()->size(); - container->setMinimumSize({200, 100}); - container->setMaximumSize(screenSize); - // root entity - const auto rootEntity = new QEntity; + QEntity *createShowCase(QEntity *parent); + QEntity *createUnionTest(QEntity *parent); - // camera - const auto cameraEntity = view->camera(); - cameraEntity->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f); - cameraEntity->setPosition({0, 0, 20.0f}); - cameraEntity->setUpVector({0, 1, 0}); - cameraEntity->setViewCenter({0, 0, 0}); + void collectEntities(QEntity *root); + void onWireframeBoxToggled(bool checked); - const auto camController = new QFirstPersonCameraController{rootEntity}; - camController->setCamera(cameraEntity); + QList m_entities; +}; - // lighting - const auto lightEntity = new QEntity{rootEntity}; - const auto light = new QPointLight{lightEntity}; - light->setColor("white"); - light->setIntensity(2.5f); - lightEntity->addComponent(light); +// ------------------------------------------------------------------------------------------------- - const auto lightTransform = new Qt3DCore::QTransform{lightEntity}; - lightTransform->setTranslation(cameraEntity->position()); - lightEntity->addComponent(lightTransform); +QEntity *Application::createShowCase(QEntity *parent) +{ + using namespace Qt3DExtras; + using namespace Qt3DRender; - // create entities - const auto createEntity = [rootEntity](QGeometryRenderer *renderer, QVector3D position, QColor color) { - const auto transform = new Qt3DCore::QTransform; - transform->setScale(1.5f); - transform->setRotation(QQuaternion::fromAxisAndAngle({1.0f, 0.0f, 0.0f}, 45.0f)); - transform->setTranslation(position); - - const auto material = new Qt3DExtras::QPhongMaterial; - material->setDiffuse(color); - - //Cuboid - const auto entity = new QEntity{rootEntity}; - entity->addComponent(renderer); - entity->addComponent(material); - entity->addComponent(transform); - }; + const auto showCase = new QEntity{parent}; const auto cuboidMesh = new QCuboidMesh; cuboidMesh->setXExtent(2); @@ -151,100 +137,228 @@ int Application::run() cylinderMesh->setRadius(1.0f); cylinderMesh->setLength(2.0f); - const auto colors = std::array { - QRgb{0x66'23'54}, - QRgb{0x66'23'23}, - QRgb{0x66'54'23}, - QRgb{0x23'66'54}, - QRgb{0x23'23'66}, - }; - // Qt3D geometries - createEntity(cuboidMesh, {-9.0f, -5.0f, -1.5f}, colors[0]); - createEntity(sphereMesh, {-9.0f, 0.0f, -1.5f}, colors[0]); - createEntity(cylinderMesh, {-9.0f, 5.0f, -1.5f}, colors[0]); + createEntity(cuboidMesh, {-9.0f, -5.0f, -1.5f}, s_colors[0], showCase); + createEntity(sphereMesh, {-9.0f, 0.0f, -1.5f}, s_colors[0], showCase); + createEntity(cylinderMesh, {-9.0f, 5.0f, -1.5f}, s_colors[0], showCase); // native QtCSG geometries - createEntity(new Qt3DCSG::Mesh{QtCSG::cube()}, {-4.5f, -5.0f, -1.5f}, colors[1]); - createEntity(new Qt3DCSG::Mesh{QtCSG::sphere()}, {-4.5f, 0.0f, -1.5f}, colors[1]); - createEntity(new Qt3DCSG::Mesh{QtCSG::cylinder()}, {-4.5f, 5.0f, -1.5f}, colors[1]); + createEntity(new Qt3DCSG::Mesh{QtCSG::cube()}, {-4.5f, -5.0f, -1.5f}, s_colors[1], showCase); + createEntity(new Qt3DCSG::Mesh{QtCSG::sphere()}, {-4.5f, 0.0f, -1.5f}, s_colors[1], showCase); + createEntity(new Qt3DCSG::Mesh{QtCSG::cylinder()}, {-4.5f, 5.0f, -1.5f}, s_colors[1], showCase); // Qt3D shapes converted into QtCSG geometries if (const auto mesh = new Qt3DCSG::Mesh{Qt3DCSG::geometry(cuboidMesh)}) - createEntity(mesh, {0.0f, -5.0f, -1.5f}, colors[2]); + createEntity(mesh, {0.0f, -5.0f, -1.5f}, s_colors[2], showCase); if (const auto mesh = new Qt3DCSG::Mesh{Qt3DCSG::geometry(sphereMesh)}) - createEntity(mesh, {0.0f, 0.0f, -1.5f}, colors[2]); + createEntity(mesh, {0.0f, 0.0f, -1.5f}, s_colors[2], showCase); if (const auto mesh = new Qt3DCSG::Mesh{Qt3DCSG::geometry(cylinderMesh)}) - createEntity(mesh, {0.0f, 5.0f, -1.5f}, colors[2]); + createEntity(mesh, {0.0f, 5.0f, -1.5f}, s_colors[2], showCase); // CSG operations on native QtCSG geometries { const auto delta = 0.3f; - const auto a = QtCSG::cube({-delta, -delta, +delta}); + const auto r = rotation(45, 1, 1, 0); + const auto a = r * QtCSG::cube({-delta, -delta, +delta}); const auto b = QtCSG::cube({+delta, +delta, -delta}); - createEntity(new Qt3DCSG::Mesh{a | b}, {4.5f, -5.0f, -1.5f}, colors[3]); + createEntity(new Qt3DCSG::Mesh{a | b}, {4.5f, -5.0f, -1.5f}, s_colors[3], showCase); } { const auto a = QtCSG::cube(); const auto b = QtCSG::sphere({}, 1.3); - createEntity(new Qt3DCSG::Mesh{a - b}, {4.5f, 0.0f, -1.5f}, colors[3]); + createEntity(new Qt3DCSG::Mesh{a - b}, {4.5f, 0.0f, -1.5f}, s_colors[3], showCase); } { const auto a = QtCSG::sphere(); const auto b = QtCSG::cylinder({}, 2, 0.8); - createEntity(new Qt3DCSG::Mesh{a & b}, {4.5f, 5.0f, -1.5f}, colors[3]); + createEntity(new Qt3DCSG::Mesh{a & b}, {4.5f, 5.0f, -1.5f}, s_colors[3], showCase); } // CSG operations on native Qt3D geometries { const auto delta = 0.3f; - const auto a = Qt3DCSG::geometry(cuboidMesh, translation(-delta, -delta, +delta)); + const auto r = rotation(45, 1, 1, 0); + const auto a = Qt3DCSG::geometry(cuboidMesh, translation(-delta, -delta, +delta) * r); const auto b = Qt3DCSG::geometry(cuboidMesh, translation(+delta, +delta, -delta)); - createEntity(new Qt3DCSG::Mesh{a | b}, {9.0f, -5.0f, -1.5f}, colors[4]); + createEntity(new Qt3DCSG::Mesh{a | b}, {9.0f, -5.0f, -1.5f}, s_colors[4], showCase); } { const auto a = Qt3DCSG::geometry(cuboidMesh); const auto b = Qt3DCSG::geometry(sphereMesh, scale(1.3)); - createEntity(new Qt3DCSG::Mesh{a - b}, {9.0f, 0.0f, -1.5f}, colors[4]); + createEntity(new Qt3DCSG::Mesh{a - b}, {9.0f, 0.0f, -1.5f}, s_colors[4], showCase); } { const auto a = Qt3DCSG::geometry(sphereMesh); const auto b = Qt3DCSG::geometry(cylinderMesh, scale(0.8, 1.0, 0.8)); - createEntity(new Qt3DCSG::Mesh{a & b}, {9.0f, 5.0f, -1.5f}, colors[4]); + createEntity(new Qt3DCSG::Mesh{a & b}, {9.0f, 5.0f, -1.5f}, s_colors[4], showCase); } + return showCase; +} + +QEntity *Application::createUnionTest(QEntity *parent) +{ + const auto unionTest = new QEntity{parent}; + + const auto createUnion = [unionTest](float delta, float x, bool adjacent, QColor color) { + const auto a = QtCSG::cube({-delta, adjacent ? 0 : -delta, adjacent ? 0 : +delta}); + const auto b = QtCSG::cube({+delta, adjacent ? 0 : +delta, adjacent ? 0 : -delta}); + const auto c = QtCSG::merge(a, b); + + const auto y = adjacent ? -6.0f : +3.0f; + createEntity(new Qt3DCSG::Mesh{c}, {x, y, -1.5f}, color, unionTest); + }; + + for (const auto adjacent: {false, true}) { + createUnion(0.0, -10.5, adjacent, s_colors[0]); + createUnion(0.5, -5.75, adjacent, s_colors[1]); + createUnion(1.0, +0.5, adjacent, s_colors[2]); + createUnion(1.5, +8.25, adjacent, s_colors[3]); + } + + return unionTest; +} + +void Application::collectEntities(QEntity *root) +{ + for (const auto scene: root->childNodes()) { + for (const auto node: scene->childNodes()) { + if (const auto entity = dynamic_cast(node)) + m_entities += entity; + } + } +} + +void Application::onWireframeBoxToggled(bool checked) +{ + const auto &renderStyle = checked ? s_wireframeVisible : s_wireframeHidden; + + for (const auto entity: m_entities) { + for (const auto material: entity->componentsOfType()) { + auto diffuseColor = material->diffuse(); + diffuseColor.setAlphaF(renderStyle.diffuseAlpha); + + material->setAlphaBlendingEnabled(checked); + material->setDiffuse(std::move(diffuseColor)); + material->setSpecular(renderStyle.specularColor); + material->setFrontLineWidth(renderStyle.lineWidth); + material->setBackLineWidth(renderStyle.lineWidth); + } + } +} + +int Application::run() +{ + // 3D view + const auto view = new Qt3DExtras::Qt3DWindow; + view->defaultFrameGraph()->setClearColor(QRgb{0x4d'4d'4f}); + + const auto container = QWidget::createWindowContainer(view); + const auto screenSize = view->screen()->size(); + container->setMinimumSize({200, 100}); + container->setMaximumSize(screenSize); + + // root entity + const auto rootEntity = new QEntity; + + // camera + const auto cameraEntity = view->camera(); + cameraEntity->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f); + cameraEntity->setPosition({0, 0, 20.0f}); + cameraEntity->setUpVector({0, 1, 0}); + cameraEntity->setViewCenter({0, 0, 0}); + + const auto cameraController = new OrbitCameraController{rootEntity}; + cameraController->setCamera(cameraEntity); + + // lighting + const auto lightEntity = new QEntity{rootEntity}; + const auto light = new Qt3DRender::QPointLight{lightEntity}; + light->setColor(Qt::white); + light->setIntensity(2.5f); + lightEntity->addComponent(light); + + const auto lightTransform = new Qt3DCore::QTransform{lightEntity}; + lightTransform->setTranslation(cameraEntity->position()); + lightEntity->addComponent(lightTransform); + + connect(cameraEntity, &Qt3DRender::QCamera::positionChanged, + lightTransform, &Qt3DCore::QTransform::setTranslation); + + // create entities + const auto showCaseEntity = createShowCase(rootEntity); + const auto unionTestEntity = createUnionTest(rootEntity); + // set root object of the scene view->setRootEntity(rootEntity); + collectEntities(rootEntity); // main window - const auto widget = new QWidget; + const auto window = new QWidget; + window->setWindowTitle(tr("QtCSG Demo")); + + const auto wireframeBox = new QCheckBox{tr("Show &Wireframes"), window}; + wireframeBox->setFocusPolicy(Qt::FocusPolicy::TabFocus); + + const auto showCaseButton = new QPushButton{tr("&1: Show Case"), window}; + showCaseButton->setFocusPolicy(Qt::FocusPolicy::TabFocus); + showCaseButton->setCheckable(true); + showCaseButton->setChecked(true); - const auto layout = new QHBoxLayout{widget}; + const auto unionTestButton = new QPushButton{tr("&2: Union Test"), window}; + unionTestButton->setFocusPolicy(Qt::FocusPolicy::TabFocus); + unionTestButton->setCheckable(true); + unionTestEntity->setEnabled(false); + + connect(showCaseButton, &QPushButton::toggled, showCaseEntity, &QEntity::setEnabled); + connect(unionTestButton, &QPushButton::toggled, unionTestEntity, &QEntity::setEnabled); + connect(wireframeBox, &QCheckBox::toggled, this, &Application::onWireframeBoxToggled); + + const auto buttonGroup = new QButtonGroup{window}; + buttonGroup->addButton(showCaseButton); + buttonGroup->addButton(unionTestButton); + + const auto buttons = new QHBoxLayout; + buttons->addWidget(wireframeBox, 1); + buttons->addWidget(showCaseButton); + buttons->addWidget(unionTestButton); + buttons->addStretch(1); + + const auto layout = new QVBoxLayout{window}; layout->addWidget(container, 1); - widget->setWindowTitle("QtCSG Demo"); - widget->show(); + layout->addLayout(buttons); + container->setFocus(); - const auto widgetSize = QSize{1200, 800}; - const auto position = toPoint((screenSize - widgetSize) / 2); - widget->setGeometry({position, widgetSize}); + const auto windowSize = QSize{1200, 800}; + const auto position = AppSupport::toPoint((screenSize - windowSize) / 2); + window->setGeometry({position, windowSize}); + window->show(); return exec(); } void Application::staticInit() { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + Utils::enabledColorfulLogging(); + + // Force Qt3D OpenGL renderer + constexpr auto rendererVariable = "QT3D_RENDERER"; + if (!qEnvironmentVariableIsSet(rendererVariable)) + qputenv(rendererVariable, "opengl"); + +#if QT_VERSION_MAJOR < 6 setAttribute(Qt::AA_EnableHighDpiScaling); #endif } } // namespace +} // namespace QtCSG::Demo int main(int argc, char *argv[]) { - return Application{argc, argv}.run(); + return QtCSG::Demo::Application{argc, argv}.run(); } diff --git a/demo/qtcsginspector.cpp b/demo/qtcsginspector.cpp new file mode 100644 index 0000000..6a0badf --- /dev/null +++ b/demo/qtcsginspector.cpp @@ -0,0 +1,639 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgappsupport.h" + +#include "orbitcameracontroller.h" +#include "wireframematerial.h" + +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace QtCSG::Inspector { + +Q_NAMESPACE + +namespace { + +const auto s_settings_recentFileList = "RecentFiles"; + +Q_LOGGING_CATEGORY(lcInspector, "qtcsg.inspector"); + +using namespace Qt3DRender; +using Qt3DCore::QEntity; +using Qt3DCore::QNode; +using Qt3DExtras::Qt3DWindow; + +using GeometryOperation = Geometry (*)(Geometry, Geometry, Options); + +auto recentFileNames() +{ + return QSettings{}.value(s_settings_recentFileList).toStringList(); +} + +auto exampleExpressions() +{ + return QStringList { + "cube()", + "cube(center=[1,1,1], r=1)", + "cylinder()", + "sphere()", + "sphere(r=1.3)", + }; +} + +void applyGeometry(QEntity *entity, Geometry geometry) +{ + for (const auto meshList = entity->componentsOfType(); + const auto mesh: meshList) + entity->removeComponent(mesh); + + entity->addComponent(new Qt3DCSG::Mesh{std::move(geometry), entity}); +} + +QRenderSurfaceSelector *makeSurfaceSelector(QFrameGraphNode *parentNode) +{ + const auto surfaceSelector = new QRenderSurfaceSelector{parentNode}; + + const auto clearBuffers = new QClearBuffers{surfaceSelector}; + clearBuffers->setBuffers(QClearBuffers::ColorDepthBuffer); + clearBuffers->setClearColor(QRgb{0x4d'4d'4f}); + + // ensure entities only are drawn by the smaller viewports + const auto noDraw = new QNoDraw{clearBuffers}; + noDraw->setEnabled(true); + + return surfaceSelector; +} + +void setupCamera(QCamera *camera, QSizeF ratio = {1, 1}) +{ + const auto w = ratio.width() * 16.0f; + const auto h = ratio.height() * 9.0f; + + camera->lens()->setPerspectiveProjection(45.0f, w/h, 0.1f, 1000.0f); + camera->setPosition({0.0f, 3.5f, 3.5f}); // FIXME: distance from geometry + camera->setUpVector({0, 1, 0}); + camera->setViewCenter({0, 0, 0}); + + const auto cameraController = new Demo::OrbitCameraController{camera}; + cameraController->setCamera(camera); +} + +QCamera *makeCamera(QSizeF ratio, QEntity *parentEntity) +{ + const auto camera = new QCamera{parentEntity}; + setupCamera(camera, std::move(ratio)); + return camera; +} + +void inspectNode(int currentStep, Node node) +{ + const auto planeNormal = node.plane().normal(); + + if (planeNormal.lengthSquared() != 1) { + qCWarning(lcInspector).nospace() + << "clipping step " << currentStep + << ": node has bad plane normal " << planeNormal; + } + + const auto polygons = node.polygons(); + for (auto i = 0; i < polygons.count(); ++i) { + const auto vertices = polygons[i].vertices(); + + for (auto j = 0, k = 1, l = 2; l < vertices.count(); j = k, k = l, ++l) { + const auto a = vertices[j].position(); + const auto b = vertices[k].position(); + const auto c = vertices[j].position(); + const auto n = normalVector(a, b, c); + + if (n != planeNormal) { + qCWarning(lcInspector).nospace() + << "clipping step " << currentStep + << ": bad normal for polygon " << i + << ", vertex [" << j << '/' << k << '/' << l + << "]; actual: " << n << ", expected: " << planeNormal; + } + } + } + + // TODO + // - show node's plane + // - highlight node's polygons + // - validate polygons + // - allow loading of crashing geometries + // - drop degenerated polygons during build + // - identify (and maybe) fix reason for degeneration +} + +struct GeometryView +{ + GeometryView(QString name, QFrameGraphNode *frameGraph, QEntity *parentEntity); + + QViewport *const viewport; + QCameraSelector *const cameraSelector; + + QEntity *const entity; + Demo::WireframeMaterial *const material; + QLayer *const layer; + + Geometry geometry; +}; + +enum MetaEvent { None, Any }; +Q_ENUM_NS(MetaEvent) + +class Application + : private AppSupport::StaticInit + , public QApplication +{ + friend class StaticInit; + +public: + using QApplication::QApplication; + + int run(); + +private: + struct InspectionMode : public AppSupport::MultiEnum + { + using MultiEnum::MultiEnum; + using enum Inspection::Event; + using enum MetaEvent; + }; + + static void staticInit(); + + auto makeSelectFile(QLineEdit *lineEdit); + auto makeLoadGeometry(GeometryView *view, QLineEdit *lineEdit); + + void addRecentExpression(QString fileName); + + void updateGeometry(); + void updateDebugGeometry(); + void updateDebugStepsLabel(); + + Qt3DWindow *setupStage(); + void setupWidgets(Qt3DWindow *stage); + + auto geometryViews() const + { + return std::array{&m_left, &m_right, &m_result, &m_debug}; + } + + QStringListModel *const m_exampleExpressions = new QStringListModel{exampleExpressions(), this}; + QStringListModel *const m_recentExpressions = new QStringListModel{recentFileNames(), this}; + + QEntity *const m_rootEntity = new QEntity; + QEntity *const m_lightEntity = new QEntity{m_rootEntity}; + QTechniqueFilter *const m_frameGraph = new QTechniqueFilter{m_rootEntity}; + QRenderSurfaceSelector *const m_surfaceSelector = makeSurfaceSelector(m_frameGraph); + + GeometryView m_left = {"left", m_surfaceSelector, m_rootEntity}; + GeometryView m_right = {"right", m_surfaceSelector, m_rootEntity}; + GeometryView m_result = {"result", m_surfaceSelector, m_rootEntity}; + GeometryView m_debug = {"debug", m_surfaceSelector, m_rootEntity}; + + QWidget *const m_window = new QWidget; + QComboBox *const m_operationBox = new QComboBox{m_window}; + QLabel *const m_debugStepsLabel = new QLabel{m_window}; + QSlider *const m_debugStepsSlider = new QSlider{m_window}; + QComboBox *const m_debugModeBox = new QComboBox{m_window}; + + std::map m_operationCounters; +}; + +auto makeNegateEnabledState(QNode *node) +{ + return [node](bool enabled) { + node->setEnabled(!enabled); + }; +} + +auto makeToggleWireframe(Demo::WireframeMaterial *material) +{ + return [material](bool enabled) { + material->setFrontLineWidth(enabled ? 0.5 : 0.0); + material->setBackLineWidth(enabled ? 0.5 : 0.0); + }; +} + +auto Application::makeSelectFile(QLineEdit *lineEdit) +{ + return [lineEdit] { + const auto initialFocusWidget = lineEdit->topLevelWidget()->focusWidget(); + auto fileName = QFileDialog::getOpenFileName(lineEdit->topLevelWidget(), {}, {}, + tr("OFF Files (*.off)")); + + if (fileName.isEmpty()) + return; + + lineEdit->setText(std::move(fileName)); + emit lineEdit->editingFinished(); + + if (dynamic_cast(initialFocusWidget)) { + lineEdit->selectAll(); + lineEdit->setFocus(); + } + }; +} + +auto Application::makeLoadGeometry(GeometryView *view, QLineEdit *lineEdit) +{ + return [this, view, lineEdit] { + auto expression = lineEdit->text(); + auto geometry = QtCSG::parseGeometry(expression); + + if (geometry.error() != Error::NoError) + geometry = QtCSG::readGeometry(expression); + + if (Utils::reportError(lcInspector(), geometry.error(), "Could not load geometry")) + return; + if (!m_exampleExpressions->stringList().contains(expression)) + addRecentExpression(std::move(expression)); + + applyGeometry(view->entity, geometry); + view->geometry = std::move(geometry); + updateGeometry(); + }; +} + +auto makeGeometryOperation(GeometryOperation operation) +{ + return QVariant::fromValue(operation); +} + +void Application::addRecentExpression(QString fileName) +{ + const auto row = m_recentExpressions->stringList().indexOf(fileName); + + if (row > 0) + m_recentExpressions->removeRow(row); + + if (row != 0) { + m_recentExpressions->insertRow(0); + m_recentExpressions->setData(m_recentExpressions->index(0), fileName); + } + + QSettings{}.setValue(s_settings_recentFileList, m_recentExpressions->stringList()); +} + +void Application::updateGeometry() +{ + const auto operation = qvariant_cast(m_operationBox->currentData()); + const auto debugMode = qvariant_cast(m_debugModeBox->currentData()); + + if (operation == nullptr) { + qCWarning(lcInspector, "No valid operation selected"); + return; + } + + m_operationCounters.clear(); + + auto inspectionHandler = [this](Inspection::Event operation, std::any) { + ++m_operationCounters[InspectionMode::Any]; + ++m_operationCounters[operation]; + return Inspection::Result::Proceed; + }; + + auto geometry = operation(m_left.geometry, m_right.geometry, + Options{std::move(inspectionHandler)}); + + m_debugStepsSlider->setEnabled(debugMode != InspectionMode::None); + m_debugStepsSlider->setRange(0, m_operationCounters[debugMode]); + m_debugStepsSlider->setValue(m_debugStepsSlider->maximum()); + + applyGeometry(m_result.entity, std::move(geometry)); + updateDebugGeometry(); +} + +void Application::updateDebugGeometry() +{ + const auto operation = qvariant_cast(m_operationBox->currentData()); + const auto mode = qvariant_cast(m_debugModeBox->currentData()); + + if (operation == nullptr) { + qCWarning(lcInspector, "No valid operation selected"); + return; + } + + const auto lastStep = m_debugStepsSlider->value(); + + m_operationCounters.clear(); + + auto inspectionHandler = [this, mode, lastStep](Inspection::Event operation, std::any detail) { + ++m_operationCounters[InspectionMode::Any]; + ++m_operationCounters[operation]; + + if (mode == InspectionMode::Any || mode == operation) { + const auto currentStep = m_operationCounters[mode]; + + if (currentStep > lastStep) + return Inspection::Result::Abort; + + if (currentStep == lastStep && mode == InspectionMode::Clip) + inspectNode(currentStep, std::any_cast(detail)); + } + + return Inspection::Result::Proceed; + }; + + auto geometry = operation(m_left.geometry, m_right.geometry, + Options{std::move(inspectionHandler)}); + + applyGeometry(m_debug.entity, std::move(geometry)); +} + +void Application::updateDebugStepsLabel() +{ + const auto current = QString::number(m_debugStepsSlider->value()); + const auto maximum = QString::number(m_debugStepsSlider->maximum()); + m_debugStepsLabel->setText(current + '/' + maximum); +} + +GeometryView::GeometryView(QString name, QFrameGraphNode *frameGraph, QEntity *parentEntity) + //: start{new QNoDraw{graph}} + : viewport{new QViewport{frameGraph}} + , cameraSelector{new QCameraSelector{viewport}} + , entity{new QEntity{parentEntity}} + , material{new Demo::WireframeMaterial{entity}} + , layer{new QLayer{entity}} +{ + viewport->setObjectName(name); + + const auto filter = new QLayerFilter{cameraSelector}; + filter->setFilterMode(QLayerFilter::AcceptAnyMatchingLayers); + filter->addLayer(layer); + + const auto noDraw = new QNoDraw{filter}; + noDraw->setEnabled(false); + + // ensure disabling the camera selector really stops rendering + QObject::connect(cameraSelector, &QCameraSelector::enabledChanged, + noDraw, makeNegateEnabledState(noDraw)); + + material->setAmbient(Qt::transparent); + material->setSpecular(Qt::transparent); + material->setDiffuse(QRgb{0xff'dd'ff'ee}); + material->setFrontLineWidth(0.5f); + + entity->addComponent(material); + entity->addComponent(layer); +} + +Qt3DWindow *Application::setupStage() +{ + const auto stage = new Qt3DWindow; + stage->setActiveFrameGraph(m_frameGraph); + stage->setRootEntity(m_rootEntity); + + m_surfaceSelector->setSurface(stage); + + // camera + setupCamera(stage->camera()); + + // lighting + const auto light = new Qt3DRender::QPointLight{m_lightEntity}; + light->setColor(Qt::white); + light->setIntensity(1.0f); + + const auto lightTransform = new Qt3DCore::QTransform{m_lightEntity}; + lightTransform->setTranslation(stage->camera()->position()); + + for (const auto view: geometryViews()) + m_lightEntity->addComponent(view->layer); + + m_lightEntity->addComponent(light); + m_lightEntity->addComponent(lightTransform); + + connect(stage->camera(), &Qt3DRender::QCamera::positionChanged, + lightTransform, &Qt3DCore::QTransform::setTranslation); + + for (auto i = 0U; i < geometryViews().size(); ++i) { + const auto view = geometryViews()[i]; + + const auto x = i < 3 ? 0.0f : 1.0f/3.0f; + const auto y = i < 3 ? i/3.0f : 0.0f; + const auto w = i < 3 ? 1.0/3.0f : 1.0 - 1.0/3.0f; + const auto h = i < 3 ? 1.0/3.0f : 1.0; + + view->viewport->setNormalizedRect({x, y, w, h}); // FIXME: move into GeometryView + + if (view == &m_debug) { + const auto camera = makeCamera({2, 3}, m_rootEntity); + view->cameraSelector->setCamera(camera); + } else { + view->cameraSelector->setCamera(stage->camera()); + } + } + + return stage; +} + +void Application::setupWidgets(Qt3DWindow *stage) +{ + // main window + m_window->setWindowTitle(tr("QtCSG Inspector")); + + const auto completionModel = new QConcatenateTablesProxyModel{this}; + completionModel->addSourceModel(m_exampleExpressions); + completionModel->addSourceModel(m_recentExpressions); + + const auto completer = new QCompleter{completionModel, this}; + completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); + + m_operationBox->addItem(tr("Union"), makeGeometryOperation(QtCSG::merge)); + m_operationBox->addItem(tr("Difference"), makeGeometryOperation(QtCSG::subtract)); + m_operationBox->addItem(tr("Intersection"), makeGeometryOperation(QtCSG::intersect)); + m_operationBox->setCurrentIndex(1); + + connect(m_operationBox, &QComboBox::currentIndexChanged, + this, &Application::updateGeometry); + + const auto leftExpressionEdit = new QLineEdit{m_window}; + leftExpressionEdit->setClearButtonEnabled(true); + leftExpressionEdit->setCompleter(completer); + leftExpressionEdit->setText("cube()"); + + const auto rightExpressionEdit = new QLineEdit{m_window}; + rightExpressionEdit->setCompleter(completer); + rightExpressionEdit->setText("sphere(r=1.3)"); + + const auto loadLeftGeometry = makeLoadGeometry(&m_left, leftExpressionEdit); + const auto loadRightGeometry = makeLoadGeometry(&m_right, rightExpressionEdit); + + connect(leftExpressionEdit, &QLineEdit::editingFinished, this, loadLeftGeometry); + connect(rightExpressionEdit, &QLineEdit::editingFinished, this, loadRightGeometry); + + const auto leftFileNameButton = new QPushButton{tr("Browse"), m_window}; + leftFileNameButton->setFocusPolicy(Qt::FocusPolicy::TabFocus); + + const auto rightFileNameButton = new QPushButton{tr("Browse"), m_window}; + rightFileNameButton->setFocusPolicy(Qt::FocusPolicy::TabFocus); + + connect(leftFileNameButton, &QPushButton::clicked, + this, makeSelectFile(leftExpressionEdit)); + connect(rightFileNameButton, &QPushButton::clicked, + this, makeSelectFile(rightExpressionEdit)); + + const auto wireframeBox = new QCheckBox{tr("Show &Wireframes"), m_window}; + wireframeBox->setChecked(true); + + for (const auto view: geometryViews()) { + auto toggleWireframe = makeToggleWireframe(view->material); + connect(wireframeBox, &QCheckBox::toggled, view->material, toggleWireframe); + toggleWireframe(wireframeBox->isEnabled()); + } + + m_debugStepsLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + m_debugStepsSlider->setOrientation(Qt::Horizontal); + + connect(m_debugStepsSlider, &QSlider::rangeChanged, + this, &Application::updateDebugStepsLabel); + connect(m_debugStepsSlider, &QSlider::valueChanged, + this, &Application::updateDebugStepsLabel); + connect(m_debugStepsSlider, &QSlider::valueChanged, + this, &Application::updateDebugGeometry); + + m_debugModeBox->addItem(tr("None"), InspectionMode{InspectionMode::None}); + m_debugModeBox->addItem(tr("Build"), InspectionMode{InspectionMode::Build}); + m_debugModeBox->addItem(tr("Invert"), InspectionMode{InspectionMode::Invert}); + m_debugModeBox->addItem(tr("Clip"), InspectionMode{InspectionMode::Clip}); + m_debugModeBox->addItem(tr("Any"), InspectionMode{InspectionMode::Any}); + + connect(m_debugModeBox, &QComboBox::currentIndexChanged, + this, &Application::updateGeometry); + + const auto bottomToolbar = new QHBoxLayout; + + bottomToolbar->addWidget(wireframeBox); + bottomToolbar->addSpacing(20); + + for (const auto view: geometryViews()) { + const auto checkBox = new QCheckBox{view->viewport->objectName(), m_window}; + checkBox->setChecked(view->cameraSelector->isEnabled()); + bottomToolbar->addWidget(checkBox); + + connect(checkBox, &QCheckBox::toggled, view->cameraSelector, &QCameraSelector::setEnabled); + } + + bottomToolbar->addStretch(1); + bottomToolbar->addSpacing(20); + + bottomToolbar->addWidget(m_debugStepsLabel); + bottomToolbar->addWidget(m_debugStepsSlider, 5); + bottomToolbar->addWidget(m_debugModeBox); + + const auto container = QWidget::createWindowContainer(stage); + const auto screenSize = stage->screen()->size(); + container->setMinimumSize({200, 100}); + container->setMaximumSize(screenSize); + + const auto layout = new QGridLayout{m_window}; + layout->addWidget(m_operationBox, 0, 0); + layout->addWidget(leftExpressionEdit, 0, 1); + layout->addWidget(leftFileNameButton, 0, 2); + layout->addWidget(rightExpressionEdit, 0, 3); + layout->addWidget(rightFileNameButton, 0, 4); + layout->addWidget(container, 1, 0, 1, 5); + layout->addLayout(bottomToolbar, 2, 0, 1, 5); + + const auto windowSize = QSize{1200, 800}; + const auto position = AppSupport::toPoint((screenSize - windowSize) / 2); + m_window->setGeometry({position, windowSize}); + m_window->show(); + + container->setFocus(); + + loadLeftGeometry(); + loadRightGeometry(); +} + +void Application::staticInit() +{ + static_assert(InspectionMode{InspectionMode::Clip}.index() == 0); + static_assert(InspectionMode{InspectionMode::None}.index() == 1); + static_assert(InspectionMode{InspectionMode::Build} == Application::InspectionMode::Build); + + setOrganizationDomain("taschenorakel.de"); + Utils::enabledColorfulLogging(); + + // Force Qt3D OpenGL renderer + constexpr auto rendererVariable = "QT3D_RENDERER"; + if (!qEnvironmentVariableIsSet(rendererVariable)) + qputenv(rendererVariable, "opengl"); + +#if QT_VERSION_MAJOR < 6 + setAttribute(Qt::AA_EnableHighDpiScaling); +#endif +} + +int Application::run() +{ + const auto stage = setupStage(); + setupWidgets(stage); + return exec(); +} + +} // namespace +} // namespace QtCSG::Inspector + +int main(int argc, char *argv[]) +{ + return QtCSG::Inspector::Application{argc, argv}.run(); +} + +#include "qtcsginspector.moc" diff --git a/demo/shaders/gl3/robustwireframe.frag b/demo/shaders/gl3/robustwireframe.frag new file mode 100644 index 0000000..f1d201d --- /dev/null +++ b/demo/shaders/gl3/robustwireframe.frag @@ -0,0 +1,94 @@ +#version 330 core + +// Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +uniform struct LineInfo { + float width; + vec4 color; +} frontLine, backLine; + +uniform vec4 ka; // Ambient reflectivity +uniform vec4 kd; // Diffuse reflectivity +uniform vec4 ks; // Specular reflectivity +uniform float shininess; // Specular shininess factor +uniform vec3 eyePosition; + +in WireframeVertex { + vec3 position; + vec3 normal; + noperspective vec4 edgeA; + noperspective vec4 edgeB; + flat int configuration; +} fs_in; + +out vec4 fragColor; + +#pragma include phong.inc.frag + +vec4 shadeLine(const in vec4 color, LineInfo line) +{ + // Find the smallest distance between the fragment and a triangle edge + float d; + + if (fs_in.configuration == 0) { + // Common configuration + d = min(fs_in.edgeA.x, fs_in.edgeA.y); + d = min(d, fs_in.edgeA.z); + } else { + // Handle configuration where screen space projection breaks down + // Compute and compare the squared distances + vec2 AF = gl_FragCoord.xy - fs_in.edgeA.xy; + float sqAF = dot(AF, AF); + float AFcosA = dot(AF, fs_in.edgeA.zw); + + d = abs(sqAF - AFcosA * AFcosA); + + vec2 BF = gl_FragCoord.xy - fs_in.edgeB.xy; + float sqBF = dot(BF, BF); + float BFcosB = dot(BF, fs_in.edgeB.zw); + + d = min(d, abs(sqBF - BFcosB * BFcosB)); + + // Only need to care about the 3rd edge for some configurations. + if (fs_in.configuration == 1 || fs_in.configuration == 2 || fs_in.configuration == 4) { + float AFcosA0 = dot(AF, normalize(fs_in.edgeB.xy - fs_in.edgeA.xy)); + d = min(d, abs(sqAF - AFcosA0 * AFcosA0)); + } + + d = sqrt(d); + } + + // Blend between line color and phong color + float mixVal; + + if (d < line.width - 1.0) { + mixVal = 1.0; + } else if (d > line.width + 1.0) { + mixVal = 0.0; + } else { + float x = d - (line.width - 1.0); + mixVal = exp2(-2.0 * (x * x)); + } + + return mix(color, line.color, mixVal); +} + +void main() +{ + LineInfo line = gl_FrontFacing ? frontLine : backLine; + vec3 worldNormal = fs_in.normal; + + if (!gl_FrontFacing) + worldNormal = -worldNormal; + + // Calculate the color from the phong model + vec3 worldView = normalize(eyePosition - fs_in.position); + vec4 color = phongFunction(ka, kd, ks, shininess, fs_in.position, worldView, worldNormal); + + // Highlight edges if requested + if (line.width > 0) + fragColor = shadeLine(color, line); + else + fragColor = color; +} diff --git a/demo/shaders/gl3/robustwireframe.geom b/demo/shaders/gl3/robustwireframe.geom new file mode 100644 index 0000000..73a3353 --- /dev/null +++ b/demo/shaders/gl3/robustwireframe.geom @@ -0,0 +1,131 @@ +#version 330 core + +// Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +layout(triangles) in; +layout(triangle_strip, max_vertices = 3) out; + +in WorldSpaceVertex { + vec3 position; + vec3 normal; +} gs_in[]; + +out WireframeVertex { + vec3 position; + vec3 normal; + noperspective vec4 edgeA; + noperspective vec4 edgeB; + flat int configuration; +} gs_out; + +uniform mat4 viewportMatrix; + +const int infoA[] = int[](0, 0, 0, 0, 1, 1, 2); +const int infoB[] = int[](1, 1, 2, 0, 2, 1, 2); +const int infoAd[] = int[](2, 2, 1, 1, 0, 0, 0); +const int infoBd[] = int[](2, 2, 1, 2, 0, 2, 1); + +vec2 transformToViewport(const in vec4 p) +{ + return vec2(viewportMatrix * (p / p.w)); +} + +void main() +{ + gs_out.configuration = int(gl_in[0].gl_Position.z < 0) * int(4) + + int(gl_in[1].gl_Position.z < 0) * int(2) + + int(gl_in[2].gl_Position.z < 0); + + // If all vertices are behind us, cull the primitive + if (gs_out.configuration == 7) + return; + + // Transform each vertex into viewport space + vec2 p[3]; + p[0] = transformToViewport(gl_in[0].gl_Position); + p[1] = transformToViewport(gl_in[1].gl_Position); + p[2] = transformToViewport(gl_in[2].gl_Position); + + if (gs_out.configuration == 0) { + // Common configuration where all vertices are within the viewport + gs_out.edgeA = vec4(0.0); + gs_out.edgeB = vec4(0.0); + + // Calculate lengths of 3 edges of triangle + float a = length(p[1] - p[2]); + float b = length(p[2] - p[0]); + float c = length(p[1] - p[0]); + + // Calculate internal angles using the cosine rule + float alpha = acos((b * b + c * c - a * a) / (2.0 * b * c)); + float beta = acos((a * a + c * c - b * b) / (2.0 * a * c)); + + // Calculate the perpendicular distance of each vertex from the opposing edge + float ha = abs(c * sin(beta)); + float hb = abs(c * sin(alpha)); + float hc = abs(b * sin(alpha)); + + // Now add this perpendicular distance as a per-vertex property in addition to + // the position and normal calculated in the vertex shader. + + // Vertex 0 (a) + gs_out.edgeA = vec4(ha, 0.0, 0.0, 0.0); + gs_out.normal = gs_in[0].normal; + gs_out.position = gs_in[0].position; + gl_Position = gl_in[0].gl_Position; + EmitVertex(); + + // Vertex 1 (b) + gs_out.edgeA = vec4(0.0, hb, 0.0, 0.0); + gs_out.normal = gs_in[1].normal; + gs_out.position = gs_in[1].position; + gl_Position = gl_in[1].gl_Position; + EmitVertex(); + + // Vertex 2 (c) + gs_out.edgeA = vec4(0.0, 0.0, hc, 0.0); + gs_out.normal = gs_in[2].normal; + gs_out.position = gs_in[2].position; + gl_Position = gl_in[2].gl_Position; + EmitVertex(); + + // Finish the primitive off + EndPrimitive(); + } else { + // Viewport projection breaks down for one or two vertices. + // Caclulate what we can here and defer rest to fragment shader. + // Since this is coherent for the entire primitive the conditional + // in the fragment shader is still cheap as all concurrent + // fragment shader invocations will take the same code path. + + // Copy across the viewport-space points for the (up to) two vertices + // in the viewport + gs_out.edgeA.xy = p[infoA[gs_out.configuration]]; + gs_out.edgeB.xy = p[infoB[gs_out.configuration]]; + + // Copy across the viewport-space edge vectors for the (up to) two vertices + // in the viewport + gs_out.edgeA.zw = normalize(gs_out.edgeA.xy - p[ infoAd[gs_out.configuration] ]); + gs_out.edgeB.zw = normalize(gs_out.edgeB.xy - p[ infoBd[gs_out.configuration] ]); + + // Pass through the other vertex attributes + gs_out.normal = gs_in[0].normal; + gs_out.position = gs_in[0].position; + gl_Position = gl_in[0].gl_Position; + EmitVertex(); + + gs_out.normal = gs_in[1].normal; + gs_out.position = gs_in[1].position; + gl_Position = gl_in[1].gl_Position; + EmitVertex(); + + gs_out.normal = gs_in[2].normal; + gs_out.position = gs_in[2].position; + gl_Position = gl_in[2].gl_Position; + EmitVertex(); + + // Finish the primitive off + EndPrimitive(); + } +} diff --git a/demo/shaders/gl3/robustwireframe.vert b/demo/shaders/gl3/robustwireframe.vert new file mode 100644 index 0000000..d3c97e1 --- /dev/null +++ b/demo/shaders/gl3/robustwireframe.vert @@ -0,0 +1,24 @@ +#version 330 core + +// Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +in vec3 vertexPosition; +in vec3 vertexNormal; + +out WorldSpaceVertex { + vec3 position; + vec3 normal; +} vs_out; + +uniform mat4 modelMatrix; +uniform mat3 modelNormalMatrix; +uniform mat4 mvp; + +void main() +{ + vs_out.position = vec3(modelMatrix * vec4(vertexPosition, 1.0)); + vs_out.normal = normalize(modelNormalMatrix * vertexNormal); + + gl_Position = mvp * vec4(vertexPosition, 1.0); +} diff --git a/demo/wireframematerial.cpp b/demo/wireframematerial.cpp new file mode 100644 index 0000000..21e3eee --- /dev/null +++ b/demo/wireframematerial.cpp @@ -0,0 +1,241 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "wireframematerial.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static void initResource() +{ + static bool initialized = false; + + if (!std::exchange(initialized, true)) + Q_INIT_RESOURCE(wireframematerial); +} + +namespace QtCSG::Demo { +namespace { + +using namespace Qt3DRender; + +auto makeFilterKey(QString name, QString value, Qt3DCore::QNode *parent = nullptr) +{ + const auto filterKey = new Qt3DRender::QFilterKey{parent}; + filterKey->setName(std::move(name)); + filterKey->setValue(std::move(value)); + return filterKey; +} + +template +auto makeSlot(ObjectType *target, void (ObjectType::*slot)(ValueType)) +{ + return [target, slot](QVariant value) { + return (target->*slot)(qvariant_cast(value)); + }; +} + +} // namespace + +WireframeMaterial::WireframeMaterial(Qt3DCore::QNode *parent) + : QMaterial{parent} + , m_ambient{new QParameter{"ka", QColor::fromRgbF(0.05f, 0.05f, 0.05f, 1.0f), this}} + , m_diffuse{new QParameter{"kd", QColor::fromRgbF(0.7f, 0.7f, 0.7f, 1.0f), this}} + , m_specular{new QParameter{"ks", QColor::fromRgbF(0.95f, 0.95f, 0.95f, 1.0f), this}} + , m_shininess{new QParameter{"shininess", 150.0f, this}} + , m_frontLineWidth{new QParameter{"frontLine.width", 0.8f, this}} + , m_frontLineColor{new QParameter{"frontLine.color", QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f), this}} + , m_backLineWidth{new QParameter{"backLine.width", 0.0f, this}} + , m_backLineColor{new QParameter{"backLine.color", QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f), this}} + , m_cullFace{new QCullFace{this}} + , m_noDepthMask{new QNoDepthMask{this}} + , m_blendEquation{new QBlendEquation{this}} + , m_blendEquationArguments{new QBlendEquationArguments{this}} +{ + initResource(); + + const auto fragmentShaderUrl = QUrl{"qrc:/shaders/gl3/robustwireframe.frag"}; + const auto geometryShaderUrl = QUrl{"qrc:/shaders/gl3/robustwireframe.geom"}; + const auto vertexShaderUrl = QUrl{"qrc:/shaders/gl3/robustwireframe.vert"}; + + m_cullFace->setEnabled(false); + m_cullFace->setMode(QCullFace::NoCulling); + m_noDepthMask->setEnabled(false); + m_blendEquation->setEnabled(false); + m_blendEquation->setBlendFunction(QBlendEquation::Add); + m_blendEquationArguments->setEnabled(false); + m_blendEquationArguments->setSourceRgb(QBlendEquationArguments::SourceAlpha); + m_blendEquationArguments->setDestinationRgb(QBlendEquationArguments::OneMinusSourceAlpha); + + const auto shaderProgram = new QShaderProgram{this}; + shaderProgram->setVertexShaderCode(QShaderProgram::loadSource(vertexShaderUrl)); + shaderProgram->setGeometryShaderCode(QShaderProgram::loadSource(geometryShaderUrl)); + shaderProgram->setFragmentShaderCode(QShaderProgram::loadSource(fragmentShaderUrl)); + + const auto renderPass = new QRenderPass{this}; + renderPass->setShaderProgram(shaderProgram); + renderPass->addRenderState(m_noDepthMask); + renderPass->addRenderState(m_blendEquationArguments); + renderPass->addRenderState(m_blendEquation); + renderPass->addRenderState(m_cullFace); + + const auto technique = new QTechnique; + technique->graphicsApiFilter()->setApi(QGraphicsApiFilter::OpenGL); + technique->graphicsApiFilter()->setProfile(QGraphicsApiFilter::CoreProfile); + technique->graphicsApiFilter()->setMajorVersion(3); + technique->graphicsApiFilter()->setMinorVersion(1); + technique->addFilterKey(makeFilterKey("renderingStyle", "forward", technique)); + technique->addParameter(m_frontLineWidth); + technique->addParameter(m_frontLineColor); + technique->addParameter(m_backLineWidth); + technique->addParameter(m_backLineColor); + + technique->addRenderPass(renderPass); + + const auto effect = new QEffect{this}; + effect->addParameter(m_ambient); + effect->addParameter(m_diffuse); + effect->addParameter(m_specular); + effect->addParameter(m_shininess); + effect->addTechnique(technique); + setEffect(effect); + + connect(m_ambient, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::ambientChanged)); + connect(m_diffuse, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::diffuseChanged)); + connect(m_specular, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::specularChanged)); + connect(m_shininess, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::shininessChanged)); + connect(m_frontLineWidth, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::frontLineWidthChanged)); + connect(m_frontLineColor, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::frontLineColorChanged)); + connect(m_backLineWidth, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::backLineWidthChanged)); + connect(m_backLineColor, &QParameter::valueChanged, + this, makeSlot(this, &WireframeMaterial::backLineColorChanged)); + connect(m_noDepthMask, &QNoDepthMask::enabledChanged, + this, &WireframeMaterial::alphaBlendingEnabledChanged); +} + +void WireframeMaterial::setAmbient(QColor newAmbient) +{ + m_ambient->setValue(std::move(newAmbient)); +} + +QColor WireframeMaterial::ambient() const +{ + return qvariant_cast(m_ambient->value()); +} + +void WireframeMaterial::setDiffuse(QColor newDiffuse) +{ + m_diffuse->setValue(std::move(newDiffuse)); +} + +QColor WireframeMaterial::diffuse() const +{ + return qvariant_cast(m_diffuse->value()); +} + +void WireframeMaterial::setSpecular(QColor newSpecular) +{ + m_specular->setValue(std::move(newSpecular)); +} + +QColor WireframeMaterial::specular() const +{ + return qvariant_cast(m_specular->value()); +} + +void WireframeMaterial::setShininess(qreal newShininess) +{ + m_shininess->setValue(newShininess); +} + +qreal WireframeMaterial::shininess() const +{ + return m_shininess->value().toFloat(); +} + +void WireframeMaterial::setFrontLineWidth(qreal newLineWidth) +{ + m_frontLineWidth->setValue(newLineWidth); +} + +qreal WireframeMaterial::frontLineWidth() const +{ + return m_frontLineWidth->value().toFloat(); +} + +void WireframeMaterial::setFrontLineColor(QColor newLineColor) +{ + m_frontLineColor->setValue(std::move(newLineColor)); +} + +QColor WireframeMaterial::frontLineColor() const +{ + return qvariant_cast(m_frontLineColor->value()); +} + +void WireframeMaterial::setBackLineWidth(qreal newLineWidth) +{ + m_backLineWidth->setValue(newLineWidth); +} + +qreal WireframeMaterial::backLineWidth() const +{ + return m_backLineWidth->value().toFloat(); +} + +void WireframeMaterial::setBackLineColor(QColor newLineColor) +{ + m_backLineColor->setValue(std::move(newLineColor)); +} + +QColor WireframeMaterial::backLineColor() const +{ + return qvariant_cast(m_backLineColor->value()); +} + +void WireframeMaterial::setAlphaBlendingEnabled(bool enabled) +{ + m_cullFace->setEnabled(enabled); + m_noDepthMask->setEnabled(enabled); + m_blendEquation->setEnabled(enabled); + m_blendEquationArguments->setEnabled(enabled); +} + +bool WireframeMaterial::isAlphaBlendingEnabled() const +{ + return m_noDepthMask->isEnabled(); +} + +} // namespace QtCSG::Demo diff --git a/demo/wireframematerial.h b/demo/wireframematerial.h new file mode 100644 index 0000000..1aee282 --- /dev/null +++ b/demo/wireframematerial.h @@ -0,0 +1,109 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef QT3DCSG_WIREFRAMEMATERIAL_H +#define QT3DCSG_WIREFRAMEMATERIAL_H + +#include + +#include +#include + +namespace Qt3DRender { +class QBlendEquation; +class QBlendEquationArguments; +class QCullFace; +class QNoDepthMask; +} // namespace Qt3DRender + +namespace QtCSG::Demo { + +class WireframeMaterial : public Qt3DRender::QMaterial +{ + Q_OBJECT + Q_PROPERTY(QColor ambient READ ambient WRITE setAmbient NOTIFY ambientChanged FINAL) + Q_PROPERTY(QColor diffuse READ diffuse WRITE setDiffuse NOTIFY diffuseChanged FINAL) + Q_PROPERTY(QColor specular READ specular WRITE setSpecular NOTIFY specularChanged FINAL) + Q_PROPERTY(qreal shininess READ shininess WRITE setShininess NOTIFY shininessChanged FINAL) + Q_PROPERTY(qreal frontLineWidth READ frontLineWidth WRITE setFrontLineWidth NOTIFY frontLineWidthChanged FINAL) + Q_PROPERTY(QColor frontLineColor READ frontLineColor WRITE setFrontLineColor NOTIFY frontLineColorChanged FINAL) + Q_PROPERTY(qreal backLineWidth READ backLineWidth WRITE setBackLineWidth NOTIFY backLineWidthChanged FINAL) + Q_PROPERTY(QColor backLineColor READ backLineColor WRITE setBackLineColor NOTIFY backLineColorChanged FINAL) + Q_PROPERTY(bool alphaBlendingEnabled READ isAlphaBlendingEnabled WRITE setAlphaBlendingEnabled NOTIFY alphaBlendingEnabledChanged FINAL) + +public: + explicit WireframeMaterial(Qt3DCore::QNode *parent = nullptr); + + void setAmbient(QColor newAmbient); + QColor ambient() const; + + void setDiffuse(QColor newDiffuse); + QColor diffuse() const; + + void setSpecular(QColor newSpecular); + QColor specular() const; + + void setShininess(qreal newShininess); + qreal shininess() const; + + void setFrontLineWidth(qreal newLineWidth); + qreal frontLineWidth() const; + + void setFrontLineColor(QColor newLineColor); + QColor frontLineColor() const; + + void setBackLineWidth(qreal newLineWidth); + qreal backLineWidth() const; + + void setBackLineColor(QColor newLineColor); + QColor backLineColor() const; + + void setAlphaBlendingEnabled(bool enabled); + bool isAlphaBlendingEnabled() const; + +signals: + void ambientChanged(QColor ambient); + void diffuseChanged(QColor diffuse); + void specularChanged(QColor specular); + void shininessChanged(qreal shininess); + void frontLineWidthChanged(qreal frontLineWidth); + void frontLineColorChanged(QColor frontLineColor); + void backLineWidthChanged(qreal backLineWidth); + void backLineColorChanged(QColor backLineColor); + + void alphaBlendingEnabledChanged(bool alphaBlendingEnabled); + +private: + Qt3DRender::QParameter *const m_ambient; + Qt3DRender::QParameter *const m_diffuse; + Qt3DRender::QParameter *const m_specular; + Qt3DRender::QParameter *const m_shininess; + Qt3DRender::QParameter *const m_frontLineWidth; + Qt3DRender::QParameter *const m_frontLineColor; + Qt3DRender::QParameter *const m_backLineWidth; + Qt3DRender::QParameter *const m_backLineColor; + + Qt3DRender::QCullFace *const m_cullFace; + Qt3DRender::QNoDepthMask *const m_noDepthMask; + Qt3DRender::QBlendEquation *const m_blendEquation; + Qt3DRender::QBlendEquationArguments *const m_blendEquationArguments; +}; + +} // namespace QtCSG::Demo + +#endif // QT3DCSG_WIREFRAMEMATERIAL_H diff --git a/demo/wireframematerial.qrc b/demo/wireframematerial.qrc new file mode 100644 index 0000000..054c59e --- /dev/null +++ b/demo/wireframematerial.qrc @@ -0,0 +1,7 @@ + + + shaders/gl3/robustwireframe.frag + shaders/gl3/robustwireframe.geom + shaders/gl3/robustwireframe.vert + + diff --git a/licenses/BSD-3-Clause.md b/licenses/BSD-3-Clause.md new file mode 100644 index 0000000..4f83233 --- /dev/null +++ b/licenses/BSD-3-Clause.md @@ -0,0 +1,28 @@ +Copyright 2014 Klaralvdalens Datakonsult AB (KDAB). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/COPYING.md b/licenses/GPL-3.0-or-later.md similarity index 100% rename from COPYING.md rename to licenses/GPL-3.0-or-later.md diff --git a/qt3dcsg/qt3dcsg.cpp b/qt3dcsg/qt3dcsg.cpp index 92c2da0..052ce3f 100644 --- a/qt3dcsg/qt3dcsg.cpp +++ b/qt3dcsg/qt3dcsg.cpp @@ -19,11 +19,12 @@ #include "qt3dcsg.h" #include +#include #include #include -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR < 6 #include #include #include @@ -34,7 +35,7 @@ namespace Qt3DCSG { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR < 6 using Qt3DRender::QAttribute; using Qt3DRender::QBuffer; #else @@ -45,17 +46,19 @@ using Qt3DCore::QBuffer; using QtCSG::Polygon; using QtCSG::Vertex; +using QtCSG::Utils::reportError; + namespace { Q_LOGGING_CATEGORY(lcGeometry, "qt3dcsg.geometry"); /// Sets buffer data from some vector template -void setData(QBuffer *buffer, QVector vector) +void setData(QBuffer *buffer, const std::vector &vector) { - const auto data = reinterpret_cast(vector.constData()); + const auto data = reinterpret_cast(vector.data()); const auto len = vector.size() * static_cast(sizeof(T)); - buffer->setData({data, len}); + buffer->setData({data, static_cast(len)}); } /// Finds buffer data. Normally it should be sufficient to just call QBuffer::data(). Unfortunatly there also @@ -65,7 +68,7 @@ QByteArray data(const QBuffer *buffer) { auto data = buffer->data(); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR < 6 if (data.isEmpty()) { QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED // generators are deprecated in Qt 5.15, still they get used by Qt3DExtras @@ -143,10 +146,9 @@ class AttributeReader : public AttributeReaderBase const void *AttributeReaderBase::entry(int index) const { const auto offset = stride() * index + m_attribute->byteOffset(); + static_assert(std::is_unsigned_v); - if (Q_UNLIKELY(offset < 0)) - return nullptr; - if (Q_UNLIKELY(offset >= m_data.size())) + if (Q_UNLIKELY(offset >= static_cast(m_data.size()))) return nullptr; return m_data.constData() + offset; @@ -289,24 +291,56 @@ QVector3D AttributeReader::at(int index) const } // namespace Geometry::Geometry(QtCSG::Geometry csg, Qt3DCore::QNode *parent) + : QGeometry{parent} { - auto vertices = QVector{}; - auto indices = QVector{}; + if (reportError(lcGeometry(), csg.error(), + "Cannot create Qt3D geometry from QtCSG geometry with errors")) + return; const auto polygons = csg.polygons(); + + if (polygons.isEmpty()) { + qCWarning(lcGeometry, "Cannot create Qt3D geometry from empty QtCSG geometry"); + return; + } + + const auto vertexCount = std::accumulate(polygons.cbegin(), polygons.cend(), 0U, + [](qsizetype n, const Polygon &p) { + return n + p.size(); + }); + + const auto indexCount = std::accumulate(polygons.cbegin(), polygons.cend(), 0U, + [](qsizetype n, const Polygon &p) { + if (Q_UNLIKELY(p.size() < 3)) + return n; + + return n + 3 * (p.size() - 2); + }); + + auto vertices = std::vector{}; + vertices.reserve(vertexCount); + + using IndexType = ushort; + auto indices = std::vector{}; + indices.reserve(indexCount); + for (const auto &p: polygons) { const auto pv = p.vertices(); - const auto i0 = vertices.count(); + const auto i0 = static_cast(vertices.size()); + Q_ASSERT(vertices.size() + pv.count() <= std::numeric_limits::max()); std::copy(pv.begin(), pv.end(), std::back_inserter(vertices)); - for (int i = 2; i < pv.count(); ++i) { - indices.append(i0); - indices.append(i0 + i - 1); - indices.append(i0 + i); + for (auto i = IndexType{2}; i < pv.count(); ++i) { + indices.emplace_back(i0); + indices.emplace_back(i0 + i - 1); + indices.emplace_back(i0 + i); } } + Q_ASSERT(vertices.size() == vertexCount); + Q_ASSERT(indices.size() == indexCount); + const auto vertexBuffer = new QBuffer{this}; setData(vertexBuffer, vertices); @@ -320,7 +354,7 @@ Geometry::Geometry(QtCSG::Geometry csg, Qt3DCore::QNode *parent) positionAttribute->setBuffer(vertexBuffer); positionAttribute->setByteStride(sizeof(Vertex)); positionAttribute->setByteOffset(offsetof(Vertex, m_position)); - positionAttribute->setCount(vertices.count()); + positionAttribute->setCount(vertices.size()); positionAttribute->setName(QAttribute::defaultPositionAttributeName()); positionAttribute->setVertexBaseType(QAttribute::Float); positionAttribute->setVertexSize(3); @@ -329,7 +363,7 @@ Geometry::Geometry(QtCSG::Geometry csg, Qt3DCore::QNode *parent) normalAttribute->setBuffer(vertexBuffer); normalAttribute->setByteStride(sizeof(Vertex)); normalAttribute->setByteOffset(offsetof(Vertex, m_normal)); - normalAttribute->setCount(vertices.count()); + normalAttribute->setCount(vertices.size()); normalAttribute->setName(QAttribute::defaultNormalAttributeName()); normalAttribute->setVertexBaseType(QAttribute::Float); normalAttribute->setVertexSize(3); @@ -337,7 +371,7 @@ Geometry::Geometry(QtCSG::Geometry csg, Qt3DCore::QNode *parent) indexAttribute->setBuffer(indexBuffer); indexAttribute->setAttributeType(QAttribute::IndexAttribute); - indexAttribute->setCount(indices.count()); + indexAttribute->setCount(indices.size()); indexAttribute->setVertexBaseType(QAttribute::UnsignedShort); addAttribute(indexAttribute); } @@ -360,8 +394,9 @@ QtCSG::Geometry geometry(QGeometry *geometry, QMatrix4x4 transformation) if (position.isValid() && normal.isValid() && index.isValid()) { const auto count = index.attribute()->count(); + polygons.reserve(count / 3); - for (auto i = 0; i < count; i += 3) { + for (auto i = 0U; i < count; i += 3) { const auto ia = index.at(i); const auto ib = index.at(i + 1); const auto ic = index.at(i + 2); @@ -377,12 +412,13 @@ QtCSG::Geometry geometry(QGeometry *geometry, QMatrix4x4 transformation) return QtCSG::Geometry{std::move(polygons)}; } -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR < 6 QtCSG::Geometry geometry(QGeometryRenderer *renderer, QMatrix4x4 transformation) { if (renderer->primitiveType() != QGeometryRenderer::Triangles) { - qCWarning(lcGeometry, "Unsupported primitive type: %d", renderer->primitiveType()); + qCWarning(lcGeometry, "Unsupported primitive type: %s", + QtCSG::Utils::keyName(renderer->primitiveType())); return QtCSG::Geometry{{}}; } @@ -394,11 +430,18 @@ QtCSG::Geometry geometry(QGeometryRenderer *renderer, QMatrix4x4 transformation) QtCSG::Geometry geometry(QGeometryRenderer *renderer, QMatrix4x4 transformation) { if (renderer->primitiveType() != QGeometryRenderer::Triangles) { - qCWarning(lcGeometry, "Unsupported primitive type: %d", renderer->primitiveType()); + qCWarning(lcGeometry, "Unsupported primitive type: %s", + QtCSG::Utils::keyName(renderer->primitiveType())); return QtCSG::Geometry{{}}; } - return geometry(renderer->view(), std::move(transformation)); + if (const auto geometry = renderer->geometry()) + return Qt3DCSG::geometry(geometry, std::move(transformation)); + if (const auto view = renderer->view()) + return Qt3DCSG::geometry(view, std::move(transformation)); + + qCWarning(lcGeometry, "Unsupported renderer without geometry or view"); + return QtCSG::Geometry{{}}; } QtCSG::Geometry geometry(Qt3DCore::QGeometryView *view, QMatrix4x4 transformation) @@ -408,7 +451,11 @@ QtCSG::Geometry geometry(Qt3DCore::QGeometryView *view, QMatrix4x4 transformatio return QtCSG::Geometry{{}}; } - return geometry(view->geometry(), std::move(transformation)); + if (const auto geometry = view->geometry()) + return Qt3DCSG::geometry(geometry, std::move(transformation)); + + qCWarning(lcGeometry, "Unsupported view without geometry"); + return QtCSG::Geometry{{}}; } #endif diff --git a/qt3dcsg/qt3dcsg.h b/qt3dcsg/qt3dcsg.h index ac322f0..8f9a1ec 100644 --- a/qt3dcsg/qt3dcsg.h +++ b/qt3dcsg/qt3dcsg.h @@ -28,7 +28,7 @@ class Geometry; namespace Qt3DCSG { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR < 6 using Qt3DRender::QGeometry; using Qt3DRender::QGeometryRenderer; #else @@ -60,7 +60,7 @@ class Mesh : public QGeometryRenderer QtCSG::Geometry geometry(QGeometry *geometry, QMatrix4x4 transformation = {}); QtCSG::Geometry geometry(QGeometryRenderer *renderer, QMatrix4x4 transformation = {}); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#if QT_VERSION_MAJOR >= 6 QtCSG::Geometry geometry(QGeometryView *view, QMatrix4x4 transformation = {}); #endif diff --git a/qtcsg/CMakeLists.txt b/qtcsg/CMakeLists.txt index fca66c2..aed6d5b 100644 --- a/qtcsg/CMakeLists.txt +++ b/qtcsg/CMakeLists.txt @@ -1,3 +1,14 @@ -add_library(QtCSG qtcsg.cpp qtcsg.h) +add_library( + QtCSG + qtcsg.cpp + qtcsg.h + qtcsgio.cpp + qtcsgio.h + qtcsgmath.cpp + qtcsgmath.h + qtcsgutils.cpp + qtcsgutils.h +) + target_include_directories(QtCSG PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) target_link_libraries(QtCSG PUBLIC Qt::Gui) diff --git a/qtcsg/qtcsg.cpp b/qtcsg/qtcsg.cpp index f933cc2..c862af9 100644 --- a/qtcsg/qtcsg.cpp +++ b/qtcsg/qtcsg.cpp @@ -17,19 +17,23 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ #include "qtcsg.h" +#include "qtcsgmath.h" +#include "qtcsgutils.h" -#include +#include +#include #include namespace QtCSG { namespace { -QVector3D lerp(QVector3D a, QVector3D b, float t) -{ - return a + (b - a) * t; -} +Q_LOGGING_CATEGORY(lcGeometry, "qtcsg.geometry"); +Q_LOGGING_CATEGORY(lcNode, "qtcsg.node"); +Q_LOGGING_CATEGORY(lcOperator, "qtcsg.operator"); + +using Utils::reportError; template void flip(T &o) @@ -37,43 +41,26 @@ void flip(T &o) o.flip(); } -} // namespace - -void Vertex::flip() -{ - m_normal = -m_normal; -} - -Vertex Vertex::interpolate(Vertex other, float t) const -{ - return Vertex{ - lerp(position(), other.position(), t), - lerp(normal(), other.normal(), t), - }; -} - -Plane Plane::fromPoints(QVector3D a, QVector3D b, QVector3D c) -{ - const auto n = QVector3D::crossProduct(b - a, c - a).normalized(); - return Plane{n, QVector3D::dotProduct(n, a)}; -} - -void Plane::flip() -{ - m_normal = -m_normal; - m_w = -m_w; -} - -void Polygon::flip() +[[nodiscard]] constexpr bool isConvexPoint(const QVector3D &a, + const QVector3D &b, + const QVector3D &c, + const QVector3D &normal, + float epsilon = 0) { - std::reverse(m_vertices.begin(), m_vertices.end()); - std::for_each(m_vertices.begin(), m_vertices.end(), &QtCSG::flip); - m_plane.flip(); + const auto cross = QVector3D::crossProduct(b - a, c - b); + const auto d = QVector3D::dotProduct(cross, normal); + return d >= epsilon; } -void Polygon::split(const Plane &plane, - QList *coplanarFront, QList *coplanarBack, - QList *front, QList *back, float epsilon) const +/// Split `polygon` by `plane` if needed, then put the polygon or polygon +/// fragments in the appropriate lists. Coplanar polygons go into either +/// `coplanarFront` or `coplanarBack` depending on their orientation with +/// respect to this plane. Polygons in front or in back of this plane go into +/// either `front` or `back`. +void split(const Polygon &polygon, const Plane &plane, + QList *coplanarFront, QList *coplanarBack, + QList *front, QList *back, + const Options &options) { enum VertexType { Coplanar = 0, @@ -82,79 +69,397 @@ void Polygon::split(const Plane &plane, Spanning = Front | Back }; + const auto vertices = polygon.vertices(); + const auto normal = polygon.plane().normal(); + const auto epsilon = options.epsilon; + // Classify each point as well as the entire polygon into one of the above four classes. auto polygonType = Coplanar; auto vertexTypes = std::vector{}; + vertexTypes.reserve(vertices.size()); - for (const auto &v: m_vertices) { - const auto t = QVector3D::dotProduct(plane.normal(), v.position()) - plane.w(); + for (const auto &v: vertices) { + const auto t = dotProduct(plane.normal(), v.position()) - plane.w(); const auto type = (t < -epsilon) ? Back : (t > epsilon) ? Front : Coplanar; polygonType = static_cast(polygonType | type); - vertexTypes.push_back(type); + vertexTypes.emplace_back(type); } // Put the polygon in the correct list, splitting it when necessary. switch (polygonType) { case Coplanar: - if (QVector3D::dotProduct(plane.normal(), m_plane.normal()) > 0) - coplanarFront->append(*this); + if (dotProduct(plane.normal(), normal) > 0) + coplanarFront->append(polygon); else - coplanarBack->append(*this); + coplanarBack->append(polygon); break; case Front: - front->append(*this); + front->append(polygon); break; case Back: - back->append(*this); + back->append(polygon); break; case Spanning: auto f = QList{}; auto b = QList{}; - for (auto i = 0; i < m_vertices.count(); ++i) { - const auto j = (i + 1) % m_vertices.count(); + for (auto i = 0; i < vertices.count(); ++i) { + const auto j = (i + 1) % vertices.count(); - const auto ti = vertexTypes[i]; - const auto tj = vertexTypes[j]; - const auto vi = m_vertices[i]; - const auto vj = m_vertices[j]; + const auto ti = vertexTypes[i]; + const auto tj = vertexTypes[j]; + const auto vi = vertices[i]; + const auto vj = vertices[j]; - if (ti != Back) - f.append(vi); - if (ti != Front) - b.append(vi); + if (ti != Back) + f.append(vi); + if (ti != Front) + b.append(vi); - if ((ti | tj) == Spanning) { - const auto t = (plane.w() - - QVector3D::dotProduct(plane.normal(), vi.position())) - / QVector3D::dotProduct(plane.normal(), vj.position() - vi.position()); - const auto v = vi.interpolate(vj, t); + if ((ti | tj) == Spanning) { + const auto t = (plane.w() + - dotProduct(plane.normal(), vi.position())) + / dotProduct(plane.normal(), vj.position() - vi.position()); + const auto v = vi.interpolated(vj, t); - f.append(v); - b.append(v); - } + f.append(v); + b.append(v); + } } if (f.count() >= 3) - front->append(Polygon{std::move(f), m_shared}); + front->append(Polygon{std::move(f), polygon.shared()}); if (b.count() >= 3) - back->append(Polygon{std::move(b), m_shared}); + back->append(Polygon{std::move(b), polygon.shared()}); break; } } -Geometry Geometry::inverse() const +} // namespace + +void Vertex::flip() +{ + m_normal = -m_normal; +} + +Vertex Vertex::transformed(const QMatrix4x4 &matrix) const +{ + auto newPosition = matrix * position(); + auto newNormal = findRotation(matrix) * normal(); // only rotate; do not translate, or scale + return Vertex{std::move(newPosition), std::move(newNormal)}; +} + +Vertex Vertex::interpolated(Vertex other, float t) const +{ + return Vertex{ + lerp(position(), other.position(), t), + lerp(normal(), other.normal(), t), + }; +} + +Plane Plane::fromPoints(QVector3D a, QVector3D b, QVector3D c) +{ + const auto n = normalVector(a, b, c); + return Plane{n, dotProduct(n, a)}; +} + +void Plane::flip() +{ + m_normal = -m_normal; + m_w = -m_w; +} + +bool Polygon::isConvex() const +{ + if (m_vertices.size() < 3) + return true; + + const auto planeNormal = m_plane.normal(); + + for (auto i = m_vertices.size() - 2, j = m_vertices.size() - 1, k = qsizetype{0}; + k < m_vertices.size(); i = j, j = k, ++k) { + if (!isConvexPoint(m_vertices[i].position(), + m_vertices[j].position(), + m_vertices[k].position(), + planeNormal)) + return false; + } + + return true; +} + +void Polygon::flip() +{ + std::reverse(m_vertices.begin(), m_vertices.end()); + std::for_each(m_vertices.begin(), m_vertices.end(), &QtCSG::flip); + m_plane.flip(); +} + +Polygon Polygon::transformed(const QMatrix4x4 &matrix) const +{ + auto transformed = QList{}; + transformed.reserve(m_vertices.count()); + + const auto applyMatrix = [matrix](const Vertex &vertex) { + return vertex.transformed(matrix); + }; + + std::transform(m_vertices.cbegin(), m_vertices.cend(), + std::back_inserter(transformed), applyMatrix); + + return Polygon{std::move(transformed)}; +} + +Geometry Geometry::inversed() const { auto inverse = QList{}; inverse.reserve(m_polygons.size()); std::copy(m_polygons.begin(), m_polygons.end(), std::back_inserter(inverse)); std::for_each(inverse.begin(), inverse.end(), &flip); - return {std::move(inverse)}; + return Geometry{std::move(inverse)}; +} + +Geometry Geometry::transformed(const QMatrix4x4 &matrix) const +{ + auto transformed = QList{}; + transformed.reserve(m_polygons.count()); + + const auto applyMatrix = [matrix](const Polygon &polygon) { + return polygon.transformed(matrix); + }; + + std::transform(m_polygons.cbegin(), m_polygons.cend(), + std::back_inserter(transformed), applyMatrix); + + return Geometry{std::move(transformed)}; +} + +void Geometry::validate(const Options &options) +{ + if (m_error != Error::NoError) + return; + + if (options.flags & Options::CheckConvexity) { + for (const auto &p: std::as_const(m_polygons)) { + if (!p.isConvex()) { + m_error = Error::ConvexityError; + break; + } + } + } +} + +namespace { + +struct EnsureValue +{ + const QVariantMap &arguments; + + template + T operator()(const QString &key, T defaultValue = {}) const + { + return (*this)(arguments.constFind(key), std::move(defaultValue)); + } + + template + T operator()(const QVariantMap::ConstIterator &iter, T defaultValue = {}) const + { + if (iter != arguments.constEnd()) + return qvariant_cast(*iter); + + return defaultValue; + } +}; + +Geometry createGeometry(QStringView primitiveName, QVariantMap arguments) +{ + const auto value = EnsureValue{arguments}; + + if (primitiveName == u"cube") { + const auto radius = arguments.value("r", 1.0f); + + if (radius.userType() == qMetaTypeId()) { + return cube(value("center", QVector3D{}), + qvariant_cast(radius)); + } + + return cube(value("center", QVector3D{}), radius.toFloat()); + } + + if (primitiveName == u"cylinder") { + const auto start = arguments.constFind("start"); + const auto end = arguments.constFind("end"); + + if (start != arguments.constEnd() || end != arguments.constEnd()) { + static const auto conflicts = std::array{"center", "h"}; + + for (const auto &conflictingName: conflicts) { + if (arguments.contains(conflictingName)) { + qCWarning(lcGeometry, + R"(Argument "%ls" conflicts with arguments )" + R"("start" and "end" of %ls primitive)", + qUtf16Printable(conflictingName), + qUtf16Printable(primitiveName.toString())); + + return Geometry{Error::FileFormatError}; + } + } + + return cylinder(value(start, QVector3D{}), + value(end, QVector3D{}), + value("r", 1.0f), + value("slices", 16)); + } else { + return cylinder(value("center", QVector3D{}), + value("h", 2.0f), + value("r", 1.0f), + value("slices", 16)); + } + } + + if (primitiveName == u"sphere") { + return sphere(value("center", QVector3D{}), + value("r", 1.0f), + value("slices", 16), + value("stacks", 8)); + } + + qCCritical(lcGeometry, R"(Unsupported primitive type: "%ls")", + qUtf16Printable(primitiveName.toString())); + + return Geometry{Error::FileFormatError}; +} + +using ArgumentTypeMap = QMap>; + +QVariant parseArgument(const QString &primitive, + const QString &argName, + const QRegularExpressionMatch &match, + const ArgumentTypeMap &argTypeMap) +{ + const auto valueSpec = argTypeMap.constFind(argName); + + if (valueSpec == argTypeMap.constEnd()) { + qCWarning(lcGeometry, R"(Unsupported argument "%ls" for %ls primitive)", + qUtf16Printable(argName), qUtf16Printable(primitive)); + return {}; + } + + auto argValue = QVariant{}; + + if (const auto scalar = match.captured(u"scalar"); !scalar.isEmpty()) { + argValue = scalar.toFloat(); + } else if (const auto x = match.captured(u"vecx"); !x.isEmpty()) { + if (const auto y = match.captured(u"vecy"); !x.isEmpty()) + if (const auto z = match.captured(u"vecz"); !x.isEmpty()) + argValue = QVector3D{x.toFloat(), y.toFloat(), z.toFloat()}; + } + + if (!valueSpec->contains(argValue.userType())) { + qCWarning(lcGeometry, R"(Unsupported value type for argument "%ls" of %ls primitive)", + qUtf16Printable(argName), qUtf16Printable(primitive)); + return {}; + } + + return argValue; +} + +} // namespace + +Geometry parseGeometry(QString expression) +{ + static const auto s_callPattern = QRegularExpression{R"(^(?[a-z]+)\((?[^)]*\))$)"}; + static const auto s_argPattern = QRegularExpression{R"(\s*(?[a-z]+)\s*=\s*(?:)" + R"((?[+-]?\d+(?:\.\d*)?)|\[)" + R"(\s*(?[+-]?\d+(?:\.\d*)?)\s*,)" + R"(\s*(?[+-]?\d+(?:\.\d*)?)\s*,)" + R"(\s*(?[+-]?\d+(?:\.\d*)?)\s*)" + R"(\])\s*[,)])"}; + + Q_ASSERT_X(s_callPattern.isValid(), "s_callPattern", qPrintable(s_callPattern.errorString())); + Q_ASSERT_X(s_argPattern.isValid(), "s_argPattern", qPrintable(s_argPattern.errorString())); + + static constexpr auto scalarType = qMetaTypeId(); + static constexpr auto vectorType = qMetaTypeId(); + + static const auto s_supportedArguments = QMap { + {"cube", {{"center", {vectorType}}, + {"r", {scalarType, vectorType}}}}, + + {"cylinder", {{"start", {vectorType}}, + {"center", {vectorType}}, + {"end", {vectorType}}, + {"h", {scalarType}}, + {"r", {scalarType}}, + {"slices", {scalarType}}}}, + + {"sphere", {{"center", {vectorType}}, + {"r", {scalarType}}, + {"slices", {scalarType}}, + {"stacks", {scalarType}}}}, + }; + + const auto parsedExpression = s_callPattern.match(expression); + + if (!parsedExpression.hasMatch()) + return Geometry{Error::FileFormatError}; + + auto primitive = parsedExpression.captured(u"name"); + const auto argSpec = s_supportedArguments.constFind(primitive); + + if (argSpec == s_supportedArguments.constEnd()) { + qCWarning(lcGeometry, R"(Unsupported primitive: "%ls")", + qUtf16Printable(primitive)); + + return Geometry{Error::NotSupportedError}; + } + + const auto argList = parsedExpression.capturedView(u"args"); + auto arguments = QVariantMap{}; + + if (argList != u")") { + if (auto it = s_argPattern.globalMatch(argList); it.hasNext()) { + auto expectedStart = 0; + + while (it.hasNext()) { + const auto match = it.next(); + + if (match.capturedStart() != expectedStart) { + const auto length = match.capturedStart() - expectedStart; + const auto expression = argList.mid(expectedStart, length); + qCWarning(lcGeometry, R"(Unexpected expression: "%ls")", + qUtf16Printable(expression.toString())); + return Geometry{Error::FileFormatError}; + } + + expectedStart = match.capturedEnd(); + auto argName = match.captured(u"name"); + + if (arguments.contains(argName)) { + qCWarning(lcGeometry, R"(Duplicate argument "%ls")", qUtf16Printable(argName)); + return Geometry{Error::FileFormatError}; + } + + auto argValue = parseArgument(primitive, argName, match, *argSpec); + + if (argValue.isNull()) + return Geometry{Error::FileFormatError}; + + arguments.insert(std::move(argName), std::move(argValue)); + } + } else { + qCWarning(lcGeometry, R"(Invalid argument list: "(%ls")", + qUtf16Printable(argList.toString())); + return Geometry{Error::FileFormatError}; + } + } + + return createGeometry(std::move(primitive), std::move(arguments)); } Geometry cube(QVector3D center, QVector3D size) @@ -228,10 +533,10 @@ Geometry cylinder(QVector3D start, QVector3D end, float radius, float slices) const auto ray = end - start; const auto axisZ = ray.normalized(); const auto isY = abs(axisZ.y()) > 0.5; - const auto axisX = QVector3D::crossProduct({isY ? 1.0f : 0, isY ? 0 : 1.0f, 0}, axisZ).normalized(); - const auto axisY = QVector3D::crossProduct(axisX, axisZ).normalized(); + const auto axisX = crossProduct({isY ? 1.0f : 0, isY ? 0 : 1.0f, 0}, axisZ).normalized(); + const auto axisY = crossProduct(axisX, axisZ).normalized(); const auto vertexStart = Vertex{start, -axisZ}; - const auto vertexEnd = Vertex{end, axisZ.normalized()}; + const auto vertexEnd = Vertex{end, axisZ}; const auto point = [=](int stack, int slice, int normalBlend) { const auto phi = 2 * M_PI * slice / slices; @@ -257,84 +562,138 @@ Geometry cylinder(QVector3D center, float height, float radius, float slices) radius, slices); } -Geometry merge(Geometry lhs, Geometry rhs) +Geometry merge(Geometry lhs, Geometry rhs, Options options) { - auto a = Node{lhs.polygons()}; - auto b = Node{rhs.polygons()}; - - a.clipTo(b); - b.clipTo(a); - b.invert(); - b.clipTo(a); - b.invert(); - - a.build(b.allPolygons()); - - return {a.allPolygons()}; + if (reportError(lcOperator(), lhs.error(), "Invalid lhs geometry")) + return Geometry{lhs.error()}; + if (reportError(lcOperator(), rhs.error(), "Invalid rhs geometry")) + return Geometry{lhs.error()}; + + auto a = Node{}; + auto b = Node{}; + + if (const auto error = a.build(lhs.polygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from lhs geometry")) + return Geometry{error}; + if (const auto error = b.build(rhs.polygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from rhs geometry")) + return Geometry{error}; + + a.clipTo(b, options); + b.clipTo(a, options); + b.invert(options); + b.clipTo(a, options); + b.invert(options); + + if (const auto error = a.build(b.allPolygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from transformed tree")) + return Geometry{error}; + + return Geometry{a.allPolygons()}; } -Geometry difference(Geometry lhs, Geometry rhs) +Geometry subtract(Geometry lhs, Geometry rhs, Options options) { - auto a = Node{lhs.polygons()}; - auto b = Node{rhs.polygons()}; - - a.invert(); - a.clipTo(b); - b.clipTo(a); - b.invert(); - b.clipTo(a); - b.invert(); - - a.build(b.allPolygons()); - a.invert(); - - return {a.allPolygons()}; + if (reportError(lcOperator(), lhs.error(), "Invalid lhs geometry")) + return Geometry{lhs.error()}; + if (reportError(lcOperator(), rhs.error(), "Invalid rhs geometry")) + return Geometry{lhs.error()}; + + auto a = Node{}; + auto b = Node{}; + + if (const auto error = a.build(lhs.polygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from lhs geometry")) + return Geometry{error}; + if (const auto error = b.build(rhs.polygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from rhs geometry")) + return Geometry{error}; + + a.invert(options); + a.clipTo(b, options); + b.clipTo(a, options); + b.invert(options); + b.clipTo(a, options); + b.invert(options); + + if (const auto error = a.build(b.allPolygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from transformed tree")) + return Geometry{error}; + + a.invert(options); + + return Geometry{a.allPolygons()}; } -Geometry intersect(Geometry lhs, Geometry rhs) +Geometry intersect(Geometry lhs, Geometry rhs, Options options) { - auto a = Node{lhs.polygons()}; - auto b = Node{rhs.polygons()}; + if (reportError(lcOperator(), lhs.error(), "Invalid lhs geometry")) + return Geometry{lhs.error()}; + if (reportError(lcOperator(), rhs.error(), "Invalid rhs geometry")) + return Geometry{lhs.error()}; + + auto a = Node{}; + auto b = Node{}; + + if (const auto error = a.build(lhs.polygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from lhs geometry")) + return Geometry{error}; + if (const auto error = b.build(rhs.polygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from rhs geometry")) + return Geometry{error}; + + a.invert(options); + b.clipTo(a, options); + b.invert(options); + a.clipTo(b, options); + b.clipTo(a, options); + + if (const auto error = a.build(b.allPolygons(), options); + reportError(lcOperator(), error, "Could not build BSP tree from transformed tree")) + return Geometry{error}; + + a.invert(options); + + return Geometry{a.allPolygons()}; +} - a.invert(); - b.clipTo(a); - b.invert(); - a.clipTo(b); - b.clipTo(a); +std::variant Node::fromPolygons(QList polygons, Options options) +{ + auto node = Node{}; - a.build(b.allPolygons()); - a.invert(); + if (const auto error = node.build(std::move(polygons), options); + reportError(lcNode(), error, "Could not build BSP tree from polygons")) + return {error}; - return {a.allPolygons()}; + return {std::move(node)}; } -Node::Node(QList polygons) +void Node::invert(const Options &options) { - build(std::move(polygons)); -} + if (Q_UNLIKELY(options.inspection)) + if (options.inspection(Inspection::Event::Invert, {}) == Inspection::Result::Abort) + return; -void Node::invert() -{ std::for_each(m_polygons.begin(), m_polygons.end(), &flip); m_plane.flip(); if (m_front) - m_front->invert(); + m_front->invert(options); if (m_back) - m_back->invert(); + m_back->invert(options); std::swap(m_front, m_back); } -Node Node::inverted() const +Node Node::inverted(const Options &options) const { auto node = *this; - node.invert(); + node.invert(options); return node; } -QList Node::clipPolygons(QList polygons) const +QList Node::clipPolygons(QList polygons, const Options &options) const { if (m_plane.isNull()) return polygons; @@ -343,27 +702,31 @@ QList Node::clipPolygons(QList polygons) const auto back = QList{}; for (const auto &p: polygons) - p.split(m_plane, &front, &back, &front, &back); + split(p, m_plane, &front, &back, &front, &back, options); if (m_front) - front = m_front->clipPolygons(front); + front = m_front->clipPolygons(front, options); if (m_back) - back = m_back->clipPolygons(back); + back = m_back->clipPolygons(back, options); else back.clear(); return front + back; } -void Node::clipTo(const Node &bsp) +void Node::clipTo(const Node &bsp, const Options &options) { - m_polygons = bsp.clipPolygons(std::move(m_polygons)); + if (Q_UNLIKELY(options.inspection)) + if (options.inspection(Inspection::Event::Clip, bsp) == Inspection::Result::Abort) + return; + + m_polygons = bsp.clipPolygons(std::move(m_polygons), options); if (m_front) - m_front->clipTo(bsp); + m_front->clipTo(bsp, options); if (m_back) - m_back->clipTo(bsp); + m_back->clipTo(bsp, options); } QList Node::allPolygons() const @@ -378,33 +741,49 @@ QList Node::allPolygons() const return polygons; } -void Node::build(QList polygons) +Error Node::build(QList polygons, int level, const Options &options) { + if (Q_UNLIKELY(level >= options.recursionLimit)) { + qWarning(lcNode, "Maximum recursion level reached"); + return Error::RecursionError; + } + + if (Q_UNLIKELY(options.inspection)) + if (options.inspection(Inspection::Event::Build, {}) == Inspection::Result::Abort) + return Error::NoError; // FIXME: AbortedError? + if (polygons.isEmpty()) - return; + return Error::NoError; if (m_plane.isNull()) m_plane = polygons.first().plane(); + auto result = Error::NoError; auto front = QList{}; auto back = QList{}; for (const auto &p: polygons) - p.split(m_plane, &m_polygons, &m_polygons, &front, &back); + split(p, m_plane, &m_polygons, &m_polygons, &front, &back, options); if (!front.empty()) { if (!m_front) m_front = std::make_shared(); - m_front->build(std::move(front)); + if (const auto error = m_front->build(std::move(front), level + 1, options); + error != Error::NoError && result == Error::NoError) + result = error; } if (!back.empty()) { if (!m_back) m_back = std::make_shared(); - m_back->build(std::move(back)); + if (const auto error = m_back->build(std::move(back), level + 1, options); + error != Error::NoError && result == Error::NoError) + result = error; } + + return result; } @@ -454,4 +833,24 @@ QDebug operator<<(QDebug debug, Vertex vertex) << ")"; } +namespace Tests { + +/// This class is an obscure attempt to export internal functions for unit tests. +/// Might be this must be replaced by more regular friend declarations. +class Helper +{ + Q_NEVER_INLINE static void split(const Polygon &polygon, const Plane &plane, + QList *coplanarFront, QList *coplanarBack, + QList *front, QList *back, Options options); +}; + +/// Defining this function out-of place should convince the compiler to export it. +void Helper::split(const Polygon &polygon, const Plane &plane, + QList *coplanarFront, QList *coplanarBack, + QList *front, QList *back, Options options) +{ + QtCSG::split(polygon, plane, coplanarFront, coplanarBack, front, back, options); +} + +} // namespace Tests } // namespace QtCSG diff --git a/qtcsg/qtcsg.h b/qtcsg/qtcsg.h index 2b8f6f6..119cc6b 100644 --- a/qtcsg/qtcsg.h +++ b/qtcsg/qtcsg.h @@ -22,6 +22,7 @@ #include #include +#include #include namespace Qt3DCSG { @@ -30,6 +31,114 @@ class Geometry; namespace QtCSG { +Q_NAMESPACE + +enum class Error +{ + NoError, + RecursionError, + ConvexityError, + NotSupportedError, + FileSystemError, + FileFormatError, +}; + +Q_ENUM_NS(Error) + +/// The following inspection interface helps visualizing, understanding, +/// and debugging the boolean geometry operations above. An inspection +/// handler installed via `Inspection::setHandler()` will called for +/// any of the operations listed in `Inspection::Event`. By returning +/// `Inspection::Result::Abort` the geometry operation can be interrupted +/// at that point. To end inspection simply reset the handler to a +/// default constructed value of `Inspection::Handler`, or nullptr. +struct Inspection +{ + enum class Event { + Build, + Invert, + Clip, + }; + + enum class Result + { + Proceed, + Abort, + }; + + using Handler = std::function; + Handler handler; + + Q_GADGET + Q_ENUM(Event) + Q_ENUM(Result) +}; + +/// Generic options for the various algorithms presented here. +/// +/// They can be build using the | operator: +/// +/// merge(a, b, Options::CheckConvexity | Options::RecursionLimit{2048}); +/// +struct Options +{ + enum Flag { + CheckConvexity = (1 << 0), + CheckPolygonNormals = (1 << 1), + CleanupPolygons = (1 << 20), + }; + + Q_DECLARE_FLAGS(Flags, Flag) + + struct RecursionLimit + { + int value = 32768; + }; + + struct Epsilon + { + float value = 1e-5f; + }; + + Flags flags = Flags{CheckConvexity | CheckPolygonNormals | CleanupPolygons}; + int recursionLimit = RecursionLimit{}.value; + float epsilon = Epsilon{}.value; + Inspection::Handler inspection; + + Options() noexcept = default; + Options(Flag newFlag) noexcept : flags{newFlag} {} + Options(Flags newFlags) noexcept : flags{newFlags} {} + Options(RecursionLimit newLimit) noexcept : recursionLimit{newLimit.value} {} + Options(Epsilon newEpsilon) noexcept : epsilon{newEpsilon.value} {} + Options(Inspection::Handler newHandler) noexcept : inspection{std::move(newHandler)} {} +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(Options::Flags) + +inline Options operator|(Options &&options, Options::Flag newFlag) noexcept +{ + options.flags |= newFlag; + return options; +} + +inline Options operator|(Options &&options, Options::RecursionLimit newLimit) noexcept +{ + options.recursionLimit = newLimit.value; + return options; +} + +inline Options operator|(Options &&options, Options::Epsilon newEpsilon) noexcept +{ + options.epsilon = newEpsilon.value; + return options; +} + +inline Options operator|(Options &&options, Inspection::Handler newHandler) noexcept +{ + options.inspection = std::move(newHandler); + return options; +} + /// Represents a vertex of a polygon. Use your own vertex class instead of this /// one to provide additional features like texture coordinates and vertex /// colors. Custom vertex classes need to provide a `pos` property and `clone()`, @@ -40,13 +149,15 @@ namespace QtCSG { class Vertex { public: + Vertex() = default; + explicit Vertex(QVector3D position, QVector3D normal) : m_position{std::move(position)} , m_normal{std::move(normal)} {} - auto position() const { return m_position; } - auto normal() const { return m_normal; } + [[nodiscard]] auto position() const { return m_position; } + [[nodiscard]] auto normal() const { return m_normal; } /// Invert all orientation-specific data (e.g. vertex normal). /// Called when the orientation of a polygon is flipped. @@ -55,10 +166,14 @@ class Vertex /// Create a new vertex between this vertex and `other` by linearly /// interpolating all properties using a parameter of `t`. Subclasses should /// override this to interpolate additional properties. - Vertex interpolate(Vertex other, float t) const; + [[nodiscard]] Vertex interpolated(Vertex other, float t) const; + + /// Returns a new vertex which has the transformations described + /// by `matrix` applied to the position and normal this vertex. + [[nodiscard]] Vertex transformed(const QMatrix4x4 &matrix) const; - auto fields() const { return std::tie(m_position, m_normal); } - bool operator==(const Vertex &rhs) const { return fields() == rhs.fields(); } + [[nodiscard]] auto fields() const { return std::tie(m_position, m_normal); } + [[nodiscard]] bool operator==(const Vertex &rhs) const { return fields() == rhs.fields(); } private: friend class Qt3DCSG::Geometry; // FIXME: Build a vertex type that's simple but also directly wraps Qt3D attributes @@ -78,16 +193,16 @@ class Plane , m_w{w} {} - auto isNull() const { return m_normal.isNull(); } - auto normal() const { return m_normal; } - auto w() const { return m_w; } + [[nodiscard]] static Plane fromPoints(QVector3D a, QVector3D b, QVector3D c); - static Plane fromPoints(QVector3D a, QVector3D b, QVector3D c); + [[nodiscard]] auto isNull() const { return m_normal.isNull(); } + [[nodiscard]] auto normal() const { return m_normal; } + [[nodiscard]] auto w() const { return m_w; } void flip(); - auto fields() const { return std::tie(m_normal, m_w); } - bool operator==(const Plane &rhs) const { return fields() == rhs.fields(); } + [[nodiscard]] auto fields() const { return std::tie(m_normal, m_w); } + [[nodiscard]] bool operator==(const Plane &rhs) const { return fields() == rhs.fields(); } private: QVector3D m_normal; @@ -104,30 +219,33 @@ class Plane class Polygon { public: + Polygon() = default; + explicit Polygon(QList vertices, QVariant shared = {}) : m_vertices{std::move(vertices)} , m_shared{std::move(shared)} - , m_plane{Plane::fromPoints(m_vertices[0].position(), m_vertices[1].position(), m_vertices[2].position())} + , m_plane{Plane::fromPoints(m_vertices[0].position(), + m_vertices[1].position(), + m_vertices[2].position())} {} - auto vertices() const { return m_vertices; } - auto shared() const { return m_shared; } - auto plane() const { return m_plane; } + [[nodiscard]] auto isEmpty() const { return m_vertices.isEmpty(); } + [[nodiscard]] auto size() const { return m_vertices.size(); } + + [[nodiscard]] auto vertices() const { return m_vertices; } + [[nodiscard]] auto shared() const { return m_shared; } + [[nodiscard]] auto plane() const { return m_plane; } + + [[nodiscard]] bool isConvex() const; void flip(); - /// Split this polygon by `plane` if needed, then put the polygon or polygon - /// fragments in the appropriate lists. Coplanar polygons go into either - /// `coplanarFront` or `coplanarBack` depending on their orientation with - /// respect to this plane. Polygons in front or in back of this plane go into - /// either `front` or `back`. - void split(const Plane &plane, - QList *coplanarFront, QList *coplanarBack, - QList *front, QList *back, - float epsilon = 1e-5) const; + /// Returns a new polygon which has the transformations described + /// by `matrix` applied to all vertices of this polygon. + [[nodiscard]] Polygon transformed(const QMatrix4x4 &matrix) const; - auto fields() const { return std::tie(m_vertices, m_shared, m_plane); } - bool operator==(const Polygon &rhs) const { return fields() == rhs.fields(); } + [[nodiscard]] auto fields() const { return std::tie(m_vertices, m_shared, m_plane); } + [[nodiscard]] bool operator==(const Polygon &rhs) const { return fields() == rhs.fields(); } private: QList m_vertices; @@ -136,21 +254,36 @@ class Polygon }; /// Holds a binary space partition tree representing a 3D solid. Two solids can -/// be combined using the `union()`, `subtract()`, and `intersect()` methods. +/// be combined using the `unite()`, `subtract()`, and `intersect()` methods. class Geometry { public: - Geometry(QList polygons) + explicit Geometry(Error error = Error::NoError) + : m_error{error} {} + explicit Geometry(QList polygons, Error error = Error::NoError) + : Geometry{std::move(polygons), Options{}, error} {} + explicit Geometry(QList polygons, Options options, Error error = Error::NoError) : m_polygons{std::move(polygons)} - {} + , m_error{error} { + validate(options); + } - auto polygons() const { return m_polygons; } + [[nodiscard]] auto isEmpty() const { return m_polygons.isEmpty(); } + [[nodiscard]] auto polygons() const { return m_polygons; } + [[nodiscard]] Error error() const { return m_error; } /// Return a new CSG solid with solid and empty space switched. - Geometry inverse() const; + [[nodiscard]] Geometry inversed() const; + + /// Returns a new geometry which has the transformations described + /// by `matrix` applied to all the polygons of this geometry. + [[nodiscard]] Geometry transformed(const QMatrix4x4 &matrix) const; private: + void validate(const Options &options); + QList m_polygons; + Error m_error; }; /// Holds a node in a BSP tree. A BSP tree is built from a collection of polygons @@ -162,33 +295,40 @@ class Node { public: Node() = default; - Node(QList polygons); + + static std::variant fromPolygons(QList polygons, Options options = {}); + + [[nodiscard]] auto plane() const { return m_plane; } + [[nodiscard]] auto polygons() const { return m_polygons; } + [[nodiscard]] auto front() const { return m_front; } + [[nodiscard]] auto back() const { return m_back; } /// Convert solid space to empty space and empty space to solid space. - void invert(); - Node inverted() const; + [[nodiscard]] Node inverted(const Options &options) const; + void invert(const Options &options); /// Recursively remove all polygons in `polygons` that are inside this BSP tree. - QList clipPolygons(QList polygons) const; + [[nodiscard]] QList clipPolygons(QList polygons, + const Options &options) const; /// Remove all polygons in this BSP tree that are inside the other BSP tree `bsp`. - void clipTo(const Node &bsp); + void clipTo(const Node &bsp, const Options &options); /// Return a list of all polygons in this BSP tree. - QList allPolygons() const; + [[nodiscard]] QList allPolygons() const; /// Build a BSP tree out of `polygons`. When called on an existing tree, the /// new polygons are filtered down to the bottom of the tree and become new /// nodes there. Each set of polygons is partitioned using the first polygon /// (no heuristic is used to pick a good split). - void build(QList polygons); - - auto plane() const { return m_plane; } - auto polygons() const { return m_polygons; } - auto front() const { return m_front; } - auto back() const { return m_back; } + Error build(QList polygons, Options options = {}) + { + return build(std::move(polygons), 0, std::move(options)); + } private: + [[nodiscard]] Error build(QList polygons, int level, const Options &options); + Plane m_plane; QList m_polygons; @@ -198,23 +338,31 @@ class Node }; /// Construct an axis-aligned solid cuboid. -Geometry cube(QVector3D center, QVector3D size); -Geometry cube(QVector3D center = {}, float size = 1); +[[nodiscard]] Geometry cube(QVector3D center, QVector3D size); +[[nodiscard]] Geometry cube(QVector3D center = {}, float size = 1); /// Construct a solid sphere. /// The `slices` and `stacks` parameters control the tessellation along the /// longitude and latitude directions. -Geometry sphere(QVector3D center = {}, float radius = 1, int slices = 16, int stacks = 8); +[[nodiscard]] Geometry sphere(QVector3D center = {}, float radius = 1, int slices = 16, int stacks = 8); /// Construct a solid cylinder. /// The `slices` parameter controls the tessellation. -Geometry cylinder(QVector3D start, QVector3D end, float radius = 1, float slices = 16); -Geometry cylinder(QVector3D center = {}, float height = 2, float radius = 1, float slices = 16); +[[nodiscard]] Geometry cylinder(QVector3D start, QVector3D end, float radius = 1, float slices = 16); +[[nodiscard]] Geometry cylinder(QVector3D center = {}, float height = 2, float radius = 1, float slices = 16); + +/// Constructs a single geometry from simple expression: +/// +/// "cube()" produces a simple cube. +/// "sphere(r=1.3)" produces a sphere of radius 1.3. +/// +/// See `testParseGeometry()` for more examples. +[[nodiscard]] Geometry parseGeometry(QString expression); /// Return a new CSG solid representing space in either this solid or in the /// solid `csg`. Neither this solid nor the solid `csg` are modified. /// -/// A.union(B) +/// A.unite(B) /// /// +-------+ +-------+ /// | | | | @@ -225,9 +373,10 @@ Geometry cylinder(QVector3D center = {}, float height = 2, float radius = 1, flo /// | | | | /// +-------+ +-------+ /// -Geometry merge(Geometry a, Geometry b); -inline auto union_(Geometry a, Geometry b) { return merge(std::move(a), std::move(b)); } -inline auto operator|(Geometry a, Geometry b) { return merge(std::move(a), std::move(b)); } +[[nodiscard]] Geometry merge(Geometry a, Geometry b, Options options = {}); + +[[nodiscard]] inline auto unite(Geometry a, Geometry b) { return merge(std::move(a), std::move(b)); } +[[nodiscard]] inline auto operator|(Geometry a, Geometry b) { return merge(std::move(a), std::move(b)); } /// Return a new CSG solid representing space in this solid but not in the /// solid `csg`. Neither this solid nor the solid `csg` are modified. @@ -243,9 +392,10 @@ inline auto operator|(Geometry a, Geometry b) { return merge(std::move(a), std:: /// | | /// +-------+ /// -Geometry difference(Geometry a, Geometry b); -inline auto substract(Geometry a, Geometry b) { return difference(std::move(a), std::move(b)); } -inline auto operator-(Geometry a, Geometry b) { return difference(std::move(a), std::move(b)); } +[[nodiscard]] Geometry subtract(Geometry a, Geometry b, Options options = {}); + +[[nodiscard]] inline auto difference(Geometry a, Geometry b) { return subtract(std::move(a), std::move(b)); } +[[nodiscard]] inline auto operator-(Geometry a, Geometry b) { return subtract(std::move(a), std::move(b)); } /// Return a new CSG solid representing space both this solid and in the /// solid `csg`. Neither this solid nor the solid `csg` are modified. @@ -261,9 +411,16 @@ inline auto operator-(Geometry a, Geometry b) { return difference(std::move(a), /// | | /// +-------+ /// -Geometry intersect(Geometry a, Geometry b); -inline auto operator&(Geometry a, Geometry b) { return intersect(std::move(a), std::move(b)); } +[[nodiscard]] Geometry intersect(Geometry a, Geometry b, Options options = {}); + +[[nodiscard]] inline auto intersection(Geometry a, Geometry b) { return intersect(std::move(a), std::move(b)); } +[[nodiscard]] inline auto operator&(Geometry a, Geometry b) { return intersect(std::move(a), std::move(b)); } +[[nodiscard]] inline Vertex operator*(const QMatrix4x4 &m, const Vertex &v) { return v.transformed(m); } +[[nodiscard]] inline Polygon operator*(const QMatrix4x4 &m, const Polygon &p) { return p.transformed(m); } +[[nodiscard]] inline Geometry operator*(const QMatrix4x4 &m, const Geometry &g) { return g.transformed(m); } + +/// Allow to dump the library's objects via Qt's logging mechanism. QDebug operator<<(QDebug debug, Geometry geometry); QDebug operator<<(QDebug debug, Plane plane); QDebug operator<<(QDebug debug, Polygon polygon); @@ -271,4 +428,9 @@ QDebug operator<<(QDebug debug, Vertex vertex); } // namespace QtCSG +Q_DECLARE_METATYPE(QtCSG::Geometry) +Q_DECLARE_METATYPE(QtCSG::Plane) +Q_DECLARE_METATYPE(QtCSG::Polygon) +Q_DECLARE_METATYPE(QtCSG::Vertex) + #endif // QTCSG_H diff --git a/qtcsg/qtcsgio.cpp b/qtcsg/qtcsgio.cpp new file mode 100644 index 0000000..5d0f546 --- /dev/null +++ b/qtcsg/qtcsgio.cpp @@ -0,0 +1,295 @@ +#include "qtcsgio.h" + +#include "qtcsgmath.h" + +#include +#include +#include +#include + +namespace QtCSG { + +namespace { + +Q_LOGGING_CATEGORY(lcInputOutput, "qtcsg.io"); + +#if defined(__cpp_concepts) && __cpp_concepts >= 202002L + +template +concept HasEmplaceBack = requires(T *object) { + object->emplaceBack(typename T::value_type{}); +}; + +template +void emplaceBack(QList &list, Args... args) +{ + static_assert(HasEmplaceBack> + || QT_VERSION_MAJOR < 6); + + if constexpr (HasEmplaceBack>) { + list.emplaceBack(std::forward(args)...); + } else { + list.append(T{std::forward(args)...}); + } +} + +#else + +template +void emplaceBack(QList &list, Args... args) +{ + list.append(T{std::forward(args)...}); +} + +#endif + +// http://www.geomview.org/docs/html/OFF.html +class OffFileFormat : public FileFormat +{ +public: + QString id() const override { return "OFF"; } + bool accepts(QString fileName) const override; + Geometry readGeometry(QIODevice *device) const override; + Error writeGeometry(Geometry geometry, QIODevice *device) const override; +}; + +bool OffFileFormat::accepts(QString fileName) const +{ + return fileName.endsWith(".off", Qt::CaseInsensitive); +} + +Geometry OffFileFormat::readGeometry(QIODevice *device) const +{ + enum class State { + Magic, + Header, + Vertices, + Faces, + }; + + auto state = State::Magic; + auto vertexCount = int{}; + auto faceCount = int{}; + auto ok = false; + + auto stream = QTextStream{device}; + auto vertices = std::vector{}; + auto polygons = QList{}; + + for (auto lineNumber = 1; !stream.atEnd(); ++lineNumber) { + const auto line = stream.readLine().trimmed(); + + if (line.startsWith('#')) + continue; + + switch (state) { + case State::Magic: + if (line != "OFF") { + qCWarning(lcInputOutput, "Unsupported file format"); + return Geometry{Error::NotSupportedError}; + } + + state = State::Header; + break; + + case State::Header: + vertexCount = line.section(' ', 0, 0).toInt(&ok); + + if (!ok) { + qCWarning(lcInputOutput, "Invalid vertex count at line %d", lineNumber); + return Geometry{Error::FileFormatError}; + } + + faceCount = line.section(' ', 1, 1).toInt(&ok); + + if (!ok) { + qCWarning(lcInputOutput, "Invalid face count at line %d", lineNumber); + return Geometry{Error::FileFormatError}; + } + + vertices.reserve(vertexCount); + polygons.reserve(faceCount); + state = State::Vertices; + break; + + case State::Vertices: + if (const auto x = line.section(' ', 0, 0).toFloat(&ok); ok) { + if (const auto y = line.section(' ', 1, 1).toFloat(&ok); ok) { + if (const auto z = line.section(' ', 2, 2).toFloat(&ok); ok) { + vertices.emplace_back(x, y, z); + + if (--vertexCount == 0) + state = State::Faces; + + break; + } + } + } + + qCWarning(lcInputOutput, "Invalid vertex at line %d", lineNumber); + return Geometry{Error::FileFormatError}; + + case State::Faces: + if (const auto n = line.section(' ', 0, 0).toInt(&ok); ok) { + auto indices = std::vector{}; + indices.reserve(n); + + for (auto i = 1; i <= n; ++i) { + const auto index = line.section(' ', i, i).toUInt(&ok); + static_assert(std::is_unsigned_v); + + if (ok && index < vertices.size()) { + indices.emplace_back(index); + } else { + qCWarning(lcInputOutput, "Invalid index at line %d, field %d", lineNumber, i); + return Geometry{Error::FileFormatError}; + } + } + + auto outline = QList{}; + outline.reserve(indices.size()); + + for (auto j = 0; j < n; ++j) { + const auto i = (j + n - 1) % n; + const auto k = (j + 1) % n; + + auto a = vertices.at(indices.at(i)); + auto b = vertices.at(indices.at(j)); + auto c = vertices.at(indices.at(k)); + auto n = crossProduct(b - a, c - a); + + n.normalize(); + + emplaceBack(outline, std::move(b), std::move(n)); + } + + emplaceBack(polygons, std::move(outline)); + + if (--faceCount == 0) + return Geometry{std::move(polygons)}; + + continue; + } + + qCWarning(lcInputOutput, "Invalid index count at line %d", lineNumber); + return Geometry{Error::FileFormatError}; + } + } + + qCWarning(lcInputOutput, "Unexpected end of file"); + return Geometry{Error::FileFormatError}; +} + +Error OffFileFormat::writeGeometry(Geometry geometry, QIODevice *device) const +{ + auto faces = std::vector>{}; + auto vertices = std::vector{}; + + const auto polygons = geometry.polygons(); + faces.reserve(polygons.size()); + + for (const auto &p: polygons) { + faces.emplace_back(); + faces.back().reserve(p.vertices().count()); + + for (const auto &v: p.vertices()) { + const auto p = v.position(); + + auto it = std::find(vertices.begin(), vertices.end(), p); + + if (it == vertices.end()) + it = vertices.emplace(vertices.end(), p.x(), p.y(), p.z()); + + faces.back().emplace_back(it - vertices.begin()); + } + } + + auto stream = QTextStream{device}; + + stream << "OFF" << Qt::endl; + stream << vertices.size() << ' ' << faces.size() << " 0" << Qt::endl; + + for (const auto &v: vertices) + stream << v.x() << ' '<< v.y() << ' '<< v.z() << Qt::endl; + + for (const auto &f: faces) { + stream << f.size(); + + for (const auto i: f) + stream << ' ' << i; + + stream << Qt::endl; + } + + return Error::NoError; +} + +Geometry readGeometry(const FileFormat *format, QString fileName) +{ + auto file = QFile{fileName}; + + if (file.open(QFile::ReadOnly)) + return format->readGeometry(&file); + + qCWarning(lcInputOutput, "%ls: %ls", + qUtf16Printable(file.fileName()), + qUtf16Printable(file.errorString())); + + return Geometry{Error::FileSystemError}; +} + +Error writeGeometry(const FileFormat *format, Geometry geometry, QString fileName) +{ + auto file = QFile{fileName}; + + if (file.open(QFile::WriteOnly)) + return format->writeGeometry(std::move(geometry), &file); + + qCWarning(lcInputOutput, "%ls: %ls", + qUtf16Printable(file.fileName()), + qUtf16Printable(file.errorString())); + + return Error::FileSystemError; +} + +} // namespace + +template<> +FileFormat::List FileFormat::supported() +{ + static const auto list = QList { + offFileFormat(), + }; + + return list; +} + +const FileFormat *offFileFormat() +{ + static const auto format = OffFileFormat{}; + return &format; +} + +Geometry readGeometry(QString fileName) +{ + for (const auto &fileFormat: FileFormat::supported()) { + if (fileFormat->accepts(fileName)) + return readGeometry(fileFormat, std::move(fileName)); + } + + qCWarning(lcInputOutput, "%ls: Unsupported file format", qUtf16Printable(fileName)); + return Geometry{Error::NotSupportedError}; +} + +Error writeGeometry(Geometry geometry, QString fileName) +{ + for (const auto &fileFormat: FileFormat::supported()) { + if (fileFormat->accepts(fileName)) + return writeGeometry(fileFormat, std::move(geometry), std::move(fileName)); + } + + qCWarning(lcInputOutput, "%ls: Unsupported file format", qUtf16Printable(fileName)); + return Error::NotSupportedError; +} + +} // namespace QtCSG diff --git a/qtcsg/qtcsgio.h b/qtcsg/qtcsgio.h new file mode 100644 index 0000000..0583791 --- /dev/null +++ b/qtcsg/qtcsgio.h @@ -0,0 +1,29 @@ +#ifndef QTCSGIO_H +#define QTCSGIO_H + +#include "qtcsg.h" + +class QIODevice; + +namespace QtCSG { + +template +struct FileFormat +{ + virtual QString id() const = 0; + virtual bool accepts(QString fileName) const = 0; + virtual T readGeometry(QIODevice *device) const = 0; + virtual Error writeGeometry(T geometry, QIODevice *device) const = 0; + + using List = QList; + static List supported(); +}; + +Geometry readGeometry(QString fileName); +Error writeGeometry(Geometry geometry, QString fileName); + +const FileFormat *offFileFormat(); + +} // namespace QtCSG + +#endif // QTCSGIO_H diff --git a/qtcsg/qtcsgmath.cpp b/qtcsg/qtcsgmath.cpp new file mode 100644 index 0000000..dd15074 --- /dev/null +++ b/qtcsg/qtcsgmath.cpp @@ -0,0 +1,67 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgmath.h" + +namespace QtCSG { + +// verify that this module's constexpr functions really areconstexpr + +#if QTCSG_QVECTOR3D_IS_CONSTEXPR + +static_assert(crossProduct({1, 0, 0}, {0, 1, 0}) == QVector3D{0, 0, 1}); +static_assert(dotProduct ({1, 0, 0}, {0, 1, 0}) == 0); + +#endif // QTCSG_QVECTOR3D_IS_CONSTEXPR + +// FIXME: add constexpr length()/sqrt()/hypot() to provide a constexpr normalVector() +// static_assert(normalVector({0, 0, 0}, {1, 0, 0}, {0, 1, 0}) == QVector3D{0, 0, 1}); + +static_assert(lerp({0, 0, 0}, {0, 0, 1}, 0.0) == QVector3D{0.0, 0.0, 0.0}); +static_assert(lerp({0, 0, 0}, {0, 0, 1}, 0.5) == QVector3D{0.0, 0.0, 0.5}); +static_assert(lerp({0, 0, 0}, {0, 0, 1}, 1.0) == QVector3D{0.0, 0.0, 1.0}); + +// implementations + +QVector3D findTranslation(const QMatrix4x4 &matrix) +{ + return {matrix(0, 3), matrix(1, 3), matrix(2, 3)}; +} + +QVector3D findScale(const QMatrix4x4 &matrix) +{ + const auto x = QVector3D{matrix(0, 0), matrix(1, 0), matrix(2, 0)}; + const auto y = QVector3D{matrix(0, 1), matrix(1, 1), matrix(2, 1)}; + const auto z = QVector3D{matrix(0, 2), matrix(1, 2), matrix(2, 2)}; + + return {x.length(), y.length(), z.length()}; +} + +QMatrix4x4 findRotation(const QMatrix4x4 &matrix) +{ + const auto s = findScale(matrix); + + return { + matrix(0, 0) / s.x(), matrix(0, 1) / s.y(), matrix(0, 2) / s.z(), 0, + matrix(1, 0) / s.x(), matrix(1, 1) / s.y(), matrix(1, 2) / s.z(), 0, + matrix(2, 0) / s.x(), matrix(2, 1) / s.y(), matrix(2, 2) / s.z(), 0, + 0, 0, 0, 1, + }; +} + +} // namespace QtCSG diff --git a/qtcsg/qtcsgmath.h b/qtcsg/qtcsgmath.h new file mode 100644 index 0000000..5f13ff3 --- /dev/null +++ b/qtcsg/qtcsgmath.h @@ -0,0 +1,99 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef QTCSGMATH_H +#define QTCSGMATH_H + +#include +#include + +namespace QtCSG { + +#define QTCSG_QVECTOR3D_IS_CONSTEXPR (QT_VERSION_MAJOR >= 6) + +#if QTCSG_QVECTOR3D_IS_CONSTEXPR +#define QTCSG_CONSTEXPR_QVECTOR3D constexpr +#else +#define QTCSG_CONSTEXPR_QVECTOR3D inline +#endif + +[[nodiscard]] QTCSG_CONSTEXPR_QVECTOR3D QVector3D crossProduct(QVector3D a, QVector3D b) noexcept +{ + return QVector3D::crossProduct(std::move(a), std::move(b)); +} + +[[nodiscard]] QTCSG_CONSTEXPR_QVECTOR3D float dotProduct(QVector3D a, QVector3D b) noexcept +{ + return QVector3D::dotProduct(std::move(a), std::move(b)); +} + +[[nodiscard]] constexpr QVector3D lerp(QVector3D a, QVector3D b, float t) noexcept +{ + return a + (b - a) * t; +} + +[[nodiscard]] inline QVector3D normalVector(QVector3D a, QVector3D b, QVector3D c) noexcept +{ + return crossProduct(b - a, c - a).normalized(); +} + +[[nodiscard]] inline auto identity() +{ + return QMatrix4x4{}; +} + +template +[[nodiscard]] inline auto translation(Args... translation) +{ + auto matrix = QMatrix4x4{}; + matrix.translate(std::forward(translation)...); + return matrix; +} + +template +[[nodiscard]] inline auto rotation(Args... rotation) +{ + auto matrix = QMatrix4x4{}; + matrix.rotate(std::forward(rotation)...); + return matrix; +} + +template +[[nodiscard]] inline auto scale(Args... scale) +{ + auto matrix = QMatrix4x4{}; + matrix.scale(std::forward(scale)...); + return matrix; +} + +[[nodiscard]] inline auto translation(QVector3D vector) +{ return translation(std::move(vector)); } + +[[nodiscard]] inline auto rotation(float angle, QVector3D axis) +{ return rotation(angle, std::move(axis)); } + +[[nodiscard]] inline auto scale(QVector3D vector) +{ return scale(std::move(vector)); } + +[[nodiscard]] QVector3D findTranslation(const QMatrix4x4 &matrix); +[[nodiscard]] QVector3D findScale (const QMatrix4x4 &matrix); +[[nodiscard]] QMatrix4x4 findRotation (const QMatrix4x4 &matrix); + +} // namespace QtCSG + +#endif // QTCSGMATH_H diff --git a/qtcsg/qtcsgutils.cpp b/qtcsg/qtcsgutils.cpp new file mode 100644 index 0000000..42d1be3 --- /dev/null +++ b/qtcsg/qtcsgutils.cpp @@ -0,0 +1,61 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgutils.h" + +#include + +namespace QtCSG::Utils { + +bool reportError(const QLoggingCategory &category, Error error, + const char *message, SourceLocation location) +{ + if (Q_LIKELY(error == Error::NoError)) + return false; + + if (category.isWarningEnabled()) { + auto logger = QMessageLogger{location.file_name(), static_cast(location.line()), + location.function_name(), category.categoryName()}; + logger.warning("%s, the reported error is %s", message, keyName(error)); + } + +#ifdef QTCSG_IGNORE_ERRORS + return false; +#else + return true; +#endif +} + +void enabledColorfulLogging() +{ +#ifdef QT_MESSAGELOGCONTEXT +#define QTCSG_MESSAGELOGCONTEXT_PATTERN "\033[0;37m (%{function} in %{file}, line %{line})\033[0m" +#else +#define QTCSG_MESSAGELOGCONTEXT_PATTERN "" +#endif + + qSetMessagePattern("%{time process} " + "%{if-critical}\033[1;31m%{endif}" + "%{if-warning}\033[1;33m%{endif}" + "%{type}%{if-category} %{category}%{endif} %{message}" + "%{if-warning}\033[0m%{endif}" + "%{if-critical}\033[0m%{endif}" + QTCSG_MESSAGELOGCONTEXT_PATTERN); +} + +} // namespace QtCSG::Utils diff --git a/qtcsg/qtcsgutils.h b/qtcsg/qtcsgutils.h new file mode 100644 index 0000000..4ea69d8 --- /dev/null +++ b/qtcsg/qtcsgutils.h @@ -0,0 +1,58 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef QTCSG_QTCSGUTILS_H +#define QTCSG_QTCSGUTILS_H + +#include "qtcsg.h" + +#include + +#ifdef __cpp_lib_source_location +#include +#else +#include +#endif + +namespace QtCSG::Utils { + +#ifdef __cpp_lib_source_location +using SourceLocation = std::source_location; +#else +using SourceLocation = std::experimental::source_location; +#endif + +/// Resolve the key name for `value` from enumeration `T`. +template>> +[[nodiscard]] inline const char *keyName(T value) +{ + static const auto metaEnum = QMetaEnum::fromType(); + return metaEnum.valueToKey(static_cast(value)); +} + +/// Check if `error` indicates a problem. If there is a problem, the function returns `true`. +/// Additionally `message` is logged to `category`; together with a description of `error`. +[[nodiscard]] bool reportError(const QLoggingCategory &category, Error error, const char *message, + SourceLocation location = SourceLocation::current()); + +/// Enable colorful logging, so that information is easier to understand. +void enabledColorfulLogging(); + +} // namespace QtCSG::Utils + +#endif // QTCSG_QTCSGUTILS_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9659c4e..df132e3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,18 @@ -add_executable(QtCSGTest qtcsgtest.cpp) -target_link_libraries(QtCSGTest PRIVATE QtCSG Qt::Test) -add_test(NAME QtCSGTest COMMAND $) +function(qtcsg_add_testsuite NAME) + set(options) + set(multivalue SOURCES LIBRARIES) + cmake_parse_arguments(TESTSUITE "${options}" "${onevalue}" "${multivalue}" ${ARGN}) + message(STATUS: "${NAME}: TESTSUITE_LIBRARIES: ${TESTSUITE_LIBRARIES}") + + qt_add_executable(${NAME} ${TESTSUITE_SOURCES}) + target_link_libraries(${NAME} PRIVATE QtCSGTestSuite ${TESTSUITE_LIBRARIES}) + add_test(NAME ${NAME} COMMAND $) +endfunction() + +add_library(QtCSGTestSuite OBJECT qtcsgtest.h) +target_link_libraries(QtCSGTestSuite PUBLIC QtCSG Qt::Test) + +qtcsg_add_testsuite(QtCSGAppSupportTest SOURCES qtcsgappsupporttest.cpp LIBRARIES QtCSGAppSupport) +qtcsg_add_testsuite(QtCSGIOTest SOURCES qtcsgiotest.cpp) +qtcsg_add_testsuite(QtCSGMathTest SOURCES qtcsgmathtest.cpp) +qtcsg_add_testsuite(QtCSGTest SOURCES qtcsgtest.cpp LIBRARIES QtCSGAssets) diff --git a/tests/qtcsgappsupporttest.cpp b/tests/qtcsgappsupporttest.cpp new file mode 100644 index 0000000..c4746cb --- /dev/null +++ b/tests/qtcsgappsupporttest.cpp @@ -0,0 +1,87 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgtest.h" + +#include + +#include + +namespace QtCSG::Tests { + +class AppSupportTest : public QObject +{ + Q_OBJECT + +public: + enum MetaEvent { None, Any }; + Q_ENUM(MetaEvent) + +private slots: + void testMetaEnum() + { + struct Mode : public AppSupport::MultiEnum + { + using MultiEnum::MultiEnum; + using enum Inspection::Event; + using enum MetaEvent; + }; + + auto mode = Mode{Mode::None}; + QCOMPARE(mode, Mode::None); + + // auto variant = QVariant::fromValue(mode); + // QCOMPARE(variant.userType(), qMetaTypeId()); + auto variant = static_cast(mode); + // QCOMPARE(variant.userType(), qMetaTypeId()); + QCOMPARE(variant, Mode{Mode::None}); + + QVERIFY(QMetaType::canConvert(variant.metaType(), QMetaType::fromType())); + + mode = qvariant_cast(variant); + QCOMPARE(mode, Mode::None); + + variant = QVariant::fromValue(Mode::Clip); + QCOMPARE(variant, Mode{Mode::Clip}); + + const auto t1 = QMetaType::fromType>(); + const auto t2 = QMetaType::fromType(); + const auto t3 = QMetaType::fromType(); + const auto t4 = QMetaType::fromType(); + + QVERIFY(QMetaType::canConvert(t1, t2)); + QVERIFY(QMetaType::canConvert(t1, t3)); + QVERIFY(QMetaType::canConvert(t1, t4)); + + QVERIFY(QMetaType::canConvert(t2, t1)); + QVERIFY(QMetaType::canConvert(t2, t3)); + QVERIFY(QMetaType::canConvert(t2, t4)); + + QVERIFY(QMetaType::canConvert(t3, t1)); + QVERIFY(QMetaType::canConvert(t3, t2)); + + QVERIFY(QMetaType::canConvert(t4, t1)); + QVERIFY(QMetaType::canConvert(t4, t2)); + } +}; + +} // namespace QtCSG::Tests + +QTEST_MAIN(QtCSG::Tests::AppSupportTest) + +#include "qtcsgappsupporttest.moc" diff --git a/tests/qtcsgiotest.cpp b/tests/qtcsgiotest.cpp new file mode 100644 index 0000000..b4aee2d --- /dev/null +++ b/tests/qtcsgiotest.cpp @@ -0,0 +1,66 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgtest.h" + +#include + +#include + +Q_DECLARE_METATYPE(const QtCSG::FileFormat *) + +namespace QtCSG::Tests { + +class IOTest : public QObject +{ + Q_OBJECT + +private slots: + void testRoundTrip_data() + { + QTest::addColumn *>("format"); + + for (const auto format: FileFormat::supported()) + QTest::addRow("%ls", qUtf16Printable(format->id())) << format; + } + + void testRoundTrip() + { + QFETCH(const FileFormat *const, format); + + auto buffer = QBuffer{}; + + const auto geometry = QtCSG::cube(); + QVERIFY2(buffer.open(QFile::WriteOnly), qUtf8Printable(buffer.errorString())); + format->writeGeometry(geometry, &buffer); + buffer.close(); + + QVERIFY2(buffer.open(QFile::ReadOnly), qUtf8Printable(buffer.errorString())); + const auto readBack = format->readGeometry(&buffer); + buffer.close(); + + QCOMPARE(readBack.error(), Error::NoError); + QCOMPARE(readBack.polygons(), geometry.polygons()); + } +}; + +} // namespace QtCSG::Tests + +QTEST_MAIN(QtCSG::Tests::IOTest) + +#include "qtcsgiotest.moc" diff --git a/tests/qtcsgmathtest.cpp b/tests/qtcsgmathtest.cpp new file mode 100644 index 0000000..15b6316 --- /dev/null +++ b/tests/qtcsgmathtest.cpp @@ -0,0 +1,136 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "qtcsgtest.h" + +#include + +namespace QtCSG::Tests { + +class MathTest : public QObject +{ + Q_OBJECT + +private slots: + void testLerp_data() + { + QTest::addColumn("a"); + QTest::addColumn("b"); + QTest::addColumn ("t"); + QTest::addColumn("expectedResult"); + + QTest::newRow("start-x") << QVector3D{0, 0, 0} << QVector3D{1, 0, 0} << 0.0f << QVector3D{0.0f, 0.0f, 0.0f}; + QTest::newRow("start-y") << QVector3D{0, 0, 0} << QVector3D{0, 1, 0} << 0.0f << QVector3D{0.0f, 0.0f, 0.0f}; + QTest::newRow("start-z") << QVector3D{0, 0, 0} << QVector3D{0, 0, 1} << 0.0f << QVector3D{0.0f, 0.0f, 0.0f}; + QTest::newRow("start-xyz") << QVector3D{0, 0, 0} << QVector3D{1, 1, 1} << 0.0f << QVector3D{0.0f, 0.0f, 0.0f}; + + QTest::newRow("middle-x") << QVector3D{0, 0, 0} << QVector3D{1, 0, 0} << 0.5f << QVector3D{0.5f, 0.0f, 0.0f}; + QTest::newRow("middle-y") << QVector3D{0, 0, 0} << QVector3D{0, 1, 0} << 0.5f << QVector3D{0.0f, 0.5f, 0.0f}; + QTest::newRow("middle-z") << QVector3D{0, 0, 0} << QVector3D{0, 0, 1} << 0.5f << QVector3D{0.0f, 0.0f, 0.5f}; + QTest::newRow("middle-xyz") << QVector3D{0, 0, 0} << QVector3D{1, 1, 1} << 0.5f << QVector3D{0.5f, 0.5f, 0.5f}; + + QTest::newRow("end-x") << QVector3D{0, 0, 0} << QVector3D{1, 0, 0} << 1.0f << QVector3D{1.0f, 0.0f, 0.0f}; + QTest::newRow("end-y") << QVector3D{0, 0, 0} << QVector3D{0, 1, 0} << 1.0f << QVector3D{0.0f, 1.0f, 0.0f}; + QTest::newRow("end-z") << QVector3D{0, 0, 0} << QVector3D{0, 0, 1} << 1.0f << QVector3D{0.0f, 0.0f, 1.0f}; + QTest::newRow("end-xyz") << QVector3D{0, 0, 0} << QVector3D{1, 1, 1} << 1.0f << QVector3D{1.0f, 1.0f, 1.0f}; + } + + void testLerp() + { + const QFETCH(QVector3D, a); + const QFETCH(QVector3D, b); + const QFETCH(float, t); + const QFETCH(QVector3D, expectedResult); + QCOMPARE(lerp(a, b, t), expectedResult); + } + + void testFindMatrixComponents_data() + { + QTest::addColumn("matrix"); + QTest::addColumn ("expectedTranslation"); + QTest::addColumn ("expectedScale"); + QTest::addColumn("expectedRotation"); + + const auto combination = [](const QVector3D &v) + { + const auto s = scale(v.x() ? 2.0f : 1.0f, + v.y() ? 4.0f : 1.0f, + v.z() ? 8.0f : 1.0f); + + const auto t = translation(v.x() * 1.0f, + v.y() * 2.0f, + v.z() * 3.0f); + + return t * rotation(90, v) * s; + }; + + const auto t0 = QVector3D{0, 0, 0}; + const auto tx = QVector3D{1, 0, 0}; + const auto ty = QVector3D{0, 2, 0}; + const auto tz = QVector3D{0, 0, 3}; + const auto txyz = QVector3D{1, 2, 3}; + + const auto s0 = QVector3D{1, 1, 1}; + const auto sx = QVector3D{2, 1, 1}; + const auto sy = QVector3D{1, 4, 1}; + const auto sz = QVector3D{1, 1, 8}; + const auto sxyz = QVector3D{2, 4, 8}; + + const auto r0 = identity(); + const auto rx = rotation(90, {1, 0, 0}); + const auto ry = rotation(90, {0, 1, 0}); + const auto rz = rotation(90, {0, 0, 1}); + const auto rxyz = rotation(90, {1, 1, 1}); + + QTest::newRow("identity") << identity() << t0 << s0 << r0; + QTest::newRow("translated-x") << translation({1, 0, 0}) << tx << s0 << r0; + QTest::newRow("translated-y") << translation({0, 2, 0}) << ty << s0 << r0; + QTest::newRow("translated-z") << translation({0, 0, 3}) << tz << s0 << r0; + QTest::newRow("translated-xyz") << translation({1, 2, 3}) << txyz << s0 << r0; + QTest::newRow("scaled-x") << scale({2, 1, 1}) << t0 << sx << r0; + QTest::newRow("scaled-y") << scale({1, 4, 1}) << t0 << sy << r0; + QTest::newRow("scaled-z") << scale({1, 1, 8}) << t0 << sz << r0; + QTest::newRow("scaled-xyz") << scale({2, 4, 8}) << t0 << sxyz << r0; + QTest::newRow("rotated-x") << rotation(90, {1, 0, 0}) << t0 << s0 << rx; + QTest::newRow("rotated-y") << rotation(90, {0, 1, 0}) << t0 << s0 << ry; + QTest::newRow("rotated-z") << rotation(90, {0, 0, 1}) << t0 << s0 << rz; + QTest::newRow("rotated-xyz") << rotation(90, {1, 1, 1}) << t0 << s0 << rxyz; + QTest::newRow("mixed-x") << combination({1, 0, 0}) << tx << sx << rx; + QTest::newRow("mixed-y") << combination({0, 1, 0}) << ty << sy << ry; + QTest::newRow("mixed-z") << combination({0, 0, 1}) << tz << sz << rz; + QTest::newRow("mixed-xyz") << combination({1, 1, 1}) << txyz << sxyz << rxyz; + } + + void testFindMatrixComponents() + { + const QFETCH(QMatrix4x4, matrix); + const QFETCH(QVector3D, expectedTranslation); + const QFETCH(QVector3D, expectedScale); + const QFETCH(QMatrix4x4, expectedRotation); + + QCOMPARE(findTranslation(matrix), expectedTranslation); + QCOMPARE(findScale(matrix), expectedScale); + QCOMPARE(findRotation(matrix), expectedRotation); + } +}; + +} // namespace QtCSG::Tests + +QTEST_MAIN(QtCSG::Tests::MathTest) + +#include "qtcsgmathtest.moc" diff --git a/tests/qtcsgtest.cpp b/tests/qtcsgtest.cpp index dc5666f..40cf8ce 100644 --- a/tests/qtcsgtest.cpp +++ b/tests/qtcsgtest.cpp @@ -16,14 +16,27 @@ * * SPDX-License-Identifier: GPL-3.0-or-later */ +#include "qtcsgtest.h" + #include +#include +#include -#include +#include namespace QtCSG::Tests { using std::make_pair; +class Helper +{ +public: + static void split(const Polygon &polygon, const Plane &plane, + QList *coplanarFront, QList *coplanarBack, + QList *front, QList *back, + Options options = {}); +}; + class Test : public QObject { Q_OBJECT @@ -77,34 +90,55 @@ private slots: void testUnion_data() { - QTest::addColumn("delta"); + QTest::addColumn("deltaX"); + QTest::addColumn("deltaY"); + QTest::addColumn("deltaZ"); - QTest::newRow("delta=0.0") << 0.0f; - QTest::newRow("delta=0.5") << 0.5f; - QTest::newRow("delta=1.0") << 1.0f; - QTest::newRow("delta=1.5") << 1.5f; + QTest::addColumn("expectedPolygonCount"); + + QTest::newRow("identity") << 0.0f << 0.0f << 0.0f << 6 * 1; + + QTest::newRow("overlapping:xyz") << 0.5f << 0.5f << 0.5f << 6 * 4; + QTest::newRow("adjacent:xyz") << 1.0f << 1.0f << 1.0f << 6 * 2; + QTest::newRow("distant:xyz") << 1.5f << 1.5f << 1.5f << 6 * 2; + + QTest::newRow("overlapping:x") << 0.5f << 0.0f << 0.0f << 4 * 3 + 2; + QTest::newRow("adjacent:x") << 1.0f << 0.0f << 0.0f << 6 * 2 - 2; + QTest::newRow("distant:x") << 1.5f << 0.0f << 0.0f << 6 * 2; } void testUnion() { - QFETCH(float, delta); + const QFETCH(float, deltaX); + const QFETCH(float, deltaY); + const QFETCH(float, deltaZ); + + const QFETCH(int, expectedPolygonCount); - const auto a = cube({-delta, -delta, +delta}); - const auto b = cube({+delta, +delta, -delta}); + const auto a = cube({-deltaX, -deltaY, +deltaZ}); + const auto b = cube({+deltaX, +deltaY, -deltaZ}); const auto c = merge(a, b); - if (qFuzzyCompare(delta, 0)) + if (qFuzzyCompare(deltaX, 0) + && qFuzzyCompare(deltaY, 0) + && qFuzzyCompare(deltaZ, 0)) QCOMPARE(a.polygons(), b.polygons()); QCOMPARE(a.polygons().count(), 6); QCOMPARE(b.polygons().count(), 6); - QCOMPARE(c.polygons().count(), 6); + QCOMPARE(c.polygons().count(), expectedPolygonCount); } void testNodeConstruct() { const auto expectedNormal = QVector3D{-1, 0, 0}; - const auto node = Node{cube().polygons()}; + const auto maybeNode = Node::fromPolygons(cube().polygons()); + + if (std::holds_alternative(maybeNode)) + QCOMPARE(std::get(maybeNode), Error::NoError); + + QVERIFY(std::holds_alternative(maybeNode)); + const auto node = std::get(maybeNode); { auto depth = 0; @@ -112,7 +146,7 @@ private slots: for (auto subNode = &node; subNode; subNode = subNode->back().get(), ++depth) { QCOMPARE(make_pair(depth, static_cast(subNode->polygons().count())), make_pair(depth, 1)); - QCOMPARE(make_pair(depth, static_cast(subNode->polygons().first().vertices().count())), + QCOMPARE(make_pair(depth, static_cast(subNode->polygons().constFirst().vertices().count())), make_pair(depth, 4)); QCOMPARE(make_pair(depth, !!subNode->front()), make_pair(depth, false)); @@ -133,7 +167,13 @@ private slots: void testNodeInvert() { const auto expectedNormal = QVector3D{1, 0, 0}; - const auto node = Node{cube().polygons()}.inverted(); + const auto maybeNode = Node::fromPolygons(cube().polygons()); + + if (std::holds_alternative(maybeNode)) + QCOMPARE(std::get(maybeNode), Error::NoError); + + QVERIFY(std::holds_alternative(maybeNode)); + const auto node = std::get(maybeNode); { auto depth = 0; @@ -141,7 +181,7 @@ private slots: for (auto subNode = &node; subNode; subNode = subNode->back().get(), ++depth) { QCOMPARE(make_pair(depth, static_cast(subNode->polygons().count())), make_pair(depth, 1)); - QCOMPARE(make_pair(depth, static_cast(subNode->polygons().first().vertices().count())), + QCOMPARE(make_pair(depth, static_cast(subNode->polygons().constFirst().vertices().count())), make_pair(depth, 4)); QCOMPARE(make_pair(depth, !!subNode->front()), make_pair(depth, depth < 5)); @@ -158,6 +198,396 @@ private slots: QCOMPARE(plane.normal(), expectedNormal); QCOMPARE(plane.w(), -1); } + + void testSplitWithAllInFront() + { + // Vertical YZ plane through the origin + const auto plane = Plane::fromPoints({0, 0, 0}, {0, 1, 0}, {0, 0, 1}); + + // Polygon in the +x hemisphere + const auto poly = Polygon{{ + Vertex{{1, 0, 0}, {1, 0, 0}}, + Vertex{{1, 1, 0}, {1, 0, 0}}, + Vertex{{1, 0, 1}, {1, 0, 0}}, + }}; + + auto cpf = QList{}; + auto cpb = QList{}; + auto front = QList{}; + auto back = QList{}; + + Helper::split(poly, plane, &cpf, &cpb, &front, &back); + + QCOMPARE(cpf.length(), 0); + QCOMPARE(cpb.length(), 0); + QCOMPARE(front.length(), 1); + QCOMPARE(back.length(), 0); + } + + void testSplitWithAllBehind() + { + // Vertical YZ plane through the origin + const auto plane = Plane::fromPoints({0, 0, 0}, {0, 1, 0}, {0, 0, 1}); + + // Polygon in the -x hemisphere + const auto poly = Polygon{{ + Vertex{{-1, 0, 0}, {1, 0, 0}}, + Vertex{{-1, 1, 0}, {1, 0, 0}}, + Vertex{{-1, 0, 1}, {1, 0, 0}}, + }}; + + auto cpf = QList{}; + auto cpb = QList{}; + auto front = QList{}; + auto back = QList{}; + + Helper::split(poly, plane, &cpf, &cpb, &front, &back); + + QCOMPARE(cpf.length(), 0); + QCOMPARE(cpb.length(), 0); + QCOMPARE(front.length(), 0); + QCOMPARE(back.length(), 1); + } + + void testSplitDownTheMiddle() + { + // Vertical YZ plane through the origin + const auto plane = Plane::fromPoints({0, 0, 0}, {0, 1, 0}, {0, 0, 1}); + + // Polygon describing a square on the XY plane with radius 2 + const auto poly = Polygon{{ + Vertex{{-1, +1, 0}, {0, 0, 1}}, + Vertex{{-1, -1, 0}, {0, 0, 1}}, + Vertex{{+1, -1, 0}, {0, 0, 1}}, + Vertex{{+1, +1, 0}, {0, 0, 1}}, + }}; + + auto cpf = QList{}; + auto cpb = QList{}; + auto front = QList{}; + auto back = QList{}; + + Helper::split(poly, plane, &cpf, &cpb, &front, &back); + + QCOMPARE(cpf.length(), 0); + QCOMPARE(cpb.length(), 0); + QCOMPARE(front.length(), 1); + QCOMPARE(back.length(), 1); + + for (const auto &v: front.constFirst().vertices()) + QVERIFY2(v.position().x() >= 0, "All front vertices must have x >= 0"); + for (const auto &v: back.constFirst().vertices()) + QVERIFY2(v.position().x() <= 0, "All back vertices must have x <= 0"); + } + + void testVertexTransform_data() + { + QTest::addColumn ("vertex"); + QTest::addColumn("matrix"); + QTest::addColumn ("expectedResult"); + QTest::addColumn ("expectedLength"); + + const auto ra = +2.577350f; + const auto rb = +0.845299f; + const auto na = +0.333333f; + const auto nb = +0.910684f; + const auto nc = -0.244017f; + + const auto v0 = Vertex{{+1, +2, +3}, {+1, +0, +0}}; + const auto sx = Vertex{{+2, +2, +3}, {+1, +0, +0}}; + const auto sy = Vertex{{+1, +4, +3}, {+1, +0, +0}}; + const auto sz = Vertex{{+1, +2, +6}, {+1, +0, +0}}; + const auto sxyz = Vertex{{+2, +4, +6}, {+1, +0, +0}}; + const auto tx = Vertex{{+2, +2, +3}, {+1, +0, +0}}; + const auto ty = Vertex{{+1, +3, +3}, {+1, +0, +0}}; + const auto tz = Vertex{{+1, +2, +4}, {+1, +0, +0}}; + const auto txyz = Vertex{{+2, +3, +4}, {+1, +0, +0}}; + const auto rx = Vertex{{+1, -3, +2}, {+1, +0, +0}}; + const auto ry = Vertex{{+3, +2, -1}, {+0, +0, -1}}; + const auto rz = Vertex{{-2, +1, +3}, {+0, +1, +0}}; + const auto rxyz = Vertex{{ra, rb, ra}, {na, nb, nc}}; + + QTest::newRow("identity") << v0 << identity() << v0 << 14.0f; + QTest::newRow("scaled-x") << v0 << scale({2, 1, 1}) << sx << 17.0f; + QTest::newRow("scaled-y") << v0 << scale({1, 2, 1}) << sy << 26.0f; + QTest::newRow("scaled-z") << v0 << scale({1, 1, 2}) << sz << 41.0f; + QTest::newRow("scaled-xyz") << v0 << scale({2, 2, 2}) << sxyz << 56.0f; + QTest::newRow("translated-x") << v0 << translation({1, 0, 0}) << tx << 17.0f; + QTest::newRow("translated-y") << v0 << translation({0, 1, 0}) << ty << 19.0f; + QTest::newRow("translated-z") << v0 << translation({0, 0, 1}) << tz << 21.0f; + QTest::newRow("translated-xyz") << v0 << translation({1, 1, 1}) << txyz << 29.0f; + QTest::newRow("rotated-x") << v0 << rotation(90, {1, 0, 0}) << rx << 14.0f; + QTest::newRow("rotated-y") << v0 << rotation(90, {0, 1, 0}) << ry << 14.0f; + QTest::newRow("rotated-z") << v0 << rotation(90, {0, 0, 1}) << rz << 14.0f; + QTest::newRow("rotated-xyz") << v0 << rotation(90, {1, 1, 1}) << rxyz << 14.0f; + } + + void testVertexTransform() + { + const QFETCH(Vertex, vertex); + const QFETCH(QMatrix4x4, matrix); + const QFETCH(Vertex, expectedResult); + const QFETCH(float, expectedLength); + + const auto transformed = vertex.transformed(matrix); + + QCOMPARE(transformed.position().lengthSquared(), expectedLength); + QCOMPARE(transformed.normal().lengthSquared(), 1.0f); + + QCOMPARE(transformed.position(), expectedResult.position()); + QCOMPARE(transformed.normal(), expectedResult.normal()); + QCOMPARE(transformed, expectedResult); + } + + void testCutOff_data() + { + QTest::addColumn("bodyFileName"); + QTest::addColumn("cutOffFileName"); + QTest::addColumn ("recursionLimit"); + + for (const auto dataDir = QDir{":/qtcsg/assets/cutoff"}; + const auto &childInfo: dataDir.entryInfoList(QDir::Dirs, QDir::Name)) { + auto childDir = QDir{childInfo.filePath()}; + + QTest::addRow("%s/l", qUtf8Printable(childInfo.fileName())) + << childDir.filePath("body.off") + << childDir.filePath("left.off") + << 20; + + QTest::addRow("%s/r", qUtf8Printable(childInfo.fileName())) + << childDir.filePath("body.off") + << childDir.filePath("right.off") + << 20; + } + } + + void testCutOff() + { + const QFETCH(int, recursionLimit); + const QFETCH(QString, bodyFileName); + const QFETCH(QString, cutOffFileName); + + const auto body = readGeometry(bodyFileName); + QCOMPARE(body.error(), Error::NoError); + + const auto cutOff = readGeometry(cutOffFileName); + QCOMPARE(cutOff.error(), Error::NoError); + + QBENCHMARK { + const auto delta = subtract(body, cutOff, Options::RecursionLimit{recursionLimit}); + QCOMPARE(delta.error(), Error::NoError); + QVERIFY(!delta.isEmpty()); + } + } + + void testParseGeometry_data() + { + QTest::addColumn("expression"); + QTest::addColumn("expectedGeometry"); + QTest::addColumn("expectedWarning"); + + QTest::newRow("cube:default") + << "cube()" + << cube() + << ""; + QTest::newRow("cube:center") + << "cube(center=[0.5,1,2.])" + << cube({0.5, 1, 2.0}) + << ""; + QTest::newRow("cube:radius:scalar") + << "cube(r=3.1)" + << cube({}, 3.1) + << ""; + QTest::newRow("cube:radius:vector") + << "cube(r=[1,2.2,3.5])" + << cube({}, {1, 2.2, 3.5}) + << ""; + QTest::newRow("cube:center+radius") + << "cube(r=5, center=[-1,+2,-3.0])" + << cube({-1, 2, -3}, 5) + << ""; + + QTest::newRow("cylinder:default") + << "cylinder()" + << cylinder() + << ""; + QTest::newRow("cylinder:start") + << "cylinder(start=[0,0,1])" + << cylinder({0, 0, 1}, {0, 0, 0}) + << ""; + QTest::newRow("cylinder:end+r") + << "cylinder(end=[0,0,-1], r=2)" + << cylinder({0, 0, 0}, {0, 0, -1}, 2) + << ""; + QTest::newRow("cylinder:start+end+r+slices") + << "cylinder(start=[1,1,1], end=[-1,-1,-1], r=1.5, slices=5)" + << cylinder({1, 1, 1}, {-1, -1, -1}, 1.5f, 5) + << ""; + QTest::newRow("cylinder:center") + << "cylinder(center=[1,2,3])" + << cylinder({1, 2, 3}) + << ""; + QTest::newRow("cylinder:center+r") + << "cylinder(center=[2,3,4], r=2)" + << cylinder({2, 3, 4}, 2, 2) + << ""; + QTest::newRow("cylinder:center+h+r+slices") + << "cylinder( center=[ 3, 4, 5 ], h = 6 , r = 7, slices=8 )" + << cylinder({3, 4, 5}, 6, 7, 8) + << ""; + + QTest::newRow("sphere:default") + << "sphere()" + << sphere() + << ""; + QTest::newRow("sphere:center+radius+slices+stacks") + << "sphere(center=[1,2,3], r=4, slices=5, stacks=6)" + << sphere({1, 2, 3}, 4, 5, 6) + << ""; + + QTest::newRow("error:filename") + << "/home/you/are/pretty.off" + << Geometry{Error::NotSupportedError} + << ""; + QTest::newRow("error:unknown-primitive") + << "unknown()" + << Geometry{Error::NotSupportedError} + << R"*(Unsupported primitive: "unknown")*"; + QTest::newRow("error:malformed-argument-list") + << "cube(bad)" + << Geometry{Error::FileFormatError} + << R"*(Invalid argument list: "(bad)")*"; + QTest::newRow("error:unknown-argument") + << "cube(unknown=23)" + << Geometry{Error::FileFormatError} + << R"*(Unsupported argument "unknown" for cube primitive)*"; + QTest::newRow("error:invalid-type") + << "cube(center=42)" + << Geometry{Error::FileFormatError} + << R"*(Unsupported value type for argument "center" of cube primitive)*"; + QTest::newRow("error:conflicting-arguments") + << "cylinder(start=[1,1,1], center=[0,0,0])" + << Geometry{Error::FileFormatError} + << R"*(Argument "center" conflicts with arguments )*" + R"*("start" and "end" of cylinder primitive)*"; + } + + void testParseGeometry() + { + const QFETCH(QString, expression); + const QFETCH(Geometry, expectedGeometry); + const QFETCH(QString, expectedWarning); + + if (!expectedWarning.isEmpty()) + QTest::ignoreMessage(QtWarningMsg, qUtf8Printable(expectedWarning)); + + const auto parsedGeometry = parseGeometry(expression); + + QCOMPARE(expectedGeometry.error(), expectedGeometry.error()); + QCOMPARE(parsedGeometry.polygons(), expectedGeometry.polygons()); + } + + void testOptions_data() + { + const auto defaults = Options{}; + const auto makeOptions = [](Options o) { return o; }; + + QTest::addColumn ("actualOptions"); + QTest::addColumn ("expectedFlags"); + QTest::addColumn ("expectedRecursionLimit"); + QTest::addColumn ("expectedEpsilon"); + QTest::addColumn ("expectedInspection"); + + QTest::newRow("single-flag") + << makeOptions(Options::CheckConvexity) + << Options::Flags{Options::CheckConvexity} + << defaults.recursionLimit + << defaults.epsilon + << false; + + QTest::newRow("multiple-flags") + << makeOptions(Options::CheckPolygonNormals | Options::CheckConvexity) + << Options::Flags{Options::CheckPolygonNormals | Options::CheckConvexity} + << defaults.recursionLimit + << defaults.epsilon + << false; + + QTest::newRow("recursion-limit") + << makeOptions(Options::RecursionLimit{1}) + << defaults.flags + << 1 + << defaults.epsilon + << false; + + QTest::newRow("epsilon") + << makeOptions(Options::Epsilon{0.1f}) + << defaults.flags + << defaults.recursionLimit + << 0.1f + << false; + + QTest::newRow("single-flag+recursion-limit") + << makeOptions(Options::CleanupPolygons + | Options::RecursionLimit{2}) + << Options::Flags{Options::CleanupPolygons} + << 2 + << defaults.epsilon + << false; + + QTest::newRow("single-flag+recursion-limit+epsilon") + << makeOptions(Options::CleanupPolygons + | Options::RecursionLimit{3} + | Options::Epsilon{0.2f}) + << Options::Flags{Options::CleanupPolygons} + << 3 + << 0.2f + << false; + + QTest::newRow("multiple-flags+recursion-limit+epsilon") + << makeOptions(Options::CheckPolygonNormals + | Options::CleanupPolygons + | Options::RecursionLimit{4} + | Options::Epsilon{0.3f}) + << Options::Flags{Options::CheckPolygonNormals | Options::CleanupPolygons} + << 4 + << 0.3f + << false; + + QTest::newRow("multiple-flags+recursion-limit+epsilon+inspect") + << makeOptions(Options::CheckPolygonNormals + | Options::CleanupPolygons + | Options::RecursionLimit{5} + | Options::Epsilon{0.4f} + | [](Inspection::Event, std::any) { + return Inspection::Result::Abort; + }) + << Options::Flags{Options::CheckPolygonNormals | Options::CleanupPolygons} + << 5 + << 0.4f + << true; + } + + void testOptions() + { + const QFETCH(Options, actualOptions); + + const QFETCH(Options::Flags, expectedFlags); + QCOMPARE(actualOptions.flags, expectedFlags); + + const QFETCH(int, expectedRecursionLimit); + QCOMPARE(actualOptions.recursionLimit, expectedRecursionLimit); + + const QFETCH(float, expectedEpsilon); + QCOMPARE(actualOptions.epsilon, expectedEpsilon); + + const QFETCH(bool, expectedInspection); + QCOMPARE(!!actualOptions.inspection, expectedInspection); + + if (actualOptions.inspection) { + QCOMPARE(actualOptions.inspection(Inspection::Event::Build, {}), + Inspection::Result::Abort); + } + } }; } // namespace QtCSG::Tests diff --git a/tests/qtcsgtest.h b/tests/qtcsgtest.h new file mode 100644 index 0000000..0695981 --- /dev/null +++ b/tests/qtcsgtest.h @@ -0,0 +1,168 @@ +/* QtCSG provides Constructive Solid Geometry (CSG) for Qt + * Copyright Ⓒ 2023 Mathias Hasselmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef QTCSGTEST_H +#define QTCSGTEST_H + +#include +#include + +namespace QtCSG::Tests::Internal { + +#if QT_VERSION_MAJOR < 6 +using QByteArrayView = QByteArray; +#endif + +/// This class provides a temporary buffer to display which detail made a custom qCompare() fail. +class ConcatBuffer +{ +public: + /// Provides a temporary buffer for tests that access methods and fields + explicit ConcatBuffer(QByteArrayView prefix, QByteArrayView suffix) + : m_prefix{std::move(prefix)} + , m_suffix{std::move(suffix)} + {} + + /// Provides a temporary buffer for tests that access via index + explicit ConcatBuffer(QByteArray prefix, QByteArray infix, std::size_t index) + : m_buffer{prefix + infix + '[' + QByteArray::number(static_cast(index)) + ']'} + , m_instantiated{true} + {} + + /// Converts this buffer into a plain old C string + [[nodiscard]] operator const char *() const noexcept + { + if (!std::exchange(m_instantiated, true)) { + m_buffer.append(m_prefix); + m_buffer.append(m_suffix); + } + + return m_buffer.constData(); + } + +private: + mutable QByteArray m_buffer; + mutable bool m_instantiated = false; + + QByteArrayView m_prefix; + QByteArrayView m_suffix; +}; + +/// Creates a temporary buffer to display which detail made a custom qCompare() fail. +template +[[nodiscard]] inline auto concat(Args... args) +{ + return ConcatBuffer{args...}; +} + +} // namespace QtCSG::Tests::Internal + +namespace QTest { + +/// Compares two vectors via fuzzy compare. +/// This is neccessary because QTest only provides a strict compare. +/// Strict comparison doesn't work well for floating point numbers. +inline bool qCompare(const QVector3D &a,const QVector3D &b, + const char *actual, const char *expected, + const char *file, int line) +{ + using QtCSG::Tests::Internal::concat; + + return qCompare(a.x(), b.x(), concat(actual, ".x()"), concat(expected, ".x()"), file, line) + && qCompare(a.y(), b.y(), concat(actual, ".y()"), concat(expected, ".y()"), file, line) + && qCompare(a.z(), b.z(), concat(actual, ".z()"), concat(expected, ".z()"), file, line); +} + +/// Compares two matrices via fuzzy compare. +/// This is neccessary because QTest only provides a strict compare. +/// Strict comparison doesn't work well for floating point numbers. +inline bool qCompare(const QMatrix4x4 &a,const QMatrix4x4 &b, + const char *actual, const char *expected, + const char *file, int line) +{ + if (!qFuzzyCompare(a, b)) { + return compare_helper(false, "Compared values are not the same", + toString(a), toString(b), actual, expected, + file, line); + } + + return true; +} + +} // namespace QTest + +#if defined(__cpp_concepts) && __cpp_concepts >= 202002L + +namespace QtCSG::Tests::Internal { + +/// This concept provides that the given type has a fields() method. +template +concept HasFieldsMethod = requires(T *instance) { + instance->fields(); +}; + +/// Compares two objects with a fields() method that returns a tuple. +/// This is useful to enable fully compare of the fields without +/// implementing a custom version of qFuzzyCompare. +/// +/// Note that this function has to occur after all other qCompare() +/// implemenations to ensure all other implemenations get picked up. +template +inline bool compareFields(const T &a, const T &b, + const char *actual, const char *expected, + const char *file, int line) +{ + using TypeType = decltype(a.fields()); + + if constexpr(N < std::tuple_size_v) { + if (!QTest::qCompare(std::get(a.fields()), + std::get(b.fields()), + concat(actual, ".fields", N), + concat(expected, ".fields", N), + file, line)) + return false; + + return compareFields(a, b, actual, expected, file, line); + } + + return true; +} + +} // namespace QtCSG::Tests::Internal + +namespace QTest { + +/// Compares two objects with a fields() method that returns a tuple. +/// This is useful to enable fully compare of the fields without +/// implementing a custom version of qFuzzyCompare. +/// +/// Note that this function has to occur after all other qCompare() +/// implemenations to ensure all other implemenations get picked up. +template +inline bool qCompare(const T &a, const T &b, + const char *actual, const char *expected, + const char *file, int line) +{ + return QtCSG::Tests::Internal::compareFields<0>(a, b, actual, expected, file, line); +} + +} // QTest + +#endif // defined(__cpp_concepts) && __cpp_concepts >= 202002L + +#endif // QTCSGTEST_H