diff --git a/.github/scripts/verify-windows-deps.ps1 b/.github/scripts/verify-windows-deps.ps1 deleted file mode 100644 index 87aa9f72..00000000 --- a/.github/scripts/verify-windows-deps.ps1 +++ /dev/null @@ -1,153 +0,0 @@ -# Verify that all non-system DLL dependencies of the given binaries are -# resolved within the same directory. -# -# Usage: -# .\verify-windows-deps.ps1 -BinDir [-Binaries volrover3.exe,cvc.dll] -# -# Exits with non-zero status (and prints what's missing) if any required -# DLL is absent from . System DLLs (kernel32, user32, msvcp140, -# vcruntime140, ucrtbase, api-ms-*, ext-ms-*, etc.) are ignored. -# -# Designed to run in GitHub Actions windows-* runners, which have -# dumpbin.exe available via the Visual Studio "Developer PowerShell" -# environment. The CI workflow activates that environment via -# ilammy/msvc-dev-cmd or the vcvars64.bat shim before invoking us. - -param( - [Parameter(Mandatory=$true)] [string] $BinDir, - [Parameter(Mandatory=$false)] [string[]] $Binaries = @() -) - -$ErrorActionPreference = 'Stop' - -if (-not (Test-Path $BinDir)) { - Write-Error "BinDir does not exist: $BinDir" - exit 2 -} - -$BinDir = (Resolve-Path $BinDir).Path -Write-Host "Verifying runtime dependencies under: $BinDir" - -# Auto-discover binaries if the caller did not name them. -if (-not $Binaries -or $Binaries.Count -eq 0) { - $Binaries = @() - Get-ChildItem -Path $BinDir -Filter *.exe -File | ForEach-Object { $Binaries += $_.Name } - Get-ChildItem -Path $BinDir -Filter *.dll -File | ForEach-Object { $Binaries += $_.Name } -} - -if (-not (Get-Command dumpbin -ErrorAction SilentlyContinue)) { - Write-Error "dumpbin.exe not on PATH. Run from a Visual Studio developer shell (vcvars64.bat / ilammy/msvc-dev-cmd)." - exit 2 -} - -# DLLs that are part of Windows / the MSVC runtime — never required to -# ship alongside our binaries. Conservative list; matched case-insensitively. -$systemDllPatterns = @( - '^kernel32\.dll$', '^user32\.dll$', '^gdi32\.dll$', '^advapi32\.dll$', - '^shell32\.dll$', '^ole32\.dll$', '^oleaut32\.dll$', '^comctl32\.dll$', - '^comdlg32\.dll$', '^ws2_32\.dll$', '^wsock32\.dll$', '^crypt32\.dll$', - '^bcrypt\.dll$', '^ncrypt\.dll$', '^secur32\.dll$', '^iphlpapi\.dll$', - '^dnsapi\.dll$', '^netapi32\.dll$', '^userenv\.dll$', '^psapi\.dll$', - '^version\.dll$', '^winmm\.dll$', '^winspool\.drv$', '^uxtheme\.dll$', - '^dwmapi\.dll$', '^dbghelp\.dll$', '^imm32\.dll$', '^rpcrt4\.dll$', - '^setupapi\.dll$', '^shlwapi\.dll$', '^urlmon\.dll$', '^wininet\.dll$', - '^d3d9\.dll$', '^d3d11\.dll$', '^d3d12\.dll$', '^dxgi\.dll$', - '^opengl32\.dll$', '^glu32\.dll$', '^msimg32\.dll$', - '^mf\.dll$', '^mfplat\.dll$', '^mfreadwrite\.dll$', - '^msvcp140\.dll$', '^msvcp140_1\.dll$', '^msvcp140_2\.dll$', - '^vcruntime140\.dll$', '^vcruntime140_1\.dll$', - '^concrt140\.dll$', '^vccorlib140\.dll$', - '^ucrtbase\.dll$', '^ucrtbased\.dll$', '^msvcrt\.dll$', - '^api-ms-.*\.dll$', '^ext-ms-.*\.dll$', - '^hvsifiletrust\.dll$', '^pdmutilities\.dll$', - # Windows codecs / WIC - '^windowscodecs\.dll$', '^propsys\.dll$', '^msctf\.dll$', - '^combase\.dll$', '^cfgmgr32\.dll$', - # GPU vendor user-mode drivers loaded by the OS, not by us - '^nvcuda\.dll$', '^nvapi.*\.dll$', - # CUDA driver API (lives with the NVIDIA driver, NOT the toolkit) - '^cuda\.dll$' -) - -function Test-IsSystemDll([string]$name) { - foreach ($pat in $systemDllPatterns) { - if ($name -imatch $pat) { return $true } - } - # Anything that lives in %windir%\System32 (or SysWOW64) is, by - # definition, a Windows system DLL we don't ship. Checking the - # filesystem is more robust than maintaining an exhaustive regex - # list — Microsoft adds new system DLLs (bcp47mrm, TextShaping, - # logoncli, ...) faster than we can enumerate them. - $sysDirs = @( - (Join-Path $env:windir 'System32'), - (Join-Path $env:windir 'SysWOW64') - ) - foreach ($d in $sysDirs) { - if (Test-Path (Join-Path $d $name)) { return $true } - } - return $false -} - -function Get-DependentDlls([string]$path) { - $out = & dumpbin /dependents $path 2>&1 - $deps = @() - $inSection = $false - foreach ($line in $out) { - if ($line -match '^\s*Image has the following dependencies:') { - $inSection = $true - continue - } - if ($inSection) { - if ($line -match '^\s*Summary') { break } - if ($line -match '^\s*([A-Za-z0-9_.+-]+\.dll)\s*$') { - $deps += $Matches[1] - } - } - } - return $deps -} - -$missing = @{} -$visited = New-Object System.Collections.Generic.HashSet[string] -$queue = New-Object System.Collections.Generic.Queue[string] -foreach ($b in $Binaries) { [void]$queue.Enqueue($b) } - -while ($queue.Count -gt 0) { - $current = $queue.Dequeue() - $key = $current.ToLowerInvariant() - if (-not $visited.Add($key)) { continue } - - $full = Join-Path $BinDir $current - if (-not (Test-Path $full)) { - if (-not (Test-IsSystemDll $current)) { - if (-not $missing.ContainsKey($key)) { $missing[$key] = $current } - } - continue - } - - Write-Host " walking $current" - $deps = Get-DependentDlls $full - foreach ($d in $deps) { - $dKey = $d.ToLowerInvariant() - if ($visited.Contains($dKey)) { continue } - if (Test-IsSystemDll $d) { continue } - $bundled = Join-Path $BinDir $d - if (Test-Path $bundled) { - [void]$queue.Enqueue($d) - } else { - if (-not $missing.ContainsKey($dKey)) { $missing[$dKey] = $d } - } - } -} - -if ($missing.Count -gt 0) { - Write-Host "" - Write-Host "::error::Missing non-system DLL dependencies under $BinDir :" - foreach ($name in ($missing.Values | Sort-Object -Unique)) { - Write-Host " - $name" - } - exit 1 -} - -Write-Host "" -Write-Host "OK: all non-system DLL dependencies resolve within $BinDir." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae2d3d64..b09955e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,14 +39,8 @@ on: # - macOS: libcvc---macos--.zip # - Windows: libcvc---windows--.zip # -# volrover3 end-user app (per OS, both Debug + Release): -# - Linux: volrover3---linux--.tar.gz -# volrover3---linux--.deb -# VolumeRover3----.AppImage -# - macOS: VolumeRover3---macos--.dmg -# VolumeRover3---macos--.zip -# - Windows: VolumeRover3---windows--.zip -# VolumeRover3---windows---setup.exe +# (The volrover3 end-user app moved to the volrover repository — +# https://github.com/transfix/volrover — along with its packaging.) # ─────────────────────────────────────────────────────────────────── jobs: @@ -129,12 +123,6 @@ jobs: kind: libcvc build_type: Debug enable_grpc: true - - name: volrover3-debug - kind: volrover3 - build_type: Debug - - name: volrover3-release - kind: volrover3 - build_type: Release name: package-linux / ${{ matrix.name }} # Heavy job (VTK from-source build): do NOT cancel in-progress runs. # Cancellation strands the cache and forces every subsequent run @@ -189,28 +177,12 @@ jobs: method: 'network' sub-packages: '["nvcc", "cudart", "cudart-dev"]' - - name: Install dependencies (volrover3) - if: matrix.kind == 'volrover3' - run: | - sudo apt-get update - # Qt6 + VTK + Boost/HDF5/FFTW/GSL/ImageMagick/CGAL/log4cplus all - # come from cvcpkg via fetch-libcvc-deps below. We only install - # the system runtime deps that Qt/VTK dlopen at runtime - # (OpenGL, X11, GLEW), plus AppImage-tooling deps (libfuse2, - # file, desktop-file-utils). - sudo apt-get install -y --no-install-recommends \ - build-essential cmake ninja-build \ - libgl1-mesa-dev libxt-dev mesa-common-dev libglew-dev \ - libxrender-dev libxcursor-dev libxinerama-dev libxi-dev \ - libfuse2 file desktop-file-utils - # ── Pull dep bundle from libcvc-deps (via cvcpkg) ── - # For volrover3 builds this provides Qt6 + VTK + all third-party - # C/C++ deps in one shot; there is no fallback build path — if + # The bundle provides the `libiimod` CMake package (no longer + # vendored in this tree) plus Boost/HDF5/FFTW/GSL/ImageMagick/ + # CGAL/log4cplus prebuilt. There is no fallback build path — if # cvcpkg cannot fetch the components, CMake configure will fail - # with "Could not find Qt6/VTK", which is the right signal. - # For libcvc-kind builds the bundle also provides the `libiimod` - # CMake package (no longer vendored in this tree). + # with an explicit "Could not find X", which is the right signal. - name: Fetch libcvc-deps id: libcvc-deps uses: ./.github/actions/fetch-libcvc-deps @@ -220,36 +192,21 @@ jobs: - name: Configure run: | - extra_prefix="" - if [ "${{ matrix.kind }}" = "volrover3" ]; then - # Qt6 + VTK + all third-party deps come from cvcpkg via - # fetch-libcvc-deps. No source-build fallback. + # libcvc needs libiimod from the libcvc-deps bundle. Fall + # back to the default cvcpkg-install prefix when the step + # gracefully returned empty (e.g. a single-bundle download + # failed but earlier bundles unpacked into $HOME/libcvc-deps). + if [ -n "${{ steps.libcvc-deps.outputs.path }}" ]; then extra_prefix="${{ steps.libcvc-deps.outputs.path }}" - if [ -z "$extra_prefix" ]; then - extra_prefix="${LIBCVC_DEPS_PREFIX:-$HOME/libcvc-deps}" - fi - elif [ "${{ matrix.kind }}" = "libcvc" ]; then - # libcvc-kind needs libiimod from the libcvc-deps bundle. - # Fall back to the default cvcpkg-install prefix when the - # step gracefully returned empty (e.g. a single-bundle - # download failed but earlier bundles unpacked into - # $HOME/libcvc-deps). - if [ -n "${{ steps.libcvc-deps.outputs.path }}" ]; then - extra_prefix="${{ steps.libcvc-deps.outputs.path }}" - else - extra_prefix="${LIBCVC_DEPS_PREFIX:-$HOME/libcvc-deps}" - fi + else + extra_prefix="${LIBCVC_DEPS_PREFIX:-$HOME/libcvc-deps}" fi - # libcvc-kind builds enable tests so the same artifacts we - # ship get exercised by ctest. volrover3 builds keep tests - # off (its install component has no test targets). - tests=OFF + # Enable tests so the same artifacts we ship get exercised + # by ctest. + tests=ON coverage=OFF - if [ "${{ matrix.kind }}" = "libcvc" ]; then - tests=ON - if [ "${{ matrix.build_type }}" = "Debug" ]; then - coverage=ON - fi + if [ "${{ matrix.build_type }}" = "Debug" ]; then + coverage=ON fi grpc=OFF if [ "${{ matrix.enable_grpc }}" = "true" ]; then @@ -263,7 +220,6 @@ jobs: -DCVC_ENABLE_CUDA=ON \ -DCMAKE_CUDA_RUNTIME_LIBRARY=Static \ -DDISABLE_CGAL=OFF \ - -DCVC_BUILD_VOLROVER3=${{ matrix.kind == 'volrover3' && 'ON' || 'OFF' }} \ -DCVC_ENABLE_MESHER=ON \ -DCVC_ENABLE_SDF=ON \ -DCVC_ENABLE_GRPC=$grpc @@ -341,86 +297,6 @@ jobs: cp "$STEM.tar.gz" "$GITHUB_WORKSPACE/" echo "STEM=$STEM" >> "$GITHUB_ENV" - - name: Build AppImage - if: matrix.kind == 'volrover3' - run: | - set -euo pipefail - STEM="VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}" - DESTDIR="$PWD/AppDir" cmake --install build --prefix /usr --component libcvc - DESTDIR="$PWD/AppDir" cmake --install build --prefix /usr --component volrover3 - curl -L -o linuxdeploy https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage - curl -L -o linuxdeploy-plugin-qt https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage - chmod +x linuxdeploy linuxdeploy-plugin-qt - cp AppDir/usr/share/applications/volrover3.desktop AppDir/ - cp AppDir/usr/share/icons/hicolor/256x256/apps/volrover_logo.png AppDir/volrover_logo.png - export QML_SOURCES_PATHS="" - export OUTPUT="${STEM}.AppImage" - # linuxdeploy walks the volrover3 ELF NEEDED entries and - # resolves them by name; libcvc.so.3 lives under - # AppDir/usr/lib/x86_64-linux-gnu/ (Debian's GNUInstallDirs - # default) which is not on the default search path. Qt6 + VTK - # come from the libcvc-deps prefix populated by cvcpkg. - deps_root="${{ steps.libcvc-deps.outputs.path }}" - if [ -z "$deps_root" ]; then - deps_root="${LIBCVC_DEPS_PREFIX:-$HOME/libcvc-deps}" - fi - vtk_lib_path="$deps_root/lib" - export LD_LIBRARY_PATH="$PWD/AppDir/usr/lib:$PWD/AppDir/usr/lib/x86_64-linux-gnu:$vtk_lib_path:${LD_LIBRARY_PATH:-}" - ./linuxdeploy --appdir AppDir --plugin qt --output appimage \ - --desktop-file AppDir/volrover3.desktop \ - --icon-file AppDir/volrover_logo.png || \ - ./linuxdeploy --appdir AppDir --output appimage \ - --desktop-file AppDir/volrover3.desktop \ - --icon-file AppDir/volrover_logo.png - # Rename the produced AppImage to the canonical STEM only if - # appimagetool didn't already write it under that name. - # (linuxdeploy honors OUTPUT=$STEM.AppImage, so usually it - # is already correct; older versions wrote a generic name.) - for f in *.AppImage; do - case "$f" in - linuxdeploy*) ;; - "${STEM}.AppImage") ;; - *) mv "$f" "${STEM}.AppImage" ;; - esac - done - - - name: Pack volrover3 tarball + .deb - if: matrix.kind == 'volrover3' - run: | - set -euo pipefail - STEM="volrover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}" - # AppDir was populated by linuxdeploy in the previous step and - # contains libcvc.so, Qt6, VTK, and all transitive deps. Pack - # that tree (not the bare cpack volrover3 component, which is - # ~700 KB and useless without the bundled runtime). - tar czf "$STEM.tar.gz" --transform 's|^AppDir|volrover3|' AppDir - - pkg=pkg-deb - rm -rf "$pkg" - mkdir -p "$pkg/opt/volrover3" "$pkg/usr/local/bin" \ - "$pkg/usr/share/applications" "$pkg/DEBIAN" - cp -a AppDir/. "$pkg/opt/volrover3/" - ln -sf /opt/volrover3/AppRun "$pkg/usr/local/bin/volrover3" - if [ -f AppDir/volrover3.desktop ]; then - cp AppDir/volrover3.desktop "$pkg/usr/share/applications/" - fi - arch_deb=$(dpkg --print-architecture) - inst_kb=$(du -sk "$pkg/opt" "$pkg/usr" | awk '{s+=$1} END {print s}') - cat > "$pkg/DEBIAN/control" < - Description: VolumeRover3 - volumetric visualization application - Self-contained build with bundled Qt6, VTK, and libcvc. - CONTROL - # dpkg expects column-1 fields; strip the heredoc indent. - sed -i 's/^ //' "$pkg/DEBIAN/control" - dpkg-deb --build --root-owner-group "$pkg" "$STEM.deb" - # One artifact per file so each shows its real extension on GitHub. - name: Upload Linux libcvc tarball if: matrix.kind == 'libcvc' @@ -431,33 +307,6 @@ jobs: if-no-files-found: error retention-days: 30 - - name: Upload Linux volrover3 tarball - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: volrover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.tar.gz - path: volrover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.tar.gz - if-no-files-found: error - retention-days: 30 - - - name: Upload Linux volrover3 .deb - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: volrover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.deb - path: volrover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.deb - if-no-files-found: error - retention-days: 30 - - - name: Upload Linux volrover3 AppImage - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.AppImage - path: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.AppImage - if-no-files-found: error - retention-days: 30 - # ──────────────────────────── macOS packaging ──────────────────────────── package-macos: runs-on: macos-latest @@ -471,12 +320,6 @@ jobs: - name: libcvc-release kind: libcvc build_type: Release - - name: volrover3-debug - kind: volrover3 - build_type: Debug - - name: volrover3-release - kind: volrover3 - build_type: Release name: package-macos / ${{ matrix.name }} # Heavy job: do not cancel in-progress runs (mirrors package-linux). concurrency: @@ -507,16 +350,9 @@ jobs: # fetch-libcvc-deps below. Only build tooling here. brew install cmake ninja zstd - - name: Install dependencies (volrover3) - if: matrix.kind == 'volrover3' - run: | - # Qt6 + VTK + all third-party C/C++ deps come from cvcpkg via - # fetch-libcvc-deps below. Only build tooling here. - brew install cmake ninja zstd - # ── Pull dep prefix via cvcpkg ── - # Provides Qt6 + VTK + all third-party C/C++ deps for volrover3 - # and the libiimod CMake package for libcvc. + # Provides the libiimod CMake package and all third-party C/C++ + # deps for libcvc. - name: Fetch libcvc-deps id: libcvc-deps-macos uses: ./.github/actions/fetch-libcvc-deps @@ -526,8 +362,6 @@ jobs: - name: Configure run: | - tests=OFF - [ "${{ matrix.kind }}" = "libcvc" ] && tests=ON # cvcpkg is the sole third-party prefix on macOS. deps_path="${{ steps.libcvc-deps-macos.outputs.path }}" if [ -z "$deps_path" ]; then @@ -536,10 +370,9 @@ jobs: export CMAKE_PREFIX_PATH="$deps_path" cmake -B build -G Ninja \ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ - -DCVC_BUILD_TESTS=$tests \ + -DCVC_BUILD_TESTS=ON \ -DCVC_ENABLE_CUDA=OFF \ -DDISABLE_CGAL=OFF \ - -DCVC_BUILD_VOLROVER3=${{ matrix.kind == 'volrover3' && 'ON' || 'OFF' }} \ -DCVC_ENABLE_MESHER=ON \ -DCVC_ENABLE_SDF=ON @@ -573,29 +406,6 @@ jobs: mv "$STEM-libcvc.zip" "$STEM.zip" cp "$STEM.zip" "$GITHUB_WORKSPACE/" - - name: Pack volrover3 .dmg + .zip - if: matrix.kind == 'volrover3' - working-directory: build - run: | - set -euo pipefail - STEM="VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}" - # CPack DragNDrop runs the install rules (which invoke - # macdeployqt) and produces a deployed VolumeRover3.app under - # _CPack_Packages/Darwin/DragNDrop//ALL_IN_ONE/, then - # wraps it into the .dmg. - cpack -G DragNDrop -D CPACK_COMPONENTS_ALL=volrover3 \ - -D CPACK_PACKAGE_FILE_NAME="$STEM" - # Zip the already-deployed .app instead of running cpack -G ZIP - # a second time. Re-running cpack triggers a fresh install + - # macdeployqt pass that hits a component-dependency edge case - # in CPack (volrover3 → libcvc) and aborts during compression. - # `ditto -c -k --keepParent` is the canonical macOS-native zip - # for app bundles (preserves resource forks, symlinks, perms, - # extended attrs, codesign metadata). - APP_DIR="_CPack_Packages/Darwin/DragNDrop/$STEM/ALL_IN_ONE" - ditto -c -k --keepParent "$APP_DIR/VolumeRover3.app" "$STEM.zip" - cp "$STEM.dmg" "$STEM.zip" "$GITHUB_WORKSPACE/" - - name: Upload macOS libcvc zip if: matrix.kind == 'libcvc' uses: actions/upload-artifact@v4 @@ -605,24 +415,6 @@ jobs: if-no-files-found: error retention-days: 30 - - name: Upload macOS volrover3 dmg - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.dmg - path: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.dmg - if-no-files-found: error - retention-days: 30 - - - name: Upload macOS volrover3 zip - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.zip - path: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.zip - if-no-files-found: error - retention-days: 30 - # ──────────────────────────── Windows packaging ──────────────────────────── package-windows: runs-on: windows-latest @@ -636,12 +428,6 @@ jobs: - name: libcvc-release kind: libcvc build_type: Release - - name: volrover3-debug - kind: volrover3 - build_type: Debug - - name: volrover3-release - kind: volrover3 - build_type: Release name: package-windows / ${{ matrix.name }} # Do NOT cancel in-progress runs; see package-linux for rationale. concurrency: @@ -692,8 +478,6 @@ jobs: - name: Configure shell: pwsh run: | - $vol = if ('${{ matrix.kind }}' -eq 'volrover3') { 'ON' } else { 'OFF' } - $tests = if ('${{ matrix.kind }}' -eq 'libcvc') { 'ON' } else { 'OFF' } $depsPath = '${{ steps.libcvc-deps-windows.outputs.path }}' if (-not $depsPath) { # cvcpkg-install fell back; try the default prefix in case @@ -708,11 +492,10 @@ jobs: -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ` -DCMAKE_CONFIGURATION_TYPES=${{ matrix.build_type }} ` -DCMAKE_PREFIX_PATH="$qtPrefix" ` - -DCVC_BUILD_TESTS=$tests ` + -DCVC_BUILD_TESTS=ON ` -DCVC_ENABLE_CUDA=ON ` -DCMAKE_CUDA_RUNTIME_LIBRARY=Static ` -DDISABLE_CGAL=OFF ` - -DCVC_BUILD_VOLROVER3=$vol ` -DCVC_ENABLE_MESHER=ON ` -DCVC_ENABLE_SDF=ON @@ -741,14 +524,10 @@ jobs: # as SEH access violations (0xc0000005) and "unknown C++ # exception" failures inside HDF5/Boost APIs. # - # cvcpkg, however, ships Qt6 only at top-level bin/ - # (Qt6Core.dll AND Qt6Cored.dll co-located there because Qt - # uses a 'd' suffix for Debug). So a Debug build still needs - # bin/ on PATH to resolve Qt6Cored.dll, otherwise Qt-based - # tests (GraphicsNodeTest, VolumeNodeTest, etc.) exit with - # STATUS_DLL_NOT_FOUND (0xc0000135). Put debug/bin FIRST so - # same-named-port DLLs load their debug variant, and bin/ - # second so Qt6's debug-suffixed DLLs still resolve. + # cvcpkg, however, ships some debug-suffixed DLLs only at + # top-level bin/. Put debug/bin FIRST so same-named-port DLLs + # load their debug variant, and bin/ second so debug-suffixed + # DLLs still resolve. run: | $depsPath = '${{ steps.libcvc-deps-windows.outputs.path }}' $isDebug = '${{ matrix.build_type }}' -eq 'Debug' @@ -807,10 +586,6 @@ jobs: -E "$stressRegex" } - - name: Install NSIS - if: matrix.kind == 'volrover3' - run: choco install nsis -y - - name: Pack libcvc zip if: matrix.kind == 'libcvc' shell: pwsh @@ -821,56 +596,6 @@ jobs: Move-Item "$stem-libcvc.zip" "$stem.zip" Copy-Item "$stem.zip" "$env:GITHUB_WORKSPACE/" - - name: Pack volrover3 zip + NSIS - if: matrix.kind == 'volrover3' - shell: pwsh - working-directory: build - run: | - $stem = "VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}" - $nsisPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\NSIS' -ErrorAction SilentlyContinue).'(default)' - if (-not $nsisPath) { - $nsisPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\NSIS' -ErrorAction SilentlyContinue).'(default)' - } - if ($nsisPath) { $env:PATH = "$nsisPath;$env:PATH" } - - # Override CPACK_COMPONENT_VOLROVER3_DEPENDS so cpack does not - # try to package the libcvc component (which has no install - # tree in this volrover3-only build). - cpack -G ZIP -C ${{ matrix.build_type }} -D CPACK_COMPONENTS_ALL=volrover3 -D CPACK_COMPONENT_VOLROVER3_DEPENDS= -D CPACK_PACKAGE_FILE_NAME="$stem" - Move-Item "$stem-volrover3.zip" "$stem.zip" - cpack -G NSIS -C ${{ matrix.build_type }} -D CPACK_COMPONENTS_ALL=volrover3 -D CPACK_COMPONENT_VOLROVER3_DEPENDS= -D CPACK_PACKAGE_FILE_NAME="$stem-setup" - - Copy-Item "$stem.zip" "$env:GITHUB_WORKSPACE/" - Copy-Item "$stem-setup.exe" "$env:GITHUB_WORKSPACE/" - - # Setup MSVC dev shell so dumpbin.exe is on PATH for the - # dependency verification step below. - - name: Activate MSVC dev shell (for dumpbin) - if: matrix.kind == 'volrover3' - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: x64 - - # Verify that the staged volrover3.exe and every DLL it transitively - # depends on are present alongside the binary. Fails CI on any - # missing non-system DLL (cudart64_*.dll, fftw3.dll, libomp140*.dll, - # zlib1.dll, etc.) so we never ship a Windows artifact that crashes - # at launch with a "DLL not found" error. - - name: Verify Windows DLL dependencies (volrover3) - if: matrix.kind == 'volrover3' - shell: pwsh - run: | - $stem = "VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}" - $extract = "$env:GITHUB_WORKSPACE\verify-bin" - if (Test-Path $extract) { Remove-Item -Recurse -Force $extract } - Expand-Archive -Path "$env:GITHUB_WORKSPACE\$stem.zip" -DestinationPath $extract - # CPack ZIP layout: /bin/volrover3.exe (CMAKE_INSTALL_BINDIR=bin) - $bin = Get-ChildItem -Path $extract -Recurse -Filter volrover3.exe -File | Select-Object -First 1 - if (-not $bin) { throw "volrover3.exe not found in extracted zip" } - & "$env:GITHUB_WORKSPACE\.github\scripts\verify-windows-deps.ps1" ` - -BinDir $bin.Directory.FullName ` - -Binaries volrover3.exe - - name: Upload Windows libcvc zip if: matrix.kind == 'libcvc' uses: actions/upload-artifact@v4 @@ -879,21 +604,3 @@ jobs: path: libcvc-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.zip if-no-files-found: error retention-days: 30 - - - name: Upload Windows volrover3 zip - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.zip - path: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}.zip - if-no-files-found: error - retention-days: 30 - - - name: Upload Windows volrover3 NSIS installer - if: matrix.kind == 'volrover3' - uses: actions/upload-artifact@v4 - with: - name: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}-setup.exe - path: VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}-${{ steps.meta.outputs.btlc }}-setup.exe - if-no-files-found: error - retention-days: 30 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8b452275..8f93b50f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,8 +1,8 @@ name: Nightly # Produces the same full artifact matrix as `release.yml` (libcvc SDK in -# debug+release × shared+static for Linux/macOS/Windows, plus the -# volrover3 app) from the current tip of `master` and publishes them to +# debug+release × shared+static for Linux/macOS/Windows) from the +# current tip of `master` and publishes them to # a rolling `nightly` GitHub Release. The rolling tag is force-recreated # on the latest master commit at the start of each publish so consumers # can pin against either `nightly` or the underlying SHA. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e94a30d9..f7e37ce2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,14 +52,8 @@ permissions: # System libraries (glibc, libstdc++, libm, Windows UCRT) are # intentionally left to the host. # -# volrover3 (end-user application, Release only): -# - Linux: volrover3--linux-.tar.gz (portable) -# volrover3--linux-.deb (Debian/Ubuntu) -# VolumeRover3--.AppImage (universal Linux) -# - macOS: VolumeRover3--.dmg (drag to /Applications) -# VolumeRover3--macos-.zip (alt. archive) -# - Windows: VolumeRover3--windows-.zip (portable) -# VolumeRover3--windows--setup.exe (NSIS installer) +# (The volrover3 end-user application moved to the volrover repository — +# https://github.com/transfix/volrover — along with its packaging.) # # Tag-driven release artifacts intentionally do not include the commit SHA # in their filenames; CI build artifacts (ci.yml) do include the SHA so @@ -91,11 +85,6 @@ jobs: kind: libcvc build_type: Release link: static - # volrover3 end-user app (Release only, shared only) - - name: volrover3 - kind: volrover3 - build_type: Release - link: shared steps: - uses: actions/checkout@v4 with: @@ -146,37 +135,18 @@ jobs: method: 'network' sub-packages: '["nvcc", "cudart", "cudart-dev"]' - - name: Install dependencies (volrover3) - if: matrix.kind == 'volrover3' - run: | - sudo apt-get update - # Qt6 + VTK + all third-party C/C++ deps come from cvcpkg via - # fetch-libcvc-deps below. Only install system runtime libs - # that Qt/VTK dlopen at runtime (OpenGL/X11/GLEW) and the - # AppImage tooling. - sudo apt-get install -y --no-install-recommends \ - build-essential cmake ninja-build \ - libgl1-mesa-dev libxt-dev mesa-common-dev libglew-dev \ - libxrender-dev libxcursor-dev libxinerama-dev libxi-dev \ - libfuse2 file desktop-file-utils - # ── Pull deps from cvcpkg (via libcvc-deps composite action) ── - # libcvc kind: required for libiimod (no longer vendored, see - # PR #63), in addition to providing Boost/HDF5/FFTW/GSL/CGAL/ - # log4cplus/ImageMagick prebuilt. - # volrover3 kind: also provides Qt6 + VTK for the app build. - # On miss the action emits an empty `path` output; both kinds - # then fail Configure with an explicit "Could not find X" error, - # which is the correct signal (no in-workflow source-build - # fallback — cvcpkg is the sole source of truth). + # Required for libiimod (no longer vendored, see PR #63), in + # addition to providing Boost/HDF5/FFTW/GSL/CGAL/log4cplus/ + # ImageMagick prebuilt. On miss the action emits an empty `path` + # output; Configure then fails with an explicit "Could not find + # X" error, which is the correct signal (no in-workflow + # source-build fallback — cvcpkg is the sole source of truth). - name: Fetch libcvc-deps id: libcvc-deps uses: ./.github/actions/fetch-libcvc-deps with: - # libcvc-deps Linux ships per-build_type archives; libcvc - # jobs pull the matching debug/release flavor, volrover3 - # is Release-only. - build_type: ${{ matrix.kind == 'volrover3' && 'Release' || matrix.build_type }} + build_type: ${{ matrix.build_type }} link: shared - name: Configure @@ -204,11 +174,10 @@ jobs: -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ $static_deps \ -DCVC_BUILD_TESTS=OFF \ - -DCVC_USING_XMLRPC=${{ matrix.kind == 'libcvc' && 'ON' || 'OFF' }} \ + -DCVC_USING_XMLRPC=ON \ -DCVC_ENABLE_CUDA=ON \ -DCMAKE_CUDA_RUNTIME_LIBRARY=Static \ -DDISABLE_CGAL=OFF \ - -DCVC_BUILD_VOLROVER3=${{ matrix.kind == 'volrover3' && 'ON' || 'OFF' }} \ -DCVC_ENABLE_MESHER=ON \ -DCVC_ENABLE_SDF=ON @@ -300,92 +269,17 @@ jobs: # Archive already lives in $GITHUB_WORKSPACE ($PWD); no copy needed. du -sh "$DIST" "$STEM.tar.gz" || true - # ── volrover3 portable tarball + .deb + AppImage ──────────── - - name: Pack volrover3 (tarball + .deb) - if: matrix.kind == 'volrover3' - working-directory: build - run: | - STEM="volrover3-${{ steps.meta.outputs.version }}-linux-${{ steps.meta.outputs.arch }}" - # Override CPACK_COMPONENT_VOLROVER3_DEPENDS so cpack does not - # try to package the libcvc component as well — with - # CPACK_ARCHIVE_COMPONENT_INSTALL=ON / CPACK_DEB_COMPONENT_INSTALL=ON, - # cpack iterates dependencies and aborts when no libcvc - # install tree exists for this volrover3-only build. - cpack -G TGZ -D CPACK_COMPONENTS_ALL=volrover3 \ - -D CPACK_COMPONENT_VOLROVER3_DEPENDS= \ - -D CPACK_PACKAGE_FILE_NAME="$STEM" - mv "$STEM-volrover3.tar.gz" "$STEM.tar.gz" - cp "$STEM.tar.gz" "$GITHUB_WORKSPACE/" - cpack -G DEB -D CPACK_COMPONENTS_ALL=volrover3 \ - -D CPACK_COMPONENT_VOLROVER3_DEPENDS= \ - -D CPACK_DEBIAN_PACKAGE_SHLIBDEPS=OFF \ - -D CPACK_PACKAGE_FILE_NAME="$STEM" - # CPack DEB names follow Debian convention; rename whatever - # .deb landed to match our naming scheme. - for f in volrover3*.deb; do [ -f "$f" ] && cp "$f" "$GITHUB_WORKSPACE/$STEM.deb"; done - - - name: Build AppImage - if: matrix.kind == 'volrover3' - run: | - set -euo pipefail - STEM="VolumeRover3-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.arch }}" - # Stage install tree - DESTDIR="$PWD/AppDir" cmake --install build --prefix /usr --component libcvc - DESTDIR="$PWD/AppDir" cmake --install build --prefix /usr --component volrover3 - # Fetch linuxdeploy + Qt plugin (pinned to known-good releases). - curl -L -o linuxdeploy https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage - curl -L -o linuxdeploy-plugin-qt https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage - chmod +x linuxdeploy linuxdeploy-plugin-qt - # linuxdeploy expects icon + .desktop at the AppDir root; mirror. - cp AppDir/usr/share/applications/volrover3.desktop AppDir/ - cp AppDir/usr/share/icons/hicolor/256x256/apps/volrover_logo.png AppDir/volrover_logo.png - export QML_SOURCES_PATHS="" - export OUTPUT="${STEM}.AppImage" - # linuxdeploy walks the volrover3 ELF NEEDED entries and - # resolves them by name; libcvc.so.3 lives under - # AppDir/usr/lib/x86_64-linux-gnu/ (Debian's GNUInstallDirs - # default) which is not on the default search path. Qt6 + VTK - # live under the cvcpkg-populated libcvc-deps prefix. - deps_root="${{ steps.libcvc-deps.outputs.path }}" - if [ -z "$deps_root" ]; then - deps_root="${LIBCVC_DEPS_PREFIX:-$HOME/libcvc-deps}" - fi - vtk_lib_path="$deps_root/lib" - export LD_LIBRARY_PATH="$PWD/AppDir/usr/lib:$PWD/AppDir/usr/lib/x86_64-linux-gnu:$vtk_lib_path:${LD_LIBRARY_PATH:-}" - ./linuxdeploy --appdir AppDir --plugin qt --output appimage \ - --desktop-file AppDir/volrover3.desktop \ - --icon-file AppDir/volrover_logo.png || \ - ./linuxdeploy --appdir AppDir --output appimage \ - --desktop-file AppDir/volrover3.desktop \ - --icon-file AppDir/volrover_logo.png - # linuxdeploy emits "VolumeRover3-x86_64.AppImage" by default if - # Name= matches; rename to our STEM for consistency. Skip files - # that are already named correctly (otherwise `mv` fails with - # "are the same file" because OUTPUT was set to ${STEM}.AppImage). - for f in *.AppImage; do - case "$f" in - linuxdeploy*) ;; - "${STEM}.AppImage") ;; - *) mv "$f" "${STEM}.AppImage" ;; - esac - done - ls -la *.AppImage - - # Explicit prefixes (libcvc-, volrover3-, VolumeRover3-) instead of - # bare *.deb / *.tar.gz / *.AppImage globs. The Jimver/cuda-toolkit - # action drops cuda_keyring.deb into $GITHUB_WORKSPACE while - # installing the toolkit, and a permissive *.deb glob sweeps that - # stray NVIDIA apt-keyring package into the release. See v3.1.1 - # release where cuda_keyring.deb had to be manually removed. + # Explicit libcvc- prefix instead of bare *.tar.gz globs. The + # Jimver/cuda-toolkit action drops cuda_keyring.deb into + # $GITHUB_WORKSPACE while installing the toolkit, and a permissive + # glob sweeps stray files into the release. See v3.1.1 release + # where cuda_keyring.deb had to be manually removed. - name: Upload Linux artifacts uses: actions/upload-artifact@v4 with: name: linux-${{ matrix.name }} path: | libcvc-*.tar.gz - volrover3-*.tar.gz - volrover3-*.deb - VolumeRover3-*.AppImage if-no-files-found: error # ──────────────────────────── macOS ──────────────────────────── @@ -411,10 +305,6 @@ jobs: kind: libcvc build_type: Release link: static - - name: volrover3 - kind: volrover3 - build_type: Release - link: shared steps: - uses: actions/checkout@v4 with: @@ -447,20 +337,13 @@ jobs: # dylibbundler is installed later inside the stage step. brew install cmake ninja zstd - - name: Install dependencies (volrover3) - if: matrix.kind == 'volrover3' - run: | - # Qt6 + VTK + all third-party C/C++ deps come from cvcpkg via - # fetch-libcvc-deps below. Only build tooling here. - brew install cmake ninja zstd - - # ── Pull deps (incl. Qt6/VTK/libiimod) from cvcpkg ── + # ── Pull deps (incl. libiimod) from cvcpkg ── # cvcpkg is the sole third-party provider on macOS. - name: Fetch libcvc-deps id: libcvc-deps uses: ./.github/actions/fetch-libcvc-deps with: - build_type: ${{ matrix.kind == 'volrover3' && 'Release' || matrix.build_type }} + build_type: ${{ matrix.build_type }} link: shared - name: Configure @@ -491,10 +374,9 @@ jobs: -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ $static_deps \ -DCVC_BUILD_TESTS=OFF \ - -DCVC_USING_XMLRPC=${{ matrix.kind == 'libcvc' && 'ON' || 'OFF' }} \ + -DCVC_USING_XMLRPC=ON \ -DCVC_ENABLE_CUDA=OFF \ -DDISABLE_CGAL=OFF \ - -DCVC_BUILD_VOLROVER3=${{ matrix.kind == 'volrover3' && 'ON' || 'OFF' }} \ -DCVC_ENABLE_MESHER=ON \ -DCVC_ENABLE_SDF=ON @@ -562,34 +444,12 @@ jobs: # Archive already lives in $GITHUB_WORKSPACE ($PWD); no copy needed. du -sh "$DIST" "$STEM.zip" || true - - name: Pack volrover3 (.dmg + .zip) - if: matrix.kind == 'volrover3' - working-directory: build - run: | - set -euo pipefail - STEM="VolumeRover3-${{ steps.meta.outputs.version }}-macos-${{ steps.meta.outputs.arch }}" - # macdeployqt runs during `cmake --install volrover3`; CPack - # will trigger that, leaving a self-contained VolumeRover3.app - # inside the staged tree before generating .dmg. - cpack -G DragNDrop -D CPACK_COMPONENTS_ALL=volrover3 \ - -D CPACK_PACKAGE_FILE_NAME="$STEM" - # Zip the deployed .app directly with ditto rather than running - # cpack -G ZIP a second time — the second cpack pass triggers a - # fresh install/macdeployqt cycle that hits a CPack component - # dependency edge case (volrover3 → libcvc) and aborts during - # compression. - APP_DIR="_CPack_Packages/Darwin/DragNDrop/$STEM/ALL_IN_ONE" - ditto -c -k --keepParent "$APP_DIR/VolumeRover3.app" "$STEM.zip" - cp "$STEM.dmg" "$STEM.zip" "$GITHUB_WORKSPACE/" - - name: Upload macOS artifacts uses: actions/upload-artifact@v4 with: name: macos-${{ matrix.name }} path: | libcvc-*.zip - VolumeRover3-*.zip - VolumeRover3-*.dmg if-no-files-found: error # ──────────────────────────── Windows ──────────────────────────── @@ -615,10 +475,6 @@ jobs: kind: libcvc build_type: Release link: static - - name: volrover3 - kind: volrover3 - build_type: Release - link: shared steps: - uses: actions/checkout@v4 with: @@ -643,7 +499,7 @@ jobs: echo "btlc=$BTLC" >> "$GITHUB_OUTPUT" echo "linksfx=$LINKSFX" >> "$GITHUB_OUTPUT" - # ── Pull all third-party deps (incl. Qt6 + VTK) from cvcpkg ── + # ── Pull all third-party deps from cvcpkg ── # cvcpkg is the sole source of truth for third-party components. # On miss (network blip, catalog outage) CMake configure fails # with an explicit "Could not find X" error — there is no vcpkg @@ -676,10 +532,7 @@ jobs: - name: Configure shell: pwsh run: | - $vol = if ('${{ matrix.kind }}' -eq 'volrover3') { 'ON' } else { 'OFF' } - # CGAL/SDF/Mesher are always built; only volrover3 toggles - # the GUI app and its extra deps. - $xmlrpc = if ('${{ matrix.kind }}' -eq 'libcvc') { 'ON' } else { 'OFF' } + $xmlrpc = 'ON' $depsPath = '${{ steps.libcvc-deps-windows.outputs.path }}' if (-not $depsPath) { $depsPath = if ($env:LIBCVC_DEPS_PREFIX) { $env:LIBCVC_DEPS_PREFIX } else { 'D:\libcvc-deps' } @@ -712,17 +565,12 @@ jobs: -DCVC_ENABLE_CUDA=ON ` -DCMAKE_CUDA_RUNTIME_LIBRARY=Static ` -DDISABLE_CGAL=OFF ` - -DCVC_BUILD_VOLROVER3=$vol ` -DCVC_ENABLE_MESHER=ON ` -DCVC_ENABLE_SDF=ON - name: Build run: cmake --build build --config ${{ matrix.build_type }} --parallel - - name: Install NSIS - if: matrix.kind == 'volrover3' - run: choco install nsis -y - - name: Stage libcvc + bundle deps if: matrix.kind == 'libcvc' shell: pwsh @@ -805,60 +653,12 @@ jobs: "$stem staged at $dist" | Write-Host Get-ChildItem "$env:GITHUB_WORKSPACE/$stem.zip" | Format-List Name,Length - - name: Pack volrover3 (zip + NSIS) - if: matrix.kind == 'volrover3' - shell: pwsh - working-directory: build - run: | - $stem = "VolumeRover3-${{ steps.meta.outputs.version }}-windows-${{ steps.meta.outputs.arch }}" - $nsisPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\NSIS' -ErrorAction SilentlyContinue).'(default)' - if (-not $nsisPath) { - $nsisPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\NSIS' -ErrorAction SilentlyContinue).'(default)' - } - if ($nsisPath) { $env:PATH = "$nsisPath;$env:PATH" } - - cpack -G ZIP -C ${{ matrix.build_type }} -D CPACK_COMPONENTS_ALL=volrover3 -D CPACK_COMPONENT_VOLROVER3_DEPENDS= -D CPACK_PACKAGE_FILE_NAME="$stem" - Move-Item "$stem-volrover3.zip" "$stem.zip" - # NSIS installs all components selected; we want only volrover3. - # The NSIS exe name is taken directly from CPACK_PACKAGE_FILE_NAME. - cpack -G NSIS -C ${{ matrix.build_type }} -D CPACK_COMPONENTS_ALL=volrover3 -D CPACK_COMPONENT_VOLROVER3_DEPENDS= -D CPACK_PACKAGE_FILE_NAME="$stem-setup" - - Copy-Item "$stem.zip" "$env:GITHUB_WORKSPACE/" - Copy-Item "$stem-setup.exe" "$env:GITHUB_WORKSPACE/" - - - name: Activate MSVC dev shell (for dumpbin) - if: matrix.kind == 'volrover3' - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: x64 - - # Refuse to ship a release artifact that would fail to launch on - # a vanilla Windows machine due to a missing DLL (cudart, fftw3, - # libomp140.x86_64, zlib, ...). dumpbin walks the EXE's PE imports - # transitively and the verification script asserts every non-system - # dependency is present in the same directory. - - name: Verify Windows DLL dependencies (volrover3) - if: matrix.kind == 'volrover3' - shell: pwsh - run: | - $stem = "VolumeRover3-${{ steps.meta.outputs.version }}-windows-${{ steps.meta.outputs.arch }}" - $extract = "$env:GITHUB_WORKSPACE\verify-bin" - if (Test-Path $extract) { Remove-Item -Recurse -Force $extract } - Expand-Archive -Path "$env:GITHUB_WORKSPACE\$stem.zip" -DestinationPath $extract - $bin = Get-ChildItem -Path $extract -Recurse -Filter volrover3.exe -File | Select-Object -First 1 - if (-not $bin) { throw "volrover3.exe not found in extracted zip" } - & "$env:GITHUB_WORKSPACE\.github\scripts\verify-windows-deps.ps1" ` - -BinDir $bin.Directory.FullName ` - -Binaries volrover3.exe - - name: Upload Windows artifacts uses: actions/upload-artifact@v4 with: name: windows-${{ matrix.name }} path: | libcvc-*.zip - VolumeRover3-*.zip - VolumeRover3-*-setup.exe if-no-files-found: error # ──────────────────────────── Release ──────────────────────────── @@ -910,9 +710,3 @@ jobs: files: | artifacts/libcvc-*.tar.gz artifacts/libcvc-*.zip - artifacts/volrover3-*.tar.gz - artifacts/volrover3-*.deb - artifacts/VolumeRover3-*.zip - artifacts/VolumeRover3-*.dmg - artifacts/VolumeRover3-*-setup.exe - artifacts/VolumeRover3-*.AppImage diff --git a/CMake/Info.plist.in b/CMake/Info.plist.in deleted file mode 100644 index a55fc0b2..00000000 --- a/CMake/Info.plist.in +++ /dev/null @@ -1,42 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en-US - CFBundleExecutable - ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleGetInfoString - ${MACOSX_BUNDLE_INFO_STRING} - CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} - CFBundleIdentifier - ${MACOSX_BUNDLE_GUI_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLongVersionString - ${MACOSX_BUNDLE_LONG_VERSION_STRING} - CFBundleName - ${MACOSX_BUNDLE_BUNDLE_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - ${MACOSX_BUNDLE_SHORT_VERSION_STRING} - CFBundleSignature - ???? - CFBundleVersion - ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - NSHighResolutionCapable - - NSHumanReadableCopyright - ${MACOSX_BUNDLE_COPYRIGHT} - NSPrincipalClass - NSApplication - NSRequiresAquaSystemAppearance - - LSMinimumSystemVersion - 11.0 - - diff --git a/CMakeLists.txt b/CMakeLists.txt index 97e8b471..92ed7111 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,29 +275,18 @@ install(FILES README.md USAGE.md LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR} COMPONENT libcvc) -# Application icons (used by both libcvc and volrover3 components on -# Linux for desktop integration; on macOS/Windows the icon is embedded -# into the app bundle / EXE rather than installed standalone). -if(NOT WIN32 AND NOT APPLE) - install(FILES share/icons/volrover_logo.png - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps - RENAME volrover_logo.png - COMPONENT volrover3 - OPTIONAL) -endif() - # ************* CPack configuration ************* include(GNUInstallDirs) # Ship Microsoft's MSVC CRT redist AND the LLVM OpenMP runtime # (libomp140.x86_64.dll) alongside our binaries. Without # CMAKE_INSTALL_OPENMP_LIBRARIES the OpenMP DLL is not bundled and -# volrover3.exe / cvc.dll fail to launch on a vanilla Windows machine +# cvc.dll / cvc.exe fail to launch on a vanilla Windows machine # with "libomp140.x86_64.dll was not found". set(CMAKE_INSTALL_OPENMP_LIBRARIES TRUE) # InstallRequiredSystemLibraries installs into bin/ by default, which -# is what we want next to volrover3.exe. -set(CMAKE_INSTALL_SYSTEM_RUNTIME_COMPONENT volrover3) +# is what we want next to cvc.exe / cvc.dll. +set(CMAKE_INSTALL_SYSTEM_RUNTIME_COMPONENT libcvc) include(InstallRequiredSystemLibraries) # Compute architecture tag for archive file names. @@ -324,7 +313,7 @@ endif() # Per-platform generator selection happens at cpack time via -G; the # CMakeLists declares all viable generators so the user can pick. if(WIN32) - set(CPACK_GENERATOR "NSIS;ZIP") + set(CPACK_GENERATOR "ZIP") elseif(APPLE) set(CPACK_GENERATOR "DragNDrop;ZIP") else() @@ -351,57 +340,26 @@ set(CPACK_STRIP_FILES ON) set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${_CVC_OS}-${_CVC_ARCH}-${_CVC_BUILD_LC}") -# Component-based packaging: separate libcvc and volrover3 artifacts -set(CPACK_COMPONENTS_ALL libcvc volrover3) +# Component-based packaging: the libcvc SDK is the only component. +# (The volrover3 end-user application moved to the volrover repository: +# https://github.com/transfix/volrover) +set(CPACK_COMPONENTS_ALL libcvc) set(CPACK_COMPONENT_LIBCVC_DISPLAY_NAME "libcvc Library") set(CPACK_COMPONENT_LIBCVC_DESCRIPTION "CVC library, headers, and CMake config files") set(CPACK_COMPONENT_LIBCVC_REQUIRED ON) -set(CPACK_COMPONENT_VOLROVER3_DISPLAY_NAME "VolumeRover3 Application") -set(CPACK_COMPONENT_VOLROVER3_DESCRIPTION "VolumeRover3 desktop visualization application") -set(CPACK_COMPONENT_VOLROVER3_DEPENDS libcvc) -# Archive component packaging (TGZ, ZIP) — put each component in its own -# archive so libcvc-*.tar.gz and volrover3-*.tar.gz are produced separately -# even when both are built in the same install tree. +# Archive component packaging (TGZ, ZIP) — keep component-style archive +# naming so libcvc-*.tar.gz continues to be produced the same way CI +# expects (cpack -D CPACK_COMPONENTS_ALL=libcvc). set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) -# DEB component packaging (Linux): produce libcvc and volrover3 .deb files. +# DEB component packaging (Linux): produce the libcvc .deb. set(CPACK_DEB_COMPONENT_INSTALL ON) set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Joe Rivera") set(CPACK_DEBIAN_PACKAGE_SECTION "libs") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_LIBCVC_PACKAGE_NAME "libcvc") set(CPACK_DEBIAN_LIBCVC_PACKAGE_SECTION "libs") -set(CPACK_DEBIAN_VOLROVER3_PACKAGE_NAME "volrover3") -set(CPACK_DEBIAN_VOLROVER3_PACKAGE_SECTION "graphics") -set(CPACK_DEBIAN_VOLROVER3_PACKAGE_DEPENDS "libcvc") - -# NSIS settings (Windows volrover3 installer) -# Provide a desktop / start-menu entry, branded icons, and modify-PATH. -set(CPACK_NSIS_DISPLAY_NAME "VolumeRover3 ${PROJECT_VERSION}") -set(CPACK_NSIS_PACKAGE_NAME "VolumeRover3") -set(CPACK_NSIS_HELP_LINK "https://github.com/transfix/libcvc") -set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/transfix/libcvc") -set(CPACK_NSIS_CONTACT "transfix@ices.utexas.edu") -set(CPACK_NSIS_MODIFY_PATH ON) -set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) -set(CPACK_NSIS_MUI_FINISHPAGE_RUN "volrover3.exe") -# Branded icons. The .ico file ships in share/icons/. -if(EXISTS "${PROJECT_SOURCE_DIR}/share/icons/volrover_logo.ico") - set(CPACK_NSIS_MUI_ICON - "${PROJECT_SOURCE_DIR}/share/icons/volrover_logo.ico") - set(CPACK_NSIS_MUI_UNIICON - "${PROJECT_SOURCE_DIR}/share/icons/volrover_logo.ico") - set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\volrover3.exe") -endif() -# CPACK_PACKAGE_EXECUTABLES creates Start-Menu entries automatically: -# pairs of (binary-name-without-ext, display-name). Setting it here means -# the NSIS installer offers a Start-Menu shortcut to volrover3. -set(CPACK_PACKAGE_EXECUTABLES - "volrover3" "VolumeRover3" -) -# CPACK_CREATE_DESKTOP_LINKS adds a Desktop shortcut for the same exe. -set(CPACK_CREATE_DESKTOP_LINKS volrover3) include(CPack) @@ -411,7 +369,3 @@ cpack_add_component(libcvc DISPLAY_NAME "libcvc Library" DESCRIPTION "CVC library, headers, and CMake config files" REQUIRED) -cpack_add_component(volrover3 - DISPLAY_NAME "VolumeRover3 Application" - DESCRIPTION "VolumeRover3 desktop visualization application" - DEPENDS libcvc) diff --git a/build_volrover3.sh b/build_volrover3.sh deleted file mode 100755 index 4fa043cd..00000000 --- a/build_volrover3.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -# Build script for VolRover3 application - -set -e # Exit on error - -echo "================================" -echo "VolRover3 Build Script" -echo "================================" -echo "" - -# Check for Qt6 -echo "Checking for Qt6..." -if pkg-config --exists Qt6Core Qt6Widgets Qt6OpenGL Qt6OpenGLWidgets 2>/dev/null; then - echo "✓ Qt6 found" - pkg-config --modversion Qt6Core -else - echo "✗ Qt6 not found" - echo " Install with: sudo apt-get install qt6-base-dev qt6-opengl-dev" - echo " Or on macOS: brew install qt@6" -fi -echo "" - -# Check for VTK -echo "Checking for VTK..." -if pkg-config --exists vtk 2>/dev/null; then - echo "✓ VTK found" - pkg-config --modversion vtk -elif [ -d "/usr/local/lib/cmake/vtk-9.0" ] || [ -d "/usr/local/lib/cmake/vtk-9.1" ] || [ -d "/usr/local/lib/cmake/vtk-9.2" ]; then - echo "✓ VTK found (CMake installation)" -else - echo "✗ VTK not found" - echo " Install with: sudo apt-get install libvtk9-dev" - echo " Or on macOS: brew install vtk" -fi -echo "" - -# Build -echo "Building libcvc with VolRover3..." -echo "" - -cd "$(dirname "$0")" - -if [ ! -d "build" ]; then - mkdir build -fi - -cd build - -cmake .. \ - -DCMAKE_BUILD_TYPE=Release \ - -DCVC_BUILD_VOLROVER3=ON \ - -DCVC_BUILD_TESTS=OFF - -echo "" -echo "Building..." -make volrover3 -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - -echo "" -echo "================================" -echo "Build complete!" -echo "================================" -echo "" -echo "Run with: ./build/bin/volrover3" -echo "" diff --git a/cvc-requirements.yaml b/cvc-requirements.yaml index 48a197b8..8c976aaf 100644 --- a/cvc-requirements.yaml +++ b/cvc-requirements.yaml @@ -33,6 +33,4 @@ components: - abseil - protobuf - grpc - - qt6 - - vtk - pthreads4w diff --git a/cvcpkg/recipes/libcvc/build.ps1 b/cvcpkg/recipes/libcvc/build.ps1 index d88d2d06..a8c3c85a 100644 --- a/cvcpkg/recipes/libcvc/build.ps1 +++ b/cvcpkg/recipes/libcvc/build.ps1 @@ -19,7 +19,6 @@ $args = @( "-DCMAKE_BUILD_TYPE=$cmakeBuildType", "-DBUILD_SHARED_LIBS=$buildSharedLibs", '-DCVC_BUILD_TESTS=OFF', - '-DCVC_BUILD_VOLROVER3=OFF', '-DCVC_ENABLE_CUDA=OFF', '-DCVC_ENABLE_GRPC=OFF' ) diff --git a/cvcpkg/recipes/libcvc/build.sh b/cvcpkg/recipes/libcvc/build.sh index 0a3ab871..9a816135 100755 --- a/cvcpkg/recipes/libcvc/build.sh +++ b/cvcpkg/recipes/libcvc/build.sh @@ -29,7 +29,6 @@ CMAKE_ARGS=( -DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE" -DBUILD_SHARED_LIBS="$BUILD_SHARED_LIBS" -DCVC_BUILD_TESTS=OFF - -DCVC_BUILD_VOLROVER3=OFF -DCVC_ENABLE_CUDA=OFF -DCVC_ENABLE_GRPC=OFF ) diff --git a/cvcpkg/recipes/libcvc/recipe.yaml b/cvcpkg/recipes/libcvc/recipe.yaml index 7b67a19e..a4aa64a4 100644 --- a/cvcpkg/recipes/libcvc/recipe.yaml +++ b/cvcpkg/recipes/libcvc/recipe.yaml @@ -43,8 +43,6 @@ depends: - name: abseil - name: protobuf - name: grpc - - name: qt6 - - name: vtk - name: pthreads4w platforms: [windows] host_tools: @@ -69,6 +67,7 @@ package: - lib/cvc* - lib/cmake/cvc/ - include/cvc/ + - include/xmlrpc/ cmake_packages: - name: cvc targets: diff --git a/docs/APPSTATE_CALLBACKS.md b/docs/APPSTATE_CALLBACKS.md deleted file mode 100644 index 6472feca..00000000 --- a/docs/APPSTATE_CALLBACKS.md +++ /dev/null @@ -1,142 +0,0 @@ -# AppState Callback System - -## Overview - -The `AppState` class provides a reactive state management system with callback notifications. All callback registration methods now return `boost::signals2::connection` objects that can be used to disconnect callbacks when they are no longer needed. - -## Basic Usage - -### Registering a Callback - -```cpp -#include - -// Get the singleton instance -AppState& state = AppState::instance(); - -// Register a callback that fires when camera changes -auto connection = state.onCameraChanged([]() { - std::cout << "Camera changed!" << std::endl; -}); - -// Make a change that triggers the callback -state.setCameraPosition(1.0, 2.0, 3.0); -``` - -### Disconnecting a Callback - -```cpp -// Disconnect when no longer needed -connection.disconnect(); - -// Further changes won't trigger the callback -state.setCameraPosition(4.0, 5.0, 6.0); // No output -``` - -## Available Callbacks - -All callback registration methods follow the same pattern: they accept a `boost::function` callback and return a `boost::signals2::connection`. - -### Geometry and Volume - -- `onGeometryChanged()` - Fires when geometry is updated -- `onVolumeChanged()` - Fires when volume data is updated -- `onWorldBoundsChanged()` - Fires when world bounding box changes - -### Visibility States - -- `onGridVisibilityChanged()` - Fires when grid visibility toggles -- `onAxisVisibilityChanged()` - Fires when axis visibility toggles -- `onGeometryBBoxVisibilityChanged()` - Fires when geometry bbox visibility changes -- `onVolumeBBoxVisibilityChanged()` - Fires when volume bbox visibility changes - -### Camera - -- `onCameraChanged()` - Fires when camera position, direction, up vector, or FOV changes -- `onCameraModeChanged()` - Fires when camera mode switches (orbit/fly) - -### Transfer Function - -- `onTransferFunctionChanged()` - Fires when transfer function is modified - -## Object Lifecycle Management - -The connection object follows RAII principles and should be stored as a member variable in classes that need to manage callback lifetime: - -```cpp -class MyRenderer { -public: - MyRenderer() { - AppState& state = AppState::instance(); - - // Register callbacks - cameraConnection_ = state.onCameraChanged([this]() { - updateCameraMatrices(); - }); - - volumeConnection_ = state.onVolumeChanged([this]() { - reloadVolumeData(); - }); - } - - ~MyRenderer() { - // Connections are automatically disconnected when destroyed - // But can also disconnect explicitly if needed - cameraConnection_.disconnect(); - volumeConnection_.disconnect(); - } - -private: - boost::signals2::connection cameraConnection_; - boost::signals2::connection volumeConnection_; - - void updateCameraMatrices() { /* ... */ } - void reloadVolumeData() { /* ... */ } -}; -``` - -## Multiple Callbacks - -Multiple callbacks can be registered for the same state change: - -```cpp -auto conn1 = state.onCameraChanged([]() { - std::cout << "Callback 1" << std::endl; -}); - -auto conn2 = state.onCameraChanged([]() { - std::cout << "Callback 2" << std::endl; -}); - -// Both callbacks will fire -state.setCameraPosition(1.0, 2.0, 3.0); - -// Disconnect only one -conn1.disconnect(); - -// Only callback 2 will fire now -state.setCameraPosition(4.0, 5.0, 6.0); -``` - -## Accessing State Values in Callbacks - -Callbacks can access the updated state values: - -```cpp -auto connection = state.onCameraChanged([&state]() { - double x, y, z; - state.getCameraPosition(x, y, z); - std::cout << "New position: " << x << ", " << y << ", " << z << std::endl; -}); -``` - -## Thread Safety - -The underlying `cvc::state` system is thread-safe. Callbacks are triggered synchronously on the thread that modifies the state. - -## Implementation Notes - -- Callbacks are implemented using Boost.Signals2 -- State changes only trigger callbacks when values actually change (setting the same value twice won't fire the callback) -- The connection object can be safely copied; all copies refer to the same connection -- Disconnecting is idempotent - calling `disconnect()` multiple times is safe diff --git a/docs/GRAPHICS_DATA_DRIVEN_UPDATES.md b/docs/GRAPHICS_DATA_DRIVEN_UPDATES.md deleted file mode 100644 index be095b21..00000000 --- a/docs/GRAPHICS_DATA_DRIVEN_UPDATES.md +++ /dev/null @@ -1,173 +0,0 @@ -# GraphicsNode Data-Driven Updates - -## Overview - -GraphicsNode now automatically monitors and responds to changes in the state tree's data field. When the geometry data in the state tree is modified, the GraphicsNode will: - -1. **Reload the geometry** from the state data -2. **Update VTK rendering** to display the new geometry -3. **Recalculate all metadata** to reflect the new geometry's properties -4. **Trigger a redraw** to show the updated visualization - -## Implementation Details - -### State Data Connection - -When `syncFromState()` is called, the GraphicsNode: -- Stores a pointer to its state node (`m_stateNode`) -- Connects to the `dataChanged` signal -- Loads initial geometry from state data if available - -```cpp -m_dataConnection = myState.dataChanged.connect([this]() { - onDataChanged(); -}); -``` - -### Data Change Handler - -The `onDataChanged()` callback is triggered when state data changes: - -```cpp -void GraphicsNode::onDataChanged() { - // 1. Load geometry from state data - const cvc::geometry& geom = boost::any_cast(m_stateNode->data()); - - // 2. Update VTK rendering - updatePolyData(geom); - - // 3. Recalculate metadata - updateMetadata(geom); - - // 4. Sync metadata back to state tree (marked read-only) - // 5. Trigger redraw - m_actor->Modified(); -} -``` - -### Computed Metadata - -The `updateMetadata()` method computes comprehensive geometry statistics: - -#### Basic Stats -- `num_vertices` - Number of vertices (read-only) -- `num_triangles` - Number of triangles (read-only) -- `num_quads` - Number of quads (read-only) -- `type` - Geometry type: "triangle_mesh", "quad_mesh", or "mixed_mesh" (read-only) - -#### Bounding Box -- `bbox_min_x`, `bbox_min_y`, `bbox_min_z` - Minimum bounds (read-only) -- `bbox_max_x`, `bbox_max_y`, `bbox_max_z` - Maximum bounds (read-only) - -#### Extents (Dimensions) -- `extent_x`, `extent_y`, `extent_z` - Width, height, depth (read-only) - -#### Center Point -- `center_x`, `center_y`, `center_z` - Geometric center (read-only) - -### Read-Only Protection - -All computed metadata is automatically marked as **read-only** in the state tree to prevent manual modification. This ensures that metadata always reflects the actual geometry data. - -## Usage Example - -```cpp -// Create a graphics node -GraphicsNode node("my_mesh"); -node.setGeometry(initialGeometry); - -// Sync to state tree -cvc::state& graphics = app.root()("graphics"); -node.syncToState(graphics); - -// Connect to state data changes -node.syncFromState(graphics); - -// Later, another part of the application modifies the geometry in state -cvc::geometry newGeometry = loadFromFile("modified.obj"); -graphics("my_mesh").data(newGeometry); // Triggers onDataChanged() - -// GraphicsNode automatically: -// - Loads the new geometry -// - Updates rendering -// - Recalculates all metadata (num_vertices, bbox, extents, etc.) -// - Updates the display -``` - -## State Tree Structure - -After syncing, the state tree looks like: - -``` -graphics -└── my_mesh - ├── [DATA: cvc::geometry object] - ├── transform: "1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1" - └── metadata - ├── num_vertices: 3 (read-only) - ├── num_triangles: 1 (read-only) - ├── num_quads: 0 (read-only) - ├── type: "triangle_mesh" (read-only) - ├── bbox_min_x: 0.0 (read-only) - ├── bbox_min_y: 0.0 (read-only) - ├── bbox_min_z: 0.0 (read-only) - ├── bbox_max_x: 1.0 (read-only) - ├── bbox_max_y: 1.0 (read-only) - ├── bbox_max_z: 0.0 (read-only) - ├── extent_x: 1.0 (read-only) - ├── extent_y: 1.0 (read-only) - ├── extent_z: 0.0 (read-only) - ├── center_x: 0.5 (read-only) - ├── center_y: 0.5 (read-only) - ├── center_z: 0.0 (read-only) - └── visible: true -``` - -## Benefits - -1. **Single Source of Truth**: Geometry data lives in the state tree, not duplicated in multiple places -2. **Automatic Synchronization**: Any change to state data automatically updates rendering -3. **Comprehensive Metadata**: All geometry properties computed automatically -4. **Read-Only Safety**: Computed metadata can't be accidentally modified -5. **Responsive UI**: Changes to geometry trigger immediate visual updates - -## Testing - -Comprehensive tests added in `GraphicsNodeTest.cpp`: - -- `MetadataFromGeometry` - Verifies basic stats computation -- `BoundingBoxMetadata` - Checks bounding box calculation -- `ExtentMetadata` - Validates extent computation -- `CenterMetadata` - Tests center point calculation -- `DataChangeTriggerUpdate` - Confirms data change triggers updates -- `MetadataSyncToState` - Ensures metadata syncs to state tree -- `ComputedMetadataReadOnly` - Verifies read-only protection -- `GeometryTypeDetection` - Tests mesh type classification -- `MetadataUpdatesOnGeometryChange` - Confirms metadata updates with geometry - -All 497 tests pass, including the 10 new GraphicsNode data-driven tests. - -## API Changes - -### New Methods - -- `void updateMetadata(const cvc::geometry& geom)` - Compute all geometry statistics -- `void onDataChanged()` - Handle state data changes (called via signal) - -### New Members - -- `cvc::state* m_stateNode` - Pointer to state tree node -- `boost::signals2::connection m_dataConnection` - Signal connection for data changes - -### Modified Methods - -- `syncFromState()` - Now connects to dataChanged signal and loads geometry from state data -- `syncToState()` - Now calls updateMetadata() to ensure fresh metadata -- `setGeometry()` - Now calls updateMetadata() to compute stats -- `~GraphicsNode()` - Disconnects signal connection - -## Implementation Files - -- `inc/volrover3/GraphicsNode.h` - Header with new members and methods -- `src/volrover3/GraphicsNode.cpp` - Implementation of data-driven updates -- `src/volrover3/tests/GraphicsNodeTest.cpp` - Comprehensive test coverage diff --git a/docs/GRAPHICS_SYSTEM.md b/docs/GRAPHICS_SYSTEM.md deleted file mode 100644 index 7f115dec..00000000 --- a/docs/GRAPHICS_SYSTEM.md +++ /dev/null @@ -1,326 +0,0 @@ -# Multi-Object Graphics System - -## Overview - -The SceneGraph now supports loading and managing multiple geometry objects simultaneously in a hierarchical structure with transformations. This system allows for: - -- Multiple independent geometry objects in the scene -- Hierarchical parent-child relationships with relative transformations -- Per-object transformation matrices (position, rotation, scale) -- Metadata storage for each graphics object -- State tree integration for persistence - -## Architecture - -### GraphicsNode - -`GraphicsNode` is the core class that represents a single graphics object with: - -- **Geometry Data**: Optional 3D mesh (points, triangles, normals, colors) -- **Transform Matrix**: 4x4 matrix for position, rotation, and scale -- **Hierarchy**: Parent-child relationships where child transforms are relative to parent -- **Metadata**: Key-value storage for arbitrary data (filename, tags, etc.) -- **State Integration**: Automatic synchronization with the state tree - -### SceneGraph Integration - -The `SceneGraph` manages all graphics objects through: - -- **Graphics Root**: A root `GraphicsNode` that contains all top-level graphics -- **Flat Lookup**: `std::map>` for fast access by name -- **State Sync**: Automatic synchronization with `volrover3.graphics` state node - -## Usage Examples - -### Adding a Single Geometry Object - -```cpp -// Load geometry from file -cvc::geometry geom = cvc::read_geometry("bunny.obj"); - -// Add to scene with a unique name -auto graphicsNode = sceneGraph->addGraphics("bunny1", geom); - -// Optional: Set position -graphicsNode->setPosition(0.0, 0.0, 5.0); - -// Optional: Set rotation (Euler angles in degrees) -graphicsNode->setRotation(45.0, 0.0, 0.0); - -// Optional: Set scale -graphicsNode->setScale(2.0, 2.0, 2.0); -``` - -### Adding Multiple Geometry Objects - -```cpp -// Load multiple geometries -cvc::geometry bunny = cvc::read_geometry("bunny.obj"); -cvc::geometry dragon = cvc::read_geometry("dragon.obj"); -cvc::geometry teapot = cvc::read_geometry("teapot.obj"); - -// Add them at different positions -auto bunny1 = sceneGraph->addGraphics("bunny1", bunny); -bunny1->setPosition(-5.0, 0.0, 0.0); - -auto dragon1 = sceneGraph->addGraphics("dragon1", dragon); -dragon1->setPosition(0.0, 0.0, 0.0); -dragon1->setScale(0.5, 0.5, 0.5); - -auto teapot1 = sceneGraph->addGraphics("teapot1", teapot); -teapot1->setPosition(5.0, 0.0, 0.0); -teapot1->setRotation(0.0, 45.0, 0.0); -``` - -### Creating Hierarchical Structures - -```cpp -// Create a parent container node (no geometry) -auto parent = sceneGraph->addGraphics("robot"); -parent->setPosition(0.0, 0.0, 0.0); - -// Load body parts -cvc::geometry body = cvc::read_geometry("robot_body.obj"); -cvc::geometry arm = cvc::read_geometry("robot_arm.obj"); - -// Create body as child of parent -auto bodyNode = std::make_shared("body"); -bodyNode->setGeometry(body); -bodyNode->setPosition(0.0, 0.0, 0.0); // Relative to parent -parent->addGraphicsChild(bodyNode); - -// Create arm as child of body -auto armNode = std::make_shared("left_arm"); -armNode->setGeometry(arm); -armNode->setPosition(-1.0, 0.5, 0.0); // Relative to body -armNode->setRotation(0.0, 0.0, 30.0); // Relative to body -bodyNode->addGraphicsChild(armNode); - -// Now transforming 'parent' will transform both body and arm -// Transforming 'body' will transform arm relative to body's new position -parent->setRotation(0.0, 90.0, 0.0); // Rotates entire robot -``` - -### Metadata Management - -```cpp -auto graphicsNode = sceneGraph->addGraphics("bunny1", bunny); - -// Store metadata -graphicsNode->setMetadata("filename", std::string("bunny.obj")); -graphicsNode->setMetadata("load_time", std::string("2025-12-30")); -graphicsNode->setMetadata("num_vertices", bunny.num_points()); - -// Retrieve metadata -if (graphicsNode->hasMetadata("filename")) { - auto filename = std::any_cast( - graphicsNode->getMetadata("filename") - ); -} -``` - -### Removing Graphics Objects - -```cpp -// Remove by name -sceneGraph->removeGraphics("bunny1"); - -// Or get reference first, then remove -auto node = sceneGraph->getGraphics("dragon1"); -if (node) { - sceneGraph->removeGraphics("dragon1"); -} -``` - -### Accessing and Manipulating Graphics - -```cpp -// Get graphics node by name -auto node = sceneGraph->getGraphics("bunny1"); -if (node) { - // Change visibility - node->setVisible(false); - - // Modify transform - node->setPosition(1.0, 2.0, 3.0); - node->setRotation(45.0, 30.0, 60.0); - - // Get world transform (includes all parent transforms) - vtkSmartPointer worldTransform = node->getWorldTransform(); -} - -// Iterate over all graphics -for (const auto& [name, node] : sceneGraph->getAllGraphics()) { - std::cout << "Graphics: " << name << " visible: " << node->isVisible() << std::endl; -} -``` - -### State Tree Integration - -Graphics objects are automatically synchronized with the state tree under `volrover3.graphics`: - -```cpp -// Save all graphics to state tree -sceneGraph->syncGraphicsToState(); - -// Later, load graphics from state tree -sceneGraph->syncGraphicsFromState(); -``` - -The state tree stores: -- Transform matrices -- Visibility flags -- Metadata -- Hierarchical structure - -## Transformation Details - -### Transform Matrix - -Each `GraphicsNode` maintains a 4x4 homogeneous transformation matrix: - -``` -[ R11 R12 R13 Tx ] -[ R21 R22 R23 Ty ] -[ R31 R32 R33 Tz ] -[ 0 0 0 1 ] -``` - -Where: -- R11-R33: Rotation and scale -- Tx, Ty, Tz: Translation - -### World Transform - -Child nodes inherit their parent's transformation. The world transform is calculated by multiplying the local transform with the parent's world transform: - -``` -WorldTransform(child) = WorldTransform(parent) × LocalTransform(child) -``` - -### Convenience Methods - -```cpp -// Identity matrix (reset all transformations) -graphicsNode->resetTransform(); - -// Set position only -graphicsNode->setPosition(x, y, z); - -// Set rotation only (Euler angles XYZ, degrees) -graphicsNode->setRotation(rx, ry, rz); - -// Set scale only -graphicsNode->setScale(sx, sy, sz); - -// Set full 4x4 matrix (row-major) -double matrix[16] = {...}; -graphicsNode->setTransform(matrix); - -// Or use VTK matrix -vtkSmartPointer vtk_matrix = ...; -graphicsNode->setTransform(vtk_matrix); -``` - -## API Reference - -### SceneGraph Methods - -```cpp -// Add graphics with geometry -std::shared_ptr addGraphics(const std::string& name, - const cvc::geometry& geom); - -// Add empty graphics node (for grouping/hierarchy) -std::shared_ptr addGraphics(const std::string& name); - -// Remove graphics by name -void removeGraphics(const std::string& name); - -// Get graphics by name -std::shared_ptr getGraphics(const std::string& name); - -// Get graphics root node -std::shared_ptr getGraphicsRoot(); - -// Get all graphics (flat map) -const std::map>& getAllGraphics() const; - -// State synchronization -void syncGraphicsToState(); -void syncGraphicsFromState(); -``` - -### GraphicsNode Methods - -```cpp -// Naming -void setName(const std::string& name); -std::string getName() const; - -// Geometry -void setGeometry(const cvc::geometry& geom); -bool hasGeometry() const; - -// Transform -void setTransform(vtkMatrix4x4* matrix); -void setTransform(const double matrix[16]); -vtkMatrix4x4* getTransform(); -void setPosition(double x, double y, double z); -void setRotation(double x, double y, double z); // degrees -void setScale(double x, double y, double z); -void resetTransform(); -vtkSmartPointer getWorldTransform() const; - -// Hierarchy -void addGraphicsChild(std::shared_ptr child); -void removeGraphicsChild(std::shared_ptr child); -std::shared_ptr findChildByName(const std::string& name); -const std::vector>& getGraphicsChildren() const; - -// Metadata -void setMetadata(const std::string& key, const std::any& value); -std::any getMetadata(const std::string& key) const; -bool hasMetadata(const std::string& key) const; -const std::map& getAllMetadata() const; - -// Visibility (inherited from SceneNode) -void setVisible(bool visible); -bool isVisible() const; - -// State integration -void syncToState(cvc::state& parentState); -void syncFromState(const cvc::state& parentState); -``` - -## Migration from Single Geometry - -The old `setGeometry()` method still exists for backward compatibility: - -```cpp -// Old way (still works) -sceneGraph->setGeometry(geom); - -// New way (recommended) -auto node = sceneGraph->addGraphics("geometry1", geom); -``` - -The old method creates a single `GeometryNode`, while the new system uses `GraphicsNode` objects stored in a hierarchy. - -## Performance Considerations - -- Each `GraphicsNode` creates its own VTK actor and mapper -- Transform updates propagate to all children recursively -- State tree synchronization should be done explicitly when needed -- Use the flat lookup map (`getAllGraphics()`) for fast access by name -- Hierarchical searches use `findChildByName()` which is recursive - -## Future Enhancements - -Potential additions: -- Bounding box visualization per graphics object -- Material/color properties per object -- Selection/picking support -- Animation/keyframe system -- LOD (Level of Detail) support -- Instancing for repeated geometry diff --git a/inc/volrover3/AppState.h b/inc/volrover3/AppState.h deleted file mode 100644 index 1ac66fb6..00000000 --- a/inc/volrover3/AppState.h +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef APPSTATE_H -#define APPSTATE_H - -#include -#include -#include -#include -#include -#include - -// Application state manager using cvc::state for reactive updates -class AppState { -public: - // Get default singleton instance (uses "volrover3" prefix) - static AppState &instance(); - - // Create instance with custom state prefix (for multiple viewers or testing) - explicit AppState(const std::string &statePrefix = "volrover3"); - - // Get the state prefix for this instance - std::string getStatePrefix() const { return m_statePrefix; } - - // State accessors with change notification - cvc::bounding_box worldBounds(); - void setWorldBounds(const cvc::bounding_box &bounds); - - // Camera control mode (0 = orbit, 1 = fly) - int cameraMode(); - void setCameraMode(int mode); - - // Camera settings - double cameraSpeed(); - void setCameraSpeed(double speed); - - double cameraSensitivity(); - void setCameraSensitivity(double sensitivity); - - bool cameraInvertMouse(); - void setCameraInvertMouse(bool invert); - - // Camera key bindings - int cameraKeyForward(); - void setCameraKeyForward(int key); - - int cameraKeyBackward(); - void setCameraKeyBackward(int key); - - int cameraKeyLeft(); - void setCameraKeyLeft(int key); - - int cameraKeyRight(); - void setCameraKeyRight(int key); - - int cameraKeyUp(); - void setCameraKeyUp(int key); - - int cameraKeyDown(); - void setCameraKeyDown(int key); - - // Camera position and orientation - void getCameraPosition(double &x, double &y, double &z); - void setCameraPosition(double x, double y, double z); - - void getCameraViewDirection(double &x, double &y, double &z); - void setCameraViewDirection(double x, double y, double z); - - void getCameraUpVector(double &x, double &y, double &z); - void setCameraUpVector(double x, double y, double z); - - double cameraFieldOfView(); - void setCameraFieldOfView(double fov); - - // Viewer options - bool showFPS(); - void setShowFPS(bool show); - - // Register callbacks for state changes - // Returns a connection object that can be used to disconnect the callback - boost::signals2::connection onWorldBoundsChanged(const boost::function &callback); - boost::signals2::connection onCameraModeChanged(const boost::function &callback); - boost::signals2::connection onCameraChanged(const boost::function &callback); - - // State tree access for debugging/inspection - cvc::state &getRootState(); - -public: - ~AppState() = default; - -private: - AppState(const AppState &) = delete; - AppState &operator=(const AppState &) = delete; - - cvc::state &getState(const std::string &path); - void initializeDefaults(); - - std::string m_statePrefix; -}; - -#endif // APPSTATE_H diff --git a/inc/volrover3/AxisNode.h b/inc/volrover3/AxisNode.h deleted file mode 100644 index d03b090d..00000000 --- a/inc/volrover3/AxisNode.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef AXISNODE_H -#define AXISNODE_H - -#include -#include -#include - -class vtkAxesActor; - -class AxisNode : public GraphicsNode { -public: - AxisNode(cvc::app &ctx, const std::string &statePath, const std::string &name = "axis"); - ~AxisNode() override; - - void setAxisLength(double length); - - cvc::bounding_box getBoundingBox() const override; - -protected: - vtkProp *getProp() override; - void handleStateChanged(const std::string &childState) override; - void applyTransformToVTK() override; // Apply transform to axes actor - -private: - vtkSmartPointer m_axesActor; -}; - -#endif // AXISNODE_H diff --git a/inc/volrover3/BBoxNode.h b/inc/volrover3/BBoxNode.h deleted file mode 100644 index 2e5f7622..00000000 --- a/inc/volrover3/BBoxNode.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef BBOXNODE_H -#define BBOXNODE_H - -#include -#include -#include - -class vtkActor; -class vtkPolyDataMapper; -class vtkActor2D; -class vtkRenderer; -class vtkMatrix4x4; - -// Simple VTK wrapper for bounding box visualization -// Not a SceneNode - controlled by parent GraphicsNode's show_bbox state -class BBoxNode { -public: - BBoxNode(); - ~BBoxNode(); - - void addToRenderer(vtkRenderer *renderer); - void removeFromRenderer(vtkRenderer *renderer); - - void setBoundingBox(const cvc::bounding_box &bbox); - cvc::bounding_box getBoundingBox() const { return m_bbox; } - - void setTransform(vtkMatrix4x4 *transform); - - void setColor(double r, double g, double b); - void getColor(double &r, double &g, double &b) const; - void setLineWidth(double width); - - // Coordinate label controls - void setCoordinatesVisible(bool visible); - bool getCoordinatesVisible() const { return m_coordinatesVisible; } - - void setCoordinateLabelColor(double r, double g, double b); - void getCoordinateLabelColor(double &r, double &g, double &b) const; - - void setCoordinateLabelFontSize(int size); - int getCoordinateLabelFontSize() const { return m_coordinateLabelFontSize; } - -private: - void createBBox(); - void createCoordinateLabels(); - - vtkSmartPointer m_actor; - vtkSmartPointer m_mapper; - cvc::bounding_box m_bbox; - vtkSmartPointer m_transform; // Store world transform for coordinate labels - - // Coordinate label members - std::vector> m_coordinateLabelActors; - bool m_coordinatesVisible; - double m_coordinateLabelColor[3]; - int m_coordinateLabelFontSize; - vtkRenderer *m_renderer; // Store renderer to re-add labels when recreated -}; - -#endif // BBOXNODE_H diff --git a/inc/volrover3/BoundingBoxDialog.h b/inc/volrover3/BoundingBoxDialog.h deleted file mode 100644 index 43e8a0f0..00000000 --- a/inc/volrover3/BoundingBoxDialog.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef BOUNDINGBOXDIALOG_H -#define BOUNDINGBOXDIALOG_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class GraphicsNode; -class SceneGraph; - -class BoundingBoxDialog : public QDialog { - Q_OBJECT - -public: - explicit BoundingBoxDialog(std::shared_ptr sceneGraph, QWidget *parent = nullptr); - -private slots: - void onGraphicsSelectionChanged(int index); - void onResetToGraphics(); - void onBBoxVisibilityChanged(bool visible); - void onBBoxColorChanged(); - void onApplyChanges(); - -private: - void setupUI(); - void populateGraphicsComboBox(); - void loadGraphicsSettings(); - void updateColorButton(); - - std::shared_ptr m_sceneGraph; - std::vector> m_graphicsList; - std::shared_ptr m_currentGraphics; - - QComboBox *m_graphicsComboBox; - QLineEdit *m_minXEdit; - QLineEdit *m_minYEdit; - QLineEdit *m_minZEdit; - QLineEdit *m_maxXEdit; - QLineEdit *m_maxYEdit; - QLineEdit *m_maxZEdit; - - QCheckBox *m_bboxVisibleCheckbox; - QPushButton *m_bboxColorButton; - double m_bboxColor[3]; -}; - -#endif // BOUNDINGBOXDIALOG_H diff --git a/inc/volrover3/CameraController.h b/inc/volrover3/CameraController.h deleted file mode 100644 index 244119e9..00000000 --- a/inc/volrover3/CameraController.h +++ /dev/null @@ -1,96 +0,0 @@ -#ifndef CAMERACONTROLLER_H -#define CAMERACONTROLLER_H - -#include -#include -#include -#include - -enum CameraMode { ORBIT_MODE = 0, FLY_MODE = 1 }; - -class CameraController : public SceneNode { -public: - CameraController(cvc::app &ctx, const std::string &statePath = "volrover3.camera"); - ~CameraController(); - - void setCamera(vtkCamera *camera); - - void handleKeyPress(int key); - void handleKeyRelease(int key); - void handleMousePress(int button); - void handleMouseRelease(int button); - void handleMouseMove(int dx, int dy); - void handleMouseWheel(int delta); - - void setMovementSpeed(double speed) { m_movementSpeed = speed; } - void setMouseSensitivity(double sensitivity) { m_mouseSensitivity = sensitivity; } - void setInvertMouse(bool invert) { m_invertMouse = invert; } - - void setMode(CameraMode mode) { m_mode = mode; } - CameraMode getMode() const { return m_mode; } - - void setOrbitCenter(double x, double y, double z); - void updateOrbitCenterFromBounds(double minX, double minY, double minZ, double maxX, double maxY, - double maxZ); - void resetView(double minX, double minY, double minZ, double maxX, double maxY, double maxZ); - - void setKeyBindings(int forward, int backward, int left, int right, int up, int down); - - // Camera state synchronization - void getCameraState(double pos[3], double dir[3], double up[3], double &fov); - void setCameraState(const double pos[3], const double dir[3], const double up[3], double fov); - void applyCameraToVTK(); - - // SceneNode overrides - void update() override; - -protected: - vtkProp *getProp() override { return nullptr; } // Camera is not a renderable prop - void handleStateChanged(const std::string &childState) override; - -private: - void updateOrientation(); - void move(double forward, double right, double up); - void orbitCamera(int dx, int dy); - void panCamera(int dx, int dy); - void initializeState(); - void syncCameraToState(); - - vtkSmartPointer m_camera; - - // Camera mode - CameraMode m_mode; - - // Orbit mode state - double m_orbitCenter[3]; - double m_orbitDistance; - double m_orbitAzimuth; - double m_orbitElevation; - - // Fly mode state - double m_position[3]; - double m_focalPoint[3]; // Track focal point to avoid snapping - double m_yaw; - double m_pitch; - - // Input state - std::set m_keysPressed; - bool m_mouseLeftPressed; - bool m_mouseRightPressed; - bool m_mouseMiddlePressed; - - // Settings - double m_movementSpeed; - double m_mouseSensitivity; - bool m_invertMouse; - - // Key bindings - int m_keyForward; - int m_keyBackward; - int m_keyStrafeLeft; - int m_keyStrafeRight; - int m_keyUp; - int m_keyDown; -}; - -#endif // CAMERACONTROLLER_H diff --git a/inc/volrover3/CameraSettingsDialog.h b/inc/volrover3/CameraSettingsDialog.h deleted file mode 100644 index f4e77c78..00000000 --- a/inc/volrover3/CameraSettingsDialog.h +++ /dev/null @@ -1,117 +0,0 @@ -#ifndef CAMERASETTINGSDIALOG_H -#define CAMERASETTINGSDIALOG_H - -#include -#include -#include -#include -#include -#include - -namespace cvc { -class state; -} - -class QKeySequenceEdit; -class QTableWidget; - -// Custom button that captures key presses for binding -class KeyBindButton : public QPushButton { - Q_OBJECT -public: - KeyBindButton(int initialKey, QWidget *parent = nullptr); - - int key() const { return m_key; } - void setKey(int key); - -signals: - void keyChanged(int key); - -protected: - void keyPressEvent(QKeyEvent *event) override; - void focusOutEvent(QFocusEvent *event) override; - -private slots: - void startCapture(); - -private: - void updateText(); - - int m_key; - bool m_waitingForKey; -}; - -class CameraSettingsDialog : public QDialog { - Q_OBJECT - -public: - struct CameraSettings { - int mode; // 0 = orbit, 1 = fly - double flySpeed; - double mouseSensitivity; - bool invertMouse; - int keyForward; - int keyBackward; - int keyStrafeLeft; - int keyStrafeRight; - int keyUp; - int keyDown; - }; - - // Camera state from state tree (for display purposes) - struct CameraState { - int mode; - double positionX, positionY, positionZ; - double viewDirX, viewDirY, viewDirZ; - double upX, upY, upZ; - double fov; - // Orbit mode - double orbitCenterX, orbitCenterY, orbitCenterZ; - double orbitDistance; - double orbitAzimuth, orbitElevation; - // Fly mode - double flyYaw, flyPitch; - double flyFocalX, flyFocalY, flyFocalZ; - }; - - // Constructor with optional camera state for live state display - explicit CameraSettingsDialog(const CameraSettings &settings, cvc::state *cameraState = nullptr, - QWidget *parent = nullptr); - ~CameraSettingsDialog(); - - CameraSettings getSettings() const; - -signals: - void resetViewRequested(); - void settingsChanged(const CameraSettings &settings); - -private slots: - void onResetDefaults(); - void onResetView(); - void updateStateDisplay(); - void emitSettingsChanged(); - -private: - void setupUI(const CameraSettings &settings); - CameraSettings getDefaultSettings() const; - CameraState readCameraState() const; - - cvc::state *m_cameraState; - boost::signals2::connection m_stateConnection; - - QComboBox *m_modeCombo; - QDoubleSpinBox *m_flySpeedSpin; - QDoubleSpinBox *m_mouseSensitivitySpin; - QCheckBox *m_invertMouseCheck; - KeyBindButton *m_keyForwardButton; - KeyBindButton *m_keyBackwardButton; - KeyBindButton *m_keyStrafeLeftButton; - KeyBindButton *m_keyStrafeRightButton; - KeyBindButton *m_keyUpButton; - KeyBindButton *m_keyDownButton; - - // State display - QTableWidget *m_stateTable; -}; - -#endif // CAMERASETTINGSDIALOG_H diff --git a/inc/volrover3/GeometryDialog.h b/inc/volrover3/GeometryDialog.h deleted file mode 100644 index f816ea27..00000000 --- a/inc/volrover3/GeometryDialog.h +++ /dev/null @@ -1,123 +0,0 @@ -#ifndef GEOMETRYDIALOG_H -#define GEOMETRYDIALOG_H - -#include -#include -#include -#include -#include - -class QComboBox; -class QDoubleSpinBox; -class QSpinBox; -class QCheckBox; -class QGroupBox; -class QTableWidget; -class QPushButton; -class SceneGraph; - -class GeometryDialog : public QDialog { - Q_OBJECT - -public: - explicit GeometryDialog(std::shared_ptr sceneGraph, QWidget *parent = nullptr); - ~GeometryDialog() = default; - -private slots: - void onGeometrySelected(int index); - void onGraphicsChildrenChanged(); - void onRenderModeChanged(int index); - void onColorChanged(); - void onSingleColorChanged(bool checked); - void onMaterialPropertyChanged(); - void onDeleteButtonClicked(); - void onNodeStateChanged(); - void onVisibilityChanged(bool checked); - void onShowBBoxChanged(bool checked); - void onBBoxColorChanged(); - void onShowExtentLabelsChanged(bool checked); - void onExtentLabelColorChanged(); - void onExtentLabelFontSizeChanged(int size); - void onInvertNormalsClicked(); - void onReorientClicked(); - void onProjectClicked(); - void onSmoothingClicked(); - void onQualityImproveClicked(); - -private: - void setupUI(); - void connectSignals(); - void populateGeometryList(); - void updatePropertiesFromNode(); - void setPropertiesEnabled(bool enabled); - void updateBBoxColorButton(); - void updateExtentLabelColorButton(); - void setOperationButtonsEnabled(bool enabled); - - std::shared_ptr m_sceneGraph; - - // UI elements - QComboBox *m_geometryComboBox; - QPushButton *m_deleteButton; - QComboBox *m_renderModeComboBox; - - // Color controls - QCheckBox *m_singleColorCheckBox; - QDoubleSpinBox *m_colorRSpinBox; - QDoubleSpinBox *m_colorGSpinBox; - QDoubleSpinBox *m_colorBSpinBox; - - // Visibility controls - QCheckBox *m_visibilityCheckBox; - - // Bounding box controls - QCheckBox *m_showBBoxCheckBox; - QPushButton *m_bboxColorButton; - double m_bboxColor[3]; - - // Extent label controls - QCheckBox *m_showExtentLabelsCheckBox; - QPushButton *m_extentLabelColorButton; - QSpinBox *m_extentLabelFontSizeSpinBox; - double m_extentLabelColor[3]; - - // Geometry operations buttons - QPushButton *m_invertNormalsButton; - QPushButton *m_reorientButton; - QPushButton *m_projectButton; - QComboBox *m_projectTargetComboBox; - QPushButton *m_smoothingButton; - QDoubleSpinBox *m_smoothingDeltaSpinBox; - QCheckBox *m_smoothingFixBoundaryCheckBox; - QCheckBox *m_smoothingPerturb1CheckBox; - QCheckBox *m_smoothingGeoFlowCheckBox; - QCheckBox *m_smoothingEnabledCheckBox; - QCheckBox *m_smoothingPerturb2CheckBox; - QPushButton *m_qualityImproveButton; - QSpinBox *m_qualityIterationsSpinBox; - QComboBox *m_qualityMethodComboBox; - - // Info tab - QTableWidget *m_infoTable; - - // Material properties - QDoubleSpinBox *m_ambientSpinBox; - QDoubleSpinBox *m_diffuseSpinBox; - QDoubleSpinBox *m_specularSpinBox; - QDoubleSpinBox *m_specularPowerSpinBox; - QDoubleSpinBox *m_opacitySpinBox; - QDoubleSpinBox *m_pointSizeSpinBox; - QDoubleSpinBox *m_lineWidthSpinBox; - - // Geometry tracking - std::vector m_geometryNames; - - // Signal connections - boost::signals2::scoped_connection m_graphicsChangedConnection; - boost::signals2::scoped_connection m_nodeStateConnection; - - // Flag to prevent recursive updates - bool m_updating; -}; - -#endif // GEOMETRYDIALOG_H diff --git a/inc/volrover3/GeometryNode.h b/inc/volrover3/GeometryNode.h deleted file mode 100644 index a0f26d2b..00000000 --- a/inc/volrover3/GeometryNode.h +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef GEOMETRYNODE_H -#define GEOMETRYNODE_H - -#include -#include -#include - -class vtkActor; -class vtkPolyDataMapper; -class vtkPolyData; - -namespace cvc { -class geometry; -class state; -} // namespace cvc - -/** - * @brief Geometry rendering modes - */ -enum class GeometryRenderMode { - POINTS, // Render as point cloud - LINES, // Render as wireframe - TRIS, // Render triangles as solid surface - QUADS, // Render quads as solid surface - TETS, // Render tetrahedral mesh (placeholder) - HEXS // Render hexahedral mesh (placeholder) -}; - -/** - * @brief GeometryNode renders cvc::geometry objects with full transform support - * - * Extends GraphicsNode to provide: - * - Geometry-specific rendering (triangles, quads) - * - Bounding box computation from geometry extents - * - State tree synchronization for geometry data - * - * Inherits from GraphicsNode: - * - Transforms (position, rotation, scale) - * - Metadata storage - * - Bounding box display - * - Hierarchical structure - */ -class GeometryNode : public GraphicsNode { -public: - GeometryNode(cvc::app &ctx, const std::string &statePath, const std::string &name = "geometry"); - ~GeometryNode() override; - - // Generic setData for template compatibility - void setData(const cvc::geometry &geom) { setGeometry(geom); } - - void setGeometry(const cvc::geometry &geom); - bool hasGeometry() const { return m_hasGeometry; } - const cvc::geometry *getGeometry() const { return m_geometry.get(); } - - // Render mode control - void setRenderMode(GeometryRenderMode mode); - GeometryRenderMode getRenderMode() const { return m_renderMode; } - - // Single color mode control - void setUseSingleColor(bool useSingleColor); - bool getUseSingleColor() const { return m_useSingleColor; } - - // Material property setters (sync with state tree) - void setColor(double r, double g, double b); - void setSpecular(double value); - void setSpecularPower(double value); - void setAmbient(double value); - void setDiffuse(double value); - void setOpacity(double value); - void setPointSize(double size); - void setLineWidth(double width); - - // Helper to convert render mode to/from string - static std::string renderModeToString(GeometryRenderMode mode); - static GeometryRenderMode stringToRenderMode(const std::string &str); - - // Implement GraphicsNode abstract methods - cvc::bounding_box getBoundingBox() const override; - - // Check if a metadata key is computed (read-only) - static bool isComputedMetadata(const std::string &key); - -protected: - vtkProp *getProp() override; - void handleStateChanged(const std::string &childState) override; - void applyTransformToVTK() override; // Apply transform to actor - void applyClipPlanes(vtkPlaneCollection *planes) override; // Apply clip planes to mapper - void updatePolyData(const cvc::geometry &geom); - void updateRenderModeVTK(); // Helper to update VTK properties from render mode - void updateMetadata(const cvc::geometry &geom); - void onDataChanged(); - -private: - bool m_hasGeometry; - std::shared_ptr m_geometry; - GeometryRenderMode m_renderMode; - bool m_useSingleColor; // When true, use single color; when false, use per-vertex colors - - vtkSmartPointer m_actor; - vtkSmartPointer m_mapper; - vtkSmartPointer m_polyData; - - boost::signals2::connection m_dataConnection; -}; - -#endif // GEOMETRYNODE_H diff --git a/inc/volrover3/GraphicsNode.h b/inc/volrover3/GraphicsNode.h deleted file mode 100644 index 228926f6..00000000 --- a/inc/volrover3/GraphicsNode.h +++ /dev/null @@ -1,215 +0,0 @@ -#ifndef GRAPHICSNODE_H -#define GRAPHICSNODE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class vtkActor2D; -class BBoxNode; -class vtkPlane; -class VolumeNode; -class NullGraphicNode; - -namespace cvc { -class geometry; -class volume; -} // namespace cvc - -/** - * @brief Abstract base class for all graphics objects in the scene - * - * GraphicsNode provides common functionality for all renderable graphics objects: - * - Transformation (position, rotation, scale) - * - Hierarchical structure (parent/child relationships) - * - Metadata storage - * - Bounding box display - * - Visibility control - * - State tree synchronization (via state_object inheritance) - * - Clipping planes based on bounding box - * - * Subclasses must implement: - * - getBoundingBox() - return the untransformed bounding box - * - getProp() - return the VTK prop for rendering - * - handleStateChanged() - respond to state tree changes - */ -class GraphicsNode : public SceneNode { -public: - GraphicsNode(cvc::app &ctx, const std::string &statePath, const std::string &name = ""); - virtual ~GraphicsNode(); - - // Identity and naming - void setName(const std::string &name) { m_name = name; } - std::string getName() const { return m_name; } - - // Pure virtual methods that subclasses must implement - virtual cvc::bounding_box - getBoundingBox() const = 0; // Return untransformed bounding box of THIS node only - - // Get combined bounding box (this node + all children) - cvc::bounding_box getCombinedBoundingBox() const; - - // Transform management - void setTransform(vtkMatrix4x4 *matrix); - void setTransform(const double matrix[16]); // Row-major 4x4 matrix - vtkMatrix4x4 *getTransform() { return m_transform; } - const vtkMatrix4x4 *getTransform() const { return m_transform; } - - // Convenience transform methods - void setPosition(double x, double y, double z); - void setRotation(double x, double y, double z); // Euler angles in degrees - void setScale(double x, double y, double z); - void resetTransform(); // Set to identity matrix - - // Get world transform (accumulated from all parents) - vtkSmartPointer getWorldTransform() const; - - // Hierarchical structure - - // Template factory method for creating child graphics nodes - // Automatically constructs the proper state path based on parent's state - // Usage: auto node = parent->addGraphicsChild("myGeom"); - template std::shared_ptr addGraphicsChild(const std::string &name) { - static_assert(std::is_base_of::value, "T must be derived from GraphicsNode"); - - // Construct state path: {parent_path}.children.{name} - std::string childStatePath = getState().fullName() + ".children." + name; - - // Create the child node with proper state path and name - auto child = std::make_shared(this->app(), childStatePath, name); - - // Add to children using the non-template version - addGraphicsChild(std::static_pointer_cast(child)); - - return child; - } - - // Non-template version for adding existing nodes (virtual to allow override) - virtual void addGraphicsChild(std::shared_ptr child); - - // Generic template method for creating child graphics nodes with data - // Usage: auto geomNode = parent->createChild("name", geomData); - // auto volNode = parent->createChild("name", volData); - template - std::shared_ptr createChild(const std::string &name, const DataType &data) { - static_assert(std::is_base_of::value, - "NodeType must be derived from GraphicsNode"); - - // Create the child node using the template factory - auto child = addGraphicsChild(name); - - // Set the data using the generic setData method - child->setData(data); - - return child; - } - - // Overload for creating child without data (uses NullGraphicNode) - std::shared_ptr createChild(const std::string &name); - - virtual void removeGraphicsChild(std::shared_ptr child); - std::shared_ptr findChildByName(const std::string &name); - const std::vector> &getGraphicsChildren() const { - return m_graphicsChildren; - } - - // Metadata management - void setMetadata(const std::string &key, const std::any &value); - std::any getMetadata(const std::string &key) const; - bool hasMetadata(const std::string &key) const; - const std::map &getAllMetadata() const { return m_metadata; } - - // Bounding box visibility - void setShowBBox(bool show); - bool getShowBBox() const { return m_showBBox; } - - // Bounding box color - void setBBoxColor(double r, double g, double b); - void getBBoxColor(double &r, double &g, double &b) const; - - // Bounding box extent labels - void setShowExtentLabels(bool show); - bool getShowExtentLabels() const; - void setExtentLabelColor(double r, double g, double b); - void getExtentLabelColor(double &r, double &g, double &b) const; - void setExtentLabelFontSize(int size); - int getExtentLabelFontSize() const; - - // Clipping plane control - void setClipChildren(bool clip); - bool getClipChildren() const { return m_clipChildren; } - vtkPlaneCollection *getClipPlanes() const { return m_clipPlanes; } - - // Label control - void setShowLabel(bool show); - bool getShowLabel() const { return m_showLabel; } - void setLabelText(const std::string &text); - std::string getLabelText() const { return m_labelText; } - void setLabelSize(int size); - int getLabelSize() const { return m_labelSize; } - void setLabelColor(double r, double g, double b); - void getLabelColor(double &r, double &g, double &b) const; - - // Override visibility to sync with metadata - void setVisible(bool visible); - - // Override update to handle transform changes - void update() override; - - // Override addToRenderer/removeFromRenderer to handle bbox - void addToRenderer(vtkRenderer *renderer) override; - void removeFromRenderer(vtkRenderer *renderer) override; - -protected: - void updateTransform(); - void updateBoundingBoxNode(); // Update bbox node with current bounds + transform - void updateLabel(); // Update label position and properties - void updateClipPlanes(); // Update clip planes based on bounding box and transform - - // Generic helper to apply world transform to a vector of VTK props - void applyWorldTransformToProps(const std::vector &props); - - // Apply transform to VTK prop - subclasses should override to apply to their specific prop type - virtual void applyTransformToVTK(); - - // Apply clip planes to children - called when clipChildren changes - void applyClipPlanesToChildren(); - - // Apply clip planes to this node's mapper/prop - subclasses override if they support clipping - virtual void applyClipPlanes(vtkPlaneCollection *planes); - - // Protected members for subclass access - std::string m_name; - vtkSmartPointer m_transform; - vtkSmartPointer m_vtkTransform; // VTK transform wrapper for m_transform - std::vector> m_graphicsChildren; - GraphicsNode *m_parent; // Weak pointer to parent for world transform calculation - std::map m_metadata; - bool m_showBBox; - std::shared_ptr m_bboxNode; - - // Label members - bool m_showLabel; - std::string m_labelText; - int m_labelSize; - double m_labelColor[3]; - vtkSmartPointer m_labelActor; - - // Clipping planes - bool m_clipChildren; // Whether to clip children to this node's bounding box - vtkSmartPointer m_clipPlanes; // Collection of 6 planes - std::array, 6> m_clipPlaneArray; // Individual planes for updates - - // State change handler override - virtual void handleStateChanged(const std::string &childState) override; -}; - -#endif // GRAPHICSNODE_H diff --git a/inc/volrover3/GraphicsParentDialog.h b/inc/volrover3/GraphicsParentDialog.h deleted file mode 100644 index 9cbfc802..00000000 --- a/inc/volrover3/GraphicsParentDialog.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef GRAPHICSPARENTDIALOG_H -#define GRAPHICSPARENTDIALOG_H - -#include -#include -#include - -class QComboBox; -class QPushButton; -class GraphicsNode; -class VolumeNode; -class SceneGraph; - -/** - * @brief Dialog for selecting a parent graphics node for new geometry or volume - * - * Allows the user to select which graphics node should be the parent - * for newly loaded geometry or volume files. Shows a hierarchical list of existing - * graphics nodes and volume graphics nodes, with the root as the default option. - */ -class GraphicsParentDialog : public QDialog { - Q_OBJECT - -public: - explicit GraphicsParentDialog(std::shared_ptr sceneGraph, QWidget *parent = nullptr); - ~GraphicsParentDialog() override; - - // Get the selected parent node name (empty string = root) - std::string getSelectedParentName() const; - - // Get the selected parent node (nullptr = root) - std::shared_ptr getSelectedParent() const; - - // Get the selected volume parent node (nullptr = root) - std::shared_ptr getSelectedVolumeParent() const; - -private: - void populateParentList(); - void addNodeToList(std::shared_ptr node, int depth = 0); - void addVolumeNodeToList(std::shared_ptr node, int depth = 0); - - std::shared_ptr m_sceneGraph; - QComboBox *m_parentComboBox; - QPushButton *m_okButton; - QPushButton *m_cancelButton; -}; - -#endif // GRAPHICSPARENTDIALOG_H diff --git a/inc/volrover3/GridNode.h b/inc/volrover3/GridNode.h deleted file mode 100644 index 02df20bb..00000000 --- a/inc/volrover3/GridNode.h +++ /dev/null @@ -1,117 +0,0 @@ -#ifndef GRIDNODE_H -#define GRIDNODE_H - -#include -#include -#include -#include - -class vtkActor; -class vtkPolyDataMapper; -class vtkRenderer; -class vtkActor2D; -class vtkTextMapper; - -class GridNode : public GraphicsNode { -public: - GridNode(cvc::app &ctx, const std::string &statePath, const std::string &name = "grid"); - ~GridNode() override; - - void setBounds(const cvc::bounding_box &bounds); - void setColor(double r, double g, double b); - - // Per-plane colors - void setYZPlaneColor(double r, double g, double b); - void setXZPlaneColor(double r, double g, double b); - void setXYPlaneColor(double r, double g, double b); - - void getYZPlaneColor(double &r, double &g, double &b) const; - void getXZPlaneColor(double &r, double &g, double &b) const; - void getXYPlaneColor(double &r, double &g, double &b) const; - - // Grid plane visibility (YZ plane at X=0, XZ plane at Y=0, XY plane at Z=0) - void setYZPlaneVisible(bool visible); - void setXZPlaneVisible(bool visible); - void setXYPlaneVisible(bool visible); - - bool isYZPlaneVisible() const { return m_yzPlaneVisible; } - bool isXZPlaneVisible() const { return m_xzPlaneVisible; } - bool isXYPlaneVisible() const { return m_xyPlaneVisible; } - - // Grid divisions per axis - void setGridDivisions(int x, int y, int z); - void getGridDivisions(int &x, int &y, int &z) const; - - // Tick intervals (show tick every N grid cells) - void setTickIntervals(int x, int y, int z); - void getTickIntervals(int &x, int &y, int &z) const; - - // Tick label properties - void setTickLabelColor(double r, double g, double b); - void getTickLabelColor(double &r, double &g, double &b) const; - - void setTickLabelFontSize(int size); - int getTickLabelFontSize() const; - - // Override to handle multiple actors - void addToRenderer(vtkRenderer *renderer) override; - void removeFromRenderer(vtkRenderer *renderer) override; - - cvc::bounding_box getBoundingBox() const override; - -protected: - vtkProp *getProp() override; // Returns first actor (for compatibility) - void handleStateChanged(const std::string &childState) override; - void applyTransformToVTK() override; // Apply transform to all grid actors - void - applyClipPlanes(vtkPlaneCollection *planes) override; // Apply clip planes to all grid mappers - -private: - void createGridPlanes(); - void createYZPlane(); // Grid at X=0 - void createXZPlane(); // Grid at Y=0 - void createXYPlane(); // Grid at Z=0 - - void createTickLabels(); - void createYZTickLabels(); - void createXZTickLabels(); - void createXYTickLabels(); - void updateTickLabelsInRenderer(); - - vtkSmartPointer m_yzActor; // YZ plane at X=0 - vtkSmartPointer m_xzActor; // XZ plane at Y=0 - vtkSmartPointer m_xyActor; // XY plane at Z=0 - - vtkSmartPointer m_yzMapper; - vtkSmartPointer m_xzMapper; - vtkSmartPointer m_xyMapper; - - // Tick label actors and mappers - std::vector> m_yzTickLabelActors; - std::vector> m_xzTickLabelActors; - std::vector> m_xyTickLabelActors; - - cvc::bounding_box m_bounds; - int m_divisionsX; - int m_divisionsY; - int m_divisionsZ; - - int m_tickIntervalX; - int m_tickIntervalY; - int m_tickIntervalZ; - - double m_yzPlaneColor[3]; - double m_xzPlaneColor[3]; - double m_xyPlaneColor[3]; - - double m_tickLabelColor[3]; - int m_tickLabelFontSize; - - bool m_yzPlaneVisible; - bool m_xzPlaneVisible; - bool m_xyPlaneVisible; - - vtkRenderer *m_renderer; // Track current renderer -}; - -#endif // GRIDNODE_H diff --git a/inc/volrover3/GridOptionsDialog.h b/inc/volrover3/GridOptionsDialog.h deleted file mode 100644 index 368121de..00000000 --- a/inc/volrover3/GridOptionsDialog.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef GRIDOPTIONSDIALOG_H -#define GRIDOPTIONSDIALOG_H - -#include -#include -#include - -class QCheckBox; -class QSpinBox; -class QDoubleSpinBox; -class QSlider; -class QPushButton; -class GridNode; - -class GridOptionsDialog : public QWidget { - Q_OBJECT - -public: - explicit GridOptionsDialog(std::shared_ptr gridNode, QWidget *parent = nullptr); - ~GridOptionsDialog() override; - -protected: - void showEvent(QShowEvent *event) override; - void closeEvent(QCloseEvent *event) override; - -private slots: - void applyChanges(); - void chooseYZPlaneColor(); - void chooseXZPlaneColor(); - void chooseXYPlaneColor(); - void chooseTickLabelColor(); - -private: - void setupUI(); - void connectSignals(); - void connectStateMonitoring(); - void disconnectStateMonitoring(); - void loadFromState(); - void onStateChanged(); - void updateColorButton(QPushButton *button, double r, double g, double b); - - // Plane visibility checkboxes - QCheckBox *m_yzPlaneCheckBox; // YZ plane at X=0 - QCheckBox *m_xzPlaneCheckBox; // XZ plane at Y=0 - QCheckBox *m_xyPlaneCheckBox; // XY plane at Z=0 - - // Tick visibility - QCheckBox *m_showTicksCheckBox; - - // Grid divisions spin boxes - QSpinBox *m_xDivisionsSpinBox; - QSpinBox *m_yDivisionsSpinBox; - QSpinBox *m_zDivisionsSpinBox; - - // Tick interval spin boxes - QSpinBox *m_xTickIntervalSpinBox; - QSpinBox *m_yTickIntervalSpinBox; - QSpinBox *m_zTickIntervalSpinBox; - - // Per-plane color buttons - QPushButton *m_yzPlaneColorButton; - QPushButton *m_xzPlaneColorButton; - QPushButton *m_xyPlaneColorButton; - - // Tick label properties - QPushButton *m_tickLabelColorButton; - QSpinBox *m_tickLabelFontSizeSpinBox; - - // Plane line width and opacity spin boxes - QDoubleSpinBox *m_yzLineWidthSpinBox; - QDoubleSpinBox *m_xzLineWidthSpinBox; - QDoubleSpinBox *m_xyLineWidthSpinBox; - QSlider *m_yzOpacitySlider; - QSlider *m_xzOpacitySlider; - QSlider *m_xyOpacitySlider; - - // Color storage - double m_yzPlaneColor[3]; - double m_xzPlaneColor[3]; - double m_xyPlaneColor[3]; - double m_tickLabelColor[3]; - - // Grid node reference - std::shared_ptr m_gridNode; - - // State change monitoring - std::vector m_stateConnections; - bool m_updatingFromState; -}; - -#endif // GRIDOPTIONSDIALOG_H diff --git a/inc/volrover3/IsosurfaceDialog.h b/inc/volrover3/IsosurfaceDialog.h deleted file mode 100644 index 010f8a31..00000000 --- a/inc/volrover3/IsosurfaceDialog.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef ISOSURFACEDIALOG_H -#define ISOSURFACEDIALOG_H - -#include -#include -#include -#include -#include - -class QComboBox; -class QDoubleSpinBox; -class QSpinBox; -class QCheckBox; -class QPushButton; -class QProgressBar; -class QLabel; -class SceneGraph; - -class IsosurfaceDialog : public QDialog { - Q_OBJECT - -public: - explicit IsosurfaceDialog(std::shared_ptr sceneGraph, QWidget *parent = nullptr); - ~IsosurfaceDialog() = default; - -private slots: - void onVolumeSelected(int index); - void onComputeClicked(); - void onGraphicsChildrenChanged(); - -private: - void setupUI(); - void connectSignals(); - void populateVolumeList(); - void updateProgress(int value); - void onComputeFinished(bool success, const std::string &message); - void setControlsEnabled(bool enabled); - - std::shared_ptr m_sceneGraph; - - // UI elements - QComboBox *m_volumeComboBox; - QDoubleSpinBox *m_isovalueSpinBox; - QComboBox *m_methodComboBox; - QSpinBox *m_improveIterationsSpinBox; - QComboBox *m_normalTypeComboBox; - QPushButton *m_computeButton; - QPushButton *m_cancelButton; - QProgressBar *m_progressBar; - QLabel *m_statusLabel; - - // Volume tracking - std::vector m_volumePaths; // Full state tree paths - - // Computation state - bool m_computing; - std::string m_activeThreadKey; - - // Signal connections - boost::signals2::scoped_connection m_graphicsChangedConnection; -}; - -#endif // ISOSURFACEDIALOG_H diff --git a/inc/volrover3/MainWindow.h b/inc/volrover3/MainWindow.h deleted file mode 100644 index bf09baff..00000000 --- a/inc/volrover3/MainWindow.h +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H - -#include -#include -#include -#include -#include -#include -#include -#include - -class VTKRenderWidget; -class TransferFunctionWidget; -class SceneGraph; -class ThreadMonitorWidget; -class StateTreeWidget; -class StateDashboardWidget; -class GridOptionsDialog; -class SDFDialog; -class IsosurfaceDialog; -class GeometryDialog; -class VolumeDialog; -class ViewerOptionsDialog; -class CameraSettingsDialog; - -class MainWindow : public QMainWindow { - Q_OBJECT - -public: - explicit MainWindow(QWidget *parent = nullptr); - ~MainWindow(); - -private slots: - void openFile(); - void toggleGrid(); - void toggleAxis(); - void editBoundingBox(); - void editCameraSettings(); - void showGridOptions(); - void showViewerOptions(); - void showThreadMonitor(); - void showStateTree(); - void showStateDashboard(); - void showSDF(); - void showIsosurface(); - void showGeometry(); - void showVolume(); - void aboutVolRover(); - void updateThreadStatus(); - void resetCamera(); - void generateStanfordBunny(); - void generateSphere(); - void generateCube(); - void generateTorus(); - void generateCone(); - -protected: - void closeEvent(QCloseEvent *event) override; - -private: - void createMenus(); - void createToolBar(); - void createDockWidgets(); - void setupConnections(); - void initializeCameraFromState(); - void setupStatusBar(); - - VTKRenderWidget *m_renderWidget; - TransferFunctionWidget *m_transferFunctionWidget; - std::shared_ptr m_sceneGraph; - ThreadMonitorWidget *m_threadMonitor; - StateTreeWidget *m_stateTreeWidget; - StateDashboardWidget *m_stateDashboardWidget; - GridOptionsDialog *m_gridOptionsDialog; - SDFDialog *m_sdfDialog; - IsosurfaceDialog *m_isosurfaceDialog; - GeometryDialog *m_geometryDialog; - VolumeDialog *m_volumeDialog; - ViewerOptionsDialog *m_viewerOptionsDialog; - CameraSettingsDialog *m_cameraDialog; - - // Toolbar - QToolBar *m_mainToolBar; - - // Status bar widgets for thread monitoring - QLabel *m_threadNameLabel; - QLabel *m_threadInfoLabel; - QProgressBar *m_threadProgressBar; - - std::vector m_connections; - - bool m_gridVisible; - bool m_axisVisible; -}; - -#endif // MAINWINDOW_H diff --git a/inc/volrover3/NullGraphicNode.h b/inc/volrover3/NullGraphicNode.h deleted file mode 100644 index 51be1253..00000000 --- a/inc/volrover3/NullGraphicNode.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef NULLGRAPHICNODE_H -#define NULLGRAPHICNODE_H - -#include -#include -#include - -class vtkActor; - -namespace cvc { -class state; -} - -/** - * @brief A graphics node that has no visual data, only a bounding box - * - * NullGraphicNode is used as a placeholder when no graphics are loaded. - * Unlike other graphics nodes, its bounding box extents are user-modifiable - * rather than being computed from data. - * - * Primary use case: Default graphic when scene is empty, showing only - * a bounding box to define the coordinate system and scene extents. - */ -class NullGraphicNode : public GraphicsNode { -public: - NullGraphicNode(cvc::app &ctx, const std::string &statePath, const std::string &name = "null"); - ~NullGraphicNode() override; - - // Set custom bounding box extents (user-modifiable) - void setBounds(const cvc::bounding_box &bbox); - void setBounds(double minX, double minY, double minZ, double maxX, double maxY, double maxZ); - - // Control whether this node's own bounds contribute to combined bbox - // When false, only children's bounds are included (useful for root nodes) - // When true, this node's bounds are included (useful for clipping regions) - void setIncludeOwnBounds(bool include); - bool getIncludeOwnBounds() const { return m_includeOwnBounds; } - - // Control whether this node's bounds automatically sync with children's combined bounds - // When true (default), bounds expand to encompass all children - // When false, bounds stay fixed (useful for clipping regions) - void setSyncBoundsWithChildren(bool sync); - bool getSyncBoundsWithChildren() const { return m_syncBoundsWithChildren; } - - // Manually trigger bounds sync to children - void syncBoundsToChildren(); - - // Override child management to auto-sync bounds - // Bring template version from base class into scope - using GraphicsNode::addGraphicsChild; - void addGraphicsChild(std::shared_ptr child) override; - - using GraphicsNode::removeGraphicsChild; - void removeGraphicsChild(std::shared_ptr child) override; - - // Implement GraphicsNode abstract methods - cvc::bounding_box getBoundingBox() const override; - -protected: - vtkProp *getProp() override; - void handleStateChanged(const std::string &childState) override; - -private: - cvc::bounding_box m_bounds; - vtkSmartPointer m_dummyActor; // Empty actor (never rendered) - bool m_includeOwnBounds; // Whether to include own bounds in combined bbox - bool m_syncBoundsWithChildren; // Whether to auto-update bounds to match children -}; - -#endif // NULLGRAPHICNODE_H diff --git a/inc/volrover3/ProceduralGeometryDialog.h b/inc/volrover3/ProceduralGeometryDialog.h deleted file mode 100644 index 1ec1ca9d..00000000 --- a/inc/volrover3/ProceduralGeometryDialog.h +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef PROCEDURALGEOMETRYDIALOG_H -#define PROCEDURALGEOMETRYDIALOG_H - -#include -#include -#include - -class QDoubleSpinBox; -class QSpinBox; -class QLabel; -class QDialogButtonBox; -class QVBoxLayout; -class QFormLayout; -class SceneGraph; - -// Enum for procedural geometry types -enum class ProceduralGeometryType { Sphere, Cube, Torus, Cone }; - -class ProceduralGeometryDialog : public QDialog { - Q_OBJECT - -public: - explicit ProceduralGeometryDialog(ProceduralGeometryType type, - std::shared_ptr sceneGraph, - QWidget *parent = nullptr); - ~ProceduralGeometryDialog() override = default; - -private slots: - void onGenerate(); - -private: - void setupUI(); - void setupSphereUI(QFormLayout *formLayout); - void setupCubeUI(QFormLayout *formLayout); - void setupTorusUI(QFormLayout *formLayout); - void setupConeUI(QFormLayout *formLayout); - - void generateSphere(); - void generateCube(); - void generateTorus(); - void generateCone(); - - std::string getUniqueName(const std::string &baseName); - - ProceduralGeometryType m_type; - std::shared_ptr m_sceneGraph; - - // Common parameters - QDoubleSpinBox *m_centerXSpinBox; - QDoubleSpinBox *m_centerYSpinBox; - QDoubleSpinBox *m_centerZSpinBox; - - // Sphere parameters - QDoubleSpinBox *m_radiusSpinBox; - QSpinBox *m_thetaResSpinBox; - QSpinBox *m_phiResSpinBox; - - // Cube parameters - QDoubleSpinBox *m_sizeXSpinBox; - QDoubleSpinBox *m_sizeYSpinBox; - QDoubleSpinBox *m_sizeZSpinBox; - - // Torus parameters - QDoubleSpinBox *m_majorRadiusSpinBox; - QDoubleSpinBox *m_minorRadiusSpinBox; - QSpinBox *m_majorResSpinBox; - QSpinBox *m_minorResSpinBox; - - // Cone parameters - QDoubleSpinBox *m_coneRadiusSpinBox; - QDoubleSpinBox *m_coneHeightSpinBox; - QSpinBox *m_coneResSpinBox; - QSpinBox *m_coneCapResSpinBox; - - QDialogButtonBox *m_buttonBox; -}; - -#endif // PROCEDURALGEOMETRYDIALOG_H diff --git a/inc/volrover3/SDFDialog.h b/inc/volrover3/SDFDialog.h deleted file mode 100644 index 30dcde0f..00000000 --- a/inc/volrover3/SDFDialog.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef SDFDIALOG_H -#define SDFDIALOG_H - -#include -#include -#include -#include - -class QComboBox; -class QSpinBox; -class QDoubleSpinBox; -class QCheckBox; -class QProgressBar; -class QPushButton; -class QLabel; -class SceneGraph; - -class SDFDialog : public QDialog { - Q_OBJECT - -public: - explicit SDFDialog(std::shared_ptr sceneGraph, QWidget *parent = nullptr); - ~SDFDialog() override = default; - -private slots: - void onComputeClicked(); - void onGeometrySelected(int index); - void updateProgress(int value); - void onComputeFinished(bool success, const std::string &message); - void onGraphicsChildrenChanged(); - -private: - void setupUI(); - void connectSignals(); - void populateGeometryList(); - void setControlsEnabled(bool enabled); - - std::shared_ptr m_sceneGraph; - - // UI controls - QComboBox *m_geometryComboBox; - QSpinBox *m_dimXSpinBox; - QSpinBox *m_dimYSpinBox; - QSpinBox *m_dimZSpinBox; - QComboBox *m_algorithmComboBox; - QCheckBox *m_flipNormalsCheckBox; - QCheckBox *m_useBoundsCheckBox; - QDoubleSpinBox *m_minXSpinBox; - QDoubleSpinBox *m_minYSpinBox; - QDoubleSpinBox *m_minZSpinBox; - QDoubleSpinBox *m_maxXSpinBox; - QDoubleSpinBox *m_maxYSpinBox; - QDoubleSpinBox *m_maxZSpinBox; - QPushButton *m_computeButton; - QPushButton *m_cancelButton; - QProgressBar *m_progressBar; - QLabel *m_statusLabel; - - // Geometry tracking - std::vector m_geometryNames; - - // State tree connection for monitoring geometry changes - boost::signals2::scoped_connection m_graphicsChildrenConnection; - - // Thread tracking - bool m_computing; - std::string m_activeThreadKey; -}; - -#endif // SDFDIALOG_H diff --git a/inc/volrover3/SceneGraph.h b/inc/volrover3/SceneGraph.h deleted file mode 100644 index 4285ba20..00000000 --- a/inc/volrover3/SceneGraph.h +++ /dev/null @@ -1,189 +0,0 @@ -#ifndef SCENEGRAPH_H -#define SCENEGRAPH_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class vtkRenderer; -class vtkMultiVolume; -class SceneNode; -class NullGraphicNode; -class GridNode; -class AxisNode; -class BBoxNode; - -namespace cvc { -class geometry; -class volume; -class state; -} // namespace cvc - -class SceneGraph { -public: - SceneGraph(const std::string &statePrefix = "volrover3"); - ~SceneGraph(); - - // Get the state prefix for this scene graph - std::string getStatePrefix() const { return m_statePrefix; } - - void setRenderer(vtkRenderer *renderer); - void update(); - - // Process pending events on the main thread - // This MUST be called regularly from the main event loop - void processEvents(); - - // Post a callback to be executed on the main thread during processEvents() - // This is thread-safe and can be called from any thread - void postEvent(std::function callback); - - // Check if a render is needed and reset the flag - bool checkAndResetRenderNeeded(); - - // Multi-object graphics management (unified for both geometry and volumes) - std::shared_ptr addGraphics(const std::string &name, const cvc::geometry &geom); - std::shared_ptr addGraphics(const std::string &name, const cvc::volume &vol); - std::shared_ptr - addGraphics(const std::string &name); // Empty graphics node for hierarchy - bool hasGraphics(const std::string &name) const; - void removeGraphics(const std::string &name); - std::shared_ptr getGraphics(const std::string &name); - std::shared_ptr getGraphicsRoot() { return m_graphicsRoot; } - std::shared_ptr getGridNode() { return m_gridNode; } - const std::map> &getAllGraphics() const { - return m_graphicsNodes; - } - void registerGraphics(const std::string &name, - std::shared_ptr node); // For manual registration - - // Generic templated method to recursively get all graphics nodes of a specific type - template std::vector> getAllGraphicsOfType() const { - std::vector> result; - - // Helper lambda for recursive traversal - std::function)> collectNodes; - collectNodes = [&](std::shared_ptr node) { - if (!node) - return; - - // Check if this node is of type T - auto typedNode = std::dynamic_pointer_cast(node); - if (typedNode) { - result.push_back(typedNode); - } - - // Recursively check all children - for (const auto &child : node->getGraphicsChildren()) { - collectNodes(child); - } - }; - - // Start traversal from graphics root - if (m_graphicsRoot) { - collectNodes(m_graphicsRoot); - } - - return result; - } - - // Convenience wrappers for common types - std::vector> getAllVolumeGraphics() const { - return getAllGraphicsOfType(); - } - size_t getVolumeGraphicsCount() const { return getAllVolumeGraphics().size(); } - std::vector> getAllGeometryGraphics() const { - return getAllGraphicsOfType(); - } - size_t getGeometryGraphicsCount() const { return getAllGeometryGraphics().size(); } - - // Multi-volume rendering control - void enableMultiVolumeRendering(bool enable); - bool isMultiVolumeRenderingEnabled() const; - - // Scene element visibility - void setGridVisible(bool visible); - void setAxisVisible(bool visible); - - // Scene element colors - void setGridColor(double r, double g, double b); - - // Grid plane visibility - void setGridPlaneVisibility(bool yz, bool xz, bool xy); - - // Grid divisions - void setGridDivisions(int x, int y, int z); - - // Grid tick intervals - void setGridTickIntervals(int x, int y, int z); - - // Per-plane grid colors - void setGridPlaneColors(double yzR, double yzG, double yzB, double xzR, double xzG, double xzB, - double xyR, double xyG, double xyB); - - // Grid tick label properties - void setGridTickLabelProperties(double r, double g, double b, int fontSize); - - // Update grid to match bounds - void updateGrid(const cvc::bounding_box &bounds); - - // Compute combined bounding box of all graphics - cvc::bounding_box computeGraphicsBounds() const; - - // Compute combined bounding box of all volumes - cvc::bounding_box computeVolumeBounds() const; - - // Transfer function update - void updateTransferFunction(const std::vector &colorTable, - const std::vector &opacityTable); - - // Signal emitted when graphics are added or removed - boost::signals2::signal graphicsChanged; - -private: - vtkRenderer *m_renderer; - std::string m_statePrefix; - - std::shared_ptr m_gridNode; - std::shared_ptr m_axisNode; - - // Event queue for thread-safe main thread execution - std::queue> m_eventQueue; - std::mutex m_eventQueueMutex; - bool m_renderNeeded; - - std::vector> m_rootNodes; - - // Multi-object graphics system (includes both geometry and volume graphics) - std::shared_ptr m_graphicsRoot; // Root node for all graphics - std::map> m_graphicsNodes; // Flat lookup by name - std::shared_ptr m_nullGraphic; // Placeholder when scene is empty - - // Multi-volume rendering state - bool m_multiVolumeRenderingEnabled; - vtkSmartPointer m_multiVolume; // For multi-volume rendering when needed - - // Private helper methods for multi-volume rendering - void setupMultiVolumeRendering(); - void teardownMultiVolumeRendering(); - void updateVolumeRendering(); - - // Null graphic management - void ensureNullGraphicIfEmpty(); - void removeNullGraphicIfPresent(); - - // Connection for root node bounds changes - boost::signals2::connection m_rootBoundsConnection; -}; - -#endif // SCENEGRAPH_H diff --git a/inc/volrover3/SceneNode.h b/inc/volrover3/SceneNode.h deleted file mode 100644 index fbc7c054..00000000 --- a/inc/volrover3/SceneNode.h +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef SCENENODE_H -#define SCENENODE_H - -#include -#include -#include -#include -#include - -class vtkProp; -class vtkRenderer; -class SceneGraph; - -class SceneNode : public cvc::state_object { -public: - SceneNode(cvc::app &ctx, const std::string &statePath); - virtual ~SceneNode(); - - // Access the app context this node is bound to. Subclasses use this when - // creating child nodes so that the singleton is not consulted. - cvc::app &app() const { return _ctx; } - - virtual void addToRenderer(vtkRenderer *renderer); - virtual void removeFromRenderer(vtkRenderer *renderer); - virtual void update(); - - void setVisible(bool visible); - bool isVisible() const { return m_visible; } - - void addChild(std::shared_ptr child); - void removeChild(std::shared_ptr child); - - // SceneGraph association (set when node is added to a scene graph) - void setSceneGraph(SceneGraph *sceneGraph); - SceneGraph *getSceneGraph() const { return m_sceneGraph; } - - // DEPRECATED: Old callback system - kept for compatibility during transition - // Use node's SceneGraph::postEvent() instead - using MainThreadCallback = std::function)>; - static void setMainThreadCallback(MainThreadCallback callback); - -protected: - virtual vtkProp *getProp() = 0; - virtual void handleStateChanged(const std::string &childState) override; - - // Execute a function on the main thread (if callback is set) - void runOnMainThread(std::function func); - -private: - static MainThreadCallback s_mainThreadCallback; - -protected: - bool m_visible; - std::vector> m_children; - vtkRenderer *m_renderer; - SceneGraph *m_sceneGraph; // Non-owning pointer to parent SceneGraph -}; - -#endif // SCENENODE_H diff --git a/inc/volrover3/StateDashboardWidget.h b/inc/volrover3/StateDashboardWidget.h deleted file mode 100644 index bdf2c2b1..00000000 --- a/inc/volrover3/StateDashboardWidget.h +++ /dev/null @@ -1,145 +0,0 @@ -#ifndef STATEDASHBOARDWIDGET_H -#define STATEDASHBOARDWIDGET_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 - -/// Comprehensive state management dashboard combining tree navigation, -/// DSL execution console, and cluster networking in a tabbed interface. -class StateDashboardWidget : public QWidget { - Q_OBJECT - -public: - explicit StateDashboardWidget(QWidget *parent = nullptr); - ~StateDashboardWidget() override; - - /// Set the root state node for the tree view. - void setRootState(cvc::state *root); - - /// Attach a scheduler for process management in the exec tab. - void setScheduler(cvc::state_exec::scheduler *sched); - - /// Attach a cluster shard for distributed networking in the cluster tab. - void setShard(cvc::state_cluster_shard *shard); - - /// Attach a membership manager for peer management. - void setMembership(cvc::state_cluster_membership *membership); - - /// Attach a coordinator for distributed exec management. - void setCoordinator(cvc::state_exec::exec_coordinator *coord); - - /// Attach a telemetry aggregator for cluster stats. - void setTelemetryAggregator(cvc::state_telemetry_aggregator *agg); - - /// Refresh all tabs. - void refresh(); - -signals: - void stateChanged(); - - // ─── State Tree tab ──────────────────────────────────────────────── -private slots: - void onTreeItemSelected(); - void onTreeSearchTextChanged(const QString &text); - void onPropertyValueChanged(int row, int column); - void onAddStateClicked(); - void onDeleteStateClicked(); - void onTreeStructureChanged(); - void onCurrentStateChanged(); - void onCurrentStateDestroyed(); - -private: - void buildStateTreeTab(QTabWidget *tabs); - void refreshStateTree(); - void populateTree(QTreeWidgetItem *parentItem, cvc::state *state); - void populateProperties(cvc::state *state); - QTreeWidgetItem *findTreeItem(QTreeWidgetItem *parent, cvc::state *target); - std::string getStateValue(cvc::state *state); - void setStateValue(cvc::state *state, const QString &valueStr); - - QLineEdit *m_treeSearch; - QTreeWidget *m_treeWidget; - QTableWidget *m_propertyTable; - QPushButton *m_addStateBtn; - QPushButton *m_deleteStateBtn; - - cvc::state *m_rootState = nullptr; - cvc::state *m_currentState = nullptr; - - boost::signals2::connection m_valueConn; - boost::signals2::connection m_treeConn; - boost::signals2::connection m_destroyConn; - - // ─── State Exec Console tab ──────────────────────────────────────── -private slots: - void onRunScriptClicked(); - void onClearOutputClicked(); - void onProcessTableSelectionChanged(); - void onPauseProcessClicked(); - void onResumeProcessClicked(); - void onKillProcessClicked(); - void refreshProcessList(); - -private: - void buildExecConsoleTab(QTabWidget *tabs); - - QPlainTextEdit *m_scriptEditor; - QPushButton *m_runBtn; - QPushButton *m_clearOutputBtn; - QPlainTextEdit *m_outputPanel; - QTableWidget *m_processTable; - QPushButton *m_pauseBtn; - QPushButton *m_resumeBtn; - QPushButton *m_killBtn; - QTimer *m_processRefreshTimer; - - cvc::state_exec::scheduler *m_scheduler = nullptr; - - // ─── Cluster & Networking tab ────────────────────────────────────── -private slots: - void onConnectPeerClicked(); - void refreshClusterInfo(); - -private: - void buildClusterTab(QTabWidget *tabs); - - QLabel *m_nodeIdLabel; - QLabel *m_clusterIdLabel; - QLabel *m_leaderLabel; - QTableWidget *m_peerTable; - QLineEdit *m_peerEndpointInput; - QPushButton *m_connectPeerBtn; - QLabel *m_busStatsLabel; - QLabel *m_shardStatsLabel; - QLabel *m_telemetryLabel; - QTimer *m_clusterRefreshTimer; - - cvc::state_cluster_shard *m_shard = nullptr; - cvc::state_cluster_membership *m_membership = nullptr; - cvc::state_exec::exec_coordinator *m_coordinator = nullptr; - cvc::state_telemetry_aggregator *m_telemetryAgg = nullptr; -}; - -#endif // STATEDASHBOARDWIDGET_H diff --git a/inc/volrover3/StateTreeWidget.h b/inc/volrover3/StateTreeWidget.h deleted file mode 100644 index 12d29cfd..00000000 --- a/inc/volrover3/StateTreeWidget.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef STATETREEWIDGET_H -#define STATETREEWIDGET_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class StateTreeWidget : public QWidget { - Q_OBJECT - -public: - explicit StateTreeWidget(QWidget *parent = nullptr); - ~StateTreeWidget() override; - - void setRootState(cvc::state *root); - void refresh(); - -signals: - void stateChanged(); // Emitted when state data is modified - -private slots: - void onTreeItemSelected(); - void onTableValueChanged(int row, int column); - void onAddStateClicked(); - void onDeleteStateClicked(); - void onCurrentStateChanged(); - void onTreeStructureChanged(); - void onCurrentStateDestroyed(); - -private: - void populateTree(QTreeWidgetItem *parentItem, cvc::state *state, const std::string &path); - void populateTable(cvc::state *state); - std::string getStateValue(cvc::state *state); - std::string getStateDataType(cvc::state *state); - void setStateValue(cvc::state *state, const QString &valueStr); - QTreeWidgetItem *findTreeItem(QTreeWidgetItem *parent, cvc::state *state); - - QTreeWidget *m_treeWidget; - QTableWidget *m_tableWidget; - QPushButton *m_addButton; - QPushButton *m_deleteButton; - - // Non-owning pointers to states (owned by the cvc::state singleton tree) - // We use the destroyed signal to track when states are deleted - cvc::state *m_rootState; - cvc::state *m_currentState; - - boost::signals2::connection m_stateChangeConnection; - boost::signals2::connection m_treeChangeConnection; - boost::signals2::connection m_currentStateDestroyedConnection; -}; - -#endif // STATETREEWIDGET_H diff --git a/inc/volrover3/ThreadMonitorWidget.h b/inc/volrover3/ThreadMonitorWidget.h deleted file mode 100644 index a4a02fa3..00000000 --- a/inc/volrover3/ThreadMonitorWidget.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef VOLROVER3_THREADMONITORWIDGET_H -#define VOLROVER3_THREADMONITORWIDGET_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ThreadMonitorWidget : public QWidget { - Q_OBJECT - -public: - explicit ThreadMonitorWidget(QWidget *parent = nullptr); - ~ThreadMonitorWidget(); - -signals: - // Emitted when a thread completes, with thread name and info for status bar - void threadCompleted(const QString &threadName, const QString &threadInfo); - -public slots: - void requestUpdate(); - void performUpdate(); - void cancelThread(const std::string &threadKey); - void cleanupCompletedThreads(); - -private: - void setupUI(); - void updateThreadTable(); - void registerCallbacks(); - void disconnectCallbacks(); - QString formatProgress(double progress); - - QTableWidget *m_threadTable; - QTimer *m_updateTimer; - QTimer *m_cleanupTimer; // Timer to remove completed threads after delay - QElapsedTimer m_lastUpdateTime; - bool m_updatePending; - - // Track when threads completed (thread key -> completion timestamp) - std::map m_completedThreads; - static const int CLEANUP_DELAY_MS = 60000; // Remove completed threads after 60 seconds - - // Callback connections for cleanup - std::vector m_connections; - - // Column indices - enum Column { - COL_NAME = 0, - COL_STATUS = 1, - COL_PROGRESS = 2, - COL_PROGRESS_BAR = 3, - COL_CANCEL = 4, - COL_COUNT = 5 - }; -}; - -#endif // VOLROVER3_THREADMONITORWIDGET_H diff --git a/inc/volrover3/TransferFunctionWidget.h b/inc/volrover3/TransferFunctionWidget.h deleted file mode 100644 index a1db7780..00000000 --- a/inc/volrover3/TransferFunctionWidget.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef TRANSFERFUNCTIONWIDGET_H -#define TRANSFERFUNCTIONWIDGET_H - -#include -#include -#include -#include -#include - -class QCustomPlot; -class QCPGraph; -class QCPColorMap; -class QComboBox; -class SceneGraph; -class VolumeNode; - -class TransferFunctionWidget : public QWidget { - Q_OBJECT - -public: - // Color control points (value, r, g, b) - struct ColorPoint { - double value; - QColor color; - }; - - // Opacity control points (value, opacity) - struct OpacityPoint { - double value; - double opacity; - }; - - explicit TransferFunctionWidget(QWidget *parent = nullptr); - ~TransferFunctionWidget(); - - void setDataRange(double min, double max); - - std::vector getColorTable() const; - std::vector getOpacityTable() const; - - void applyPreset(const QString &presetName); - - // Volume selection - void setSceneGraph(SceneGraph *sceneGraph); - void refreshVolumeList(); - std::shared_ptr getSelectedVolume() const; - -signals: - void transferFunctionChanged(); - void selectedVolumeChanged(std::shared_ptr volume); - -private slots: - void onPresetChanged(int index); - void onColorMapClicked(double x, double y); - void onOpacityGraphChanged(); - void onVolumeSelected(int index); - void onGraphicsChildrenChanged(); - void onVolumeTransferFunctionChanged(); - -private: - void setupUI(); - void createDefaultTransferFunction(); - void updateColorBar(); - void loadTransferFunctionFromVolume(std::shared_ptr volume); - void connectToVolumeState(std::shared_ptr volume); - void disconnectFromVolumeState(); - - QComboBox *m_presetCombo; - QComboBox *m_volumeCombo; - QWidget *m_colorBarWidget; - QWidget *m_opacityWidget; - - double m_dataMin; - double m_dataMax; - - std::vector m_colorPoints; - std::vector m_opacityPoints; - - SceneGraph *m_sceneGraph; - std::vector> m_volumes; - - // State tree connections - boost::signals2::scoped_connection m_graphicsChildrenConnection; - boost::signals2::scoped_connection m_colorTFConnection; - boost::signals2::scoped_connection m_opacityTFConnection; - - // Track if we're updating from state to prevent feedback loops - // Use counter instead of bool to handle nested/queued updates - int m_updatingFromState = 0; -}; - -#endif // TRANSFERFUNCTIONWIDGET_H diff --git a/inc/volrover3/VTKRenderWidget.h b/inc/volrover3/VTKRenderWidget.h deleted file mode 100644 index 1d8810b1..00000000 --- a/inc/volrover3/VTKRenderWidget.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef VTKRENDERWIDGET_H -#define VTKRENDERWIDGET_H - -#include -#include -#include -#include - -// Qt5/Qt6 compatibility -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#define QVTK_WIDGET_BASE QVTKOpenGLNativeWidget -#else -#include -#define QVTK_WIDGET_BASE QVTKOpenGLWidget -#endif - -class vtkRenderer; -class vtkRenderWindow; -class vtkGenericOpenGLRenderWindow; -class vtkCornerAnnotation; -class SceneGraph; -class CameraController; - -class VTKRenderWidget : public QVTK_WIDGET_BASE { - Q_OBJECT - -public: - explicit VTKRenderWidget(QWidget *parent = nullptr); - ~VTKRenderWidget(); - - void setSceneGraph(std::shared_ptr sceneGraph); - void resetCamera(); - void render(); // Force an immediate render - - // FPS display control - void setShowFPS(bool show); - bool showFPS() const { return m_showFPS; } - - CameraController *getCameraController() { return m_cameraController.get(); } - -protected: - void keyPressEvent(QKeyEvent *event) override; - void keyReleaseEvent(QKeyEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - -private slots: - void processSceneGraphEvents(); - void updateFPSDisplay(); - -private: - void initializeVTK(); - void updateCamera(); - - vtkSmartPointer m_renderWindow; - vtkSmartPointer m_renderer; - std::shared_ptr m_sceneGraph; - std::unique_ptr m_cameraController; - QTimer m_eventTimer; // Timer for processing SceneGraph events - - // FPS display - vtkSmartPointer m_fpsAnnotation; - QTimer m_fpsTimer; // Timer for updating FPS display - bool m_showFPS; - - QPoint m_lastMousePos; -}; - -#endif // VTKRENDERWIDGET_H diff --git a/inc/volrover3/ViewerOptionsDialog.h b/inc/volrover3/ViewerOptionsDialog.h deleted file mode 100644 index f0f97b6f..00000000 --- a/inc/volrover3/ViewerOptionsDialog.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef VIEWEROPTIONSDIALOG_H -#define VIEWEROPTIONSDIALOG_H - -#include -#include -#include - -class QCheckBox; -class QComboBox; -class QPushButton; -class VTKRenderWidget; -class SceneGraph; - -class ViewerOptionsDialog : public QWidget { - Q_OBJECT - -public: - explicit ViewerOptionsDialog(VTKRenderWidget *renderWidget, - std::shared_ptr sceneGraph, QWidget *parent = nullptr); - ~ViewerOptionsDialog() override; - -protected: - void showEvent(QShowEvent *event) override; - void closeEvent(QCloseEvent *event) override; - -private slots: - void onShowFPSChanged(bool checked); - void onGraphicsRootChanged(int index); - void onCameraChanged(int index); - void refreshGraphicsRoots(); - void refreshCameras(); - -private: - void setupUI(); - void connectSignals(); - void loadFromState(); - - VTKRenderWidget *m_renderWidget; - std::shared_ptr m_sceneGraph; - - // Display options - QCheckBox *m_showFPSCheckBox; - - // Scene selection - QComboBox *m_graphicsRootComboBox; - QPushButton *m_refreshRootsButton; - - // Camera selection - QComboBox *m_cameraComboBox; - QPushButton *m_refreshCamerasButton; - - std::vector m_connections; -}; - -#endif // VIEWEROPTIONSDIALOG_H diff --git a/inc/volrover3/VolumeDialog.h b/inc/volrover3/VolumeDialog.h deleted file mode 100644 index ff68b83c..00000000 --- a/inc/volrover3/VolumeDialog.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef VOLUMEDIALOG_H -#define VOLUMEDIALOG_H - -#include -#include -#include -#include -#include - -class QComboBox; -class QDoubleSpinBox; -class QCheckBox; -class QGroupBox; -class SceneGraph; - -class VolumeDialog : public QDialog { - Q_OBJECT - -public: - explicit VolumeDialog(std::shared_ptr sceneGraph, QWidget *parent = nullptr); - ~VolumeDialog() = default; - -private slots: - void onVolumeSelected(int index); - void onGraphicsChildrenChanged(); - void onMaterialPropertyChanged(); - void onDeleteButtonClicked(); - void onNodeStateChanged(); - -private: - void setupUI(); - void connectSignals(); - void populateVolumeList(); - void updatePropertiesFromNode(); - void setPropertiesEnabled(bool enabled); - - std::shared_ptr m_sceneGraph; - - // UI elements - QComboBox *m_volumeComboBox; - QPushButton *m_deleteButton; - - // Rendering properties - QCheckBox *m_shadingCheckBox; - QDoubleSpinBox *m_ambientSpinBox; - QDoubleSpinBox *m_diffuseSpinBox; - QDoubleSpinBox *m_specularSpinBox; - QDoubleSpinBox *m_specularPowerSpinBox; - QDoubleSpinBox *m_scalarOpacityUnitDistanceSpinBox; - QDoubleSpinBox *m_sampleDistanceSpinBox; - QCheckBox *m_autoAdjustSampleDistancesCheckBox; - - // Volume tracking - std::vector m_volumePaths; // Full state tree paths - - // Signal connections - boost::signals2::scoped_connection m_graphicsChangedConnection; - boost::signals2::scoped_connection m_nodeStateConnection; - - // Flag to prevent recursive updates - bool m_updating; -}; - -#endif // VOLUMEDIALOG_H diff --git a/inc/volrover3/VolumeNode.h b/inc/volrover3/VolumeNode.h deleted file mode 100644 index 9e7a2fc0..00000000 --- a/inc/volrover3/VolumeNode.h +++ /dev/null @@ -1,135 +0,0 @@ -#ifndef VOLUMENODE_H -#define VOLUMENODE_H - -#include -#include -#include -#include - -class vtkVolume; -class vtkSmartVolumeMapper; -class vtkImageData; -class vtkColorTransferFunction; -class vtkPiecewiseFunction; -class vtkVolumeProperty; - -namespace cvc { -class volume; -class state; -} // namespace cvc - -/** - * @brief VolumeNode renders cvc::volume objects with full transform support - * - * Extends GraphicsNode to provide: - * - Volume-specific rendering (ray casting, GPU volume rendering) - * - Transfer function control (color and opacity) - * - Bounding box computation from volume bounds - * - State tree synchronization for volume data - * - * Inherits from GraphicsNode: - * - Transforms (position, rotation, scale) - * - Metadata storage - * - Bounding box display - * - Hierarchical structure - */ -class VolumeNode : public GraphicsNode { -public: - VolumeNode(cvc::app &ctx, const std::string &statePath, const std::string &name = "volume"); - ~VolumeNode() override; - - // Generic setData for template compatibility - void setData(const cvc::volume &vol) { setVolume(vol); } - - void setVolume(const cvc::volume &vol); - bool hasVolume() const { return m_hasVolume; } - const cvc::volume *getVolume() const { return m_volume.get(); } - - void setTransferFunction(const std::vector &colorTable, - const std::vector &opacityTable); - void setDefaultTransferFunction(); - - std::vector getTransferFunctionColorTable() const; - std::vector getTransferFunctionOpacityTable() const; - - // Volume rendering property getters and setters - void setShading(bool enabled); - bool getShading() const { return m_shading; } - - void setAmbient(double value); - double getAmbient() const { return m_ambient; } - - void setDiffuse(double value); - double getDiffuse() const { return m_diffuse; } - - void setSpecular(double value); - double getSpecular() const { return m_specular; } - - void setSpecularPower(double value); - double getSpecularPower() const { return m_specularPower; } - - void setScalarOpacityUnitDistance(double value); - double getScalarOpacityUnitDistance() const { return m_scalarOpacityUnitDistance; } - - void setSampleDistance(double value); - double getSampleDistance() const { return m_sampleDistance; } - - void setAutoAdjustSampleDistances(bool enabled); - bool getAutoAdjustSampleDistances() const { return m_autoAdjustSampleDistances; } - - // Implement GraphicsNode abstract methods - cvc::bounding_box getBoundingBox() const override; - - // Override to add logging - void addToRenderer(vtkRenderer *renderer) override; - - // Check if a metadata key is computed (read-only) - static bool isComputedMetadata(const std::string &key); - -protected: - vtkProp *getProp() override; - void handleStateChanged(const std::string &childState) override; - void applyTransformToVTK() override; // Apply transform to volume - void applyClipPlanes(vtkPlaneCollection *planes) override; // Apply clip planes to volume mapper - void updateImageData(const cvc::volume &vol); - void updateTransferFunctions(); - void updateMetadata(const cvc::volume &vol); - void onDataChanged(); - -private: - bool m_hasVolume; - std::shared_ptr m_volume; - - vtkSmartPointer m_vtkVolume; - vtkSmartPointer m_mapper; - vtkSmartPointer m_imageData; - vtkSmartPointer m_colorFunc; - vtkSmartPointer m_opacityFunc; - vtkSmartPointer m_volumeProperty; - - double m_dataMin; - double m_dataMax; - - // Volume rendering properties - bool m_shading; - double m_ambient; - double m_diffuse; - double m_specular; - double m_specularPower; - double m_scalarOpacityUnitDistance; - double m_sampleDistance; - bool m_autoAdjustSampleDistances; - - cvc::state *m_stateNode; - boost::signals2::connection m_dataConnection; - boost::signals2::connection m_shadingConnection; - boost::signals2::connection m_ambientConnection; - boost::signals2::connection m_diffuseConnection; - boost::signals2::connection m_specularConnection; - boost::signals2::connection m_specularPowerConnection; - boost::signals2::connection m_scalarOpacityUnitDistanceConnection; - boost::signals2::connection m_sampleDistanceConnection; - boost::signals2::connection m_autoAdjustSampleDistancesConnection; -}; - -#endif // VOLUMENODE_H diff --git a/inc/volrover3/volrover3_app.h b/inc/volrover3/volrover3_app.h deleted file mode 100644 index 417f61c4..00000000 --- a/inc/volrover3/volrover3_app.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef VOLROVER3_APP_H -#define VOLROVER3_APP_H - -#include - -// -------------------------------------------------------------------- -// volrover3_app() -// -------------------------------------------------------------------- -// Purpose: -// Returns a reference to the per-process cvc::app instance used by -// volrover3. Uses a function-local static so callers never touch -// cvc::app::instance() (the singleton being phased out). -// -------------------------------------------------------------------- -namespace volrover3 { -cvc::app &app(); -} - -#endif diff --git a/share/applications/volrover3.desktop.in b/share/applications/volrover3.desktop.in deleted file mode 100644 index 7ad1e33c..00000000 --- a/share/applications/volrover3.desktop.in +++ /dev/null @@ -1,13 +0,0 @@ -[Desktop Entry] -Type=Application -Version=1.0 -Name=VolumeRover3 -GenericName=Volumetric Data Visualizer -Comment=Interactive visualization of volumetric data -Exec=volrover3 %F -Icon=volrover_logo -Terminal=false -Categories=Graphics;Science;DataVisualization;Education; -StartupNotify=true -MimeType=application/x-rawiv;application/x-mrc;application/x-hdf5; -Keywords=volume;visualization;rendering;medical;scientific; diff --git a/share/icons/volrover_logo.icns b/share/icons/volrover_logo.icns deleted file mode 100644 index dfb15caa..00000000 Binary files a/share/icons/volrover_logo.icns and /dev/null differ diff --git a/share/icons/volrover_logo.ico b/share/icons/volrover_logo.ico deleted file mode 100644 index 660384a3..00000000 Binary files a/share/icons/volrover_logo.ico and /dev/null differ diff --git a/share/icons/volrover_logo.png b/share/icons/volrover_logo.png deleted file mode 100644 index 2ffcebf1..00000000 Binary files a/share/icons/volrover_logo.png and /dev/null differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aeaa65f1..72ed5224 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,28 +13,6 @@ endif() # Note: CUDA is now integrated via native CMake support (3.17+) # No special clean targets needed - handled automatically -# Build volrover3 application if Qt (5 or 6) and VTK are available -option(CVC_BUILD_VOLROVER3 "Build VolRover3 application" ON) -if(CVC_BUILD_VOLROVER3) - find_package(Qt6 QUIET COMPONENTS Core Widgets OpenGL OpenGLWidgets) - if(NOT Qt6_FOUND) - find_package(Qt5 QUIET COMPONENTS Core Widgets OpenGL) - endif() - find_package(VTK QUIET) - - if((Qt6_FOUND OR Qt5_FOUND) AND VTK_FOUND) - if(Qt6_FOUND) - message(STATUS "Building VolRover3 application (Qt6 and VTK found)") - else() - message(STATUS "Building VolRover3 application (Qt5 and VTK found)") - endif() - add_subdirectory(volrover3) - else() - if(NOT Qt6_FOUND AND NOT Qt5_FOUND) - message(STATUS "Neither Qt6 nor Qt5 found - skipping VolRover3 build") - endif() - if(NOT VTK_FOUND) - message(STATUS "VTK not found - skipping VolRover3 build") - endif() - endif() -endif() +# Note: the VolRover3 application moved to the volrover repository +# (https://github.com/transfix/volrover), where it consumes libcvc as +# an external SDK via find_package(cvc CONFIG). diff --git a/src/cvc/CMakeLists.txt b/src/cvc/CMakeLists.txt index 6ab1482c..fc5fe7d3 100644 --- a/src/cvc/CMakeLists.txt +++ b/src/cvc/CMakeLists.txt @@ -970,6 +970,18 @@ install(DIRECTORY ${PROJECT_SOURCE_DIR}/inc/cvc/ FILES_MATCHING PATTERN "*.h" ) +# The vendored XmlRpc++ headers must always ship with the SDK: +# cvc/utility/utility.h unconditionally does #include , +# so external find_package(cvc) consumers need these headers on the +# include path even when the xmlrpc network library itself was not +# built (CVC_USING_XMLRPC=OFF). When CVC_USING_XMLRPC=ON the xmlrpc +# subproject installs the same files; the duplicate rule is idempotent. +install(DIRECTORY ${PROJECT_SOURCE_DIR}/inc/xmlrpc + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + COMPONENT libcvc + FILES_MATCHING PATTERN "*.h" +) + install(FILES ${PROJECT_BINARY_DIR}/inc/cvc/core/config.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cvc/core COMPONENT libcvc diff --git a/src/volrover3/AppState.cpp b/src/volrover3/AppState.cpp deleted file mode 100644 index 3b9cfc72..00000000 --- a/src/volrover3/AppState.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -AppState &AppState::instance() { - static AppState instance; // Uses default parameter "volrover3" - return instance; -} - -AppState::AppState(const std::string &statePrefix) : m_statePrefix(statePrefix) { - initializeDefaults(); -} - -void AppState::initializeDefaults() { - // Initialize default world bounds to match null graphic default - cvc::bounding_box defaultBounds(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5); - std::string boundsStr = boost::lexical_cast(defaultBounds[0]) + "," + - boost::lexical_cast(defaultBounds[1]) + "," + - boost::lexical_cast(defaultBounds[2]) + "," + - boost::lexical_cast(defaultBounds[3]) + "," + - boost::lexical_cast(defaultBounds[4]) + "," + - boost::lexical_cast(defaultBounds[5]); - getState("world_bounds").value(boundsStr); - getState("world_bounds").comment("Computed from graphics bounds - read only"); - getState("world_bounds").readOnly(true); - - // Grid and axis visibility now managed by GridNode/AxisNode state trees - - // Initialize camera settings - getState("camera.mode").value(0); // 0 = orbit, 1 = fly - getState("camera.speed").value(5.0); - getState("camera.sensitivity").value(1.0); - getState("camera.invert_mouse").value(false); - - // Initialize camera key bindings (Qt::Key enum values) - getState("camera.key_forward").value(static_cast(Qt::Key_W)); - getState("camera.key_backward").value(static_cast(Qt::Key_S)); - getState("camera.key_left").value(static_cast(Qt::Key_A)); - getState("camera.key_right").value(static_cast(Qt::Key_D)); - getState("camera.key_up").value(static_cast(Qt::Key_Space)); - getState("camera.key_down").value(static_cast(Qt::Key_Control)); - - // Initialize camera position (looking at origin from distance) - getState("camera.position.x").value(0.0); - getState("camera.position.y").value(-10.0); - getState("camera.position.z").value(5.0); - - // Initialize camera view direction (looking at origin) - getState("camera.view_direction.x").value(0.0); - getState("camera.view_direction.y").value(1.0); - getState("camera.view_direction.z").value(-0.5); - - // Initialize camera up vector (standard Z-up) - getState("camera.up_vector.x").value(0.0); - getState("camera.up_vector.y").value(0.0); - getState("camera.up_vector.z").value(1.0); - - // Initialize field of view (degrees) - getState("camera.fov").value(60.0); - - // Initialize viewer options - getState("viewer.show_fps").value(false); -} - -cvc::state &AppState::getState(const std::string &path) { - return cvc::state::instance(volrover3::app())(m_statePrefix)(path); -} - -cvc::state &AppState::getRootState() { - return cvc::state::instance(volrover3::app())(m_statePrefix); -} - -cvc::bounding_box AppState::worldBounds() { - std::string boundsStr = getState("world_bounds").value(); - std::vector values = getState("world_bounds").values(); - - if (values.size() == 6) { - return cvc::bounding_box( - boost::lexical_cast(values[0]), boost::lexical_cast(values[1]), - boost::lexical_cast(values[2]), boost::lexical_cast(values[3]), - boost::lexical_cast(values[4]), boost::lexical_cast(values[5])); - } - - return cvc::bounding_box(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0); -} - -void AppState::setWorldBounds(const cvc::bounding_box &bounds) { - std::string boundsStr = boost::lexical_cast(bounds[0]) + "," + - boost::lexical_cast(bounds[1]) + "," + - boost::lexical_cast(bounds[2]) + "," + - boost::lexical_cast(bounds[3]) + "," + - boost::lexical_cast(bounds[4]) + "," + - boost::lexical_cast(bounds[5]); - // Temporarily allow write to update computed bounds - getState("world_bounds").readOnly(false); - getState("world_bounds").value(boundsStr); - getState("world_bounds").readOnly(true); -} - -// =========================== -// Camera Methods -// =========================== - -int AppState::cameraMode() { return getState("camera.mode").value(); } - -void AppState::setCameraMode(int mode) { getState("camera.mode").value(mode); } - -boost::signals2::connection -AppState::onWorldBoundsChanged(const boost::function &callback) { - return getState("world_bounds").valueChanged.connect(callback); -} - -boost::signals2::connection AppState::onCameraModeChanged(const boost::function &callback) { - return getState("camera.mode").valueChanged.connect(callback); -} - -// Camera settings -double AppState::cameraSpeed() { return getState("camera.speed").value(); } - -void AppState::setCameraSpeed(double speed) { getState("camera.speed").value(speed); } - -double AppState::cameraSensitivity() { return getState("camera.sensitivity").value(); } - -void AppState::setCameraSensitivity(double sensitivity) { - getState("camera.sensitivity").value(sensitivity); -} - -bool AppState::cameraInvertMouse() { return getState("camera.invert_mouse").value(); } - -void AppState::setCameraInvertMouse(bool invert) { getState("camera.invert_mouse").value(invert); } - -int AppState::cameraKeyForward() { return getState("camera.key_forward").value(); } - -void AppState::setCameraKeyForward(int key) { getState("camera.key_forward").value(key); } - -int AppState::cameraKeyBackward() { return getState("camera.key_backward").value(); } - -void AppState::setCameraKeyBackward(int key) { getState("camera.key_backward").value(key); } - -int AppState::cameraKeyLeft() { return getState("camera.key_left").value(); } - -void AppState::setCameraKeyLeft(int key) { getState("camera.key_left").value(key); } - -int AppState::cameraKeyRight() { return getState("camera.key_right").value(); } - -void AppState::setCameraKeyRight(int key) { getState("camera.key_right").value(key); } - -int AppState::cameraKeyUp() { return getState("camera.key_up").value(); } - -void AppState::setCameraKeyUp(int key) { getState("camera.key_up").value(key); } - -int AppState::cameraKeyDown() { return getState("camera.key_down").value(); } - -void AppState::setCameraKeyDown(int key) { getState("camera.key_down").value(key); } - -void AppState::getCameraPosition(double &x, double &y, double &z) { - x = getState("camera.position.x").value(); - y = getState("camera.position.y").value(); - z = getState("camera.position.z").value(); -} - -void AppState::setCameraPosition(double x, double y, double z) { - getState("camera.position.x").value(x); - getState("camera.position.y").value(y); - getState("camera.position.z").value(z); -} - -void AppState::getCameraViewDirection(double &x, double &y, double &z) { - x = getState("camera.view_direction.x").value(); - y = getState("camera.view_direction.y").value(); - z = getState("camera.view_direction.z").value(); -} - -void AppState::setCameraViewDirection(double x, double y, double z) { - getState("camera.view_direction.x").value(x); - getState("camera.view_direction.y").value(y); - getState("camera.view_direction.z").value(z); -} - -void AppState::getCameraUpVector(double &x, double &y, double &z) { - x = getState("camera.up_vector.x").value(); - y = getState("camera.up_vector.y").value(); - z = getState("camera.up_vector.z").value(); -} - -void AppState::setCameraUpVector(double x, double y, double z) { - getState("camera.up_vector.x").value(x); - getState("camera.up_vector.y").value(y); - getState("camera.up_vector.z").value(z); -} - -double AppState::cameraFieldOfView() { return getState("camera.fov").value(); } - -void AppState::setCameraFieldOfView(double fov) { getState("camera.fov").value(fov); } - -bool AppState::showFPS() { return getState("viewer.show_fps").value(); } - -void AppState::setShowFPS(bool show) { getState("viewer.show_fps").value(show); } - -boost::signals2::connection AppState::onCameraChanged(const boost::function &callback) { - // Connect to the "camera" parent node's childChanged signal - // Any child value change (camera.position.x, camera.fov, etc.) will trigger this - return getState("camera").childChanged.connect([callback](const std::string &) { callback(); }); -} diff --git a/src/volrover3/AxisNode.cpp b/src/volrover3/AxisNode.cpp deleted file mode 100644 index d7130e0d..00000000 --- a/src/volrover3/AxisNode.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include -#include -#include -#include -#include -#include - -AxisNode::AxisNode(cvc::app &ctx, const std::string &statePath, const std::string &name) - : GraphicsNode(ctx, statePath, name), m_axesActor(vtkSmartPointer::New()) { - // Set axis length - m_axesActor->SetTotalLength(2.0, 2.0, 2.0); - m_axesActor->SetShaftTypeToLine(); - m_axesActor->SetAxisLabels(1); - - // Configure X axis label - m_axesActor->GetXAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone(); - m_axesActor->GetXAxisCaptionActor2D()->GetCaptionTextProperty()->SetFontSize(20); - m_axesActor->GetXAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(1.0, 0.0, 0.0); - - // Configure Y axis label - m_axesActor->GetYAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone(); - m_axesActor->GetYAxisCaptionActor2D()->GetCaptionTextProperty()->SetFontSize(20); - m_axesActor->GetYAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.0, 1.0, 0.0); - - // Configure Z axis label - m_axesActor->GetZAxisCaptionActor2D()->GetTextActor()->SetTextScaleModeToNone(); - m_axesActor->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetFontSize(20); - m_axesActor->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(0.0, 0.0, 1.0); - - // Initialize state tree with all rendering attributes - // Use batch scope to prevent callbacks from firing until all values are set - if (!statePath.empty()) { - cvc::state_change_batch_scope batch(*this); - - getState("visible").value(1); // Visible by default - - // Axis length (for all axes) - getState("axis_length").value(2.0); - - // Shaft type (0=cylinder, 1=line) - getState("shaft_type_line").value(1); - - // Show axis labels - getState("show_labels").value(1); - - // Label font size - getState("label_font_size").value(20); - - // X axis label color (red by default) - getState("x_label_color_r").value(1.0); - getState("x_label_color_g").value(0.0); - getState("x_label_color_b").value(0.0); - - // Y axis label color (green by default) - getState("y_label_color_r").value(0.0); - getState("y_label_color_g").value(1.0); - getState("y_label_color_b").value(0.0); - - // Z axis label color (blue by default) - getState("z_label_color_r").value(0.0); - getState("z_label_color_g").value(0.0); - getState("z_label_color_b").value(1.0); - } // batch ends here, callbacks fire with all values initialized -} - -AxisNode::~AxisNode() {} - -void AxisNode::applyTransformToVTK() { - // Use generic helper to apply world transform - applyWorldTransformToProps({m_axesActor}); -} - -vtkProp *AxisNode::getProp() { return m_axesActor; } - -void AxisNode::setAxisLength(double length) { getState("axis_length").value(length); } - -cvc::bounding_box AxisNode::getBoundingBox() const { - // Axis doesn't contribute to scene bounds - it's just a visualization helper - return cvc::bounding_box(0, 0, 0, 0, 0, 0); -} - -void AxisNode::handleStateChanged(const std::string &childState) { - // Synchronize rendering attributes from state tree - // All VTK operations MUST be wrapped in runOnMainThread() for thread safety - if (childState == "axis_length") { - runOnMainThread([this]() { - double length = getState("axis_length").value(); - m_axesActor->SetTotalLength(length, length, length); - }); - } else if (childState == "shaft_type_line") { - runOnMainThread([this]() { - bool useLine = getState("shaft_type_line").value(); - if (useLine) { - m_axesActor->SetShaftTypeToLine(); - } else { - m_axesActor->SetShaftTypeToCylinder(); - } - }); - } else if (childState == "show_labels") { - runOnMainThread([this]() { - bool showLabels = getState("show_labels").value(); - m_axesActor->SetAxisLabels(showLabels ? 1 : 0); - }); - } else if (childState == "label_font_size") { - runOnMainThread([this]() { - int fontSize = getState("label_font_size").value(); - m_axesActor->GetXAxisCaptionActor2D()->GetCaptionTextProperty()->SetFontSize(fontSize); - m_axesActor->GetYAxisCaptionActor2D()->GetCaptionTextProperty()->SetFontSize(fontSize); - m_axesActor->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetFontSize(fontSize); - }); - } else if (childState == "x_label_color_r" || childState == "x_label_color_g" || - childState == "x_label_color_b") { - runOnMainThread([this]() { - try { - double r = getState("x_label_color_r").value(); - double g = getState("x_label_color_g").value(); - double b = getState("x_label_color_b").value(); - m_axesActor->GetXAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(r, g, b); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "y_label_color_r" || childState == "y_label_color_g" || - childState == "y_label_color_b") { - runOnMainThread([this]() { - try { - double r = getState("y_label_color_r").value(); - double g = getState("y_label_color_g").value(); - double b = getState("y_label_color_b").value(); - m_axesActor->GetYAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(r, g, b); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "z_label_color_r" || childState == "z_label_color_g" || - childState == "z_label_color_b") { - runOnMainThread([this]() { - try { - double r = getState("z_label_color_r").value(); - double g = getState("z_label_color_g").value(); - double b = getState("z_label_color_b").value(); - m_axesActor->GetZAxisCaptionActor2D()->GetCaptionTextProperty()->SetColor(r, g, b); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else { - // Delegate to parent for common fields (visible, show_bbox, label, etc.) - GraphicsNode::handleStateChanged(childState); - } -} diff --git a/src/volrover3/BBoxNode.cpp b/src/volrover3/BBoxNode.cpp deleted file mode 100644 index 010e24e7..00000000 --- a/src/volrover3/BBoxNode.cpp +++ /dev/null @@ -1,267 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -BBoxNode::BBoxNode() - : m_actor(vtkSmartPointer::New()), - m_mapper(vtkSmartPointer::New()), m_bbox(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0), - m_transform(vtkSmartPointer::New()), m_coordinatesVisible(true), - m_coordinateLabelFontSize(12), m_renderer(nullptr) { - m_transform->Identity(); - m_actor->SetMapper(m_mapper); - - // Set default appearance - m_actor->GetProperty()->SetColor(1.0, 1.0, 0.0); // Yellow - m_actor->GetProperty()->SetLineWidth(2.0); - m_actor->GetProperty()->SetOpacity(1.0); - - // Default coordinate label color (white) - m_coordinateLabelColor[0] = m_coordinateLabelColor[1] = m_coordinateLabelColor[2] = 1.0; - - createBBox(); -} - -BBoxNode::~BBoxNode() {} - -void BBoxNode::addToRenderer(vtkRenderer *renderer) { - if (renderer) { - m_renderer = renderer; // Store renderer reference - renderer->AddActor(m_actor); - if (m_coordinatesVisible) { - for (auto &actor : m_coordinateLabelActors) { - renderer->AddViewProp(actor); - } - } - } -} - -void BBoxNode::removeFromRenderer(vtkRenderer *renderer) { - if (renderer) { - renderer->RemoveActor(m_actor); - for (auto &actor : m_coordinateLabelActors) { - renderer->RemoveViewProp(actor); - } - if (renderer == m_renderer) { - m_renderer = nullptr; - } - } -} - -void BBoxNode::setBoundingBox(const cvc::bounding_box &bbox) { - m_bbox = bbox; - createBBox(); - createCoordinateLabels(); -} - -void BBoxNode::setColor(double r, double g, double b) { m_actor->GetProperty()->SetColor(r, g, b); } - -void BBoxNode::getColor(double &r, double &g, double &b) const { - double *color = m_actor->GetProperty()->GetColor(); - r = color[0]; - g = color[1]; - b = color[2]; -} - -void BBoxNode::setLineWidth(double width) { m_actor->GetProperty()->SetLineWidth(width); } - -void BBoxNode::setTransform(vtkMatrix4x4 *transform) { - if (transform && m_actor) { - // Store the transform for coordinate label positioning - m_transform->DeepCopy(transform); - - // Apply to bbox actor - vtkSmartPointer vtkTrans = vtkSmartPointer::New(); - vtkTrans->SetMatrix(transform); - m_actor->SetUserTransform(vtkTrans); - - // Update coordinate labels with new transformed positions - createCoordinateLabels(); - } -} - -void BBoxNode::createBBox() { - vtkSmartPointer points = vtkSmartPointer::New(); - vtkSmartPointer lines = vtkSmartPointer::New(); - - double minX = m_bbox[0]; - double minY = m_bbox[1]; - double minZ = m_bbox[2]; - double maxX = m_bbox[3]; - double maxY = m_bbox[4]; - double maxZ = m_bbox[5]; - - // Create 8 corner points - vtkIdType p0 = points->InsertNextPoint(minX, minY, minZ); - vtkIdType p1 = points->InsertNextPoint(maxX, minY, minZ); - vtkIdType p2 = points->InsertNextPoint(maxX, maxY, minZ); - vtkIdType p3 = points->InsertNextPoint(minX, maxY, minZ); - vtkIdType p4 = points->InsertNextPoint(minX, minY, maxZ); - vtkIdType p5 = points->InsertNextPoint(maxX, minY, maxZ); - vtkIdType p6 = points->InsertNextPoint(maxX, maxY, maxZ); - vtkIdType p7 = points->InsertNextPoint(minX, maxY, maxZ); - - // Create 12 edges - vtkIdType edges[12][2] = { - {p0, p1}, {p1, p2}, {p2, p3}, {p3, p0}, // Bottom face - {p4, p5}, {p5, p6}, {p6, p7}, {p7, p4}, // Top face - {p0, p4}, {p1, p5}, {p2, p6}, {p3, p7} // Vertical edges - }; - - for (int i = 0; i < 12; ++i) { - lines->InsertNextCell(2); - lines->InsertCellPoint(edges[i][0]); - lines->InsertCellPoint(edges[i][1]); - } - - vtkSmartPointer polyData = vtkSmartPointer::New(); - polyData->SetPoints(points); - polyData->SetLines(lines); - - m_mapper->SetInputData(polyData); -} - -void BBoxNode::setCoordinatesVisible(bool visible) { - if (m_coordinatesVisible == visible) - return; - - m_coordinatesVisible = visible; - - // Update visibility of existing labels - for (auto &actor : m_coordinateLabelActors) { - actor->SetVisibility(visible); - } - - // If we have a renderer, add/remove labels - if (m_renderer) { - if (visible) { - for (auto &actor : m_coordinateLabelActors) { - m_renderer->AddViewProp(actor); - } - } else { - for (auto &actor : m_coordinateLabelActors) { - m_renderer->RemoveViewProp(actor); - } - } - } -} - -void BBoxNode::setCoordinateLabelColor(double r, double g, double b) { - m_coordinateLabelColor[0] = r; - m_coordinateLabelColor[1] = g; - m_coordinateLabelColor[2] = b; - - for (auto &actor : m_coordinateLabelActors) { - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(actor->GetMapper()); - if (mapper) { - mapper->GetTextProperty()->SetColor(r, g, b); - } - } -} - -void BBoxNode::getCoordinateLabelColor(double &r, double &g, double &b) const { - r = m_coordinateLabelColor[0]; - g = m_coordinateLabelColor[1]; - b = m_coordinateLabelColor[2]; -} - -void BBoxNode::setCoordinateLabelFontSize(int size) { - m_coordinateLabelFontSize = std::max(1, size); - - for (auto &actor : m_coordinateLabelActors) { - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(actor->GetMapper()); - if (mapper) { - mapper->GetTextProperty()->SetFontSize(m_coordinateLabelFontSize); - } - } -} - -void BBoxNode::createCoordinateLabels() { - // Remove old labels from renderer first - if (m_renderer) { - for (auto &actor : m_coordinateLabelActors) { - // Only remove if actor was actually added to a renderer - if (actor->GetReferenceCount() > 1) { - m_renderer->RemoveViewProp(actor); - } - } - } - - // Clear existing labels - m_coordinateLabelActors.clear(); - - if (!m_coordinatesVisible) - return; - - double minX = m_bbox[0]; - double minY = m_bbox[1]; - double minZ = m_bbox[2]; - double maxX = m_bbox[3]; - double maxY = m_bbox[4]; - double maxZ = m_bbox[5]; - - double spanX = maxX - minX; - double spanY = maxY - minY; - double spanZ = maxZ - minZ; - - if (spanX <= 0.0 || spanY <= 0.0 || spanZ <= 0.0) - return; - - // Helper lambda to create a label at world-transformed position - auto createLabel = [&](double x, double y, double z, const std::string &text) { - // Transform local position to world position - double localPos[4] = {x, y, z, 1.0}; - double worldPos[4]; - m_transform->MultiplyPoint(localPos, worldPos); - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(text.c_str()); - textMapper->GetTextProperty()->SetFontSize(m_coordinateLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_coordinateLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(worldPos[0], worldPos[1], worldPos[2]); - textActor->SetVisibility(m_coordinatesVisible); - - m_coordinateLabelActors.push_back(textActor); - }; - - // Show coordinates at the 2 opposing corners (min and max) of the bounding - // box - std::ostringstream oss; - - // Minimum corner - oss << "Min: (" << std::fixed << std::setprecision(2) << minX << ", " << minY << ", " << minZ - << ")"; - createLabel(minX, minY, minZ, oss.str()); - - // Maximum corner - oss.str(""); - oss << "Max: (" << std::fixed << std::setprecision(2) << maxX << ", " << maxY << ", " << maxZ - << ")"; - createLabel(maxX, maxY, maxZ, oss.str()); - - // Add new labels to renderer if we have one and coordinates are visible - if (m_renderer && m_coordinatesVisible) { - for (auto &actor : m_coordinateLabelActors) { - m_renderer->AddViewProp(actor); - } - } -} diff --git a/src/volrover3/BoundingBoxDialog.cpp b/src/volrover3/BoundingBoxDialog.cpp deleted file mode 100644 index 4878e62a..00000000 --- a/src/volrover3/BoundingBoxDialog.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -BoundingBoxDialog::BoundingBoxDialog(std::shared_ptr sceneGraph, QWidget *parent) - : QDialog(parent), m_sceneGraph(sceneGraph), m_currentGraphics(nullptr) { - setWindowTitle(tr("Bounding Box Settings")); - m_bboxColor[0] = m_bboxColor[1] = m_bboxColor[2] = 1.0; - setupUI(); - populateGraphicsComboBox(); - - // Select first graphics if available - if (m_graphicsComboBox->count() > 0) { - m_graphicsComboBox->setCurrentIndex(0); - onGraphicsSelectionChanged(0); - } -} - -void BoundingBoxDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Graphics selector - QGroupBox *selectorGroup = new QGroupBox(tr("Graphic")); - QFormLayout *selectorLayout = new QFormLayout(selectorGroup); - - m_graphicsComboBox = new QComboBox(); - connect(m_graphicsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &BoundingBoxDialog::onGraphicsSelectionChanged); - selectorLayout->addRow(tr("Select Graphic:"), m_graphicsComboBox); - - mainLayout->addWidget(selectorGroup); - - // Bounds group (read-only display of computed bounds) - QGroupBox *boundsGroup = new QGroupBox(tr("Computed Bounds")); - QFormLayout *formLayout = new QFormLayout(boundsGroup); - - m_minXEdit = new QLineEdit(); - m_minYEdit = new QLineEdit(); - m_minZEdit = new QLineEdit(); - m_maxXEdit = new QLineEdit(); - m_maxYEdit = new QLineEdit(); - m_maxZEdit = new QLineEdit(); - - // Make read-only - bounds are computed from geometry/volume data - m_minXEdit->setReadOnly(true); - m_minYEdit->setReadOnly(true); - m_minZEdit->setReadOnly(true); - m_maxXEdit->setReadOnly(true); - m_maxYEdit->setReadOnly(true); - m_maxZEdit->setReadOnly(true); - - formLayout->addRow(tr("Min X:"), m_minXEdit); - formLayout->addRow(tr("Min Y:"), m_minYEdit); - formLayout->addRow(tr("Min Z:"), m_minZEdit); - formLayout->addRow(tr("Max X:"), m_maxXEdit); - formLayout->addRow(tr("Max Y:"), m_maxYEdit); - formLayout->addRow(tr("Max Z:"), m_maxZEdit); - - mainLayout->addWidget(boundsGroup); - - // Bounding box rendering group - QGroupBox *renderGroup = new QGroupBox(tr("Bounding Box Rendering")); - QFormLayout *renderLayout = new QFormLayout(renderGroup); - - m_bboxVisibleCheckbox = new QCheckBox(); - connect(m_bboxVisibleCheckbox, &QCheckBox::toggled, this, - &BoundingBoxDialog::onBBoxVisibilityChanged); - renderLayout->addRow(tr("Show Bounding Box:"), m_bboxVisibleCheckbox); - - QHBoxLayout *colorLayout = new QHBoxLayout(); - m_bboxColorButton = new QPushButton(); - m_bboxColorButton->setFixedSize(50, 25); - connect(m_bboxColorButton, &QPushButton::clicked, this, &BoundingBoxDialog::onBBoxColorChanged); - colorLayout->addWidget(m_bboxColorButton); - colorLayout->addStretch(); - renderLayout->addRow(tr("Color:"), colorLayout); - - mainLayout->addWidget(renderGroup); - - // Dialog buttons - QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - mainLayout->addWidget(buttonBox); - - updateColorButton(); -} - -void BoundingBoxDialog::populateGraphicsComboBox() { - m_graphicsComboBox->clear(); - m_graphicsList.clear(); - - if (!m_sceneGraph) - return; - - // Add root graphic first - auto root = m_sceneGraph->getGraphicsRoot(); - if (root) { - m_graphicsComboBox->addItem(tr("(Root - All Graphics)")); - m_graphicsList.push_back(root); - } - - // Add all other graphics - const auto &allGraphics = m_sceneGraph->getAllGraphics(); - for (const auto &pair : allGraphics) { - m_graphicsComboBox->addItem(QString::fromStdString(pair.first)); - m_graphicsList.push_back(pair.second); - } -} - -void BoundingBoxDialog::onGraphicsSelectionChanged(int index) { - if (index < 0 || index >= static_cast(m_graphicsList.size())) { - m_currentGraphics = nullptr; - return; - } - - m_currentGraphics = m_graphicsList[index]; - loadGraphicsSettings(); -} - -void BoundingBoxDialog::loadGraphicsSettings() { - if (!m_currentGraphics) { - return; - } - - // Load bounding box - cvc::bounding_box bounds = m_currentGraphics->getBoundingBox(); - m_minXEdit->setText(QString::number(bounds[0])); - m_minYEdit->setText(QString::number(bounds[1])); - m_minZEdit->setText(QString::number(bounds[2])); - m_maxXEdit->setText(QString::number(bounds[3])); - m_maxYEdit->setText(QString::number(bounds[4])); - m_maxZEdit->setText(QString::number(bounds[5])); - - // Load bbox visibility - m_bboxVisibleCheckbox->setChecked(m_currentGraphics->getShowBBox()); - - // Load bbox color - m_currentGraphics->getBBoxColor(m_bboxColor[0], m_bboxColor[1], m_bboxColor[2]); - updateColorButton(); -} - -void BoundingBoxDialog::onResetToGraphics() { - // For now, bounds are computed automatically, so this is a no-op - // Could potentially re-compute or reload - if (m_currentGraphics) { - loadGraphicsSettings(); - } -} - -void BoundingBoxDialog::onBBoxVisibilityChanged(bool visible) { - if (m_currentGraphics) { - m_currentGraphics->setShowBBox(visible); - onApplyChanges(); - } -} - -void BoundingBoxDialog::onBBoxColorChanged() { - if (!m_currentGraphics) - return; - - QColor currentColor = QColor::fromRgbF(m_bboxColor[0], m_bboxColor[1], m_bboxColor[2]); - QColor color = QColorDialog::getColor(currentColor, this, tr("Choose Bounding Box Color")); - - if (color.isValid()) { - m_bboxColor[0] = color.redF(); - m_bboxColor[1] = color.greenF(); - m_bboxColor[2] = color.blueF(); - updateColorButton(); - - m_currentGraphics->setBBoxColor(m_bboxColor[0], m_bboxColor[1], m_bboxColor[2]); - onApplyChanges(); - } -} - -void BoundingBoxDialog::onApplyChanges() { - // Trigger a render update - if (m_sceneGraph) { - m_sceneGraph->update(); - } -} - -void BoundingBoxDialog::updateColorButton() { - int r = static_cast(m_bboxColor[0] * 255); - int g = static_cast(m_bboxColor[1] * 255); - int b = static_cast(m_bboxColor[2] * 255); - - QString style = QString("background-color: rgb(%1, %2, %3);").arg(r).arg(g).arg(b); - m_bboxColorButton->setStyleSheet(style); -} diff --git a/src/volrover3/CMakeLists.txt b/src/volrover3/CMakeLists.txt deleted file mode 100644 index 5146be2a..00000000 --- a/src/volrover3/CMakeLists.txt +++ /dev/null @@ -1,540 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -# Enable automoc, autouic, autorcc for Qt -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTORCC ON) - -# Find required packages -# Try Qt6 first, fall back to Qt5 if VTK requires it -find_package(Qt6 QUIET COMPONENTS Core Widgets OpenGL OpenGLWidgets Test) -if(NOT Qt6_FOUND) - find_package(Qt5 REQUIRED COMPONENTS Core Widgets OpenGL Test) - set(QT_VERSION_MAJOR 5) -else() - set(QT_VERSION_MAJOR 6) -endif() - -find_package(VTK REQUIRED) -find_package(OpenGL REQUIRED) - -# Source files -set(VOLROVER3_SOURCES - main.cpp - volrover3_app.cpp - MainWindow.cpp - VTKRenderWidget.cpp - SceneGraph.cpp - SceneNode.cpp - GeometryNode.cpp - GraphicsNode.cpp - VolumeNode.cpp - NullGraphicNode.cpp - GridNode.cpp - AxisNode.cpp - BBoxNode.cpp - CameraController.cpp - TransferFunctionWidget.cpp - ThreadMonitorWidget.cpp - StateTreeWidget.cpp - StateDashboardWidget.cpp - AppState.cpp - BoundingBoxDialog.cpp - CameraSettingsDialog.cpp - GridOptionsDialog.cpp - ViewerOptionsDialog.cpp - SDFDialog.cpp - IsosurfaceDialog.cpp - GeometryDialog.cpp - VolumeDialog.cpp - GraphicsParentDialog.cpp - ProceduralGeometryDialog.cpp -) - -# Header files -set(VOLROVER3_HEADERS - ${PROJECT_SOURCE_DIR}/inc/volrover3/MainWindow.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/VTKRenderWidget.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/SceneGraph.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/SceneNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/GeometryNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/GraphicsNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/VolumeNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/GridNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/AxisNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/BBoxNode.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/CameraController.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/TransferFunctionWidget.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/ThreadMonitorWidget.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/StateTreeWidget.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/StateDashboardWidget.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/AppState.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/BoundingBoxDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/CameraSettingsDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/GridOptionsDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/ViewerOptionsDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/SDFDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/IsosurfaceDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/GeometryDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/VolumeDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/GraphicsParentDialog.h - ${PROJECT_SOURCE_DIR}/inc/volrover3/ProceduralGeometryDialog.h -) - -# Platform-specific bundle / icon resources -# ───────────────────────────────────────── -# Linux: a simple PNG icon installed alongside a .desktop file. -# macOS: the .icns is copied into the .app bundle's Resources/ directory. -# Windows: the .ico is compiled into the EXE via a generated .rc file. -set(_VOLROVER3_ICON_DIR "${PROJECT_SOURCE_DIR}/share/icons") -set(_VOLROVER3_ICON_PNG "${_VOLROVER3_ICON_DIR}/volrover_logo.png") -set(_VOLROVER3_ICON_ICNS "${_VOLROVER3_ICON_DIR}/volrover_logo.icns") -set(_VOLROVER3_ICON_ICO "${_VOLROVER3_ICON_DIR}/volrover_logo.ico") - -set(_VOLROVER3_PLATFORM_SOURCES "") - -if(WIN32 AND EXISTS "${_VOLROVER3_ICON_ICO}") - # Generate a Windows resource script that embeds the icon and basic - # version info into the EXE so Explorer + Task-Manager show the logo. - set(_VOLROVER3_RC "${CMAKE_CURRENT_BINARY_DIR}/volrover3.rc") - file(WRITE "${_VOLROVER3_RC}" -"#include \n" -"IDI_ICON1 ICON DISCARDABLE \"${_VOLROVER3_ICON_ICO}\"\n" -) - list(APPEND _VOLROVER3_PLATFORM_SOURCES "${_VOLROVER3_RC}") -endif() - -if(APPLE AND EXISTS "${_VOLROVER3_ICON_ICNS}") - # Mark the .icns as a bundle resource so CMake copies it into - # Contents/Resources/ during install. - set_source_files_properties("${_VOLROVER3_ICON_ICNS}" PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources") - list(APPEND _VOLROVER3_PLATFORM_SOURCES "${_VOLROVER3_ICON_ICNS}") -endif() - -# Create executable -add_executable(volrover3 - ${VOLROVER3_SOURCES} - ${VOLROVER3_HEADERS} - ${_VOLROVER3_PLATFORM_SOURCES} -) - -# Apple bundle / Windows GUI subsystem properties -if(APPLE) - set_target_properties(volrover3 PROPERTIES - MACOSX_BUNDLE TRUE - MACOSX_BUNDLE_BUNDLE_NAME "VolumeRover3" - MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" - MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" - MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_VERSION}" - MACOSX_BUNDLE_GUI_IDENTIFIER "edu.utexas.cvc.volrover3" - MACOSX_BUNDLE_INFO_STRING "VolumeRover3 ${PROJECT_VERSION}" - MACOSX_BUNDLE_COPYRIGHT "Copyright (c) The University of Texas at Austin / CVC" - MACOSX_BUNDLE_ICON_FILE "volrover_logo.icns" - MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/CMake/Info.plist.in" - OUTPUT_NAME "VolumeRover3" - ) -elseif(WIN32) - # WIN32_EXECUTABLE TRUE => /SUBSYSTEM:WINDOWS so no console window pops - # up when launching from Explorer. - set_target_properties(volrover3 PROPERTIES WIN32_EXECUTABLE TRUE) -endif() - -target_include_directories(volrover3 PRIVATE ${PROJECT_SOURCE_DIR}/inc) - -# Link libraries -if(QT_VERSION_MAJOR EQUAL 6) - target_link_libraries(volrover3 - cvc::cvc - Qt6::Core - Qt6::Widgets - Qt6::OpenGL - Qt6::OpenGLWidgets - ${VTK_LIBRARIES} - ${OPENGL_LIBRARIES} - ) -else() - target_link_libraries(volrover3 - cvc::cvc - Qt5::Core - Qt5::Widgets - Qt5::OpenGL - ${VTK_LIBRARIES} - ${OPENGL_LIBRARIES} - ) -endif() - -# Auto-initialize VTK modules -vtk_module_autoinit( - TARGETS volrover3 - MODULES ${VTK_LIBRARIES} -) - -# Install target -if(APPLE) - # Drop the .app bundle directly under the install prefix so a user can - # drag VolumeRover3.app into /Applications. - install(TARGETS volrover3 - BUNDLE DESTINATION . COMPONENT volrover3 - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT volrover3 - ) -else() - install(TARGETS volrover3 - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - COMPONENT volrover3 - ) -endif() - -# Linux: install a Freedesktop .desktop file and PNG icon for the menu / -# launcher. The hicolor PNG installation is owned by the root CMakeLists. -if(UNIX AND NOT APPLE) - if(EXISTS "${PROJECT_SOURCE_DIR}/share/applications/volrover3.desktop.in") - configure_file( - "${PROJECT_SOURCE_DIR}/share/applications/volrover3.desktop.in" - "${CMAKE_CURRENT_BINARY_DIR}/volrover3.desktop" - @ONLY) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/volrover3.desktop" - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications - COMPONENT volrover3) - endif() -endif() - -# Bundle the README + LICENSE with the volrover3 component for the -# end-user-facing distributions. -install(FILES - "${CMAKE_CURRENT_SOURCE_DIR}/README.md" - "${PROJECT_SOURCE_DIR}/LICENSE" - DESTINATION ${CMAKE_INSTALL_DOCDIR}/volrover3 - COMPONENT volrover3) - -# Windows: bundle runtime DLLs alongside the executable AND run windeployqt -# to copy Qt platform plugins (qwindows.dll), styles, and translations. -# Without windeployqt the resulting EXE will not start on a vanilla -# Windows machine because Qt cannot find a platform plugin. -if(WIN32) - install(FILES $ - DESTINATION ${CMAKE_INSTALL_BINDIR} - COMPONENT volrover3) - - # ── Belt-and-suspenders runtime-dependency walker ── - # - # $ only resolves DLLs that arrived - # via CMake imported targets with IMPORTED_LOCATION set (e.g. modern - # Qt6, vcpkg-built cgal, vtk). It silently misses: - # - fftw3.dll : found via legacy CMake/FindFFTW.cmake which only - # sets variables, no imported target. - # - zlib1.dll, libpng16.dll, etc. : transitive deps pulled in via - # hdf5/imagemagick that don't - # propagate through cvc's - # interface link libraries. - # - cudart64_*.dll : if a future change accidentally re-introduces - # a dynamic CUDA::cudart link. - # - # file(GET_RUNTIME_DEPENDENCIES) is CMake's PE-import walker. We run - # it at install time over the staged volrover3.exe and cvc.dll, then - # copy every non-system DLL it finds into bin/. Search dirs include - # the build tree, CMAKE_PREFIX_PATH (vcpkg + Qt + VTK) and - # CUDAToolkit_BIN_DIR (in case dynamic cudart slips in). - # - # We pre-compute the search-dir list at configure time and store it - # as a quoted CMake list so paths with spaces survive the install - # script generation intact. - set(_cvc_runtime_search_dirs - "${CMAKE_BINARY_DIR}/bin" - "${CMAKE_BINARY_DIR}/lib" - ) - foreach(_p IN LISTS CMAKE_PREFIX_PATH) - list(APPEND _cvc_runtime_search_dirs "${_p}/bin") - endforeach() - if(DEFINED CUDAToolkit_BIN_DIR) - list(APPEND _cvc_runtime_search_dirs "${CUDAToolkit_BIN_DIR}") - endif() - - install(CODE " - set(_search_dirs \"${_cvc_runtime_search_dirs}\") - list(PREPEND _search_dirs \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}\") - if(DEFINED ENV{QT_ROOT_DIR}) - list(APPEND _search_dirs \"\$ENV{QT_ROOT_DIR}/bin\") - endif() - if(DEFINED ENV{VCPKG_INSTALLATION_ROOT}) - list(APPEND _search_dirs \"\$ENV{VCPKG_INSTALLATION_ROOT}/installed/x64-windows/bin\") - endif() - - file(GET_RUNTIME_DEPENDENCIES - EXECUTABLES \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/volrover3.exe\" - LIBRARIES \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/cvc.dll\" - RESOLVED_DEPENDENCIES_VAR _resolved - UNRESOLVED_DEPENDENCIES_VAR _unresolved - CONFLICTING_DEPENDENCIES_PREFIX _conflicting - DIRECTORIES \${_search_dirs} - PRE_EXCLUDE_REGEXES - [[api-ms-.*]] - [[ext-ms-.*]] - POST_EXCLUDE_REGEXES - [[.*/system32/.*\\.dll]] - [[.*/SysWOW64/.*\\.dll]] - [[.*/[Ww]indows/.*\\.dll]] - ) - - foreach(_dll IN LISTS _resolved) - message(STATUS \"Bundling runtime dep: \${_dll}\") - file(INSTALL \"\${_dll}\" - DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}\" - FOLLOW_SYMLINK_CHAIN) - endforeach() - - if(_unresolved) - message(STATUS \"Unresolved runtime deps (assumed system): \${_unresolved}\") - endif() - " COMPONENT volrover3) - - if(QT_VERSION_MAJOR EQUAL 6) - set(_qt_bin_hint "${Qt6_DIR}/../../../bin") - else() - set(_qt_bin_hint "${Qt5_DIR}/../../../bin") - endif() - # Prefer aqtinstall's windeployqt (exported via $ENV{QT_ROOT_DIR}) over - # any vcpkg-shipped one. vcpkg's qt6-base port lacks translations/ - # catalogs.json and (for debug builds) Qt6OpenGLWidgetsd.dll, so its - # windeployqt fails the dependency walk and emits warnings about - # missing translation catalogs. Search aqtinstall's bin first, then - # the Qt cmake-config-derived hint, then fall back to PATH. - find_program(WINDEPLOYQT_EXECUTABLE - NAMES windeployqt - HINTS "$ENV{QT_ROOT_DIR}/bin" "${_qt_bin_hint}" - NO_CMAKE_SYSTEM_PATH) - if(WINDEPLOYQT_EXECUTABLE) - # CPack runs `cmake --install` per-component before invoking the - # archive / NSIS generator, so this install(CODE) script is the right - # hook to materialize Qt's platform plugins into the staged tree. - # - # Pass --debug / --release matching the install config so windeployqt - # walks the correctly-suffixed Qt6 modules (Qt6Cored.dll vs - # Qt6Core.dll). Without this it always asks for the release modules, - # which emits "module Qt6*d could not be found" warnings on Debug - # installs and silently ships the wrong-config DLLs (the bundled - # volrover3.exe is linked against Qt6*d.dll, so the Debug package - # then fails to launch on a clean machine). - install(CODE " - execute_process(COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" - $,--debug,--release> - --no-translations - --no-system-d3d-compiler - --no-opengl-sw - --no-compiler-runtime - \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/volrover3.exe\") - " COMPONENT volrover3) - else() - message(WARNING "windeployqt not found — Windows volrover3 packages " - "will be missing Qt platform plugins and will fail to launch on " - "machines without Qt installed.") - endif() -endif() - -# macOS: run macdeployqt during install so the .app bundle is fully -# self-contained (Qt frameworks, plugins, VTK & libcvc dylibs all -# rewritten with @executable_path/.. references). -if(APPLE) - if(QT_VERSION_MAJOR EQUAL 6) - set(_qt_bin_hint "${Qt6_DIR}/../../../bin") - else() - set(_qt_bin_hint "${Qt5_DIR}/../../../bin") - endif() - find_program(MACDEPLOYQT_EXECUTABLE - NAMES macdeployqt - HINTS "${_qt_bin_hint}") - if(MACDEPLOYQT_EXECUTABLE) - install(CODE " - execute_process(COMMAND \"${MACDEPLOYQT_EXECUTABLE}\" - \"\${CMAKE_INSTALL_PREFIX}/VolumeRover3.app\" - -always-overwrite) - " COMPONENT volrover3) - else() - message(WARNING "macdeployqt not found — macOS volrover3 .app will " - "not be self-contained and will fail to launch without a Qt " - "installation on the target machine.") - endif() -endif() - -# Tests -if(CVC_BUILD_TESTS) - # Create a library with all volrover3 components except main.cpp for testing - set(VOLROVER3_LIB_SOURCES ${VOLROVER3_SOURCES}) - list(REMOVE_ITEM VOLROVER3_LIB_SOURCES main.cpp) - - add_library(volrover3_lib STATIC ${VOLROVER3_LIB_SOURCES} ${VOLROVER3_HEADERS}) - target_include_directories(volrover3_lib PUBLIC ${PROJECT_SOURCE_DIR}/inc) - - if(QT_VERSION_MAJOR EQUAL 6) - target_link_libraries(volrover3_lib - cvc::cvc - Qt6::Core - Qt6::Widgets - Qt6::OpenGL - Qt6::OpenGLWidgets - ${VTK_LIBRARIES} - ${OPENGL_LIBRARIES} - ) - else() - target_link_libraries(volrover3_lib - cvc::cvc - Qt5::Core - Qt5::Widgets - Qt5::OpenGL - ${VTK_LIBRARIES} - ${OPENGL_LIBRARIES} - ) - endif() - - vtk_module_autoinit( - TARGETS volrover3_lib - MODULES ${VTK_LIBRARIES} - ) - - # AppState tests - add_executable(volrover3_appstate_test - tests/AppStateTest.cpp) - target_link_libraries(volrover3_appstate_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME AppStateTest COMMAND volrover3_appstate_test) - - # SceneGraph tests - add_executable(volrover3_scenegraph_test - tests/SceneGraphTest.cpp) - target_link_libraries(volrover3_scenegraph_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME SceneGraphTest COMMAND volrover3_scenegraph_test) - - # TransferFunction tests - add_executable(volrover3_transferfunction_test - tests/TransferFunctionTest.cpp) - target_link_libraries(volrover3_transferfunction_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME TransferFunctionTest COMMAND volrover3_transferfunction_test) - - # CameraController tests - add_executable(volrover3_camera_test - tests/CameraControllerTest.cpp) - target_link_libraries(volrover3_camera_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME CameraControllerTest COMMAND volrover3_camera_test) - - # GridNode tests - add_executable(volrover3_gridnode_test - tests/GridNodeTest.cpp) - target_link_libraries(volrover3_gridnode_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME GridNodeTest COMMAND volrover3_gridnode_test) - - # StateTreeWidget tests - add_executable(volrover3_statetree_test - tests/StateTreeWidgetTest.cpp) - target_link_libraries(volrover3_statetree_test - volrover3_lib - GTest::gtest) - add_test(NAME StateTreeWidgetTest COMMAND volrover3_statetree_test) - - # StateDashboardWidget tests - add_executable(volrover3_statedashboard_test - tests/StateDashboardWidgetTest.cpp) - target_link_libraries(volrover3_statedashboard_test - volrover3_lib - GTest::gtest) - add_test(NAME StateDashboardWidgetTest COMMAND volrover3_statedashboard_test) - - # GraphicsNode tests - add_executable(volrover3_graphicsnode_test - tests/GraphicsNodeTest.cpp) - target_link_libraries(volrover3_graphicsnode_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME GraphicsNodeTest COMMAND volrover3_graphicsnode_test) - - # VolumeNode tests - add_executable(volrover3_volumenode_test - tests/VolumeNodeTest.cpp) - target_link_libraries(volrover3_volumenode_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME VolumeNodeTest COMMAND volrover3_volumenode_test) - - # BBoxNode tests - add_executable(volrover3_bboxnode_test - tests/BBoxNodeTest.cpp) - target_link_libraries(volrover3_bboxnode_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME BBoxNodeTest COMMAND volrover3_bboxnode_test) - - # NullGraphicNode tests - add_executable(volrover3_nullgraphicnode_test - tests/NullGraphicNodeTest.cpp) - target_link_libraries(volrover3_nullgraphicnode_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME NullGraphicNodeTest COMMAND volrover3_nullgraphicnode_test) - - # BoundingBox Semantics tests - add_executable(volrover3_bbox_semantics_test - tests/BoundingBoxSemanticsTest.cpp) - target_link_libraries(volrover3_bbox_semantics_test - volrover3_lib - GTest::gtest - GTest::gtest_main) - add_test(NAME BoundingBoxSemanticsTest COMMAND volrover3_bbox_semantics_test) - - # GeometryDialog tests - add_executable(volrover3_geometrydialog_test - tests/GeometryDialogTest.cpp) - target_link_libraries(volrover3_geometrydialog_test - volrover3_lib - Qt${QT_VERSION_MAJOR}::Test - GTest::gtest - GTest::gtest_main) - add_test(NAME GeometryDialogTest COMMAND volrover3_geometrydialog_test) - - # VolumeDialog tests - add_executable(volrover3_volumedialog_test - tests/VolumeDialogTest.cpp) - target_link_libraries(volrover3_volumedialog_test - volrover3_lib - Qt${QT_VERSION_MAJOR}::Test - GTest::gtest - GTest::gtest_main) - add_test(NAME VolumeDialogTest COMMAND volrover3_volumedialog_test) - - # All volrover3 tests require a Qt platform plugin; use offscreen when no - # display is available (headless CI, SSH, etc.). - set_tests_properties( - AppStateTest - SceneGraphTest - TransferFunctionTest - CameraControllerTest - GridNodeTest - StateTreeWidgetTest - StateDashboardWidgetTest - GraphicsNodeTest - VolumeNodeTest - BBoxNodeTest - NullGraphicNodeTest - BoundingBoxSemanticsTest - GeometryDialogTest - VolumeDialogTest - PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" - ) -endif() - diff --git a/src/volrover3/CameraController.cpp b/src/volrover3/CameraController.cpp deleted file mode 100644 index 906feff0..00000000 --- a/src/volrover3/CameraController.cpp +++ /dev/null @@ -1,777 +0,0 @@ -#include -#include -#include -#include -#include - -CameraController::CameraController(cvc::app &ctx, const std::string &statePath) - : SceneNode(ctx, statePath), m_camera(nullptr), m_mode(ORBIT_MODE), m_orbitDistance(10.0), - m_orbitAzimuth(0.0), m_orbitElevation(30.0), m_yaw(0.0), m_pitch(0.0), - m_mouseLeftPressed(false), m_mouseRightPressed(false), m_mouseMiddlePressed(false), - m_movementSpeed(5.0), m_mouseSensitivity(1.0), m_invertMouse(false), m_keyForward(Qt::Key_W), - m_keyBackward(Qt::Key_S), m_keyStrafeLeft(Qt::Key_A), m_keyStrafeRight(Qt::Key_D), - m_keyUp(Qt::Key_Space), m_keyDown(Qt::Key_Control) { - m_position[0] = 0.0; - m_position[1] = 0.0; - m_position[2] = 5.0; - m_focalPoint[0] = 0.0; - m_focalPoint[1] = 0.0; - m_focalPoint[2] = 0.0; - - m_orbitCenter[0] = 0.0; - m_orbitCenter[1] = 0.0; - m_orbitCenter[2] = 0.0; - - initializeState(); -} - -CameraController::~CameraController() {} - -void CameraController::initializeState() { - // Camera mode - getState("mode").value(static_cast(m_mode)); - - // Current camera state (computed from VTK camera) - getState("position.x").value(m_position[0]); - getState("position.y").value(m_position[1]); - getState("position.z").value(m_position[2]); - getState("view_direction.x").value(0.0); - getState("view_direction.y").value(0.0); - getState("view_direction.z").value(-1.0); - getState("up_vector.x").value(0.0); - getState("up_vector.y").value(1.0); - getState("up_vector.z").value(0.0); - getState("fov").value(30.0); - - // Orbit mode state - getState("orbit.center.x").value(m_orbitCenter[0]); - getState("orbit.center.y").value(m_orbitCenter[1]); - getState("orbit.center.z").value(m_orbitCenter[2]); - getState("orbit.distance").value(m_orbitDistance); - getState("orbit.azimuth").value(m_orbitAzimuth); - getState("orbit.elevation").value(m_orbitElevation); - - // Fly mode state - getState("fly.position.x").value(m_position[0]); - getState("fly.position.y").value(m_position[1]); - getState("fly.position.z").value(m_position[2]); - getState("fly.focal_point.x").value(m_focalPoint[0]); - getState("fly.focal_point.y").value(m_focalPoint[1]); - getState("fly.focal_point.z").value(m_focalPoint[2]); - getState("fly.yaw").value(m_yaw); - getState("fly.pitch").value(m_pitch); - - // Settings - getState("settings.movement_speed").value(m_movementSpeed); - getState("settings.mouse_sensitivity").value(m_mouseSensitivity); - getState("settings.invert_mouse").value(m_invertMouse); - - // Key bindings - getState("keys.forward").value(m_keyForward); - getState("keys.backward").value(m_keyBackward); - getState("keys.strafe_left").value(m_keyStrafeLeft); - getState("keys.strafe_right").value(m_keyStrafeRight); - getState("keys.up").value(m_keyUp); - getState("keys.down").value(m_keyDown); - - // Input state (read-only) - getState("input.mouse_left_pressed").value(m_mouseLeftPressed); - getState("input.mouse_left_pressed").readOnly(true); - getState("input.mouse_right_pressed").value(m_mouseRightPressed); - getState("input.mouse_right_pressed").readOnly(true); - - // Keys pressed - store as count for simplicity (read-only) - getState("input.keys_pressed_count").value(static_cast(m_keysPressed.size())); - getState("input.keys_pressed_count").readOnly(true); -} - -void CameraController::handleStateChanged(const std::string &childState) { - if (!m_camera) - return; - - bool needsOrbitUpdate = false; - bool needsFlyUpdate = false; - - // Camera mode - if (childState == "mode") { - m_mode = static_cast(getState("mode").value()); - // Mode change should trigger appropriate camera update - if (m_mode == ORBIT_MODE) { - needsOrbitUpdate = true; - } else { - needsFlyUpdate = true; - } - } - // Orbit mode state - else if (childState == "orbit.center.x") { - m_orbitCenter[0] = getState("orbit.center.x").value(); - needsOrbitUpdate = (m_mode == ORBIT_MODE); - } else if (childState == "orbit.center.y") { - m_orbitCenter[1] = getState("orbit.center.y").value(); - needsOrbitUpdate = (m_mode == ORBIT_MODE); - } else if (childState == "orbit.center.z") { - m_orbitCenter[2] = getState("orbit.center.z").value(); - needsOrbitUpdate = (m_mode == ORBIT_MODE); - } else if (childState == "orbit.distance") { - m_orbitDistance = getState("orbit.distance").value(); - needsOrbitUpdate = (m_mode == ORBIT_MODE); - } else if (childState == "orbit.azimuth") { - m_orbitAzimuth = getState("orbit.azimuth").value(); - needsOrbitUpdate = (m_mode == ORBIT_MODE); - } else if (childState == "orbit.elevation") { - m_orbitElevation = getState("orbit.elevation").value(); - needsOrbitUpdate = (m_mode == ORBIT_MODE); - } - // Fly mode state - else if (childState == "fly.position.x") { - m_position[0] = getState("fly.position.x").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.position.y") { - m_position[1] = getState("fly.position.y").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.position.z") { - m_position[2] = getState("fly.position.z").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.focal_point.x") { - m_focalPoint[0] = getState("fly.focal_point.x").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.focal_point.y") { - m_focalPoint[1] = getState("fly.focal_point.y").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.focal_point.z") { - m_focalPoint[2] = getState("fly.focal_point.z").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.yaw") { - m_yaw = getState("fly.yaw").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } else if (childState == "fly.pitch") { - m_pitch = getState("fly.pitch").value(); - needsFlyUpdate = (m_mode == FLY_MODE); - } - // Direct camera state changes (position, view_direction, up_vector, fov) - // These bypass the mode-specific parameters and directly set the camera - else if (childState.find("position.") == 0 || childState.find("view_direction.") == 0 || - childState.find("up_vector.") == 0 || childState == "fov") { - // Direct camera manipulation - apply immediately - double pos[3], dir[3], up[3], fov; - pos[0] = getState("position.x").value(); - pos[1] = getState("position.y").value(); - pos[2] = getState("position.z").value(); - dir[0] = getState("view_direction.x").value(); - dir[1] = getState("view_direction.y").value(); - dir[2] = getState("view_direction.z").value(); - up[0] = getState("up_vector.x").value(); - up[1] = getState("up_vector.y").value(); - up[2] = getState("up_vector.z").value(); - fov = getState("fov").value(); - - runOnMainThread([this, pos, dir, up, fov]() { - m_camera->SetPosition(pos[0], pos[1], pos[2]); - m_camera->SetFocalPoint(pos[0] + dir[0], pos[1] + dir[1], pos[2] + dir[2]); - m_camera->SetViewUp(up[0], up[1], up[2]); - m_camera->SetViewAngle(fov); - }); - } - // Settings - else if (childState == "settings.movement_speed") { - m_movementSpeed = getState("settings.movement_speed").value(); - } else if (childState == "settings.mouse_sensitivity") { - m_mouseSensitivity = getState("settings.mouse_sensitivity").value(); - } else if (childState == "settings.invert_mouse") { - m_invertMouse = getState("settings.invert_mouse").value(); - } - // Key bindings - else if (childState == "keys.forward") { - m_keyForward = getState("keys.forward").value(); - } else if (childState == "keys.backward") { - m_keyBackward = getState("keys.backward").value(); - } else if (childState == "keys.strafe_left") { - m_keyStrafeLeft = getState("keys.strafe_left").value(); - } else if (childState == "keys.strafe_right") { - m_keyStrafeRight = getState("keys.strafe_right").value(); - } else if (childState == "keys.up") { - m_keyUp = getState("keys.up").value(); - } else if (childState == "keys.down") { - m_keyDown = getState("keys.down").value(); - } - - // Apply camera updates if needed - if (needsOrbitUpdate) { - orbitCamera(0, 0); // Reposition camera using current orbit parameters - } else if (needsFlyUpdate) { - updateOrientation(); // Update camera using current fly parameters - } -} - -void CameraController::setCamera(vtkCamera *camera) { - m_camera = camera; - if (m_camera) { - runOnMainThread([this]() { - double *pos = m_camera->GetPosition(); - m_position[0] = pos[0]; - m_position[1] = pos[1]; - m_position[2] = pos[2]; - - // Initialize orbit parameters from current camera - double *focal = m_camera->GetFocalPoint(); - m_orbitCenter[0] = focal[0]; - m_orbitCenter[1] = focal[1]; - m_orbitCenter[2] = focal[2]; - - double dx = pos[0] - focal[0]; - double dy = pos[1] - focal[1]; - double dz = pos[2] - focal[2]; - m_orbitDistance = std::sqrt(dx * dx + dy * dy + dz * dz); - }); - - // Sync initial camera state to state tree - syncCameraToState(); - - // Also update orbit parameters in state - getState("orbit.center.x").value(m_orbitCenter[0]); - getState("orbit.center.y").value(m_orbitCenter[1]); - getState("orbit.center.z").value(m_orbitCenter[2]); - getState("orbit.distance").value(m_orbitDistance); - } -} - -void CameraController::setOrbitCenter(double x, double y, double z) { - m_orbitCenter[0] = x; - m_orbitCenter[1] = y; - m_orbitCenter[2] = z; - - // Update state - getState("orbit.center.x").value(x); - getState("orbit.center.y").value(y); - getState("orbit.center.z").value(z); -} - -void CameraController::setKeyBindings(int forward, int backward, int left, int right, int up, - int down) { - m_keyForward = forward; - m_keyBackward = backward; - m_keyStrafeLeft = left; - m_keyStrafeRight = right; - m_keyUp = up; - m_keyDown = down; - - // Update state - getState("keys.forward").value(forward); - getState("keys.backward").value(backward); - getState("keys.strafe_left").value(left); - getState("keys.strafe_right").value(right); - getState("keys.up").value(up); - getState("keys.down").value(down); -} - -void CameraController::getCameraState(double pos[3], double dir[3], double up[3], double &fov) { - if (!m_camera) - return; - - runOnMainThread([this, pos, dir, up, &fov]() { - double *camPos = m_camera->GetPosition(); - pos[0] = camPos[0]; - pos[1] = camPos[1]; - pos[2] = camPos[2]; - - // Get view direction from focal point - double *focal = m_camera->GetFocalPoint(); - double dx = focal[0] - camPos[0]; - double dy = focal[1] - camPos[1]; - double dz = focal[2] - camPos[2]; - double len = std::sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0.0001) { - dir[0] = dx / len; - dir[1] = dy / len; - dir[2] = dz / len; - } else { - dir[0] = 0.0; - dir[1] = 1.0; - dir[2] = 0.0; - } - - double *camUp = m_camera->GetViewUp(); - up[0] = camUp[0]; - up[1] = camUp[1]; - up[2] = camUp[2]; - - fov = m_camera->GetViewAngle(); - }); -} - -void CameraController::setCameraState(const double pos[3], const double dir[3], const double up[3], - double fov) { - if (!m_camera) - return; - - // Update internal position state - m_position[0] = pos[0]; - m_position[1] = pos[1]; - m_position[2] = pos[2]; - - runOnMainThread([this, pos, dir, up, fov]() { - // Set camera position - m_camera->SetPosition(pos[0], pos[1], pos[2]); - - // Set focal point based on view direction - // Place focal point 1 unit in front of camera - m_camera->SetFocalPoint(pos[0] + dir[0], pos[1] + dir[1], pos[2] + dir[2]); - - // Set up vector - m_camera->SetViewUp(up[0], up[1], up[2]); - - // Set field of view - m_camera->SetViewAngle(fov); - }); - - // Sync camera state to state tree - syncCameraToState(); -} - -void CameraController::applyCameraToVTK() { - if (!m_camera) - return; - - if (m_mode == ORBIT_MODE) { - // In orbit mode, use orbit parameters - updateOrientation(); - } else { - // In fly mode, use fly parameters - updateOrientation(); - } -} - -void CameraController::handleKeyPress(int key) { - m_keysPressed.insert(key); - - // Update read-only input state - getState("input.keys_pressed_count").readOnly(false); - getState("input.keys_pressed_count").value(static_cast(m_keysPressed.size())); - getState("input.keys_pressed_count").readOnly(true); -} - -void CameraController::handleKeyRelease(int key) { - m_keysPressed.erase(key); - - // Update read-only input state - getState("input.keys_pressed_count").readOnly(false); - getState("input.keys_pressed_count").value(static_cast(m_keysPressed.size())); - getState("input.keys_pressed_count").readOnly(true); -} - -void CameraController::handleMousePress(int button) { - if (button == Qt::LeftButton) { - m_mouseLeftPressed = true; - getState("input.mouse_left_pressed").readOnly(false); - getState("input.mouse_left_pressed").value(true); - getState("input.mouse_left_pressed").readOnly(true); - } else if (button == Qt::RightButton) { - m_mouseRightPressed = true; - getState("input.mouse_right_pressed").readOnly(false); - getState("input.mouse_right_pressed").value(true); - getState("input.mouse_right_pressed").readOnly(true); - } else if (button == Qt::MiddleButton) { - m_mouseMiddlePressed = true; - getState("input.mouse_middle_pressed").readOnly(false); - getState("input.mouse_middle_pressed").value(true); - getState("input.mouse_middle_pressed").readOnly(true); - } -} - -void CameraController::handleMouseRelease(int button) { - if (button == Qt::LeftButton) { - m_mouseLeftPressed = false; - getState("input.mouse_left_pressed").readOnly(false); - getState("input.mouse_left_pressed").value(false); - getState("input.mouse_left_pressed").readOnly(true); - } else if (button == Qt::RightButton) { - m_mouseRightPressed = false; - getState("input.mouse_right_pressed").readOnly(false); - getState("input.mouse_right_pressed").value(false); - getState("input.mouse_right_pressed").readOnly(true); - } else if (button == Qt::MiddleButton) { - m_mouseMiddlePressed = false; - getState("input.mouse_middle_pressed").readOnly(false); - getState("input.mouse_middle_pressed").value(false); - getState("input.mouse_middle_pressed").readOnly(true); - } -} - -void CameraController::handleMouseMove(int dx, int dy) { - if (m_mouseLeftPressed || m_mouseRightPressed) { - if (m_mode == ORBIT_MODE) { - orbitCamera(dx, dy); - } else { - // Fly mode - update yaw and pitch - double yawDelta = dx * m_mouseSensitivity * 0.2; - double pitchDelta = dy * m_mouseSensitivity * 0.2; - - if (m_invertMouse) { - pitchDelta = -pitchDelta; - } - - m_yaw += yawDelta; - m_pitch += pitchDelta; - - // Clamp pitch to avoid gimbal lock - const double maxPitch = 89.0; - if (m_pitch > maxPitch) - m_pitch = maxPitch; - if (m_pitch < -maxPitch) - m_pitch = -maxPitch; - - updateOrientation(); - } - } else if (m_mouseMiddlePressed) { - // Middle mouse button - pan camera - panCamera(dx, dy); - } -} - -void CameraController::handleMouseWheel(int delta) { - if (m_mode == ORBIT_MODE) { - // Zoom by changing orbit distance - double zoomFactor = (delta > 0 ? 0.9 : 1.1); - m_orbitDistance *= zoomFactor; - if (m_orbitDistance < 0.1) - m_orbitDistance = 0.1; - orbitCamera(0, 0); - } else { - // Fly mode - move forward/backward - double amount = (delta > 0 ? 1.0 : -1.0) * m_movementSpeed * 0.1; - move(amount, 0.0, 0.0); - } -} - -void CameraController::update() { - if (!m_camera) - return; - - // Only handle keyboard movement in fly mode - if (m_mode == FLY_MODE) { - double forward = 0.0; - double right = 0.0; - double up = 0.0; - - double frameSpeed = m_movementSpeed * 0.016; // Assume ~60fps - - if (m_keysPressed.count(m_keyForward)) - forward += frameSpeed; - if (m_keysPressed.count(m_keyBackward)) - forward -= frameSpeed; - if (m_keysPressed.count(m_keyStrafeRight)) - right += frameSpeed; - if (m_keysPressed.count(m_keyStrafeLeft)) - right -= frameSpeed; - if (m_keysPressed.count(m_keyUp)) - up += frameSpeed; - if (m_keysPressed.count(m_keyDown)) - up -= frameSpeed; - - if (forward != 0.0 || right != 0.0 || up != 0.0) { - move(forward, right, up); - } - } -} - -void CameraController::updateOrientation() { - if (!m_camera) - return; - - // Convert yaw and pitch to radians - double yawRad = m_yaw * M_PI / 180.0; - double pitchRad = m_pitch * M_PI / 180.0; - - // Calculate forward vector - double forward[3]; - forward[0] = cos(pitchRad) * sin(yawRad); - forward[1] = sin(pitchRad); - forward[2] = -cos(pitchRad) * cos(yawRad); - - // Update focal point based on new orientation - m_focalPoint[0] = m_position[0] + forward[0]; - m_focalPoint[1] = m_position[1] + forward[1]; - m_focalPoint[2] = m_position[2] + forward[2]; - - runOnMainThread([this]() { - // Update camera - m_camera->SetPosition(m_position); - m_camera->SetFocalPoint(m_focalPoint); - m_camera->SetViewUp(0, 1, 0); - }); - - // Update state - getState("fly.position.x").value(m_position[0]); - getState("fly.position.y").value(m_position[1]); - getState("fly.position.z").value(m_position[2]); - getState("fly.focal_point.x").value(m_focalPoint[0]); - getState("fly.focal_point.y").value(m_focalPoint[1]); - getState("fly.focal_point.z").value(m_focalPoint[2]); - getState("fly.yaw").value(m_yaw); - getState("fly.pitch").value(m_pitch); - - syncCameraToState(); -} - -void CameraController::syncCameraToState() { - if (!m_camera) - return; - - double pos[3], dir[3], up[3], fov; - getCameraState(pos, dir, up, fov); - - // Update position in state - getState("position.x").value(pos[0]); - getState("position.y").value(pos[1]); - getState("position.z").value(pos[2]); - - // Update view direction in state - getState("view_direction.x").value(dir[0]); - getState("view_direction.y").value(dir[1]); - getState("view_direction.z").value(dir[2]); - - // Update up vector in state - getState("up_vector.x").value(up[0]); - getState("up_vector.y").value(up[1]); - getState("up_vector.z").value(up[2]); - - // Update field of view - getState("fov").value(fov); -} - -void CameraController::move(double forward, double right, double up) { - if (!m_camera) - return; - - // Convert yaw to radians - double yawRad = m_yaw * M_PI / 180.0; - - // Calculate forward and right vectors - double forwardVec[3]; - forwardVec[0] = sin(yawRad); - forwardVec[1] = 0.0; // Keep movement on horizontal plane - forwardVec[2] = -cos(yawRad); - - double rightVec[3]; - rightVec[0] = cos(yawRad); - rightVec[1] = 0.0; - rightVec[2] = sin(yawRad); - - // Update position - m_position[0] += forward * forwardVec[0] + right * rightVec[0]; - m_position[1] += up; // Vertical movement - m_position[2] += forward * forwardVec[2] + right * rightVec[2]; - - // Update focal point to maintain current view direction - runOnMainThread([this, forward, right, up, forwardVec, rightVec]() { - double *focal = m_camera->GetFocalPoint(); - m_focalPoint[0] = focal[0] + forward * forwardVec[0] + right * rightVec[0]; - m_focalPoint[1] = focal[1] + up; - m_focalPoint[2] = focal[2] + forward * forwardVec[2] + right * rightVec[2]; - }); - - updateOrientation(); -} - -void CameraController::orbitCamera(int dx, int dy) { - if (!m_camera) - return; - - // Update orbit angles - m_orbitAzimuth += dx * m_mouseSensitivity * 0.5; - m_orbitElevation -= dy * m_mouseSensitivity * 0.5; - - // Clamp elevation - if (m_orbitElevation > 89.0) - m_orbitElevation = 89.0; - if (m_orbitElevation < -89.0) - m_orbitElevation = -89.0; - - // Update state - getState("orbit.azimuth").value(m_orbitAzimuth); - getState("orbit.elevation").value(m_orbitElevation); - - // Convert to radians - double azimuthRad = m_orbitAzimuth * M_PI / 180.0; - double elevationRad = m_orbitElevation * M_PI / 180.0; - - // Calculate camera position on sphere around orbit center - double x = m_orbitCenter[0] + m_orbitDistance * cos(elevationRad) * sin(azimuthRad); - double y = m_orbitCenter[1] + m_orbitDistance * sin(elevationRad); - double z = m_orbitCenter[2] + m_orbitDistance * cos(elevationRad) * cos(azimuthRad); - - runOnMainThread([this, x, y, z]() { - // Update camera - m_camera->SetPosition(x, y, z); - m_camera->SetFocalPoint(m_orbitCenter); - m_camera->SetViewUp(0, 1, 0); - }); - - syncCameraToState(); -} - -void CameraController::panCamera(int dx, int dy) { - if (!m_camera) - return; - - // Pan speed factor - double panSpeed = 0.001 * m_mouseSensitivity; - - if (m_mode == ORBIT_MODE) { - // In orbit mode, pan by moving the orbit center - // Get camera right and up vectors - double pos[3], focal[3], up[3]; - m_camera->GetPosition(pos); - m_camera->GetFocalPoint(focal); - m_camera->GetViewUp(up); - - // Calculate right vector (cross product of view direction and up) - double viewDir[3] = {focal[0] - pos[0], focal[1] - pos[1], focal[2] - pos[2]}; - double right[3]; - right[0] = viewDir[1] * up[2] - viewDir[2] * up[1]; - right[1] = viewDir[2] * up[0] - viewDir[0] * up[2]; - right[2] = viewDir[0] * up[1] - viewDir[1] * up[0]; - - // Normalize vectors - double rightLen = sqrt(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]); - double upLen = sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]); - if (rightLen > 1e-9 && upLen > 1e-9) { - for (int i = 0; i < 3; i++) { - right[i] /= rightLen; - up[i] /= upLen; - } - - // Pan is proportional to distance from center - double panFactor = m_orbitDistance * panSpeed; - - // Update orbit center - m_orbitCenter[0] += (-dx * right[0] + dy * up[0]) * panFactor; - m_orbitCenter[1] += (-dx * right[1] + dy * up[1]) * panFactor; - m_orbitCenter[2] += (-dx * right[2] + dy * up[2]) * panFactor; - - // Update state - getState("orbit.center.x").value(m_orbitCenter[0]); - getState("orbit.center.y").value(m_orbitCenter[1]); - getState("orbit.center.z").value(m_orbitCenter[2]); - - // Reposition camera around new orbit center - orbitCamera(0, 0); - } - } else { - // Fly mode - pan by moving both position and focal point - // Get camera right and up vectors - double pos[3], focal[3], up[3]; - m_camera->GetPosition(pos); - m_camera->GetFocalPoint(focal); - m_camera->GetViewUp(up); - - // Calculate right vector - double viewDir[3] = {focal[0] - pos[0], focal[1] - pos[1], focal[2] - pos[2]}; - double right[3]; - right[0] = viewDir[1] * up[2] - viewDir[2] * up[1]; - right[1] = viewDir[2] * up[0] - viewDir[0] * up[2]; - right[2] = viewDir[0] * up[1] - viewDir[1] * up[0]; - - // Normalize vectors - double rightLen = sqrt(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]); - double upLen = sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]); - if (rightLen > 1e-9 && upLen > 1e-9) { - for (int i = 0; i < 3; i++) { - right[i] /= rightLen; - up[i] /= upLen; - } - - // Pan factor for fly mode - double panFactor = 0.1 * panSpeed * m_movementSpeed; - - // Pan delta - double deltaX = (-dx * right[0] + dy * up[0]) * panFactor; - double deltaY = (-dx * right[1] + dy * up[1]) * panFactor; - double deltaZ = (-dx * right[2] + dy * up[2]) * panFactor; - - // Update position and focal point - m_position[0] += deltaX; - m_position[1] += deltaY; - m_position[2] += deltaZ; - m_focalPoint[0] += deltaX; - m_focalPoint[1] += deltaY; - m_focalPoint[2] += deltaZ; - - // Apply to camera - runOnMainThread([this]() { - m_camera->SetPosition(m_position); - m_camera->SetFocalPoint(m_focalPoint); - }); - - syncCameraToState(); - } - } -} - -void CameraController::updateOrbitCenterFromBounds(double minX, double minY, double minZ, - double maxX, double maxY, double maxZ) { - // Set orbit center to center of bounding box - m_orbitCenter[0] = (minX + maxX) * 0.5; - m_orbitCenter[1] = (minY + maxY) * 0.5; - m_orbitCenter[2] = (minZ + maxZ) * 0.5; - - // Update camera focal point to maintain orbit - if (m_camera && m_mode == ORBIT_MODE) { - runOnMainThread([this]() { m_camera->SetFocalPoint(m_orbitCenter); }); - } -} - -void CameraController::resetView(double minX, double minY, double minZ, double maxX, double maxY, - double maxZ) { - if (!m_camera) - return; - - // Calculate bounding box center and size - double centerX = (minX + maxX) * 0.5; - double centerY = (minY + maxY) * 0.5; - double centerZ = (minZ + maxZ) * 0.5; - - double sizeX = maxX - minX; - double sizeY = maxY - minY; - double sizeZ = maxZ - minZ; - double maxSize = std::max({sizeX, sizeY, sizeZ}); - - // Position camera to view entire scene - double distance = maxSize * 2.0; // Distance to see entire bounding box - - if (m_mode == ORBIT_MODE) { - // Update orbit center to bounding box center - m_orbitCenter[0] = centerX; - m_orbitCenter[1] = centerY; - m_orbitCenter[2] = centerZ; - m_orbitDistance = distance; - m_orbitAzimuth = 45.0; - m_orbitElevation = 30.0; - - // Update state - getState("orbit.center.x").value(m_orbitCenter[0]); - getState("orbit.center.y").value(m_orbitCenter[1]); - getState("orbit.center.z").value(m_orbitCenter[2]); - getState("orbit.distance").value(m_orbitDistance); - getState("orbit.azimuth").value(m_orbitAzimuth); - getState("orbit.elevation").value(m_orbitElevation); - - // Position camera - orbitCamera(0, 0); - } else { - // Fly mode - position camera back from center - m_position[0] = centerX; - m_position[1] = centerY + maxSize * 0.5; - m_position[2] = centerZ + distance; - - m_focalPoint[0] = centerX; - m_focalPoint[1] = centerY; - m_focalPoint[2] = centerZ; - - // Calculate yaw and pitch to look at center - m_yaw = 0.0; - m_pitch = -20.0; - - updateOrientation(); - } -} diff --git a/src/volrover3/CameraSettingsDialog.cpp b/src/volrover3/CameraSettingsDialog.cpp deleted file mode 100644 index 974f3dc2..00000000 --- a/src/volrover3/CameraSettingsDialog.cpp +++ /dev/null @@ -1,365 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// KeyBindButton implementation -KeyBindButton::KeyBindButton(int initialKey, QWidget *parent) - : QPushButton(parent), m_key(initialKey), m_waitingForKey(false) { - updateText(); - connect(this, &QPushButton::clicked, this, &KeyBindButton::startCapture); -} - -void KeyBindButton::setKey(int key) { - m_key = key; - m_waitingForKey = false; - updateText(); -} - -void KeyBindButton::keyPressEvent(QKeyEvent *event) { - if (m_waitingForKey) { - m_key = event->key(); - m_waitingForKey = false; - updateText(); - emit keyChanged(m_key); - clearFocus(); - } else { - QPushButton::keyPressEvent(event); - } -} - -void KeyBindButton::focusOutEvent(QFocusEvent *event) { - if (m_waitingForKey) { - m_waitingForKey = false; - updateText(); - } - QPushButton::focusOutEvent(event); -} - -void KeyBindButton::startCapture() { - m_waitingForKey = true; - updateText(); - setFocus(); -} - -void KeyBindButton::updateText() { - if (m_waitingForKey) { - setText(tr("Press a key...")); - setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }"); - } else { - setText(QKeySequence(m_key).toString()); - setStyleSheet(""); - } -} - -// CameraSettingsDialog implementation - -CameraSettingsDialog::CameraSettingsDialog(const CameraSettings &settings, cvc::state *cameraState, - QWidget *parent) - : QDialog(parent), m_cameraState(cameraState), m_stateTable(nullptr) { - setWindowTitle(tr("Camera Settings")); - setupUI(settings); - - // Connect UI controls to emit settingsChanged signal - connect(m_modeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_flySpeedSpin, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_mouseSensitivitySpin, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_invertMouseCheck, &QCheckBox::toggled, this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_keyForwardButton, &KeyBindButton::keyChanged, this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_keyBackwardButton, &KeyBindButton::keyChanged, this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_keyStrafeLeftButton, &KeyBindButton::keyChanged, this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_keyStrafeRightButton, &KeyBindButton::keyChanged, this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_keyUpButton, &KeyBindButton::keyChanged, this, - &CameraSettingsDialog::emitSettingsChanged); - connect(m_keyDownButton, &KeyBindButton::keyChanged, this, - &CameraSettingsDialog::emitSettingsChanged); - - // Subscribe to camera state changes if provided - if (m_cameraState) { - m_stateConnection = m_cameraState->childChanged.connect([this](const std::string &) { - // Update UI on main thread using Qt's queued connection mechanism - QMetaObject::invokeMethod(this, "updateStateDisplay", Qt::QueuedConnection); - }); - updateStateDisplay(); // Initial update - } -} - -CameraSettingsDialog::~CameraSettingsDialog() { - // Disconnect from state changes - m_stateConnection.disconnect(); -} - -void CameraSettingsDialog::setupUI(const CameraSettings &settings) { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Create tab widget - QTabWidget *tabWidget = new QTabWidget(this); - - // === Mode Tab === - QWidget *modeTab = new QWidget(); - QVBoxLayout *modeTabLayout = new QVBoxLayout(modeTab); - - // Camera mode selection - QGroupBox *modeGroup = new QGroupBox(tr("Camera Mode")); - QFormLayout *modeLayout = new QFormLayout(modeGroup); - - m_modeCombo = new QComboBox(); - m_modeCombo->addItem(tr("Orbit (Trackball)"), 0); - m_modeCombo->addItem(tr("Fly (FPS)"), 1); - m_modeCombo->setCurrentIndex(settings.mode); - modeLayout->addRow(tr("Mode:"), m_modeCombo); - - modeTabLayout->addWidget(modeGroup); - - // Movement settings - QGroupBox *movementGroup = new QGroupBox(tr("Movement Settings")); - QFormLayout *movementLayout = new QFormLayout(movementGroup); - - m_flySpeedSpin = new QDoubleSpinBox(); - m_flySpeedSpin->setRange(0.1, 100.0); - m_flySpeedSpin->setSingleStep(0.5); - m_flySpeedSpin->setValue(settings.flySpeed); - m_flySpeedSpin->setSuffix(tr(" units/sec")); - movementLayout->addRow(tr("Fly Speed:"), m_flySpeedSpin); - - m_mouseSensitivitySpin = new QDoubleSpinBox(); - m_mouseSensitivitySpin->setRange(0.1, 10.0); - m_mouseSensitivitySpin->setSingleStep(0.1); - m_mouseSensitivitySpin->setValue(settings.mouseSensitivity); - movementLayout->addRow(tr("Mouse Sensitivity:"), m_mouseSensitivitySpin); - - m_invertMouseCheck = new QCheckBox(tr("Invert mouse Y-axis")); - m_invertMouseCheck->setChecked(settings.invertMouse); - movementLayout->addRow("", m_invertMouseCheck); - - modeTabLayout->addWidget(movementGroup); - modeTabLayout->addStretch(); - - // === Key Bindings Tab === - QWidget *keysTab = new QWidget(); - QVBoxLayout *keysTabLayout = new QVBoxLayout(keysTab); - - QGroupBox *keysGroup = new QGroupBox(tr("Key Bindings (Click to rebind)")); - QFormLayout *keysLayout = new QFormLayout(keysGroup); - - m_keyForwardButton = new KeyBindButton(settings.keyForward); - keysLayout->addRow(tr("Forward:"), m_keyForwardButton); - - m_keyBackwardButton = new KeyBindButton(settings.keyBackward); - keysLayout->addRow(tr("Backward:"), m_keyBackwardButton); - - m_keyStrafeLeftButton = new KeyBindButton(settings.keyStrafeLeft); - keysLayout->addRow(tr("Strafe Left:"), m_keyStrafeLeftButton); - - m_keyStrafeRightButton = new KeyBindButton(settings.keyStrafeRight); - keysLayout->addRow(tr("Strafe Right:"), m_keyStrafeRightButton); - - m_keyUpButton = new KeyBindButton(settings.keyUp); - keysLayout->addRow(tr("Up:"), m_keyUpButton); - - m_keyDownButton = new KeyBindButton(settings.keyDown); - keysLayout->addRow(tr("Down:"), m_keyDownButton); - - keysTabLayout->addWidget(keysGroup); - keysTabLayout->addStretch(); - - // === State Tab === - QWidget *stateTab = new QWidget(); - QVBoxLayout *stateTabLayout = new QVBoxLayout(stateTab); - - QLabel *stateLabel = new QLabel(tr("Camera state from state tree (read-only):"), stateTab); - stateTabLayout->addWidget(stateLabel); - - m_stateTable = new QTableWidget(this); - m_stateTable->setObjectName("cameraStateTable"); - m_stateTable->setColumnCount(2); - m_stateTable->setHorizontalHeaderLabels({tr("Property"), tr("Value")}); - m_stateTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); - m_stateTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); - m_stateTable->verticalHeader()->setVisible(false); - m_stateTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - m_stateTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_stateTable->setAlternatingRowColors(true); - stateTabLayout->addWidget(m_stateTable); - - // Add tabs - tabWidget->addTab(modeTab, tr("Mode")); - tabWidget->addTab(keysTab, tr("Key Bindings")); - tabWidget->addTab(stateTab, tr("State")); - - mainLayout->addWidget(tabWidget); - - // Reset to defaults and reset view buttons - QHBoxLayout *resetLayout = new QHBoxLayout(); - QPushButton *resetButton = new QPushButton(tr("Reset to Defaults")); - connect(resetButton, &QPushButton::clicked, this, &CameraSettingsDialog::onResetDefaults); - resetLayout->addWidget(resetButton); - - QPushButton *resetViewButton = new QPushButton(tr("Reset View")); - resetViewButton->setToolTip(tr("Position camera to view the entire scene")); - connect(resetViewButton, &QPushButton::clicked, this, &CameraSettingsDialog::onResetView); - resetLayout->addWidget(resetViewButton); - mainLayout->addLayout(resetLayout); -} - -void CameraSettingsDialog::updateStateDisplay() { - if (!m_cameraState || !m_stateTable) - return; - - CameraState state = readCameraState(); - - // Define the properties to display - struct Property { - QString name; - QString value; - }; - - QList properties = { - {tr("Mode"), state.mode == 0 ? tr("Orbit") : tr("Fly")}, - {tr("Position X"), QString::number(state.positionX, 'f', 4)}, - {tr("Position Y"), QString::number(state.positionY, 'f', 4)}, - {tr("Position Z"), QString::number(state.positionZ, 'f', 4)}, - {tr("View Direction X"), QString::number(state.viewDirX, 'f', 4)}, - {tr("View Direction Y"), QString::number(state.viewDirY, 'f', 4)}, - {tr("View Direction Z"), QString::number(state.viewDirZ, 'f', 4)}, - {tr("Up Vector X"), QString::number(state.upX, 'f', 4)}, - {tr("Up Vector Y"), QString::number(state.upY, 'f', 4)}, - {tr("Up Vector Z"), QString::number(state.upZ, 'f', 4)}, - {tr("Field of View"), QString::number(state.fov, 'f', 2) + QString::fromUtf8("°")}, - {tr("---Orbit Mode---"), ""}, - {tr("Center X"), QString::number(state.orbitCenterX, 'f', 4)}, - {tr("Center Y"), QString::number(state.orbitCenterY, 'f', 4)}, - {tr("Center Z"), QString::number(state.orbitCenterZ, 'f', 4)}, - {tr("Distance"), QString::number(state.orbitDistance, 'f', 4)}, - {tr("Azimuth"), QString::number(state.orbitAzimuth, 'f', 2) + QString::fromUtf8("°")}, - {tr("Elevation"), QString::number(state.orbitElevation, 'f', 2) + QString::fromUtf8("°")}, - {tr("---Fly Mode---"), ""}, - {tr("Yaw"), QString::number(state.flyYaw, 'f', 2) + QString::fromUtf8("°")}, - {tr("Pitch"), QString::number(state.flyPitch, 'f', 2) + QString::fromUtf8("°")}, - {tr("Focal Point X"), QString::number(state.flyFocalX, 'f', 4)}, - {tr("Focal Point Y"), QString::number(state.flyFocalY, 'f', 4)}, - {tr("Focal Point Z"), QString::number(state.flyFocalZ, 'f', 4)}, - }; - - m_stateTable->setRowCount(properties.size()); - for (int i = 0; i < properties.size(); ++i) { - m_stateTable->setItem(i, 0, new QTableWidgetItem(properties[i].name)); - m_stateTable->setItem(i, 1, new QTableWidgetItem(properties[i].value)); - - // Style section headers - if (properties[i].name.startsWith("---")) { - QFont boldFont; - boldFont.setBold(true); - m_stateTable->item(i, 0)->setFont(boldFont); - m_stateTable->item(i, 0)->setBackground(QColor(220, 220, 220)); - m_stateTable->item(i, 1)->setBackground(QColor(220, 220, 220)); - } - } -} - -CameraSettingsDialog::CameraState CameraSettingsDialog::readCameraState() const { - CameraState state = {}; - if (!m_cameraState) - return state; - - // Read from state tree children using operator() - state.mode = (*m_cameraState)("mode").value(); - state.positionX = (*m_cameraState)("position.x").value(); - state.positionY = (*m_cameraState)("position.y").value(); - state.positionZ = (*m_cameraState)("position.z").value(); - state.viewDirX = (*m_cameraState)("view_direction.x").value(); - state.viewDirY = (*m_cameraState)("view_direction.y").value(); - state.viewDirZ = (*m_cameraState)("view_direction.z").value(); - state.upX = (*m_cameraState)("up_vector.x").value(); - state.upY = (*m_cameraState)("up_vector.y").value(); - state.upZ = (*m_cameraState)("up_vector.z").value(); - state.fov = (*m_cameraState)("fov").value(); - state.orbitCenterX = (*m_cameraState)("orbit.center.x").value(); - state.orbitCenterY = (*m_cameraState)("orbit.center.y").value(); - state.orbitCenterZ = (*m_cameraState)("orbit.center.z").value(); - state.orbitDistance = (*m_cameraState)("orbit.distance").value(); - state.orbitAzimuth = (*m_cameraState)("orbit.azimuth").value(); - state.orbitElevation = (*m_cameraState)("orbit.elevation").value(); - state.flyYaw = (*m_cameraState)("fly.yaw").value(); - state.flyPitch = (*m_cameraState)("fly.pitch").value(); - state.flyFocalX = (*m_cameraState)("fly.focal_point.x").value(); - state.flyFocalY = (*m_cameraState)("fly.focal_point.y").value(); - state.flyFocalZ = (*m_cameraState)("fly.focal_point.z").value(); - - return state; -} - -CameraSettingsDialog::CameraSettings CameraSettingsDialog::getSettings() const { - CameraSettings settings; - settings.mode = m_modeCombo->currentData().toInt(); - settings.flySpeed = m_flySpeedSpin->value(); - settings.mouseSensitivity = m_mouseSensitivitySpin->value(); - settings.invertMouse = m_invertMouseCheck->isChecked(); - settings.keyForward = m_keyForwardButton->key(); - settings.keyBackward = m_keyBackwardButton->key(); - settings.keyStrafeLeft = m_keyStrafeLeftButton->key(); - settings.keyStrafeRight = m_keyStrafeRightButton->key(); - settings.keyUp = m_keyUpButton->key(); - settings.keyDown = m_keyDownButton->key(); - return settings; -} - -CameraSettingsDialog::CameraSettings CameraSettingsDialog::getDefaultSettings() const { - CameraSettings settings; - settings.mode = 0; // Orbit mode - settings.flySpeed = 5.0; - settings.mouseSensitivity = 1.0; - settings.invertMouse = false; - settings.keyForward = Qt::Key_W; - settings.keyBackward = Qt::Key_S; - settings.keyStrafeLeft = Qt::Key_A; - settings.keyStrafeRight = Qt::Key_D; - settings.keyUp = Qt::Key_Space; - settings.keyDown = Qt::Key_Control; - return settings; -} - -void CameraSettingsDialog::onResetDefaults() { - CameraSettings defaults = getDefaultSettings(); - m_modeCombo->setCurrentIndex(defaults.mode); - m_flySpeedSpin->setValue(defaults.flySpeed); - m_mouseSensitivitySpin->setValue(defaults.mouseSensitivity); - m_invertMouseCheck->setChecked(defaults.invertMouse); - m_keyForwardButton->setKey(defaults.keyForward); - m_keyBackwardButton->setKey(defaults.keyBackward); - m_keyStrafeLeftButton->setKey(defaults.keyStrafeLeft); - m_keyStrafeRightButton->setKey(defaults.keyStrafeRight); - m_keyUpButton->setKey(defaults.keyUp); - m_keyDownButton->setKey(defaults.keyDown); - - // Emit settings changed for real-time application - emitSettingsChanged(); -} - -void CameraSettingsDialog::onResetView() { - // Signal to parent to reset the camera view - // This will be handled by MainWindow - emit resetViewRequested(); -} - -void CameraSettingsDialog::emitSettingsChanged() { - // Get current settings and emit signal for real-time application - emit settingsChanged(getSettings()); -} diff --git a/src/volrover3/GeometryDialog.cpp b/src/volrover3/GeometryDialog.cpp deleted file mode 100644 index 6a9b3f91..00000000 --- a/src/volrover3/GeometryDialog.cpp +++ /dev/null @@ -1,1428 +0,0 @@ -#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 - -GeometryDialog::GeometryDialog(std::shared_ptr sceneGraph, QWidget *parent) - : QDialog(parent), m_sceneGraph(sceneGraph), m_geometryComboBox(nullptr), - m_renderModeComboBox(nullptr), m_singleColorCheckBox(nullptr), m_colorRSpinBox(nullptr), - m_colorGSpinBox(nullptr), m_colorBSpinBox(nullptr), m_visibilityCheckBox(nullptr), - m_showBBoxCheckBox(nullptr), m_bboxColorButton(nullptr), m_invertNormalsButton(nullptr), - m_reorientButton(nullptr), m_projectButton(nullptr), m_projectTargetComboBox(nullptr), - m_smoothingButton(nullptr), m_smoothingDeltaSpinBox(nullptr), - m_smoothingFixBoundaryCheckBox(nullptr), m_smoothingPerturb1CheckBox(nullptr), - m_smoothingGeoFlowCheckBox(nullptr), m_smoothingEnabledCheckBox(nullptr), - m_smoothingPerturb2CheckBox(nullptr), m_qualityImproveButton(nullptr), - m_qualityIterationsSpinBox(nullptr), m_qualityMethodComboBox(nullptr), - m_ambientSpinBox(nullptr), m_diffuseSpinBox(nullptr), m_specularSpinBox(nullptr), - m_specularPowerSpinBox(nullptr), m_opacitySpinBox(nullptr), m_pointSizeSpinBox(nullptr), - m_lineWidthSpinBox(nullptr), m_infoTable(nullptr), m_updating(false) { - m_bboxColor[0] = m_bboxColor[1] = m_bboxColor[2] = 1.0; // Default white - setWindowTitle(tr("Geometry Properties")); - setMinimumWidth(400); - setupUI(); - connectSignals(); - populateGeometryList(); - - // Connect to SceneGraph's graphicsChanged signal to update the combo box - // when graphics are added or removed - if (m_sceneGraph) { - m_graphicsChangedConnection = m_sceneGraph->graphicsChanged.connect([this]() { - QMetaObject::invokeMethod(this, "onGraphicsChildrenChanged", Qt::QueuedConnection); - }); - } -} - -void GeometryDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Geometry Selection Group (always visible at top) - QGroupBox *selectionGroup = new QGroupBox(tr("Geometry Selection"), this); - QVBoxLayout *selectionVLayout = new QVBoxLayout(selectionGroup); - - // Combo box and delete button in horizontal layout - QHBoxLayout *comboLayout = new QHBoxLayout(); - m_geometryComboBox = new QComboBox(this); - m_geometryComboBox->setObjectName("geometryComboBox"); - m_deleteButton = new QPushButton(tr("Delete"), this); - m_deleteButton->setToolTip(tr("Remove selected geometry from scene")); - comboLayout->addWidget(new QLabel(tr("Geometry:"), this)); - comboLayout->addWidget(m_geometryComboBox, 1); - comboLayout->addWidget(m_deleteButton); - selectionVLayout->addLayout(comboLayout); - - mainLayout->addWidget(selectionGroup); - - // Create tab widget for geometry properties - QTabWidget *tabWidget = new QTabWidget(this); - - // === Appearance Tab === - QWidget *appearanceTab = new QWidget(); - QVBoxLayout *appearanceLayout = new QVBoxLayout(appearanceTab); - - // Render Mode Group - QGroupBox *renderGroup = new QGroupBox(tr("Render Mode"), appearanceTab); - QFormLayout *renderLayout = new QFormLayout(renderGroup); - - m_renderModeComboBox = new QComboBox(this); - m_renderModeComboBox->setObjectName("renderModeComboBox"); - m_renderModeComboBox->addItem(tr("Surface (Triangles)"), - static_cast(GeometryRenderMode::TRIS)); - m_renderModeComboBox->addItem(tr("Surface (Quads)"), static_cast(GeometryRenderMode::QUADS)); - m_renderModeComboBox->addItem(tr("Wireframe"), static_cast(GeometryRenderMode::LINES)); - m_renderModeComboBox->addItem(tr("Points"), static_cast(GeometryRenderMode::POINTS)); - renderLayout->addRow(tr("Mode:"), m_renderModeComboBox); - - appearanceLayout->addWidget(renderGroup); - - // Color Group - QGroupBox *colorGroup = new QGroupBox(tr("Color"), appearanceTab); - QVBoxLayout *colorVLayout = new QVBoxLayout(colorGroup); - - // Single color checkbox - m_singleColorCheckBox = new QCheckBox(tr("Use single color (override vertex colors)"), this); - m_singleColorCheckBox->setObjectName("singleColorCheckBox"); - m_singleColorCheckBox->setChecked(false); - m_singleColorCheckBox->setToolTip( - tr("When enabled, all vertices use the color specified below.\nWhen disabled, per-vertex " - "colors from the geometry data are used if available.")); - colorVLayout->addWidget(m_singleColorCheckBox); - - // Color controls in form layout - QFormLayout *colorLayout = new QFormLayout(); - - m_colorRSpinBox = new QDoubleSpinBox(this); - m_colorRSpinBox->setObjectName("colorRSpinBox"); - m_colorRSpinBox->setRange(0.0, 1.0); - m_colorRSpinBox->setSingleStep(0.01); - m_colorRSpinBox->setDecimals(3); - colorLayout->addRow(tr("Red:"), m_colorRSpinBox); - - m_colorGSpinBox = new QDoubleSpinBox(this); - m_colorGSpinBox->setObjectName("colorGSpinBox"); - m_colorGSpinBox->setRange(0.0, 1.0); - m_colorGSpinBox->setSingleStep(0.01); - m_colorGSpinBox->setDecimals(3); - colorLayout->addRow(tr("Green:"), m_colorGSpinBox); - - m_colorBSpinBox = new QDoubleSpinBox(this); - m_colorBSpinBox->setObjectName("colorBSpinBox"); - m_colorBSpinBox->setRange(0.0, 1.0); - m_colorBSpinBox->setSingleStep(0.01); - m_colorBSpinBox->setDecimals(3); - colorLayout->addRow(tr("Blue:"), m_colorBSpinBox); - - colorVLayout->addLayout(colorLayout); - - appearanceLayout->addWidget(colorGroup); - - // Opacity in appearance tab - QGroupBox *opacityGroup = new QGroupBox(tr("Transparency"), appearanceTab); - QFormLayout *opacityLayout = new QFormLayout(opacityGroup); - - m_opacitySpinBox = new QDoubleSpinBox(this); - m_opacitySpinBox->setRange(0.0, 1.0); - m_opacitySpinBox->setSingleStep(0.01); - m_opacitySpinBox->setDecimals(3); - opacityLayout->addRow(tr("Opacity:"), m_opacitySpinBox); - - appearanceLayout->addWidget(opacityGroup); - appearanceLayout->addStretch(); - - // === Material Tab === - QWidget *materialTab = new QWidget(); - QVBoxLayout *materialLayout = new QVBoxLayout(materialTab); - - QGroupBox *materialGroup = new QGroupBox(tr("Material Properties"), materialTab); - QFormLayout *matLayout = new QFormLayout(materialGroup); - - m_ambientSpinBox = new QDoubleSpinBox(this); - m_ambientSpinBox->setRange(0.0, 1.0); - m_ambientSpinBox->setSingleStep(0.01); - m_ambientSpinBox->setDecimals(3); - matLayout->addRow(tr("Ambient:"), m_ambientSpinBox); - - m_diffuseSpinBox = new QDoubleSpinBox(this); - m_diffuseSpinBox->setRange(0.0, 1.0); - m_diffuseSpinBox->setSingleStep(0.01); - m_diffuseSpinBox->setDecimals(3); - matLayout->addRow(tr("Diffuse:"), m_diffuseSpinBox); - - m_specularSpinBox = new QDoubleSpinBox(this); - m_specularSpinBox->setRange(0.0, 1.0); - m_specularSpinBox->setSingleStep(0.01); - m_specularSpinBox->setDecimals(3); - matLayout->addRow(tr("Specular:"), m_specularSpinBox); - - m_specularPowerSpinBox = new QDoubleSpinBox(this); - m_specularPowerSpinBox->setRange(0.0, 128.0); - m_specularPowerSpinBox->setSingleStep(1.0); - m_specularPowerSpinBox->setDecimals(1); - matLayout->addRow(tr("Specular Power:"), m_specularPowerSpinBox); - - materialLayout->addWidget(materialGroup); - materialLayout->addStretch(); - - // === Rendering Tab === - QWidget *renderingTab = new QWidget(); - QVBoxLayout *renderingLayout = new QVBoxLayout(renderingTab); - - QGroupBox *sizeGroup = new QGroupBox(tr("Point and Line Properties"), renderingTab); - QFormLayout *sizeLayout = new QFormLayout(sizeGroup); - - m_pointSizeSpinBox = new QDoubleSpinBox(this); - m_pointSizeSpinBox->setRange(0.1, 50.0); - m_pointSizeSpinBox->setSingleStep(0.5); - m_pointSizeSpinBox->setDecimals(1); - sizeLayout->addRow(tr("Point Size:"), m_pointSizeSpinBox); - - m_lineWidthSpinBox = new QDoubleSpinBox(this); - m_lineWidthSpinBox->setRange(0.1, 50.0); - m_lineWidthSpinBox->setSingleStep(0.5); - m_lineWidthSpinBox->setDecimals(1); - sizeLayout->addRow(tr("Line Width:"), m_lineWidthSpinBox); - - renderingLayout->addWidget(sizeGroup); - - // Visibility Group - QGroupBox *visibilityGroup = new QGroupBox(tr("Visibility"), renderingTab); - QVBoxLayout *visibilityLayout = new QVBoxLayout(visibilityGroup); - - m_visibilityCheckBox = new QCheckBox(tr("Visible"), this); - m_visibilityCheckBox->setObjectName("visibilityCheckBox"); - m_visibilityCheckBox->setChecked(true); - m_visibilityCheckBox->setToolTip(tr("Show or hide this geometry in the scene")); - visibilityLayout->addWidget(m_visibilityCheckBox); - - renderingLayout->addWidget(visibilityGroup); - - // Bounding Box Group - QGroupBox *bboxGroup = new QGroupBox(tr("Bounding Box"), renderingTab); - QVBoxLayout *bboxLayout = new QVBoxLayout(bboxGroup); - - m_showBBoxCheckBox = new QCheckBox(tr("Show Bounding Box"), this); - m_showBBoxCheckBox->setObjectName("showBBoxCheckBox"); - m_showBBoxCheckBox->setChecked(false); - m_showBBoxCheckBox->setToolTip(tr("Display the bounding box of this geometry")); - bboxLayout->addWidget(m_showBBoxCheckBox); - - QHBoxLayout *bboxColorLayout = new QHBoxLayout(); - bboxColorLayout->addWidget(new QLabel(tr("Color:"), this)); - m_bboxColorButton = new QPushButton(this); - m_bboxColorButton->setFixedSize(50, 25); - m_bboxColorButton->setToolTip(tr("Click to change bounding box color")); - updateBBoxColorButton(); - bboxColorLayout->addWidget(m_bboxColorButton); - bboxColorLayout->addStretch(); - bboxLayout->addLayout(bboxColorLayout); - - // Extent Labels - m_showExtentLabelsCheckBox = new QCheckBox(tr("Show Extent Labels"), this); - m_showExtentLabelsCheckBox->setObjectName("showExtentLabelsCheckBox"); - m_showExtentLabelsCheckBox->setChecked(false); - m_showExtentLabelsCheckBox->setToolTip( - tr("Display min/max coordinate labels on the bounding box")); - bboxLayout->addWidget(m_showExtentLabelsCheckBox); - - QHBoxLayout *extentLabelColorLayout = new QHBoxLayout(); - extentLabelColorLayout->addWidget(new QLabel(tr("Label Color:"), this)); - m_extentLabelColorButton = new QPushButton(this); - m_extentLabelColorButton->setFixedSize(50, 25); - m_extentLabelColorButton->setToolTip(tr("Click to change extent label color")); - // Initialize extent label color to white - m_extentLabelColor[0] = m_extentLabelColor[1] = m_extentLabelColor[2] = 1.0; - updateExtentLabelColorButton(); - extentLabelColorLayout->addWidget(m_extentLabelColorButton); - extentLabelColorLayout->addStretch(); - bboxLayout->addLayout(extentLabelColorLayout); - - QHBoxLayout *extentLabelFontSizeLayout = new QHBoxLayout(); - extentLabelFontSizeLayout->addWidget(new QLabel(tr("Font Size:"), this)); - m_extentLabelFontSizeSpinBox = new QSpinBox(this); - m_extentLabelFontSizeSpinBox->setObjectName("extentLabelFontSizeSpinBox"); - m_extentLabelFontSizeSpinBox->setRange(8, 72); - m_extentLabelFontSizeSpinBox->setValue(12); - m_extentLabelFontSizeSpinBox->setToolTip(tr("Set the font size for extent labels")); - extentLabelFontSizeLayout->addWidget(m_extentLabelFontSizeSpinBox); - extentLabelFontSizeLayout->addStretch(); - bboxLayout->addLayout(extentLabelFontSizeLayout); - - renderingLayout->addWidget(bboxGroup); - renderingLayout->addStretch(); - - // === Operations Tab === - QWidget *operationsTab = new QWidget(); - QVBoxLayout *operationsTabLayout = new QVBoxLayout(operationsTab); - - // Normals Group - QGroupBox *normalsGroup = new QGroupBox(tr("Normals"), operationsTab); - QVBoxLayout *normalsLayout = new QVBoxLayout(normalsGroup); - - // Invert Normals button - m_invertNormalsButton = new QPushButton(tr("Invert Normals"), this); - m_invertNormalsButton->setObjectName("invertNormalsButton"); - m_invertNormalsButton->setToolTip(tr("Invert all vertex and face normals of this geometry")); - normalsLayout->addWidget(m_invertNormalsButton); - - // Reorient button - m_reorientButton = new QPushButton(tr("Reorient"), this); - m_reorientButton->setObjectName("reorientButton"); - m_reorientButton->setToolTip(tr("Make normals consistent across the mesh")); - normalsLayout->addWidget(m_reorientButton); - - operationsTabLayout->addWidget(normalsGroup); - - // Project Group - QGroupBox *projectGroup = new QGroupBox(tr("Projection"), operationsTab); - QVBoxLayout *projectGroupLayout = new QVBoxLayout(projectGroup); - - QHBoxLayout *projectLayout = new QHBoxLayout(); - m_projectButton = new QPushButton(tr("Project"), this); - m_projectButton->setObjectName("projectButton"); - m_projectButton->setToolTip(tr("Project boundary vertices to target geometry")); - projectLayout->addWidget(m_projectButton); - projectLayout->addWidget(new QLabel(tr("Target:"), this)); - m_projectTargetComboBox = new QComboBox(this); - m_projectTargetComboBox->setObjectName("projectTargetComboBox"); - m_projectTargetComboBox->setToolTip(tr("Select target geometry for projection")); - projectLayout->addWidget(m_projectTargetComboBox, 1); - projectGroupLayout->addLayout(projectLayout); - - operationsTabLayout->addWidget(projectGroup); - - // Smoothing Group - QGroupBox *smoothingGroup = new QGroupBox(tr("Smoothing"), operationsTab); - QVBoxLayout *smoothingGroupLayout = new QVBoxLayout(smoothingGroup); - - // Smoothing section - first row with button and delta - QHBoxLayout *smoothingLayout = new QHBoxLayout(); - m_smoothingButton = new QPushButton(tr("Smooth"), this); - m_smoothingButton->setObjectName("smoothingButton"); - m_smoothingButton->setToolTip(tr("Apply smoothing to the mesh")); - smoothingLayout->addWidget(m_smoothingButton); - smoothingLayout->addWidget(new QLabel(tr("Delta:"), this)); - m_smoothingDeltaSpinBox = new QDoubleSpinBox(this); - m_smoothingDeltaSpinBox->setObjectName("smoothingDeltaSpinBox"); - m_smoothingDeltaSpinBox->setRange(0.001, 1.0); - m_smoothingDeltaSpinBox->setSingleStep(0.01); - m_smoothingDeltaSpinBox->setValue(0.1); - m_smoothingDeltaSpinBox->setDecimals(3); - m_smoothingDeltaSpinBox->setToolTip(tr("Smoothing delta parameter (default 0.1)")); - smoothingLayout->addWidget(m_smoothingDeltaSpinBox); - smoothingLayout->addStretch(); - smoothingGroupLayout->addLayout(smoothingLayout); - - // Smoothing options - second row with checkboxes - QHBoxLayout *smoothingOptionsLayout = new QHBoxLayout(); - m_smoothingFixBoundaryCheckBox = new QCheckBox(tr("Fix Boundary"), this); - m_smoothingFixBoundaryCheckBox->setObjectName("smoothingFixBoundaryCheckBox"); - m_smoothingFixBoundaryCheckBox->setToolTip(tr("Keep boundary vertices fixed during smoothing")); - smoothingOptionsLayout->addWidget(m_smoothingFixBoundaryCheckBox); - m_smoothingPerturb1CheckBox = new QCheckBox(tr("Perturb 1"), this); - m_smoothingPerturb1CheckBox->setObjectName("smoothingPerturb1CheckBox"); - m_smoothingPerturb1CheckBox->setToolTip(tr("Apply initial perturbation before smoothing")); - smoothingOptionsLayout->addWidget(m_smoothingPerturb1CheckBox); - m_smoothingGeoFlowCheckBox = new QCheckBox(tr("Geo Flow"), this); - m_smoothingGeoFlowCheckBox->setObjectName("smoothingGeoFlowCheckBox"); - m_smoothingGeoFlowCheckBox->setChecked(true); // Default enabled - m_smoothingGeoFlowCheckBox->setToolTip(tr("Apply geometric flow smoothing")); - smoothingOptionsLayout->addWidget(m_smoothingGeoFlowCheckBox); - m_smoothingEnabledCheckBox = new QCheckBox(tr("Smooth"), this); - m_smoothingEnabledCheckBox->setObjectName("smoothingEnabledCheckBox"); - m_smoothingEnabledCheckBox->setChecked(true); // Default enabled - m_smoothingEnabledCheckBox->setToolTip(tr("Apply smoothing pass")); - smoothingOptionsLayout->addWidget(m_smoothingEnabledCheckBox); - m_smoothingPerturb2CheckBox = new QCheckBox(tr("Perturb 2"), this); - m_smoothingPerturb2CheckBox->setObjectName("smoothingPerturb2CheckBox"); - m_smoothingPerturb2CheckBox->setToolTip(tr("Apply final perturbation after smoothing")); - smoothingOptionsLayout->addWidget(m_smoothingPerturb2CheckBox); - smoothingOptionsLayout->addStretch(); - smoothingGroupLayout->addLayout(smoothingOptionsLayout); - - operationsTabLayout->addWidget(smoothingGroup); - - // Quality Improve Group - QGroupBox *qualityGroup = new QGroupBox(tr("Quality Improvement"), operationsTab); - QVBoxLayout *qualityGroupLayout = new QVBoxLayout(qualityGroup); - - QHBoxLayout *qualityLayout = new QHBoxLayout(); - m_qualityImproveButton = new QPushButton(tr("Quality Improve"), this); - m_qualityImproveButton->setObjectName("qualityImproveButton"); - m_qualityImproveButton->setToolTip(tr("Improve mesh quality")); - qualityLayout->addWidget(m_qualityImproveButton); - qualityLayout->addWidget(new QLabel(tr("Iters:"), this)); - m_qualityIterationsSpinBox = new QSpinBox(this); - m_qualityIterationsSpinBox->setObjectName("qualityIterationsSpinBox"); - m_qualityIterationsSpinBox->setRange(1, 100); - m_qualityIterationsSpinBox->setValue(1); - m_qualityIterationsSpinBox->setToolTip(tr("Number of improvement iterations")); - qualityLayout->addWidget(m_qualityIterationsSpinBox); - qualityLayout->addWidget(new QLabel(tr("Method:"), this)); - m_qualityMethodComboBox = new QComboBox(this); - m_qualityMethodComboBox->setObjectName("qualityMethodComboBox"); - m_qualityMethodComboBox->addItem(tr("No Improve"), 0); - m_qualityMethodComboBox->addItem(tr("Geo Flow"), 1); - m_qualityMethodComboBox->addItem(tr("Edge Contract"), 2); - m_qualityMethodComboBox->addItem(tr("Joe Liu"), 3); - m_qualityMethodComboBox->addItem(tr("Minimal Vol"), 4); - m_qualityMethodComboBox->addItem(tr("Optimization"), 5); - m_qualityMethodComboBox->setCurrentIndex(1); // Default to Geo Flow - m_qualityMethodComboBox->setToolTip(tr("Select mesh improvement method")); - qualityLayout->addWidget(m_qualityMethodComboBox); - qualityLayout->addStretch(); - qualityGroupLayout->addLayout(qualityLayout); - - operationsTabLayout->addWidget(qualityGroup); - operationsTabLayout->addStretch(); - - // === Info Tab === - QWidget *infoTab = new QWidget(); - QVBoxLayout *infoLayout = new QVBoxLayout(infoTab); - - QLabel *infoLabel = new QLabel(tr("Geometry node metadata:"), infoTab); - infoLayout->addWidget(infoLabel); - - m_infoTable = new QTableWidget(this); - m_infoTable->setObjectName("infoTable"); - m_infoTable->setColumnCount(2); - m_infoTable->setHorizontalHeaderLabels({tr("Property"), tr("Value")}); - m_infoTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); - m_infoTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); - m_infoTable->verticalHeader()->setVisible(false); - m_infoTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - m_infoTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_infoTable->setAlternatingRowColors(true); - infoLayout->addWidget(m_infoTable); - - // Add tabs to tab widget - tabWidget->addTab(appearanceTab, tr("Appearance")); - tabWidget->addTab(materialTab, tr("Material")); - tabWidget->addTab(renderingTab, tr("Rendering")); - tabWidget->addTab(operationsTab, tr("Operations")); - tabWidget->addTab(infoTab, tr("Info")); - - mainLayout->addWidget(tabWidget); - - setLayout(mainLayout); -} - -void GeometryDialog::connectSignals() { - connect(m_geometryComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &GeometryDialog::onGeometrySelected); - connect(m_deleteButton, &QPushButton::clicked, this, &GeometryDialog::onDeleteButtonClicked); - connect(m_renderModeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &GeometryDialog::onRenderModeChanged); - - // Color signals - connect(m_singleColorCheckBox, &QCheckBox::toggled, this, &GeometryDialog::onSingleColorChanged); - connect(m_colorRSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onColorChanged); - connect(m_colorGSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onColorChanged); - connect(m_colorBSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onColorChanged); - - // Material property signals - connect(m_ambientSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - connect(m_diffuseSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - connect(m_specularSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - connect(m_specularPowerSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - connect(m_opacitySpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - connect(m_pointSizeSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - connect(m_lineWidthSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GeometryDialog::onMaterialPropertyChanged); - - // Visibility signals - connect(m_visibilityCheckBox, &QCheckBox::toggled, this, &GeometryDialog::onVisibilityChanged); - - // Bounding box signals - connect(m_showBBoxCheckBox, &QCheckBox::toggled, this, &GeometryDialog::onShowBBoxChanged); - connect(m_bboxColorButton, &QPushButton::clicked, this, &GeometryDialog::onBBoxColorChanged); - connect(m_showExtentLabelsCheckBox, &QCheckBox::toggled, this, - &GeometryDialog::onShowExtentLabelsChanged); - connect(m_extentLabelColorButton, &QPushButton::clicked, this, - &GeometryDialog::onExtentLabelColorChanged); - connect(m_extentLabelFontSizeSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GeometryDialog::onExtentLabelFontSizeChanged); - - // Geometry operations signals - connect(m_invertNormalsButton, &QPushButton::clicked, this, - &GeometryDialog::onInvertNormalsClicked); - connect(m_reorientButton, &QPushButton::clicked, this, &GeometryDialog::onReorientClicked); - connect(m_projectButton, &QPushButton::clicked, this, &GeometryDialog::onProjectClicked); - connect(m_smoothingButton, &QPushButton::clicked, this, &GeometryDialog::onSmoothingClicked); - connect(m_qualityImproveButton, &QPushButton::clicked, this, - &GeometryDialog::onQualityImproveClicked); -} - -void GeometryDialog::populateGeometryList() { - m_geometryComboBox->clear(); - m_projectTargetComboBox->clear(); - m_geometryNames.clear(); - - if (!m_sceneGraph) - return; - - // Get all geometry nodes recursively - auto allGeometries = m_sceneGraph->getAllGeometryGraphics(); - - for (const auto &geomNode : allGeometries) { - if (geomNode && geomNode->getGeometry() && !geomNode->getGeometry()->empty()) { - std::string name = geomNode->getName(); - m_geometryNames.push_back(name); - m_geometryComboBox->addItem(QString::fromStdString(name)); - m_projectTargetComboBox->addItem(QString::fromStdString(name)); - } - } - - if (m_geometryComboBox->count() == 0) { - setPropertiesEnabled(false); - } else { - setPropertiesEnabled(true); - onGeometrySelected(0); - } -} - -void GeometryDialog::onGraphicsChildrenChanged() { - if (!m_sceneGraph) - return; - - // Get current geometry count - size_t currentCount = m_geometryNames.size(); - - // Count geometry nodes in scene graph recursively - size_t sceneGeomCount = 0; - auto allGeometries = m_sceneGraph->getAllGeometryGraphics(); - for (const auto &geomNode : allGeometries) { - if (geomNode && geomNode->getGeometry() && !geomNode->getGeometry()->empty()) { - sceneGeomCount++; - } - } - - // If counts differ, refresh the list - if (sceneGeomCount != currentCount) { - // Save current selection - QString currentSelection; - int currentIndex = m_geometryComboBox->currentIndex(); - if (currentIndex >= 0 && currentIndex < static_cast(m_geometryNames.size())) { - currentSelection = QString::fromStdString(m_geometryNames[currentIndex]); - } - - // Refresh the list - populateGeometryList(); - - // Try to restore the previous selection - if (!currentSelection.isEmpty()) { - int newIndex = m_geometryComboBox->findText(currentSelection); - if (newIndex >= 0) { - m_geometryComboBox->setCurrentIndex(newIndex); - } - } - } -} - -void GeometryDialog::onGeometrySelected(int index) { - if (m_updating) - return; - - // Disconnect from previous node's state changes - m_nodeStateConnection.disconnect(); - - if (index < 0 || index >= static_cast(m_geometryNames.size())) { - setPropertiesEnabled(false); - return; - } - - // Connect to selected node's state changes - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (geomNode) { - // Connect to the node's childChanged signal (fires when any child state changes) - // Use AutoConnection so Qt determines the best way to invoke (direct or queued) - m_nodeStateConnection = geomNode->getState().childChanged.connect([this](const std::string &) { - QMetaObject::invokeMethod(this, "onNodeStateChanged", Qt::AutoConnection); - }); - } - - setPropertiesEnabled(true); - updatePropertiesFromNode(); -} - -void GeometryDialog::updatePropertiesFromNode() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) - return; - - m_updating = true; - - // Update render mode - GeometryRenderMode mode = geomNode->getRenderMode(); - int modeIndex = m_renderModeComboBox->findData(static_cast(mode)); - if (modeIndex >= 0) { - m_renderModeComboBox->setCurrentIndex(modeIndex); - } - - // Update color from state tree directly - try { - m_colorRSpinBox->setValue(geomNode->getState("color_r").value()); - m_colorGSpinBox->setValue(geomNode->getState("color_g").value()); - m_colorBSpinBox->setValue(geomNode->getState("color_b").value()); - } catch (const std::exception &) { - // Use defaults if state not available - } catch (...) { - // Catch all other exceptions - } - - // Update material properties from state tree - try { - m_ambientSpinBox->setValue(geomNode->getState("ambient").value()); - } catch (const std::exception &) { - } catch (...) { - } - try { - m_diffuseSpinBox->setValue(geomNode->getState("diffuse").value()); - } catch (const std::exception &) { - } catch (...) { - } - try { - m_specularSpinBox->setValue(geomNode->getState("specular").value()); - } catch (const std::exception &) { - } catch (...) { - } - try { - m_specularPowerSpinBox->setValue(geomNode->getState("specular_power").value()); - } catch (const std::exception &) { - } catch (...) { - } - try { - m_opacitySpinBox->setValue(geomNode->getState("opacity").value()); - } catch (const std::exception &) { - } catch (...) { - } - try { - m_pointSizeSpinBox->setValue(geomNode->getState("point_size").value()); - } catch (const std::exception &) { - } catch (...) { - } - try { - m_lineWidthSpinBox->setValue(geomNode->getState("line_width").value()); - } catch (const std::exception &) { - } catch (...) { - } - - // Update single color checkbox - try { - m_singleColorCheckBox->setChecked(geomNode->getState("use_single_color").value()); - } catch (const std::exception &) { - m_singleColorCheckBox->setChecked(false); - } catch (...) { - m_singleColorCheckBox->setChecked(false); - } - - // Update visibility checkbox - try { - int visible = geomNode->getState("visible").value(); - m_visibilityCheckBox->setChecked(visible != 0); - } catch (const std::exception &) { - m_visibilityCheckBox->setChecked(true); - } catch (...) { - m_visibilityCheckBox->setChecked(true); - } - - // Update bounding box controls - try { - int showBBox = geomNode->getState("show_bbox").value(); - m_showBBoxCheckBox->setChecked(showBBox != 0); - } catch (const std::exception &) { - m_showBBoxCheckBox->setChecked(false); - } catch (...) { - m_showBBoxCheckBox->setChecked(false); - } - - // Update bounding box color - geomNode->getBBoxColor(m_bboxColor[0], m_bboxColor[1], m_bboxColor[2]); - updateBBoxColorButton(); - - // Update extent label controls - try { - int showExtentLabels = geomNode->getState("show_extent_labels").value(); - m_showExtentLabelsCheckBox->setChecked(showExtentLabels != 0); - } catch (const std::exception &) { - m_showExtentLabelsCheckBox->setChecked(false); - } catch (...) { - m_showExtentLabelsCheckBox->setChecked(false); - } - - // Update extent label color - try { - m_extentLabelColor[0] = geomNode->getState("extent_label_color_r").value(); - m_extentLabelColor[1] = geomNode->getState("extent_label_color_g").value(); - m_extentLabelColor[2] = geomNode->getState("extent_label_color_b").value(); - } catch (const std::exception &) { - m_extentLabelColor[0] = m_extentLabelColor[1] = m_extentLabelColor[2] = 1.0; // Default to white - } catch (...) { - m_extentLabelColor[0] = m_extentLabelColor[1] = m_extentLabelColor[2] = 1.0; // Default to white - } - updateExtentLabelColorButton(); - - // Update extent label font size - try { - int fontSize = geomNode->getState("extent_label_font_size").value(); - m_extentLabelFontSizeSpinBox->setValue(fontSize); - } catch (const std::exception &) { - m_extentLabelFontSizeSpinBox->setValue(12); // Default font size - } catch (...) { - m_extentLabelFontSizeSpinBox->setValue(12); // Default font size - } - - // Update info table with metadata - m_infoTable->setRowCount(0); - const auto &metadata = geomNode->getAllMetadata(); - for (const auto &kv : metadata) { - int row = m_infoTable->rowCount(); - m_infoTable->insertRow(row); - - m_infoTable->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(kv.first))); - - // Convert std::any to string for display - QString valueStr; - try { - if (kv.second.type() == typeid(int)) { - valueStr = QString::number(std::any_cast(kv.second)); - } else if (kv.second.type() == typeid(double)) { - valueStr = QString::number(std::any_cast(kv.second), 'g', 6); - } else if (kv.second.type() == typeid(float)) { - valueStr = QString::number(std::any_cast(kv.second), 'g', 6); - } else if (kv.second.type() == typeid(std::string)) { - valueStr = QString::fromStdString(std::any_cast(kv.second)); - } else if (kv.second.type() == typeid(bool)) { - valueStr = std::any_cast(kv.second) ? tr("true") : tr("false"); - } else { - valueStr = tr(""); - } - } catch (...) { - valueStr = tr(""); - } - - m_infoTable->setItem(row, 1, new QTableWidgetItem(valueStr)); - } - - m_updating = false; -} - -void GeometryDialog::onNodeStateChanged() { - // Update UI from state tree when node state changes - updatePropertiesFromNode(); -} - -void GeometryDialog::onRenderModeChanged(int index) { - if (m_updating) - return; - - int geomIndex = m_geometryComboBox->currentIndex(); - if (geomIndex < 0 || geomIndex >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[geomIndex]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) - return; - - GeometryRenderMode mode = - static_cast(m_renderModeComboBox->currentData().toInt()); - geomNode->setRenderMode(mode); -} - -void GeometryDialog::onColorChanged() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) - return; - - geomNode->setColor(m_colorRSpinBox->value(), m_colorGSpinBox->value(), m_colorBSpinBox->value()); -} - -void GeometryDialog::onSingleColorChanged(bool checked) { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) - return; - - geomNode->setUseSingleColor(checked); -} - -void GeometryDialog::onMaterialPropertyChanged() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) - return; - - // Determine which property changed and update it - QObject *sender = QObject::sender(); - - if (sender == m_ambientSpinBox) { - geomNode->setAmbient(m_ambientSpinBox->value()); - } else if (sender == m_diffuseSpinBox) { - geomNode->setDiffuse(m_diffuseSpinBox->value()); - } else if (sender == m_specularSpinBox) { - geomNode->setSpecular(m_specularSpinBox->value()); - } else if (sender == m_specularPowerSpinBox) { - geomNode->setSpecularPower(m_specularPowerSpinBox->value()); - } else if (sender == m_opacitySpinBox) { - geomNode->setOpacity(m_opacitySpinBox->value()); - } else if (sender == m_pointSizeSpinBox) { - geomNode->setPointSize(m_pointSizeSpinBox->value()); - } else if (sender == m_lineWidthSpinBox) { - geomNode->setLineWidth(m_lineWidthSpinBox->value()); - } -} - -void GeometryDialog::onDeleteButtonClicked() { - if (!m_sceneGraph) - return; - - int currentIndex = m_geometryComboBox->currentIndex(); - if (currentIndex < 0 || currentIndex >= static_cast(m_geometryNames.size())) { - return; - } - - std::string geometryName = m_geometryNames[currentIndex]; - - // Confirm deletion - QMessageBox::StandardButton reply; - reply = QMessageBox::question( - this, tr("Delete Geometry"), - tr("Are you sure you want to delete '%1'?").arg(QString::fromStdString(geometryName)), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - m_sceneGraph->removeGraphics(geometryName); - // The combo box will update automatically via the state tree signal - } -} - -void GeometryDialog::setPropertiesEnabled(bool enabled) { - m_deleteButton->setEnabled(enabled); - m_renderModeComboBox->setEnabled(enabled); - m_singleColorCheckBox->setEnabled(enabled); - m_colorRSpinBox->setEnabled(enabled); - m_colorGSpinBox->setEnabled(enabled); - m_colorBSpinBox->setEnabled(enabled); - m_ambientSpinBox->setEnabled(enabled); - m_diffuseSpinBox->setEnabled(enabled); - m_specularSpinBox->setEnabled(enabled); - m_specularPowerSpinBox->setEnabled(enabled); - m_opacitySpinBox->setEnabled(enabled); - m_pointSizeSpinBox->setEnabled(enabled); - m_lineWidthSpinBox->setEnabled(enabled); - m_visibilityCheckBox->setEnabled(enabled); - m_showBBoxCheckBox->setEnabled(enabled); - m_bboxColorButton->setEnabled(enabled); - setOperationButtonsEnabled(enabled); -} - -void GeometryDialog::setOperationButtonsEnabled(bool enabled) { - m_invertNormalsButton->setEnabled(enabled); - m_reorientButton->setEnabled(enabled); - m_projectButton->setEnabled(enabled); - m_projectTargetComboBox->setEnabled(enabled); - m_smoothingButton->setEnabled(enabled); - m_smoothingDeltaSpinBox->setEnabled(enabled); - m_smoothingFixBoundaryCheckBox->setEnabled(enabled); - m_smoothingPerturb1CheckBox->setEnabled(enabled); - m_smoothingGeoFlowCheckBox->setEnabled(enabled); - m_smoothingEnabledCheckBox->setEnabled(enabled); - m_smoothingPerturb2CheckBox->setEnabled(enabled); - m_qualityImproveButton->setEnabled(enabled); - m_qualityIterationsSpinBox->setEnabled(enabled); - m_qualityMethodComboBox->setEnabled(enabled); -} - -void GeometryDialog::onVisibilityChanged(bool checked) { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - - if (!graphicsNode) - return; - - graphicsNode->setVisible(checked); -} - -void GeometryDialog::onShowBBoxChanged(bool checked) { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - - if (!graphicsNode) - return; - - graphicsNode->setShowBBox(checked); -} - -void GeometryDialog::onBBoxColorChanged() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - QColor currentColor = QColor::fromRgbF(m_bboxColor[0], m_bboxColor[1], m_bboxColor[2]); - QColor color = QColorDialog::getColor(currentColor, this, tr("Select Bounding Box Color")); - - if (color.isValid()) { - m_bboxColor[0] = color.redF(); - m_bboxColor[1] = color.greenF(); - m_bboxColor[2] = color.blueF(); - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - - if (graphicsNode) { - graphicsNode->setBBoxColor(m_bboxColor[0], m_bboxColor[1], m_bboxColor[2]); - } - - updateBBoxColorButton(); - } -} - -void GeometryDialog::updateBBoxColorButton() { - int r = static_cast(m_bboxColor[0] * 255); - int g = static_cast(m_bboxColor[1] * 255); - int b = static_cast(m_bboxColor[2] * 255); - QString style = QString("background-color: rgb(%1, %2, %3);").arg(r).arg(g).arg(b); - m_bboxColorButton->setStyleSheet(style); -} - -void GeometryDialog::onShowExtentLabelsChanged(bool checked) { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - - if (!graphicsNode) - return; - - graphicsNode->setShowExtentLabels(checked); -} - -void GeometryDialog::onExtentLabelColorChanged() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - QColor currentColor = - QColor::fromRgbF(m_extentLabelColor[0], m_extentLabelColor[1], m_extentLabelColor[2]); - QColor color = QColorDialog::getColor(currentColor, this, tr("Select Extent Label Color")); - - if (color.isValid()) { - m_extentLabelColor[0] = color.redF(); - m_extentLabelColor[1] = color.greenF(); - m_extentLabelColor[2] = color.blueF(); - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - - if (graphicsNode) { - graphicsNode->setExtentLabelColor(m_extentLabelColor[0], m_extentLabelColor[1], - m_extentLabelColor[2]); - } - - updateExtentLabelColorButton(); - } -} - -void GeometryDialog::updateExtentLabelColorButton() { - int r = static_cast(m_extentLabelColor[0] * 255); - int g = static_cast(m_extentLabelColor[1] * 255); - int b = static_cast(m_extentLabelColor[2] * 255); - QString style = QString("background-color: rgb(%1, %2, %3);").arg(r).arg(g).arg(b); - m_extentLabelColorButton->setStyleSheet(style); -} - -void GeometryDialog::onExtentLabelFontSizeChanged(int size) { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - - if (!graphicsNode) - return; - - graphicsNode->setExtentLabelFontSize(size); -} - -void GeometryDialog::onInvertNormalsClicked() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode || !geomNode->getGeometry()) - return; - - // Get a copy of the geometry for thread-safe operation - cvc::geometry geom = *geomNode->getGeometry(); - - // Create unique thread key - std::string threadKey = "invert_normals_" + geomName; - - // Disable button while processing - m_invertNormalsButton->setEnabled(false); - - // Start the operation in a background thread - volrover3::app().startThread( - threadKey, - [this, geom, geomName, threadKey]() mutable { - // Use thread_feedback for proper progress tracking - cvc::app::thread_feedback feedback(volrover3::app(), threadKey); - - try { - volrover3::app().threadProgress(threadKey, 0.1); - volrover3::app().threadInfo(threadKey, "Inverting normals..."); - - // Perform the normal inversion - geom.invert_normals(); - - volrover3::app().threadProgress(threadKey, 0.9); - volrover3::app().threadInfo(threadKey, "Updating scene..."); - - // Post scene update to main thread - m_sceneGraph->postEvent([this, geom, geomName, threadKey]() { - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (geomNode) { - geomNode->setGeometry(geom); - } - - volrover3::app().finishThreadProgress(threadKey); - - // Re-enable button on Qt thread - QMetaObject::invokeMethod( - this, [this]() { m_invertNormalsButton->setEnabled(true); }, Qt::QueuedConnection); - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { m_invertNormalsButton->setEnabled(true); }, Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error inverting normals: ") + e.what(); - QMetaObject::invokeMethod( - this, - [this, errorMsg]() { - m_invertNormalsButton->setEnabled(true); - QMessageBox::warning(this, tr("Error"), QString::fromStdString(errorMsg)); - }, - Qt::QueuedConnection); - } - }, - false // Don't wait for existing thread - ); -} - -void GeometryDialog::onReorientClicked() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode || !geomNode->getGeometry()) - return; - - // Get a copy of the geometry for thread-safe operation - cvc::geometry geom = *geomNode->getGeometry(); - - // Create unique thread key - std::string threadKey = "reorient_" + geomName; - - // Disable button while processing - m_reorientButton->setEnabled(false); - - // Start the operation in a background thread - volrover3::app().startThread( - threadKey, - [this, geom, geomName, threadKey]() mutable { - cvc::app::thread_feedback feedback(volrover3::app(), threadKey); - - try { - volrover3::app().threadProgress(threadKey, 0.1); - volrover3::app().threadInfo(threadKey, "Reorienting mesh..."); - - // Perform the reorient operation - geom.reorient(); - - volrover3::app().threadProgress(threadKey, 0.9); - volrover3::app().threadInfo(threadKey, "Updating scene..."); - - // Post scene update to main thread - m_sceneGraph->postEvent([this, geom, geomName, threadKey]() { - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (geomNode) { - geomNode->setGeometry(geom); - } - - volrover3::app().finishThreadProgress(threadKey); - - QMetaObject::invokeMethod( - this, [this]() { m_reorientButton->setEnabled(true); }, Qt::QueuedConnection); - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { m_reorientButton->setEnabled(true); }, Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error reorienting: ") + e.what(); - QMetaObject::invokeMethod( - this, - [this, errorMsg]() { - m_reorientButton->setEnabled(true); - QMessageBox::warning(this, tr("Error"), QString::fromStdString(errorMsg)); - }, - Qt::QueuedConnection); - } - }, - false); -} - -void GeometryDialog::onProjectClicked() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - int targetIndex = m_projectTargetComboBox->currentIndex(); - if (targetIndex < 0 || targetIndex >= static_cast(m_geometryNames.size())) - return; - - // Don't project onto self - if (index == targetIndex) { - QMessageBox::warning(this, tr("Warning"), tr("Cannot project geometry onto itself")); - return; - } - - const std::string &geomName = m_geometryNames[index]; - const std::string &targetName = m_geometryNames[targetIndex]; - - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - auto targetGraphicsNode = m_sceneGraph->getGraphics(targetName); - auto targetGeomNode = std::dynamic_pointer_cast(targetGraphicsNode); - - if (!geomNode || !geomNode->getGeometry() || !targetGeomNode || !targetGeomNode->getGeometry()) - return; - - // Get copies of the geometries - cvc::geometry geom = *geomNode->getGeometry(); - cvc::geometry targetGeom = *targetGeomNode->getGeometry(); - - std::string threadKey = "project_" + geomName; - - m_projectButton->setEnabled(false); - - volrover3::app().startThread( - threadKey, - [this, geom, targetGeom, geomName, threadKey]() mutable { - cvc::app::thread_feedback feedback(volrover3::app(), threadKey); - - try { - volrover3::app().threadProgress(threadKey, 0.1); - volrover3::app().threadInfo(threadKey, "Projecting to target geometry..."); - - // Perform the projection - geom.project(targetGeom); - - volrover3::app().threadProgress(threadKey, 0.9); - volrover3::app().threadInfo(threadKey, "Updating scene..."); - - m_sceneGraph->postEvent([this, geom, geomName, threadKey]() { - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (geomNode) { - geomNode->setGeometry(geom); - } - - volrover3::app().finishThreadProgress(threadKey); - - QMetaObject::invokeMethod( - this, [this]() { m_projectButton->setEnabled(true); }, Qt::QueuedConnection); - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { m_projectButton->setEnabled(true); }, Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error projecting: ") + e.what(); - QMetaObject::invokeMethod( - this, - [this, errorMsg]() { - m_projectButton->setEnabled(true); - QMessageBox::warning(this, tr("Error"), QString::fromStdString(errorMsg)); - }, - Qt::QueuedConnection); - } - }, - false); -} - -void GeometryDialog::onSmoothingClicked() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode || !geomNode->getGeometry()) - return; - - // Get parameters from UI - float delta = static_cast(m_smoothingDeltaSpinBox->value()); - bool fixBoundary = m_smoothingFixBoundaryCheckBox->isChecked(); - bool perturb1 = m_smoothingPerturb1CheckBox->isChecked(); - bool geoFlow = m_smoothingGeoFlowCheckBox->isChecked(); - bool smoothingEnabled = m_smoothingEnabledCheckBox->isChecked(); - bool perturb2 = m_smoothingPerturb2CheckBox->isChecked(); - - cvc::geometry geom = *geomNode->getGeometry(); - - std::string threadKey = "smoothing_" + geomName; - - m_smoothingButton->setEnabled(false); - - volrover3::app().startThread( - threadKey, - [this, geom, delta, fixBoundary, perturb1, geoFlow, smoothingEnabled, perturb2, geomName, - threadKey]() mutable { - cvc::app::thread_feedback feedback(volrover3::app(), threadKey); - - try { - volrover3::app().threadProgress(threadKey, 0.1); - volrover3::app().threadInfo(threadKey, "Smoothing mesh..."); - - // Perform the smoothing operation with all parameters - geom.smoothing(volrover3::app(), delta, fixBoundary, perturb1, geoFlow, smoothingEnabled, - perturb2); - - volrover3::app().threadProgress(threadKey, 0.9); - volrover3::app().threadInfo(threadKey, "Updating scene..."); - - m_sceneGraph->postEvent([this, geom, geomName, threadKey]() { - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (geomNode) { - geomNode->setGeometry(geom); - } - - volrover3::app().finishThreadProgress(threadKey); - - QMetaObject::invokeMethod( - this, [this]() { m_smoothingButton->setEnabled(true); }, Qt::QueuedConnection); - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { m_smoothingButton->setEnabled(true); }, Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error smoothing: ") + e.what(); - QMetaObject::invokeMethod( - this, - [this, errorMsg]() { - m_smoothingButton->setEnabled(true); - QMessageBox::warning(this, tr("Error"), QString::fromStdString(errorMsg)); - }, - Qt::QueuedConnection); - } - }, - false); -} - -void GeometryDialog::onQualityImproveClicked() { - if (m_updating) - return; - - int index = m_geometryComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode || !geomNode->getGeometry()) - return; - - // Get parameters from UI - int iterations = m_qualityIterationsSpinBox->value(); - int methodInt = m_qualityMethodComboBox->currentData().toInt(); - cvc::improvement_method method = static_cast(methodInt); - - cvc::geometry geom = *geomNode->getGeometry(); - - std::string threadKey = "quality_improve_" + geomName; - - m_qualityImproveButton->setEnabled(false); - - volrover3::app().startThread( - threadKey, - [this, geom, iterations, method, geomName, threadKey]() mutable { - cvc::app::thread_feedback feedback(volrover3::app(), threadKey); - - try { - volrover3::app().threadProgress(threadKey, 0.1); - volrover3::app().threadInfo(threadKey, "Improving mesh quality..."); - - // Perform the quality improvement - geom.quality_improve(iterations, method); - - volrover3::app().threadProgress(threadKey, 0.9); - volrover3::app().threadInfo(threadKey, "Updating scene..."); - - m_sceneGraph->postEvent([this, geom, geomName, threadKey]() { - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (geomNode) { - geomNode->setGeometry(geom); - } - - volrover3::app().finishThreadProgress(threadKey); - - QMetaObject::invokeMethod( - this, [this]() { m_qualityImproveButton->setEnabled(true); }, Qt::QueuedConnection); - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { m_qualityImproveButton->setEnabled(true); }, Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error improving quality: ") + e.what(); - QMetaObject::invokeMethod( - this, - [this, errorMsg]() { - m_qualityImproveButton->setEnabled(true); - QMessageBox::warning(this, tr("Error"), QString::fromStdString(errorMsg)); - }, - Qt::QueuedConnection); - } - }, - false); -} diff --git a/src/volrover3/GeometryNode.cpp b/src/volrover3/GeometryNode.cpp deleted file mode 100644 index 42550656..00000000 --- a/src/volrover3/GeometryNode.cpp +++ /dev/null @@ -1,678 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -GeometryNode::GeometryNode(cvc::app &ctx, const std::string &statePath, const std::string &name) - : GraphicsNode(ctx, statePath, name), m_hasGeometry(false), - m_renderMode(GeometryRenderMode::TRIS), m_useSingleColor(false), - m_actor(vtkSmartPointer::New()), - m_mapper(vtkSmartPointer::New()), - m_polyData(vtkSmartPointer::New()) { - m_mapper->SetInputData(m_polyData); - m_actor->SetMapper(m_mapper); - - // Set default material properties - m_actor->GetProperty()->SetColor(0.8, 0.8, 0.9); - m_actor->GetProperty()->SetSpecular(0.3); - m_actor->GetProperty()->SetSpecularPower(20); - - // Initialize state tree with all rendering attributes - if (!statePath.empty()) { - getState("visible").value(1); // Visible by default - - // Render mode - getState("render_mode").value(renderModeToString(m_renderMode)); - - // Single color mode (default: false - use per-vertex colors if available) - getState("use_single_color").value(false); - - // Material color (RGB 0-1) - getState("color_r").value(0.8); - getState("color_g").value(0.8); - getState("color_b").value(0.9); - - // Material properties - getState("specular").value(0.3); - getState("specular_power").value(20.0); - getState("ambient").value(0.0); // VTK default - getState("diffuse").value(1.0); // VTK default - getState("opacity").value(1.0); - - // Point/line rendering properties - getState("point_size").value(3.0); - getState("line_width").value(1.0); - } -} - -GeometryNode::~GeometryNode() { m_dataConnection.disconnect(); } - -void GeometryNode::applyTransformToVTK() { - // Use generic helper to apply world transform - applyWorldTransformToProps({m_actor}); -} - -void GeometryNode::applyClipPlanes(vtkPlaneCollection *planes) { - if (m_mapper) { - if (planes && planes->GetNumberOfItems() > 0) { - m_mapper->SetClippingPlanes(planes); - } else { - m_mapper->RemoveAllClippingPlanes(); - } - } -} - -void GeometryNode::handleStateChanged(const std::string &childState) { - // Handle geometry-specific state changes - // All VTK operations MUST be wrapped in runOnMainThread() for thread safety - if (childState == "render_mode") { - runOnMainThread([this]() { - std::string renderModeStr = getState("render_mode").value(); - GeometryRenderMode newMode = stringToRenderMode(renderModeStr); - if (m_renderMode != newMode) { - m_renderMode = newMode; - updateRenderModeVTK(); - } - }); - } else if (childState == "color_r" || childState == "color_g" || childState == "color_b") { - runOnMainThread([this]() { - // Only update if all color components can be read and actor exists - if (!m_actor) - return; - try { - double r = getState("color_r").value(); - double g = getState("color_g").value(); - double b = getState("color_b").value(); - m_actor->GetProperty()->SetColor(r, g, b); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "specular") { - runOnMainThread([this]() { - if (!m_actor) - return; - double specular = getState("specular").value(); - m_actor->GetProperty()->SetSpecular(specular); - }); - } else if (childState == "specular_power") { - runOnMainThread([this]() { - if (!m_actor) - return; - double specularPower = getState("specular_power").value(); - m_actor->GetProperty()->SetSpecularPower(specularPower); - }); - } else if (childState == "ambient") { - runOnMainThread([this]() { - if (!m_actor) - return; - double ambient = getState("ambient").value(); - m_actor->GetProperty()->SetAmbient(ambient); - }); - } else if (childState == "diffuse") { - runOnMainThread([this]() { - if (!m_actor) - return; - double diffuse = getState("diffuse").value(); - m_actor->GetProperty()->SetDiffuse(diffuse); - }); - } else if (childState == "opacity") { - runOnMainThread([this]() { - if (!m_actor) - return; - double opacity = getState("opacity").value(); - m_actor->GetProperty()->SetOpacity(opacity); - }); - } else if (childState == "point_size") { - runOnMainThread([this]() { - if (!m_actor) - return; - double pointSize = getState("point_size").value(); - m_actor->GetProperty()->SetPointSize(pointSize); - }); - } else if (childState == "line_width") { - runOnMainThread([this]() { - if (!m_actor) - return; - double lineWidth = getState("line_width").value(); - m_actor->GetProperty()->SetLineWidth(lineWidth); - }); - } else if (childState == "use_single_color") { - runOnMainThread([this]() { - try { - bool useSingleColor = getState("use_single_color").value(); - if (m_useSingleColor != useSingleColor) { - m_useSingleColor = useSingleColor; - // Re-apply geometry colors - if (m_hasGeometry && m_geometry) { - updatePolyData(*m_geometry); - } - } - } catch (...) { - // Ignore if state not available - } - }); - } else { - // Delegate to parent for common graphics fields - // Parent will handle its own runOnMainThread wrapping - GraphicsNode::handleStateChanged(childState); - } -} - -std::string GeometryNode::renderModeToString(GeometryRenderMode mode) { - return std::to_string(static_cast(mode)); -} - -GeometryRenderMode GeometryNode::stringToRenderMode(const std::string &str) { - try { - int mode = std::stoi(str); - if (mode >= 0 && mode <= 5) { - return static_cast(mode); - } - } catch (...) { - } - return GeometryRenderMode::TRIS; // Default -} - -void GeometryNode::setRenderMode(GeometryRenderMode mode) { - if (m_renderMode == mode) - return; - - m_renderMode = mode; - - // Update state tree - getState("render_mode").value(renderModeToString(mode)); - - // Update VTK rendering on main thread - runOnMainThread([this]() { updateRenderModeVTK(); }); -} - -void GeometryNode::updateRenderModeVTK() { - // Guard: Don't update VTK if actor not initialized - if (!m_actor) - return; - - // Update VTK rendering based on mode - switch (m_renderMode) { - case GeometryRenderMode::POINTS: - m_actor->GetProperty()->SetRepresentationToPoints(); - m_actor->GetProperty()->SetPointSize(getState("point_size").value()); - break; - - case GeometryRenderMode::LINES: - m_actor->GetProperty()->SetRepresentationToWireframe(); - m_actor->GetProperty()->SetLineWidth(getState("line_width").value()); - break; - - case GeometryRenderMode::TRIS: - case GeometryRenderMode::QUADS: - m_actor->GetProperty()->SetRepresentationToSurface(); - break; - - case GeometryRenderMode::TETS: - case GeometryRenderMode::HEXS: - // Placeholder: For now, render as wireframe - // TODO: Implement proper volumetric mesh rendering - m_actor->GetProperty()->SetRepresentationToWireframe(); - break; - } - - // Trigger re-render if we have geometry - if (m_hasGeometry && m_geometry) { - updatePolyData(*m_geometry); - } - - // Mark everything as modified to trigger re-render - if (m_polyData) - m_polyData->Modified(); - if (m_mapper) - m_mapper->Modified(); - if (m_actor) - m_actor->Modified(); - - // Request a render update (if we have a renderer with a render window) - if (m_renderer && m_renderer->GetRenderWindow()) { - m_renderer->GetRenderWindow()->Render(); - } -} - -void GeometryNode::setColor(double r, double g, double b) { - getState("color_r").value(r); - getState("color_g").value(g); - getState("color_b").value(b); -} - -void GeometryNode::setSpecular(double value) { getState("specular").value(value); } - -void GeometryNode::setSpecularPower(double value) { getState("specular_power").value(value); } - -void GeometryNode::setAmbient(double value) { getState("ambient").value(value); } - -void GeometryNode::setDiffuse(double value) { getState("diffuse").value(value); } - -void GeometryNode::setOpacity(double value) { getState("opacity").value(value); } - -void GeometryNode::setPointSize(double size) { getState("point_size").value(size); } - -void GeometryNode::setLineWidth(double width) { getState("line_width").value(width); } - -void GeometryNode::setUseSingleColor(bool useSingleColor) { - if (m_useSingleColor == useSingleColor) - return; - - m_useSingleColor = useSingleColor; - getState("use_single_color").value(useSingleColor); - - // Re-apply geometry colors on main thread - runOnMainThread([this]() { - if (m_hasGeometry && m_geometry) { - updatePolyData(*m_geometry); - } - }); -} - -vtkProp *GeometryNode::getProp() { return m_actor; } - -void GeometryNode::setGeometry(const cvc::geometry &geom) { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // CRITICAL: Entire method must run on main thread to avoid Qt threading errors - // Even creating std::shared_ptr or setting member variables can trigger VTK - // smart pointer operations that touch Qt objects - runOnMainThread([this, geom]() { - // Store the geometry object - m_geometry = std::make_shared(geom); - m_hasGeometry = true; // Set this BEFORE setRenderMode so it can update - - // Auto-detect render mode from geometry type - GeometryRenderMode autoMode = GeometryRenderMode::TRIS; // default - - switch (geom.get_geometry_type()) { - case cvc::geometry::SURFACE_TRI: - autoMode = GeometryRenderMode::TRIS; - break; - case cvc::geometry::SURFACE_QUAD: - autoMode = GeometryRenderMode::QUADS; - break; - case cvc::geometry::VOLUME_TET: - autoMode = GeometryRenderMode::TETS; - break; - case cvc::geometry::VOLUME_HEX: - autoMode = GeometryRenderMode::HEXS; - break; - case cvc::geometry::MIXED: - // For mixed, prefer tris if available, otherwise quads - if (geom.num_tris() > 0) { - autoMode = GeometryRenderMode::TRIS; - } else if (geom.num_quads() > 0) { - autoMode = GeometryRenderMode::QUADS; - } else if (geom.num_tets() > 0) { - autoMode = GeometryRenderMode::TETS; - } else if (geom.num_hexs() > 0) { - autoMode = GeometryRenderMode::HEXS; - } - break; - } - - // Update render mode and geometry data - m_renderMode = autoMode; - getState("render_mode").value(renderModeToString(autoMode)); - updateRenderModeVTK(); // This calls updatePolyData() internally - updateBoundingBoxNode(); - - updateMetadata(geom); - - // Notify parent to resync bounds if it's a NullGraphicNode with auto-sync enabled - if (m_parent) { - auto nullParent = dynamic_cast(m_parent); - if (nullParent) { - nullParent->syncBoundsToChildren(); - } - } - }); -} - -void GeometryNode::updatePolyData(const cvc::geometry &geom) { - // Create VTK points from geometry - vtkSmartPointer points = vtkSmartPointer::New(); - points->SetNumberOfPoints(geom.num_points()); - - for (size_t i = 0; i < geom.num_points(); ++i) { - const auto &pt = geom.points()[i]; - points->SetPoint(i, pt[0], pt[1], pt[2]); - } - - // Clear existing cells - m_polyData->SetVerts(nullptr); - m_polyData->SetLines(nullptr); - m_polyData->SetPolys(nullptr); - - // Create cells based on render mode - switch (m_renderMode) { - case GeometryRenderMode::POINTS: { - // Render as point cloud - vtkSmartPointer vertices = vtkSmartPointer::New(); - for (size_t i = 0; i < geom.num_points(); ++i) { - vertices->InsertNextCell(1); - vertices->InsertCellPoint(i); - } - m_polyData->SetVerts(vertices); - break; - } - - case GeometryRenderMode::LINES: { - // Render as wireframe using edge connectivity - vtkSmartPointer lines = vtkSmartPointer::New(); - - // Add lines from line array if available - for (size_t i = 0; i < geom.num_lines(); ++i) { - const auto &line = geom.lines()[i]; - lines->InsertNextCell(2); - lines->InsertCellPoint(line[0]); - lines->InsertCellPoint(line[1]); - } - - // Add triangle edges - for (size_t i = 0; i < geom.num_tris(); ++i) { - const auto &tri = geom.tris()[i]; - // Three edges per triangle - lines->InsertNextCell(2); - lines->InsertCellPoint(tri[0]); - lines->InsertCellPoint(tri[1]); - - lines->InsertNextCell(2); - lines->InsertCellPoint(tri[1]); - lines->InsertCellPoint(tri[2]); - - lines->InsertNextCell(2); - lines->InsertCellPoint(tri[2]); - lines->InsertCellPoint(tri[0]); - } - - // Add quad edges - for (size_t i = 0; i < geom.num_quads(); ++i) { - const auto &quad = geom.quads()[i]; - // Four edges per quad - lines->InsertNextCell(2); - lines->InsertCellPoint(quad[0]); - lines->InsertCellPoint(quad[1]); - - lines->InsertNextCell(2); - lines->InsertCellPoint(quad[1]); - lines->InsertCellPoint(quad[2]); - - lines->InsertNextCell(2); - lines->InsertCellPoint(quad[2]); - lines->InsertCellPoint(quad[3]); - - lines->InsertNextCell(2); - lines->InsertCellPoint(quad[3]); - lines->InsertCellPoint(quad[0]); - } - - m_polyData->SetLines(lines); - break; - } - - case GeometryRenderMode::TRIS: { - // Render triangles as solid surface - vtkSmartPointer triangles = vtkSmartPointer::New(); - - for (size_t i = 0; i < geom.num_tris(); ++i) { - const auto &tri = geom.tris()[i]; - triangles->InsertNextCell(3); - triangles->InsertCellPoint(tri[0]); - triangles->InsertCellPoint(tri[1]); - triangles->InsertCellPoint(tri[2]); - } - - m_polyData->SetPolys(triangles); - break; - } - - case GeometryRenderMode::QUADS: { - // Render quads as solid surface - vtkSmartPointer quads = vtkSmartPointer::New(); - - for (size_t i = 0; i < geom.num_quads(); ++i) { - const auto &quad = geom.quads()[i]; - quads->InsertNextCell(4); - quads->InsertCellPoint(quad[0]); - quads->InsertCellPoint(quad[1]); - quads->InsertCellPoint(quad[2]); - quads->InsertCellPoint(quad[3]); - } - - m_polyData->SetPolys(quads); - break; - } - - case GeometryRenderMode::TETS: { - // TODO: Implement tetrahedral mesh rendering - // For now, render as wireframe edges - vtkSmartPointer lines = vtkSmartPointer::New(); - - for (size_t i = 0; i < geom.num_tets(); ++i) { - const auto &tet = geom.tets()[i]; - // 6 edges per tet: (0,1), (0,2), (0,3), (1,2), (1,3), (2,3) - const int edges[6][2] = {{0, 1}, {0, 2}, {0, 3}, {1, 2}, {1, 3}, {2, 3}}; - for (int e = 0; e < 6; ++e) { - lines->InsertNextCell(2); - lines->InsertCellPoint(tet[edges[e][0]]); - lines->InsertCellPoint(tet[edges[e][1]]); - } - } - - m_polyData->SetLines(lines); - break; - } - - case GeometryRenderMode::HEXS: { - // TODO: Implement hexahedral mesh rendering - // For now, render as wireframe edges - vtkSmartPointer lines = vtkSmartPointer::New(); - - for (size_t i = 0; i < geom.num_hexs(); ++i) { - const auto &hex = geom.hexs()[i]; - // 12 edges per hex - const int edges[12][2] = { - {0, 1}, {1, 2}, {2, 3}, {3, 0}, // Bottom face - {4, 5}, {5, 6}, {6, 7}, {7, 4}, // Top face - {0, 4}, {1, 5}, {2, 6}, {3, 7} // Vertical edges - }; - for (int e = 0; e < 12; ++e) { - lines->InsertNextCell(2); - lines->InsertCellPoint(hex[edges[e][0]]); - lines->InsertCellPoint(hex[edges[e][1]]); - } - } - - m_polyData->SetLines(lines); - break; - } - } - - // Update polydata points - m_polyData->SetPoints(points); - - // Add normals if available - if (geom.normals().size() == geom.num_points()) { - vtkSmartPointer normals = vtkSmartPointer::New(); - normals->SetNumberOfComponents(3); - normals->SetNumberOfTuples(geom.num_points()); - normals->SetName("Normals"); - - for (size_t i = 0; i < geom.num_points(); ++i) { - const auto &n = geom.normals()[i]; - normals->SetTuple3(i, n[0], n[1], n[2]); - } - - m_polyData->GetPointData()->SetNormals(normals); - } else { - m_polyData->GetPointData()->SetNormals(nullptr); - } - - // Add per-vertex colors if available AND single color mode is disabled - if (!m_useSingleColor && geom.colors().size() == geom.num_points()) { - vtkSmartPointer colors = vtkSmartPointer::New(); - colors->SetNumberOfComponents(3); - colors->SetNumberOfTuples(geom.num_points()); - colors->SetName("Colors"); - - for (size_t i = 0; i < geom.num_points(); ++i) { - const auto &c = geom.colors()[i]; - colors->SetTuple3(i, c[0], c[1], c[2]); - } - - m_polyData->GetPointData()->SetScalars(colors); - // Tell VTK mapper to use vertex colors - m_mapper->SetScalarModeToUsePointData(); - m_mapper->ScalarVisibilityOn(); - } else { - // Use single color from actor property - clear per-vertex colors - m_polyData->GetPointData()->SetScalars(nullptr); - m_mapper->ScalarVisibilityOff(); - } - - m_polyData->Modified(); -} - -cvc::bounding_box GeometryNode::getBoundingBox() const { - if (m_geometry) { - try { - return m_geometry->extents(); - } catch (...) { - // extents() can throw for empty/invalid geometry - return cvc::bounding_box(0, 0, 0, 0, 0, 0); - } - } - // Return empty bounding box - return cvc::bounding_box(0, 0, 0, 0, 0, 0); -} - -// Note: syncToState and syncFromState removed - state_object handles state synchronization -// automatically -bool GeometryNode::isComputedMetadata(const std::string &key) { - // These metadata keys are computed from geometry data and should be read-only - static const std::set computedKeys = {"num_vertices", - "num_triangles", - "num_quads", - "num_lines", - "bbox_min_x", - "bbox_min_y", - "bbox_min_z", - "bbox_max_x", - "bbox_max_y", - "bbox_max_z", - "extent_x", - "extent_y", - "extent_z", - "center_x", - "center_y", - "center_z", - "bounding_box", - "type", - "filename", - "combined_bbox_min_x", - "combined_bbox_min_y", - "combined_bbox_min_z", - "combined_bbox_max_x", - "combined_bbox_max_y", - "combined_bbox_max_z", - "combined_extent_x", - "combined_extent_y", - "combined_extent_z", - "combined_center_x", - "combined_center_y", - "combined_center_z"}; - - return computedKeys.find(key) != computedKeys.end(); -} - -void GeometryNode::updateMetadata(const cvc::geometry &geom) { - // Update all geometry statistics as metadata - setMetadata("num_vertices", static_cast(geom.num_points())); - setMetadata("num_triangles", static_cast(geom.num_tris())); - setMetadata("num_quads", static_cast(geom.num_quads())); - - // Only compute bounding box if geometry has points - if (geom.num_points() > 0) { - try { - // Get bounding box extents - auto bbox = geom.extents(); - - setMetadata("bbox_min_x", bbox.minx); - setMetadata("bbox_min_y", bbox.miny); - setMetadata("bbox_min_z", bbox.minz); - setMetadata("bbox_max_x", bbox.maxx); - setMetadata("bbox_max_y", bbox.maxy); - setMetadata("bbox_max_z", bbox.maxz); - - // Store combined bounding box string for computeGraphicsBounds() - std::string bboxStr = std::to_string(bbox.minx) + "," + std::to_string(bbox.miny) + "," + - std::to_string(bbox.minz) + "," + std::to_string(bbox.maxx) + "," + - std::to_string(bbox.maxy) + "," + std::to_string(bbox.maxz); - setMetadata("bounding_box", bboxStr); - - // Compute extents (dimensions) - double extentX = bbox.maxx - bbox.minx; - double extentY = bbox.maxy - bbox.miny; - double extentZ = bbox.maxz - bbox.minz; - - setMetadata("extent_x", extentX); - setMetadata("extent_y", extentY); - setMetadata("extent_z", extentZ); - - // Compute center point - setMetadata("center_x", (bbox.minx + bbox.maxx) / 2.0); - setMetadata("center_y", (bbox.miny + bbox.maxy) / 2.0); - setMetadata("center_z", (bbox.minz + bbox.maxz) / 2.0); - } catch (...) { - // Failed to compute bounding box for empty or invalid geometry - } - } - - // Add geometry type - std::string geomType = "mesh"; - if (geom.num_tris() > 0 && geom.num_quads() == 0) { - geomType = "triangle_mesh"; - } else if (geom.num_quads() > 0 && geom.num_tris() == 0) { - geomType = "quad_mesh"; - } else if (geom.num_tris() > 0 && geom.num_quads() > 0) { - geomType = "mixed_mesh"; - } else if (geom.num_points() == 0) { - geomType = "empty"; - } - setMetadata("type", geomType); -} - -void GeometryNode::onDataChanged() { - // Called when state data changes - reload geometry from state - // Note: With state_object, we access state via getState() instead of m_stateNode - if (getState().isData()) { - try { - const cvc::geometry &geom = boost::any_cast(getState().data()); - setGeometry(geom); - } catch (...) { - // Failed to load geometry from state - } - } -} diff --git a/src/volrover3/GraphicsNode.cpp b/src/volrover3/GraphicsNode.cpp deleted file mode 100644 index a99861b5..00000000 --- a/src/volrover3/GraphicsNode.cpp +++ /dev/null @@ -1,973 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -GraphicsNode::GraphicsNode(cvc::app &ctx, const std::string &statePath, const std::string &name) - : SceneNode(ctx, statePath), m_name(name), m_transform(vtkSmartPointer::New()), - m_vtkTransform(vtkSmartPointer::New()), m_parent(nullptr), m_showBBox(false), - m_bboxNode(std::make_shared()), m_showLabel(false), m_labelText(name), - m_labelSize(14), m_labelActor(vtkSmartPointer::New()), m_clipChildren(false), - m_clipPlanes(vtkSmartPointer::New()) { - // Initialize transform to identity - m_transform->Identity(); - m_vtkTransform->SetMatrix(m_transform); - - // Initialize label color to white - m_labelColor[0] = m_labelColor[1] = m_labelColor[2] = 1.0; - - // Setup label actor - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(m_labelText.c_str()); - textMapper->GetTextProperty()->SetFontSize(m_labelSize); - textMapper->GetTextProperty()->SetColor(m_labelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - m_labelActor->SetMapper(textMapper); - m_labelActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - m_labelActor->SetVisibility(m_showLabel); - - // Initialize clip planes (6 planes for bounding box faces) - for (int i = 0; i < 6; ++i) { - m_clipPlaneArray[i] = vtkSmartPointer::New(); - m_clipPlanes->AddItem(m_clipPlaneArray[i]); - } - - // Initialize state tree values if we have a valid state path - // Don't batch during construction - initial values should be set silently - // Handlers will fire when values change AFTER construction completes - if (!statePath.empty()) { - getState("show_bbox").value(0); - getState("show_label").value(0); - getState("label_text").value(name); - getState("label_size").value(14); - getState("label_color").value(std::string("1.0,1.0,1.0")); - - // Transform state attributes - getState("position").value(std::string("0.0,0.0,0.0")); - getState("rotation").value(std::string("0.0,0.0,0.0")); - getState("scale").value(std::string("1.0,1.0,1.0")); - - // Full matrix (16 values, row-major) - getState("matrix").value(std::string("1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1")); - - // Clip planes - getState("clip_children").value(0); - } -} - -GraphicsNode::~GraphicsNode() {} - -void GraphicsNode::setTransform(vtkMatrix4x4 *matrix) { - if (matrix) { - m_transform->DeepCopy(matrix); - - // Update state tree (matrix in row-major format) - std::ostringstream oss; - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - if (i > 0 || j > 0) - oss << ","; - oss << m_transform->GetElement(i, j); - } - } - getState("matrix").value(oss.str()); - - updateTransform(); - } -} - -void GraphicsNode::setTransform(const double matrix[16]) { - // Input is row-major, VTK uses row-major storage - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - m_transform->SetElement(i, j, matrix[i * 4 + j]); - } - } - - // Update state tree (matrix in row-major format) - std::ostringstream oss; - for (int i = 0; i < 16; ++i) { - if (i > 0) - oss << ","; - oss << matrix[i]; - } - getState("matrix").value(oss.str()); - - updateTransform(); -} - -void GraphicsNode::setPosition(double x, double y, double z) { - m_transform->SetElement(0, 3, x); - m_transform->SetElement(1, 3, y); - m_transform->SetElement(2, 3, z); - - // Update state tree - std::ostringstream oss; - oss << x << "," << y << "," << z; - getState("position").value(oss.str()); - - updateTransform(); -} - -void GraphicsNode::setRotation(double x, double y, double z) { - // Create transform with rotation - vtkSmartPointer transform = vtkSmartPointer::New(); - transform->Identity(); - transform->RotateZ(z); - transform->RotateY(y); - transform->RotateX(x); - - // Preserve current translation - double tx = m_transform->GetElement(0, 3); - double ty = m_transform->GetElement(1, 3); - double tz = m_transform->GetElement(2, 3); - - m_transform->DeepCopy(transform->GetMatrix()); - m_transform->SetElement(0, 3, tx); - m_transform->SetElement(1, 3, ty); - m_transform->SetElement(2, 3, tz); - - // Update state tree - std::ostringstream oss; - oss << x << "," << y << "," << z; - getState("rotation").value(oss.str()); - - updateTransform(); -} - -void GraphicsNode::setScale(double x, double y, double z) { - // Get current translation - double tx = m_transform->GetElement(0, 3); - double ty = m_transform->GetElement(1, 3); - double tz = m_transform->GetElement(2, 3); - - // Extract rotation part (normalize the 3x3 upper-left) - vtkSmartPointer rotation = vtkSmartPointer::New(); - for (int i = 0; i < 3; ++i) { - double len = 0.0; - for (int j = 0; j < 3; ++j) { - double val = m_transform->GetElement(i, j); - len += val * val; - } - len = std::sqrt(len); - if (len > 0.0) { - for (int j = 0; j < 3; ++j) { - rotation->SetElement(i, j, m_transform->GetElement(i, j) / len); - } - } - } - - // Apply new scale to rotation - for (int i = 0; i < 3; ++i) { - double scale = (i == 0) ? x : (i == 1) ? y : z; - for (int j = 0; j < 3; ++j) { - m_transform->SetElement(i, j, rotation->GetElement(i, j) * scale); - } - } - - // Restore translation - m_transform->SetElement(0, 3, tx); - m_transform->SetElement(1, 3, ty); - m_transform->SetElement(2, 3, tz); - - // Update state tree - std::ostringstream oss; - oss << x << "," << y << "," << z; - getState("scale").value(oss.str()); - - updateTransform(); -} - -void GraphicsNode::resetTransform() { - m_transform->Identity(); - - // Update state tree to identity - getState("position").value(std::string("0.0,0.0,0.0")); - getState("rotation").value(std::string("0.0,0.0,0.0")); - getState("scale").value(std::string("1.0,1.0,1.0")); - getState("matrix").value(std::string("1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1")); - - updateTransform(); -} - -vtkSmartPointer GraphicsNode::getWorldTransform() const { - vtkSmartPointer worldTransform = vtkSmartPointer::New(); - - if (m_parent) { - // Get parent's world transform - vtkSmartPointer parentWorld = m_parent->getWorldTransform(); - // Multiply: worldTransform = parentWorld * m_transform - vtkMatrix4x4::Multiply4x4(parentWorld, m_transform, worldTransform); - } else { - // No parent, local transform is world transform - worldTransform->DeepCopy(m_transform); - } - - return worldTransform; -} - -void GraphicsNode::updateTransform() { - // Update VTK transform wrapper - m_vtkTransform->SetMatrix(m_transform); - m_vtkTransform->Modified(); - - // Apply to VTK prop (subclasses override this) - applyTransformToVTK(); - - // Update all children - for (auto &child : m_graphicsChildren) { - child->updateTransform(); - } - - // Update bbox if visible - if (m_showBBox) { - updateBoundingBoxNode(); - } - - // Update clip planes if clipping is enabled - if (m_clipChildren) { - updateClipPlanes(); - } -} - -void GraphicsNode::applyWorldTransformToProps(const std::vector &props) { - if (props.empty()) - return; - - // Compute world transform once - auto worldTransform = getWorldTransform(); - vtkSmartPointer vtkWorldTransform = vtkSmartPointer::New(); - vtkWorldTransform->SetMatrix(worldTransform); - - // Apply to all props (cast to vtkProp3D which has SetUserTransform) - for (vtkProp *prop : props) { - if (prop) { - vtkProp3D *prop3D = vtkProp3D::SafeDownCast(prop); - if (prop3D) { - prop3D->SetUserTransform(vtkWorldTransform.Get()); - } - } - } -} - -void GraphicsNode::applyTransformToVTK() { - // Base class does nothing - subclasses override to apply transform to their - // specific VTK prop E.g., GeometryNode calls - // applyWorldTransformToProps({m_actor}) -} - -void GraphicsNode::updateBoundingBoxNode() { - if (!m_bboxNode) - return; - - // Get COMBINED bounding box (this node + all children) in LOCAL space - // This ensures the bbox shows the full extent including children - cvc::bounding_box bbox = getCombinedBoundingBox(); - - // Get world transform - vtkSmartPointer worldTransform = getWorldTransform(); - - // Update bbox on main thread (VTK operations must be on main thread) - runOnMainThread([this, bbox, worldTransform]() { - if (m_bboxNode) { - // Set the bounding box geometry in local space - m_bboxNode->setBoundingBox(bbox); - - // Apply the world transform to the bbox actor so it renders correctly - m_bboxNode->setTransform(worldTransform); - } - }); -} - -void GraphicsNode::handleStateChanged(const std::string &childState) { - // Marshal to main thread via event queue - runOnMainThread([this, childState]() { - // Handle state changes for graphics-specific fields - if (childState == "show_bbox") { - int showBBox = getState("show_bbox").value(); - setShowBBox(showBBox != 0); - } else if (childState == "show_label") { - int showLabel = getState("show_label").value(); - setShowLabel(showLabel != 0); - } else if (childState == "label_text") { - std::string labelText = getState("label_text").value(); - setLabelText(labelText); - } else if (childState == "label_size") { - int labelSize = getState("label_size").value(); - setLabelSize(labelSize); - } else if (childState == "label_color") { - try { - std::string colorStr = getState("label_color").value(); - std::istringstream iss(colorStr); - double r, g, b; - char comma; - if (iss >> r >> comma >> g >> comma >> b) { - setLabelColor(r, g, b); - } - } catch (const boost::bad_lexical_cast &) { - // Ignore - state initialization may trigger before all components are - // set - } - } else if (childState == "position") { - try { - std::string posStr = getState("position").value(); - std::istringstream iss(posStr); - double x, y, z; - char comma; - if (iss >> x >> comma >> y >> comma >> z) { - // Directly update matrix without triggering state update (avoid - // loop) - m_transform->SetElement(0, 3, x); - m_transform->SetElement(1, 3, y); - m_transform->SetElement(2, 3, z); - updateTransform(); - } - } catch (const boost::bad_lexical_cast &) { - } - } else if (childState == "rotation") { - try { - std::string rotStr = getState("rotation").value(); - std::istringstream iss(rotStr); - double rx, ry, rz; - char comma; - if (iss >> rx >> comma >> ry >> comma >> rz) { - // Create transform with rotation - vtkSmartPointer transform = vtkSmartPointer::New(); - transform->Identity(); - transform->RotateZ(rz); - transform->RotateY(ry); - transform->RotateX(rx); - - // Preserve current translation - double tx = m_transform->GetElement(0, 3); - double ty = m_transform->GetElement(1, 3); - double tz = m_transform->GetElement(2, 3); - - m_transform->DeepCopy(transform->GetMatrix()); - m_transform->SetElement(0, 3, tx); - m_transform->SetElement(1, 3, ty); - m_transform->SetElement(2, 3, tz); - - updateTransform(); - } - } catch (const boost::bad_lexical_cast &) { - } - } else if (childState == "scale") { - try { - std::string scaleStr = getState("scale").value(); - std::istringstream iss(scaleStr); - double sx, sy, sz; - char comma; - if (iss >> sx >> comma >> sy >> comma >> sz) { - // Get current translation - double tx = m_transform->GetElement(0, 3); - double ty = m_transform->GetElement(1, 3); - double tz = m_transform->GetElement(2, 3); - - // Extract rotation part (normalize the 3x3 upper-left) - vtkSmartPointer rotation = vtkSmartPointer::New(); - for (int i = 0; i < 3; ++i) { - double len = 0.0; - for (int j = 0; j < 3; ++j) { - double val = m_transform->GetElement(i, j); - len += val * val; - } - len = std::sqrt(len); - if (len > 0.0) { - for (int j = 0; j < 3; ++j) { - rotation->SetElement(i, j, m_transform->GetElement(i, j) / len); - } - } - } - - // Apply new scale to rotation - for (int i = 0; i < 3; ++i) { - double scale = (i == 0) ? sx : (i == 1) ? sy : sz; - for (int j = 0; j < 3; ++j) { - m_transform->SetElement(i, j, rotation->GetElement(i, j) * scale); - } - } - - // Restore translation - m_transform->SetElement(0, 3, tx); - m_transform->SetElement(1, 3, ty); - m_transform->SetElement(2, 3, tz); - - updateTransform(); - } - } catch (const boost::bad_lexical_cast &) { - } - } else if (childState == "matrix") { - try { - std::string matrixStr = getState("matrix").value(); - std::istringstream iss(matrixStr); - double values[16]; - char comma; - - // Read 16 comma-separated values - for (int i = 0; i < 16; ++i) { - if (i > 0) - iss >> comma; - if (!(iss >> values[i])) - break; - } - - // Update matrix (row-major input) - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - m_transform->SetElement(i, j, values[i * 4 + j]); - } - } - updateTransform(); - } catch (const boost::bad_lexical_cast &) { - } - } else if (childState == "clip_children") { - int clip = getState("clip_children").value(); - setClipChildren(clip != 0); - } else { - // Delegate to parent for common fields like visible - // Parent will NOT wrap again - we're already on main thread - SceneNode::handleStateChanged(childState); - } - - // Request render after any state change - if (m_renderer && m_renderer->GetRenderWindow()) { - m_renderer->GetRenderWindow()->Render(); - } - }); -} - -void GraphicsNode::addGraphicsChild(std::shared_ptr child) { - if (!child) - return; - - // Add to graphics children list - m_graphicsChildren.push_back(child); - - // Set parent pointer - child->m_parent = this; - - // Propagate SceneGraph reference to child - child->setSceneGraph(m_sceneGraph); - - // Also add as SceneNode child so it gets rendered - addChild(child); - - // Update child's transform to reflect new parent - child->updateTransform(); - - // Update this node's bounding box to include the new child - if (m_showBBox) { - updateBoundingBoxNode(); - } -} - -std::shared_ptr GraphicsNode::createChild(const std::string &name) { - // Create NullGraphicNode child for placeholder/hierarchy purposes - return addGraphicsChild(name); -} - -void GraphicsNode::removeGraphicsChild(std::shared_ptr child) { - if (!child) - return; - - // Remove from graphics children - auto it = std::find(m_graphicsChildren.begin(), m_graphicsChildren.end(), child); - if (it != m_graphicsChildren.end()) { - m_graphicsChildren.erase(it); - child->m_parent = nullptr; - child->updateTransform(); - } - - // Also remove as SceneNode child - removeChild(child); - - // Update this node's bounding box after removing child - if (m_showBBox) { - updateBoundingBoxNode(); - } -} - -std::shared_ptr GraphicsNode::findChildByName(const std::string &name) { - for (auto &child : m_graphicsChildren) { - if (child->getName() == name) { - return child; - } - // Recursively search in child's children - auto found = child->findChildByName(name); - if (found) { - return found; - } - } - return nullptr; -} - -cvc::bounding_box GraphicsNode::getCombinedBoundingBox() const { - // Check if this is a NullGraphicNode and if it should include own bounds - const NullGraphicNode *nullNode = dynamic_cast(this); - bool includeOwnBounds = true; - if (nullNode) { - includeOwnBounds = nullNode->getIncludeOwnBounds(); - } - - // Accumulate extents without creating invalid bbox - double acc_minx = std::numeric_limits::max(); - double acc_miny = std::numeric_limits::max(); - double acc_minz = std::numeric_limits::max(); - double acc_maxx = std::numeric_limits::lowest(); - double acc_maxy = std::numeric_limits::lowest(); - double acc_maxz = std::numeric_limits::lowest(); - - // Include own bounds if requested - if (includeOwnBounds) { - cvc::bounding_box ownBBox = getBoundingBox(); - acc_minx = ownBBox[0]; - acc_miny = ownBBox[1]; - acc_minz = ownBBox[2]; - acc_maxx = ownBBox[3]; - acc_maxy = ownBBox[4]; - acc_maxz = ownBBox[5]; - } - - // Expand to include all children (transformed to this node's local space) - for (const auto &child : m_graphicsChildren) { - if (!child) - continue; - - // Get child's combined bbox (includes child's descendants in child's - // local space) - cvc::bounding_box childBBox = child->getCombinedBoundingBox(); - - // Skip invalid bounding boxes - if (childBBox[0] > childBBox[3] || childBBox[1] > childBBox[4] || childBBox[2] > childBBox[5]) { - continue; - } - - // Transform child's bbox by child's local transform to get it in this - // node's space - vtkMatrix4x4 *childTransform = child->getTransform(); - - // Transform all 8 corners of child's bbox - double corners[8][3] = { - {childBBox[0], childBBox[1], childBBox[2]}, // min, min, min - {childBBox[3], childBBox[1], childBBox[2]}, // max, min, min - {childBBox[0], childBBox[4], childBBox[2]}, // min, max, min - {childBBox[3], childBBox[4], childBBox[2]}, // max, max, min - {childBBox[0], childBBox[1], childBBox[5]}, // min, min, max - {childBBox[3], childBBox[1], childBBox[5]}, // max, min, max - {childBBox[0], childBBox[4], childBBox[5]}, // min, max, max - {childBBox[3], childBBox[4], childBBox[5]} // max, max, max - }; - - double minx = std::numeric_limits::max(); - double miny = std::numeric_limits::max(); - double minz = std::numeric_limits::max(); - double maxx = std::numeric_limits::lowest(); - double maxy = std::numeric_limits::lowest(); - double maxz = std::numeric_limits::lowest(); - - for (int i = 0; i < 8; ++i) { - double in[4] = {corners[i][0], corners[i][1], corners[i][2], 1.0}; - double out[4]; - childTransform->MultiplyPoint(in, out); - - minx = std::min(minx, out[0]); - miny = std::min(miny, out[1]); - minz = std::min(minz, out[2]); - maxx = std::max(maxx, out[0]); - maxy = std::max(maxy, out[1]); - maxz = std::max(maxz, out[2]); - } - - // Expand accumulated extents to include transformed child - acc_minx = std::min(acc_minx, minx); - acc_miny = std::min(acc_miny, miny); - acc_minz = std::min(acc_minz, minz); - acc_maxx = std::max(acc_maxx, maxx); - acc_maxy = std::max(acc_maxy, maxy); - acc_maxz = std::max(acc_maxz, maxz); - } - - // Create final bounding box from accumulated extents - // If no valid extents were accumulated, return a default small box - if (acc_minx > acc_maxx || acc_miny > acc_maxy || acc_minz > acc_maxz) { - return cvc::bounding_box(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5); - } - - return cvc::bounding_box(acc_minx, acc_miny, acc_minz, acc_maxx, acc_maxy, acc_maxz); -} - -void GraphicsNode::setMetadata(const std::string &key, const std::any &value) { - m_metadata[key] = value; - - // Also sync to state tree for persistence and visibility - // Create metadata substate if needed - try { - std::string metadataPath = "metadata." + key; - - // Convert std::any to appropriate type and store in state - if (value.type() == typeid(int)) { - getState(metadataPath).value(std::any_cast(value)); - getState(metadataPath).readOnly(true); - } else if (value.type() == typeid(double)) { - getState(metadataPath).value(std::any_cast(value)); - getState(metadataPath).readOnly(true); - } else if (value.type() == typeid(std::string)) { - getState(metadataPath).value(std::any_cast(value)); - getState(metadataPath).readOnly(true); - } else if (value.type() == typeid(const char *)) { - getState(metadataPath).value(std::string(std::any_cast(value))); - getState(metadataPath).readOnly(true); - } else if (value.type() == typeid(bool)) { - getState(metadataPath).value(std::any_cast(value)); - getState(metadataPath).readOnly(true); - } - // Add more types as needed - } catch (...) { - // Ignore metadata sync errors - } -} - -std::any GraphicsNode::getMetadata(const std::string &key) const { - auto it = m_metadata.find(key); - if (it != m_metadata.end()) { - return it->second; - } - return std::any(); -} - -bool GraphicsNode::hasMetadata(const std::string &key) const { - return m_metadata.find(key) != m_metadata.end(); -} - -void GraphicsNode::update() { - // With state_object, we don't need manual syncing - // The state tree automatically synchronizes via handleStateChanged() - // Just propagate to children - SceneNode::update(); -} - -void GraphicsNode::setVisible(bool visible) { - SceneNode::setVisible(visible); - - // Update label visibility (wrap VTK operation) - if (m_labelActor) { - runOnMainThread([this, visible]() { - if (m_labelActor) { - m_labelActor->SetVisibility(m_showLabel && visible); - } - }); - } -} - -void GraphicsNode::setShowBBox(bool show) { - if (m_showBBox == show) - return; - - m_showBBox = show; - - // Update state tree value - getState("show_bbox").value(show ? 1 : 0); - - if (m_bboxNode && m_renderer) { - // Wrap VTK operations in runOnMainThread - runOnMainThread([this, show]() { - if (m_bboxNode && m_renderer) { - if (show) { - updateBoundingBoxNode(); - m_bboxNode->addToRenderer(m_renderer); - } else { - m_bboxNode->removeFromRenderer(m_renderer); - } - } - }); - } -} - -void GraphicsNode::setBBoxColor(double r, double g, double b) { - if (m_bboxNode) { - m_bboxNode->setColor(r, g, b); - } -} - -void GraphicsNode::getBBoxColor(double &r, double &g, double &b) const { - if (m_bboxNode) { - m_bboxNode->getColor(r, g, b); - } else { - r = g = b = 1.0; - } -} - -void GraphicsNode::setShowExtentLabels(bool show) { - // Update state tree value - getState("show_extent_labels").value(show ? 1 : 0); - - if (m_bboxNode) { - m_bboxNode->setCoordinatesVisible(show); - } -} - -bool GraphicsNode::getShowExtentLabels() const { - if (m_bboxNode) { - return m_bboxNode->getCoordinatesVisible(); - } - return false; -} - -void GraphicsNode::setExtentLabelColor(double r, double g, double b) { - // Update state tree values - getState("extent_label_color_r").value(r); - getState("extent_label_color_g").value(g); - getState("extent_label_color_b").value(b); - - if (m_bboxNode) { - m_bboxNode->setCoordinateLabelColor(r, g, b); - } -} - -void GraphicsNode::getExtentLabelColor(double &r, double &g, double &b) const { - if (m_bboxNode) { - m_bboxNode->getCoordinateLabelColor(r, g, b); - } else { - r = g = b = 1.0; - } -} - -void GraphicsNode::setExtentLabelFontSize(int size) { - // Update state tree value - getState("extent_label_font_size").value(size); - - if (m_bboxNode) { - m_bboxNode->setCoordinateLabelFontSize(size); - } -} - -int GraphicsNode::getExtentLabelFontSize() const { - if (m_bboxNode) { - return m_bboxNode->getCoordinateLabelFontSize(); - } - return 12; // Default font size -} - -void GraphicsNode::setShowLabel(bool show) { - if (m_showLabel == show) - return; - - m_showLabel = show; - m_labelActor->SetVisibility(m_showLabel && isVisible()); - - // Add or remove from renderer if needed - if (m_renderer) { - if (m_showLabel && isVisible()) { - updateLabel(); - m_renderer->AddViewProp(m_labelActor); - } else { - m_renderer->RemoveViewProp(m_labelActor); - } - } -} - -void GraphicsNode::setLabelText(const std::string &text) { - m_labelText = text; - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(m_labelActor->GetMapper()); - if (mapper) { - mapper->SetInput(m_labelText.c_str()); - } -} - -void GraphicsNode::setLabelSize(int size) { - m_labelSize = std::max(1, size); - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(m_labelActor->GetMapper()); - if (mapper) { - mapper->GetTextProperty()->SetFontSize(m_labelSize); - } -} - -void GraphicsNode::setLabelColor(double r, double g, double b) { - m_labelColor[0] = r; - m_labelColor[1] = g; - m_labelColor[2] = b; - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(m_labelActor->GetMapper()); - if (mapper) { - mapper->GetTextProperty()->SetColor(r, g, b); - } -} - -void GraphicsNode::getLabelColor(double &r, double &g, double &b) const { - r = m_labelColor[0]; - g = m_labelColor[1]; - b = m_labelColor[2]; -} - -void GraphicsNode::updateLabel() { - // Position label at center of bounding box in LOCAL space - cvc::bounding_box bbox = getBoundingBox(); - double centerX = (bbox[0] + bbox[3]) / 2.0; - double centerY = (bbox[1] + bbox[4]) / 2.0; - double centerZ = (bbox[2] + bbox[5]) / 2.0; - - // Transform center to world space - vtkSmartPointer worldTransform = getWorldTransform(); - double localCenter[4] = {centerX, centerY, centerZ, 1.0}; - double worldCenter[4]; - worldTransform->MultiplyPoint(localCenter, worldCenter); - - m_labelActor->GetPositionCoordinate()->SetValue(worldCenter[0], worldCenter[1], worldCenter[2]); -} - -void GraphicsNode::addToRenderer(vtkRenderer *renderer) { - // Call base implementation to add the main prop - SceneNode::addToRenderer(renderer); - - // Add bbox if it should be visible - if (m_showBBox && m_bboxNode) { - // Update and capture references before queuing - updateBoundingBoxNode(); - auto bboxNode = m_bboxNode; - runOnMainThread([bboxNode, renderer]() { bboxNode->addToRenderer(renderer); }); - } - - // Add label if it should be visible - if (m_showLabel && m_labelActor) { - // Update and capture the actor before queuing - updateLabel(); - vtkActor2D *labelActor = m_labelActor; - runOnMainThread([labelActor, renderer]() { renderer->AddViewProp(labelActor); }); - } -} - -void GraphicsNode::removeFromRenderer(vtkRenderer *renderer) { - // Remove label - capture the actor pointer to avoid accessing 'this' after - // deletion - vtkActor2D *labelActor = m_labelActor; - if (labelActor) { - runOnMainThread([labelActor, renderer]() { renderer->RemoveViewProp(labelActor); }); - } - - // Remove bbox - if (m_bboxNode) { - m_bboxNode->removeFromRenderer(renderer); - } - - // Call base implementation to remove the main prop - SceneNode::removeFromRenderer(renderer); -} - -void GraphicsNode::setClipChildren(bool clip) { - if (m_clipChildren == clip) - return; - - m_clipChildren = clip; - - // Update state tree - getState("clip_children").value(clip ? 1 : 0); - - if (m_clipChildren) { - // Enable clipping - update and apply planes - updateClipPlanes(); - applyClipPlanesToChildren(); - } else { - // Disable clipping - remove planes from children - for (auto &child : m_graphicsChildren) { - child->runOnMainThread([child]() { child->applyClipPlanes(nullptr); }); - } - } -} - -void GraphicsNode::updateClipPlanes() { - // Get this node's OWN bounding box (not combined) - cvc::bounding_box bbox = getBoundingBox(); - double bounds[6] = {bbox.minx, bbox.maxx, bbox.miny, bbox.maxy, bbox.minz, bbox.maxz}; - - // Get world transform to transform the planes - vtkSmartPointer worldTransform = getWorldTransform(); - - // Define 6 plane normals in local space - // Order: +X, -X, +Y, -Y, +Z, -Z - double normals[6][3] = { - {1.0, 0.0, 0.0}, // +X face (points inward: -X) - {-1.0, 0.0, 0.0}, // -X face (points inward: +X) - {0.0, 1.0, 0.0}, // +Y face (points inward: -Y) - {0.0, -1.0, 0.0}, // -Y face (points inward: +Y) - {0.0, 0.0, 1.0}, // +Z face (points inward: -Z) - {0.0, 0.0, -1.0} // -Z face (points inward: +Z) - }; - - // Plane origins in local space (centers of each face) - double origins[6][3] = { - {bounds[1], (bounds[2] + bounds[3]) / 2.0, (bounds[4] + bounds[5]) / 2.0}, // +X - {bounds[0], (bounds[2] + bounds[3]) / 2.0, (bounds[4] + bounds[5]) / 2.0}, // -X - {(bounds[0] + bounds[1]) / 2.0, bounds[3], (bounds[4] + bounds[5]) / 2.0}, // +Y - {(bounds[0] + bounds[1]) / 2.0, bounds[2], (bounds[4] + bounds[5]) / 2.0}, // -Y - {(bounds[0] + bounds[1]) / 2.0, (bounds[2] + bounds[3]) / 2.0, bounds[5]}, // +Z - {(bounds[0] + bounds[1]) / 2.0, (bounds[2] + bounds[3]) / 2.0, bounds[4]} // -Z - }; - - // Transform and set each plane - for (int i = 0; i < 6; ++i) { - // Transform origin to world space - double worldOrigin[4] = {origins[i][0], origins[i][1], origins[i][2], 1.0}; - double transformedOrigin[4]; - worldTransform->MultiplyPoint(worldOrigin, transformedOrigin); - - // Transform normal to world space (using transpose of inverse for - // normals) For orthogonal transforms (rotation + uniform scale), we can - // use the matrix directly - vtkSmartPointer normalMatrix = vtkSmartPointer::New(); - normalMatrix->DeepCopy(worldTransform); - normalMatrix->Invert(); - normalMatrix->Transpose(); - - double worldNormal[4] = {normals[i][0], normals[i][1], normals[i][2], 0.0}; - double transformedNormal[4]; - normalMatrix->MultiplyPoint(worldNormal, transformedNormal); - - // Normalize the transformed normal - double len = std::sqrt(transformedNormal[0] * transformedNormal[0] + - transformedNormal[1] * transformedNormal[1] + - transformedNormal[2] * transformedNormal[2]); - if (len > 0.0) { - transformedNormal[0] /= len; - transformedNormal[1] /= len; - transformedNormal[2] /= len; - } - - // Set plane - m_clipPlaneArray[i]->SetOrigin(transformedOrigin[0], transformedOrigin[1], - transformedOrigin[2]); - m_clipPlaneArray[i]->SetNormal(transformedNormal[0], transformedNormal[1], - transformedNormal[2]); - } - - // Apply updated planes to children - if (m_clipChildren) { - applyClipPlanesToChildren(); - } -} - -void GraphicsNode::applyClipPlanesToChildren() { - for (auto &child : m_graphicsChildren) { - child->runOnMainThread([this, child]() { child->applyClipPlanes(m_clipPlanes); }); - } -} - -void GraphicsNode::applyClipPlanes(vtkPlaneCollection *planes) { - // Base implementation does nothing - // Subclasses that support clipping (GeometryNode, VolumeNode, GridNode) - // override this -} diff --git a/src/volrover3/GraphicsParentDialog.cpp b/src/volrover3/GraphicsParentDialog.cpp deleted file mode 100644 index 9a751086..00000000 --- a/src/volrover3/GraphicsParentDialog.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -GraphicsParentDialog::GraphicsParentDialog(std::shared_ptr sceneGraph, QWidget *parent) - : QDialog(parent), m_sceneGraph(sceneGraph), m_parentComboBox(new QComboBox(this)), - m_okButton(new QPushButton(tr("OK"), this)), - m_cancelButton(new QPushButton(tr("Cancel"), this)) { - setWindowTitle(tr("Select Parent Graphics Node")); - setModal(true); - - // Create layout - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Add description label - QLabel *descLabel = new QLabel(tr("Select the parent node for the new graphics object.\n" - "The new object will be placed under the selected node."), - this); - descLabel->setWordWrap(true); - mainLayout->addWidget(descLabel); - - // Add combo box - QLabel *comboLabel = new QLabel(tr("Parent Node:"), this); - mainLayout->addWidget(comboLabel); - mainLayout->addWidget(m_parentComboBox); - - // Populate the list - populateParentList(); - - // Add buttons - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - buttonLayout->addWidget(m_okButton); - buttonLayout->addWidget(m_cancelButton); - mainLayout->addLayout(buttonLayout); - - // Connect signals - connect(m_okButton, &QPushButton::clicked, this, &QDialog::accept); - connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - resize(400, 200); -} - -GraphicsParentDialog::~GraphicsParentDialog() {} - -void GraphicsParentDialog::populateParentList() { - m_parentComboBox->clear(); - - // Add root option (empty parent) - m_parentComboBox->addItem(tr("(Root - No Parent)"), QVariant(QString(""))); - - // Add all graphics nodes hierarchically (includes both geometry and volumes) - auto graphicsRoot = m_sceneGraph->getGraphicsRoot(); - if (graphicsRoot) { - for (const auto &child : graphicsRoot->getGraphicsChildren()) { - addNodeToList(child, 0); - } - } - - // Select root by default - m_parentComboBox->setCurrentIndex(0); -} - -void GraphicsParentDialog::addNodeToList(std::shared_ptr node, int depth) { - if (!node) - return; - - // Determine if this is a volume or geometry node - bool isVolume = (std::dynamic_pointer_cast(node) != nullptr); - - // Create indented display name with icon - QString indent(depth * 2, ' '); - QString icon = isVolume ? "🔲" : "📦"; - QString type = isVolume ? "Volume" : "Geometry"; - QString displayName = indent + icon + " " + type + ": " + QString::fromStdString(node->getName()); - - // Add to combo box with node name as data (prefixed with type) - QString prefix = isVolume ? "vol:" : "geom:"; - m_parentComboBox->addItem(displayName, - QVariant(prefix + QString::fromStdString(node->getName()))); - - // Recursively add children - for (const auto &child : node->getGraphicsChildren()) { - addNodeToList(child, depth + 1); - } -} - -std::string GraphicsParentDialog::getSelectedParentName() const { - QString name = m_parentComboBox->currentData().toString(); - return name.toStdString(); -} - -std::shared_ptr GraphicsParentDialog::getSelectedParent() const { - std::string parentName = getSelectedParentName(); - if (parentName.empty()) { - return nullptr; // Root - } - - // Check if it's a geometry node (prefixed with "geom:") - if (parentName.substr(0, 5) == "geom:") { - return m_sceneGraph->getGraphics(parentName.substr(5)); - } - - // Check if it's a volume node (prefixed with "vol:") - if (parentName.substr(0, 4) == "vol:") { - // Return the volume node as a GraphicsNode (volumes can parent both geometry and volumes) - return std::dynamic_pointer_cast(m_sceneGraph->getGraphics(parentName.substr(4))); - } - - return nullptr; -} - -std::shared_ptr GraphicsParentDialog::getSelectedVolumeParent() const { - std::string parentName = getSelectedParentName(); - if (parentName.empty()) { - return nullptr; // Root - } - - // Check if it's a volume node (prefixed with "vol:") - if (parentName.substr(0, 4) == "vol:") { - return std::dynamic_pointer_cast(m_sceneGraph->getGraphics(parentName.substr(4))); - } - - // Check if it's a geometry node (prefixed with "geom:") - volumes can be children of geometry - if (parentName.substr(0, 5) == "geom:") { - auto geomNode = m_sceneGraph->getGraphics(parentName.substr(5)); - // Try to cast to VolumeNode (in case geometry node is actually a volume) - return std::dynamic_pointer_cast(geomNode); - } - - return nullptr; -} diff --git a/src/volrover3/GridNode.cpp b/src/volrover3/GridNode.cpp deleted file mode 100644 index d8e6fa3d..00000000 --- a/src/volrover3/GridNode.cpp +++ /dev/null @@ -1,871 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -GridNode::GridNode(cvc::app &ctx, const std::string &statePath, const std::string &name) - : GraphicsNode(ctx, statePath, name), m_yzActor(vtkSmartPointer::New()), - m_xzActor(vtkSmartPointer::New()), m_xyActor(vtkSmartPointer::New()), - m_yzMapper(vtkSmartPointer::New()), - m_xzMapper(vtkSmartPointer::New()), - m_xyMapper(vtkSmartPointer::New()), - m_bounds(-10.0, -10.0, -10.0, 10.0, 10.0, 10.0), m_divisionsX(64), m_divisionsY(64), - m_divisionsZ(64), m_tickIntervalX(8), m_tickIntervalY(8), m_tickIntervalZ(8), - m_tickLabelFontSize(12), m_yzPlaneVisible(true), m_xzPlaneVisible(true), - m_xyPlaneVisible(true), m_renderer(nullptr) { - // Initialize default colors - m_yzPlaneColor[0] = m_yzPlaneColor[1] = m_yzPlaneColor[2] = 0.5; - m_xzPlaneColor[0] = m_xzPlaneColor[1] = m_xzPlaneColor[2] = 0.5; - m_xyPlaneColor[0] = m_xyPlaneColor[1] = m_xyPlaneColor[2] = 0.5; - m_tickLabelColor[0] = m_tickLabelColor[1] = m_tickLabelColor[2] = 1.0; // White - - // Setup YZ plane actor (at X=0) - m_yzActor->SetMapper(m_yzMapper); - m_yzActor->GetProperty()->SetColor(m_yzPlaneColor); - m_yzActor->GetProperty()->SetLineWidth(1.0); - m_yzActor->GetProperty()->SetOpacity(0.5); - - // Setup XZ plane actor (at Y=0) - m_xzActor->SetMapper(m_xzMapper); - m_xzActor->GetProperty()->SetColor(m_xzPlaneColor); - m_xzActor->GetProperty()->SetLineWidth(1.0); - m_xzActor->GetProperty()->SetOpacity(0.5); - - // Setup XY plane actor (at Z=0) - m_xyActor->SetMapper(m_xyMapper); - m_xyActor->GetProperty()->SetColor(m_xyPlaneColor); - m_xyActor->GetProperty()->SetLineWidth(1.0); - m_xyActor->GetProperty()->SetOpacity(0.5); - - // Initialize state tree with all rendering attributes - // Use batch scope to prevent callbacks from firing until all values are set - if (!statePath.empty()) { - cvc::state_change_batch_scope batch(*this); - - getState("visible").value(1); // Visible by default - - // YZ Plane (organized under yz_plane. hierarchy) - getState("yz_plane.visible").value(1); - getState("yz_plane.color_r").value(0.5); - getState("yz_plane.color_g").value(0.5); - getState("yz_plane.color_b").value(0.5); - getState("yz_plane.line_width").value(1.0); - getState("yz_plane.opacity").value(0.5); - - // XZ Plane (organized under xz_plane. hierarchy) - getState("xz_plane.visible").value(1); - getState("xz_plane.color_r").value(0.5); - getState("xz_plane.color_g").value(0.5); - getState("xz_plane.color_b").value(0.5); - getState("xz_plane.line_width").value(1.0); - getState("xz_plane.opacity").value(0.5); - - // XY Plane (organized under xy_plane. hierarchy) - getState("xy_plane.visible").value(1); - getState("xy_plane.color_r").value(0.5); - getState("xy_plane.color_g").value(0.5); - getState("xy_plane.color_b").value(0.5); - getState("xy_plane.line_width").value(1.0); - getState("xy_plane.opacity").value(0.5); - - // Grid divisions - getState("divisions_x").value(64); - getState("divisions_y").value(64); - getState("divisions_z").value(64); - - // Tick properties (organized under tics. hierarchy) - getState("tics.interval_x").value(8); - getState("tics.interval_y").value(8); - getState("tics.interval_z").value(8); - - getState("tics.label_color_r").value(1.0); - getState("tics.label_color_g").value(1.0); - getState("tics.label_color_b").value(1.0); - getState("tics.label_font_size").value(12); - - // Tick visibility (hidden by default) - getState("tics.visible").value(0); - } // batch ends here, callbacks fire with all values initialized - - createGridPlanes(); -} - -GridNode::~GridNode() {} - -void GridNode::applyTransformToVTK() { - // Use generic helper to apply world transform to all three grid actors - applyWorldTransformToProps({m_yzActor, m_xzActor, m_xyActor}); -} - -void GridNode::applyClipPlanes(vtkPlaneCollection *planes) { - // Apply clip planes to all three grid mappers - if (planes && planes->GetNumberOfItems() > 0) { - if (m_yzMapper) - m_yzMapper->SetClippingPlanes(planes); - if (m_xzMapper) - m_xzMapper->SetClippingPlanes(planes); - if (m_xyMapper) - m_xyMapper->SetClippingPlanes(planes); - } else { - if (m_yzMapper) - m_yzMapper->RemoveAllClippingPlanes(); - if (m_xzMapper) - m_xzMapper->RemoveAllClippingPlanes(); - if (m_xyMapper) - m_xyMapper->RemoveAllClippingPlanes(); - } -} - -vtkProp *GridNode::getProp() { - // Return first actor for compatibility with base class - return m_yzActor; -} - -void GridNode::addToRenderer(vtkRenderer *renderer) { - if (renderer && isVisible()) { - m_renderer = renderer; // Store renderer reference - if (m_yzPlaneVisible) { - renderer->AddActor(m_yzActor); - for (auto &actor : m_yzTickLabelActors) { - renderer->AddViewProp(actor); - } - } - if (m_xzPlaneVisible) { - renderer->AddActor(m_xzActor); - for (auto &actor : m_xzTickLabelActors) { - renderer->AddViewProp(actor); - } - } - if (m_xyPlaneVisible) { - renderer->AddActor(m_xyActor); - for (auto &actor : m_xyTickLabelActors) { - renderer->AddViewProp(actor); - } - } - } -} - -void GridNode::removeFromRenderer(vtkRenderer *renderer) { - if (renderer) { - renderer->RemoveActor(m_yzActor); - renderer->RemoveActor(m_xzActor); - renderer->RemoveActor(m_xyActor); - - for (auto &actor : m_yzTickLabelActors) { - renderer->RemoveViewProp(actor); - } - for (auto &actor : m_xzTickLabelActors) { - renderer->RemoveViewProp(actor); - } - for (auto &actor : m_xyTickLabelActors) { - renderer->RemoveViewProp(actor); - } - - m_renderer = nullptr; // Clear renderer reference - } -} - -void GridNode::setBounds(const cvc::bounding_box &bounds) { - std::cout << "[DEBUG] GridNode::setBounds called with bounds: [" << bounds[0] << "," << bounds[1] - << "," << bounds[2] << "] to [" << bounds[3] << "," << bounds[4] << "," << bounds[5] - << "]" << std::endl; - m_bounds = bounds; - createGridPlanes(); - updateTickLabelsInRenderer(); -} - -void GridNode::setColor(double r, double g, double b) { - setYZPlaneColor(r, g, b); - setXZPlaneColor(r, g, b); - setXYPlaneColor(r, g, b); -} - -cvc::bounding_box GridNode::getBoundingBox() const { - // Grid doesn't contribute to scene bounds - it's just a visualization - // helper - return cvc::bounding_box(0, 0, 0, 0, 0, 0); -} - -void GridNode::handleStateChanged(const std::string &childState) { - // Synchronize rendering attributes from state tree - // All VTK operations MUST be wrapped in runOnMainThread() for thread safety - if (childState == "yz_plane.visible") { - runOnMainThread([this]() { - m_yzPlaneVisible = getState("yz_plane.visible").value(); - m_yzActor->SetVisibility(m_yzPlaneVisible); - for (auto &actor : m_yzTickLabelActors) { - actor->SetVisibility(m_yzPlaneVisible); - } - }); - } else if (childState == "xz_plane.visible") { - runOnMainThread([this]() { - m_xzPlaneVisible = getState("xz_plane.visible").value(); - m_xzActor->SetVisibility(m_xzPlaneVisible); - for (auto &actor : m_xzTickLabelActors) { - actor->SetVisibility(m_xzPlaneVisible); - } - }); - } else if (childState == "xy_plane.visible") { - runOnMainThread([this]() { - m_xyPlaneVisible = getState("xy_plane.visible").value(); - m_xyActor->SetVisibility(m_xyPlaneVisible); - for (auto &actor : m_xyTickLabelActors) { - actor->SetVisibility(m_xyPlaneVisible); - } - }); - } else if (childState == "yz_plane.color_r" || childState == "yz_plane.color_g" || - childState == "yz_plane.color_b") { - runOnMainThread([this]() { - // Only update if all color components can be read - try { - m_yzPlaneColor[0] = getState("yz_plane.color_r").value(); - m_yzPlaneColor[1] = getState("yz_plane.color_g").value(); - m_yzPlaneColor[2] = getState("yz_plane.color_b").value(); - m_yzActor->GetProperty()->SetColor(m_yzPlaneColor); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "yz_plane.line_width") { - runOnMainThread([this]() { - double lineWidth = getState("yz_plane.line_width").value(); - m_yzActor->GetProperty()->SetLineWidth(lineWidth); - }); - } else if (childState == "yz_plane.opacity") { - runOnMainThread([this]() { - double opacity = getState("yz_plane.opacity").value(); - m_yzActor->GetProperty()->SetOpacity(opacity); - }); - } else if (childState == "xz_plane.color_r" || childState == "xz_plane.color_g" || - childState == "xz_plane.color_b") { - runOnMainThread([this]() { - try { - m_xzPlaneColor[0] = getState("xz_plane.color_r").value(); - m_xzPlaneColor[1] = getState("xz_plane.color_g").value(); - m_xzPlaneColor[2] = getState("xz_plane.color_b").value(); - m_xzActor->GetProperty()->SetColor(m_xzPlaneColor); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "xz_plane.line_width") { - runOnMainThread([this]() { - double lineWidth = getState("xz_plane.line_width").value(); - m_xzActor->GetProperty()->SetLineWidth(lineWidth); - }); - } else if (childState == "xz_plane.opacity") { - runOnMainThread([this]() { - double opacity = getState("xz_plane.opacity").value(); - m_xzActor->GetProperty()->SetOpacity(opacity); - }); - } else if (childState == "xy_plane.color_r" || childState == "xy_plane.color_g" || - childState == "xy_plane.color_b") { - runOnMainThread([this]() { - try { - m_xyPlaneColor[0] = getState("xy_plane.color_r").value(); - m_xyPlaneColor[1] = getState("xy_plane.color_g").value(); - m_xyPlaneColor[2] = getState("xy_plane.color_b").value(); - m_xyActor->GetProperty()->SetColor(m_xyPlaneColor); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "xy_plane.line_width") { - runOnMainThread([this]() { - double lineWidth = getState("xy_plane.line_width").value(); - m_xyActor->GetProperty()->SetLineWidth(lineWidth); - }); - } else if (childState == "xy_plane.opacity") { - runOnMainThread([this]() { - double opacity = getState("xy_plane.opacity").value(); - m_xyActor->GetProperty()->SetOpacity(opacity); - }); - } else if (childState == "divisions_x" || childState == "divisions_y" || - childState == "divisions_z") { - runOnMainThread([this]() { - try { - m_divisionsX = std::max(1, getState("divisions_x").value()); - m_divisionsY = std::max(1, getState("divisions_y").value()); - m_divisionsZ = std::max(1, getState("divisions_z").value()); - createGridPlanes(); - updateTickLabelsInRenderer(); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "tics.interval_x" || childState == "tics.interval_y" || - childState == "tics.interval_z") { - runOnMainThread([this]() { - try { - m_tickIntervalX = std::max(1, getState("tics.interval_x").value()); - m_tickIntervalY = std::max(1, getState("tics.interval_y").value()); - m_tickIntervalZ = std::max(1, getState("tics.interval_z").value()); - updateTickLabelsInRenderer(); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "tics.label_color_r" || childState == "tics.label_color_g" || - childState == "tics.label_color_b") { - runOnMainThread([this]() { - try { - m_tickLabelColor[0] = getState("tics.label_color_r").value(); - m_tickLabelColor[1] = getState("tics.label_color_g").value(); - m_tickLabelColor[2] = getState("tics.label_color_b").value(); - - // Update all existing labels - auto updateLabels = [&](std::vector> &actors) { - for (auto &actor : actors) { - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(actor->GetMapper()); - if (mapper) { - mapper->GetTextProperty()->SetColor(m_tickLabelColor); - } - } - }; - - updateLabels(m_yzTickLabelActors); - updateLabels(m_xzTickLabelActors); - updateLabels(m_xyTickLabelActors); - } catch (const boost::bad_lexical_cast &) { - // Ignore - values not fully initialized yet - } - }); - } else if (childState == "tics.label_font_size") { - runOnMainThread([this]() { - m_tickLabelFontSize = std::max(1, getState("tics.label_font_size").value()); - - // Update all existing labels - auto updateLabels = [&](std::vector> &actors) { - for (auto &actor : actors) { - vtkTextMapper *mapper = vtkTextMapper::SafeDownCast(actor->GetMapper()); - if (mapper) { - mapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - } - } - }; - - updateLabels(m_yzTickLabelActors); - updateLabels(m_xzTickLabelActors); - updateLabels(m_xyTickLabelActors); - }); - } else if (childState == "tics.visible") { - runOnMainThread([this]() { - // Update tick label visibility in renderer - updateTickLabelsInRenderer(); - }); - } else { - // Delegate to parent for common fields (visible, show_bbox, label, etc.) - GraphicsNode::handleStateChanged(childState); - } -} - -void GridNode::setYZPlaneColor(double r, double g, double b) { - getState("yz_plane.color_r").value(r); - getState("yz_plane.color_g").value(g); - getState("yz_plane.color_b").value(b); -} - -void GridNode::setXZPlaneColor(double r, double g, double b) { - getState("xz_plane.color_r").value(r); - getState("xz_plane.color_g").value(g); - getState("xz_plane.color_b").value(b); -} - -void GridNode::setXYPlaneColor(double r, double g, double b) { - getState("xy_plane.color_r").value(r); - getState("xy_plane.color_g").value(g); - getState("xy_plane.color_b").value(b); -} - -void GridNode::getYZPlaneColor(double &r, double &g, double &b) const { - r = m_yzPlaneColor[0]; - g = m_yzPlaneColor[1]; - b = m_yzPlaneColor[2]; -} - -void GridNode::getXZPlaneColor(double &r, double &g, double &b) const { - r = m_xzPlaneColor[0]; - g = m_xzPlaneColor[1]; - b = m_xzPlaneColor[2]; -} - -void GridNode::getXYPlaneColor(double &r, double &g, double &b) const { - r = m_xyPlaneColor[0]; - g = m_xyPlaneColor[1]; - b = m_xyPlaneColor[2]; -} - -void GridNode::setYZPlaneVisible(bool visible) { - getState("yz_plane.visible").value(visible ? 1 : 0); -} - -void GridNode::setXZPlaneVisible(bool visible) { - getState("xz_plane.visible").value(visible ? 1 : 0); -} - -void GridNode::setXYPlaneVisible(bool visible) { - getState("xy_plane.visible").value(visible ? 1 : 0); -} - -void GridNode::setGridDivisions(int x, int y, int z) { - getState("divisions_x").value(std::max(1, x)); - getState("divisions_y").value(std::max(1, y)); - getState("divisions_z").value(std::max(1, z)); -} - -void GridNode::getGridDivisions(int &x, int &y, int &z) const { - x = m_divisionsX; - y = m_divisionsY; - z = m_divisionsZ; -} - -void GridNode::setTickIntervals(int x, int y, int z) { - getState("tics.interval_x").value(std::max(1, x)); - getState("tics.interval_y").value(std::max(1, y)); - getState("tics.interval_z").value(std::max(1, z)); -} - -void GridNode::getTickIntervals(int &x, int &y, int &z) const { - x = m_tickIntervalX; - y = m_tickIntervalY; - z = m_tickIntervalZ; -} - -void GridNode::setTickLabelColor(double r, double g, double b) { - getState("tics.label_color_r").value(r); - getState("tics.label_color_g").value(g); - getState("tics.label_color_b").value(b); -} - -void GridNode::getTickLabelColor(double &r, double &g, double &b) const { - r = m_tickLabelColor[0]; - g = m_tickLabelColor[1]; - b = m_tickLabelColor[2]; -} - -void GridNode::setTickLabelFontSize(int size) { - getState("tics.label_font_size").value(std::max(1, size)); -} - -int GridNode::getTickLabelFontSize() const { return m_tickLabelFontSize; } - -void GridNode::createGridPlanes() { - createYZPlane(); - createXZPlane(); - createXYPlane(); -} - -void GridNode::createYZPlane() { - // Create grid at X=minX (YZ plane at minimum corner) - vtkSmartPointer points = vtkSmartPointer::New(); - vtkSmartPointer lines = vtkSmartPointer::New(); - - double minX = m_bounds[0]; - double minY = m_bounds[1]; - double minZ = m_bounds[2]; - double maxY = m_bounds[4]; - double maxZ = m_bounds[5]; - - double spanY = maxY - minY; - double spanZ = maxZ - minZ; - - double spacingY = spanY / m_divisionsY; - double spacingZ = spanZ / m_divisionsZ; - - if (spacingY > 0.0 && spacingZ > 0.0) { - // Vertical lines (along Z axis) - for (int i = 0; i <= m_divisionsY; ++i) { - double y = minY + i * spacingY; - vtkIdType id1 = points->InsertNextPoint(minX, y, minZ); - vtkIdType id2 = points->InsertNextPoint(minX, y, maxZ); - lines->InsertNextCell(2); - lines->InsertCellPoint(id1); - lines->InsertCellPoint(id2); - } - - // Horizontal lines (along Y axis) - for (int i = 0; i <= m_divisionsZ; ++i) { - double z = minZ + i * spacingZ; - vtkIdType id1 = points->InsertNextPoint(minX, minY, z); - vtkIdType id2 = points->InsertNextPoint(minX, maxY, z); - lines->InsertNextCell(2); - lines->InsertCellPoint(id1); - lines->InsertCellPoint(id2); - } - } - - vtkSmartPointer polyData = vtkSmartPointer::New(); - polyData->SetPoints(points); - polyData->SetLines(lines); - m_yzMapper->SetInputData(polyData); -} - -void GridNode::createXZPlane() { - // Create grid at Y=minY (XZ plane at minimum corner) - vtkSmartPointer points = vtkSmartPointer::New(); - vtkSmartPointer lines = vtkSmartPointer::New(); - - double minX = m_bounds[0]; - double minY = m_bounds[1]; - double minZ = m_bounds[2]; - double maxX = m_bounds[3]; - double maxZ = m_bounds[5]; - - double spanX = maxX - minX; - double spanZ = maxZ - minZ; - - double spacingX = spanX / m_divisionsX; - double spacingZ = spanZ / m_divisionsZ; - - if (spacingX > 0.0 && spacingZ > 0.0) { - // Lines along Z axis - for (int i = 0; i <= m_divisionsX; ++i) { - double x = minX + i * spacingX; - vtkIdType id1 = points->InsertNextPoint(x, minY, minZ); - vtkIdType id2 = points->InsertNextPoint(x, minY, maxZ); - lines->InsertNextCell(2); - lines->InsertCellPoint(id1); - lines->InsertCellPoint(id2); - } - - // Lines along X axis - for (int i = 0; i <= m_divisionsZ; ++i) { - double z = minZ + i * spacingZ; - vtkIdType id1 = points->InsertNextPoint(minX, minY, z); - vtkIdType id2 = points->InsertNextPoint(maxX, minY, z); - lines->InsertNextCell(2); - lines->InsertCellPoint(id1); - lines->InsertCellPoint(id2); - } - } - - vtkSmartPointer polyData = vtkSmartPointer::New(); - polyData->SetPoints(points); - polyData->SetLines(lines); - m_xzMapper->SetInputData(polyData); -} - -void GridNode::createXYPlane() { - // Create grid at Z=minZ (XY plane at minimum corner) - vtkSmartPointer points = vtkSmartPointer::New(); - vtkSmartPointer lines = vtkSmartPointer::New(); - - double minX = m_bounds[0]; - double minY = m_bounds[1]; - double minZ = m_bounds[2]; - double maxX = m_bounds[3]; - double maxY = m_bounds[4]; - - double spanX = maxX - minX; - double spanY = maxY - minY; - - double spacingX = spanX / m_divisionsX; - double spacingY = spanY / m_divisionsY; - - if (spacingX > 0.0 && spacingY > 0.0) { - // Lines along Y axis - for (int i = 0; i <= m_divisionsX; ++i) { - double x = minX + i * spacingX; - vtkIdType id1 = points->InsertNextPoint(x, minY, minZ); - vtkIdType id2 = points->InsertNextPoint(x, maxY, minZ); - lines->InsertNextCell(2); - lines->InsertCellPoint(id1); - lines->InsertCellPoint(id2); - } - - // Lines along X axis - for (int i = 0; i <= m_divisionsY; ++i) { - double y = minY + i * spacingY; - vtkIdType id1 = points->InsertNextPoint(minX, y, minZ); - vtkIdType id2 = points->InsertNextPoint(maxX, y, minZ); - lines->InsertNextCell(2); - lines->InsertCellPoint(id1); - lines->InsertCellPoint(id2); - } - } - - vtkSmartPointer polyData = vtkSmartPointer::New(); - polyData->SetPoints(points); - polyData->SetLines(lines); - m_xyMapper->SetInputData(polyData); -} - -void GridNode::createTickLabels() { - createYZTickLabels(); - createXZTickLabels(); - createXYTickLabels(); -} - -void GridNode::createYZTickLabels() { - // Clear existing labels - m_yzTickLabelActors.clear(); - - if (!m_yzPlaneVisible || m_tickIntervalY <= 0 || m_tickIntervalZ <= 0) - return; - - double minY = m_bounds[1]; - double minZ = m_bounds[2]; - double maxY = m_bounds[4]; - double maxZ = m_bounds[5]; - - double spanY = maxY - minY; - double spanZ = maxZ - minZ; - - double spacingY = spanY / m_divisionsY; - double spacingZ = spanZ / m_divisionsZ; - - if (spacingY <= 0.0 || spacingZ <= 0.0) - return; - - double minX = m_bounds[0]; - - // Create labels only along bottom edge (minZ) at Y intervals - for (int j = 0; j <= m_divisionsY; j += m_tickIntervalY) { - double y = minY + j * spacingY; - double z = minZ; // Bottom edge only - - std::ostringstream oss; - oss << j; - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(oss.str().c_str()); - textMapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_tickLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(minX, y, z); - - textActor->SetVisibility(m_yzPlaneVisible); - m_yzTickLabelActors.push_back(textActor); - } - - // Create labels only along left edge (minY) at Z intervals (skip corner) - for (int k = m_tickIntervalZ; k <= m_divisionsZ; k += m_tickIntervalZ) { - double y = minY; // Left edge only - double z = minZ + k * spacingZ; - - std::ostringstream oss; - oss << k; - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(oss.str().c_str()); - textMapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_tickLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(minX, y, z); - - textActor->SetVisibility(m_yzPlaneVisible); - m_yzTickLabelActors.push_back(textActor); - } -} - -void GridNode::createXZTickLabels() { - // Clear existing labels - m_xzTickLabelActors.clear(); - - if (!m_xzPlaneVisible || m_tickIntervalX <= 0 || m_tickIntervalZ <= 0) - return; - - double minX = m_bounds[0]; - double minZ = m_bounds[2]; - double maxX = m_bounds[3]; - double maxZ = m_bounds[5]; - - double spanX = maxX - minX; - double spanZ = maxZ - minZ; - - double spacingX = spanX / m_divisionsX; - double spacingZ = spanZ / m_divisionsZ; - - if (spacingX <= 0.0 || spacingZ <= 0.0) - return; - - double minY = m_bounds[1]; - - // Create labels only along bottom edge (minZ) at X intervals - for (int i = 0; i <= m_divisionsX; i += m_tickIntervalX) { - double x = minX + i * spacingX; - double z = minZ; // Bottom edge only - - std::ostringstream oss; - oss << i; - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(oss.str().c_str()); - textMapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_tickLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(x, minY, z); - - textActor->SetVisibility(m_xzPlaneVisible); - m_xzTickLabelActors.push_back(textActor); - } - - // Create labels only along left edge (minX) at Z intervals (skip corner) - for (int k = m_tickIntervalZ; k <= m_divisionsZ; k += m_tickIntervalZ) { - double x = minX; // Left edge only - double z = minZ + k * spacingZ; - - std::ostringstream oss; - oss << k; - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(oss.str().c_str()); - textMapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_tickLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(x, minY, z); - - textActor->SetVisibility(m_xzPlaneVisible); - m_xzTickLabelActors.push_back(textActor); - } -} - -void GridNode::createXYTickLabels() { - // Clear existing labels - m_xyTickLabelActors.clear(); - - if (!m_xyPlaneVisible || m_tickIntervalX <= 0 || m_tickIntervalY <= 0) - return; - - double minX = m_bounds[0]; - double minY = m_bounds[1]; - double maxX = m_bounds[3]; - double maxY = m_bounds[4]; - - double spanX = maxX - minX; - double spanY = maxY - minY; - - double spacingX = spanX / m_divisionsX; - double spacingY = spanY / m_divisionsY; - - if (spacingX <= 0.0 || spacingY <= 0.0) - return; - - double minZ = m_bounds[2]; - - // Create labels only along bottom edge (minY) at X intervals - for (int i = 0; i <= m_divisionsX; i += m_tickIntervalX) { - double x = minX + i * spacingX; - double y = minY; // Bottom edge only - - std::ostringstream oss; - oss << i; - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(oss.str().c_str()); - textMapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_tickLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(x, y, minZ); - - textActor->SetVisibility(m_xyPlaneVisible); - m_xyTickLabelActors.push_back(textActor); - } - - // Create labels only along left edge (minX) at Y intervals (skip corner) - for (int j = m_tickIntervalY; j <= m_divisionsY; j += m_tickIntervalY) { - double x = minX; // Left edge only - double y = minY + j * spacingY; - - std::ostringstream oss; - oss << j; - - vtkSmartPointer textMapper = vtkSmartPointer::New(); - textMapper->SetInput(oss.str().c_str()); - textMapper->GetTextProperty()->SetFontSize(m_tickLabelFontSize); - textMapper->GetTextProperty()->SetColor(m_tickLabelColor); - textMapper->GetTextProperty()->SetJustificationToCentered(); - textMapper->GetTextProperty()->SetVerticalJustificationToCentered(); - - vtkSmartPointer textActor = vtkSmartPointer::New(); - textActor->SetMapper(textMapper); - - textActor->GetPositionCoordinate()->SetCoordinateSystemToWorld(); - textActor->GetPositionCoordinate()->SetValue(x, y, minZ); - - textActor->SetVisibility(m_xyPlaneVisible); - m_xyTickLabelActors.push_back(textActor); - } -} - -void GridNode::updateTickLabelsInRenderer() { - // Remove old tick labels from renderer if present - if (m_renderer) { - for (auto &actor : m_yzTickLabelActors) { - m_renderer->RemoveViewProp(actor); - } - for (auto &actor : m_xzTickLabelActors) { - m_renderer->RemoveViewProp(actor); - } - for (auto &actor : m_xyTickLabelActors) { - m_renderer->RemoveViewProp(actor); - } - } - - // Create new tick labels - createTickLabels(); - - // Add new tick labels to renderer if present and ticks are visible - bool ticksVisible = getState("tics.visible").value(); - if (m_renderer && isVisible() && ticksVisible) { - if (m_yzPlaneVisible) { - for (auto &actor : m_yzTickLabelActors) { - m_renderer->AddViewProp(actor); - } - } - if (m_xzPlaneVisible) { - for (auto &actor : m_xzTickLabelActors) { - m_renderer->AddViewProp(actor); - } - } - if (m_xyPlaneVisible) { - for (auto &actor : m_xyTickLabelActors) { - m_renderer->AddViewProp(actor); - } - } - } -} diff --git a/src/volrover3/GridOptionsDialog.cpp b/src/volrover3/GridOptionsDialog.cpp deleted file mode 100644 index a93620d9..00000000 --- a/src/volrover3/GridOptionsDialog.cpp +++ /dev/null @@ -1,596 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -GridOptionsDialog::GridOptionsDialog(std::shared_ptr gridNode, QWidget *parent) - : QWidget(parent), m_gridNode(gridNode), m_yzPlaneCheckBox(nullptr), m_xzPlaneCheckBox(nullptr), - m_xyPlaneCheckBox(nullptr), m_xDivisionsSpinBox(nullptr), m_yDivisionsSpinBox(nullptr), - m_zDivisionsSpinBox(nullptr), m_xTickIntervalSpinBox(nullptr), - m_yTickIntervalSpinBox(nullptr), m_zTickIntervalSpinBox(nullptr), - m_yzPlaneColorButton(nullptr), m_xzPlaneColorButton(nullptr), m_xyPlaneColorButton(nullptr), - m_tickLabelColorButton(nullptr), m_tickLabelFontSizeSpinBox(nullptr), - m_yzLineWidthSpinBox(nullptr), m_xzLineWidthSpinBox(nullptr), m_xyLineWidthSpinBox(nullptr), - m_yzOpacitySlider(nullptr), m_xzOpacitySlider(nullptr), m_xyOpacitySlider(nullptr), - m_updatingFromState(false) { - setWindowTitle(tr("Grid Options")); - setMinimumWidth(400); - setAttribute(Qt::WA_DeleteOnClose); - setupUI(); - connectSignals(); - connectStateMonitoring(); - loadFromState(); -} - -GridOptionsDialog::~GridOptionsDialog() { disconnectStateMonitoring(); } - -void GridOptionsDialog::showEvent(QShowEvent *event) { - QWidget::showEvent(event); - // Reload state from GridNode when dialog is shown to ensure sync - loadFromState(); -} - -void GridOptionsDialog::closeEvent(QCloseEvent *event) { - disconnectStateMonitoring(); - QWidget::closeEvent(event); -} - -void GridOptionsDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Create tab widget - QTabWidget *tabWidget = new QTabWidget(this); - - // === Visibility Tab === - QWidget *visibilityTab = new QWidget(); - QVBoxLayout *visibilityLayout = new QVBoxLayout(visibilityTab); - - QGroupBox *visibilityGroup = new QGroupBox(tr("Grid Plane Visibility"), visibilityTab); - QVBoxLayout *visLayout = new QVBoxLayout(visibilityGroup); - - m_yzPlaneCheckBox = new QCheckBox(tr("YZ Plane (X = 0)"), visibilityTab); - m_xzPlaneCheckBox = new QCheckBox(tr("XZ Plane (Y = 0)"), visibilityTab); - m_xyPlaneCheckBox = new QCheckBox(tr("XY Plane (Z = 0)"), visibilityTab); - - visLayout->addWidget(m_yzPlaneCheckBox); - visLayout->addWidget(m_xzPlaneCheckBox); - visLayout->addWidget(m_xyPlaneCheckBox); - - visibilityLayout->addWidget(visibilityGroup); - visibilityLayout->addStretch(); - - // === Divisions Tab === - QWidget *divisionsTab = new QWidget(); - QVBoxLayout *divisionsLayout = new QVBoxLayout(divisionsTab); - - QGroupBox *divisionsGroup = new QGroupBox(tr("Grid Divisions"), divisionsTab); - QVBoxLayout *divLayout = new QVBoxLayout(divisionsGroup); - - QHBoxLayout *xDivLayout = new QHBoxLayout(); - xDivLayout->addWidget(new QLabel(tr("X Divisions:"), divisionsTab)); - m_xDivisionsSpinBox = new QSpinBox(divisionsTab); - m_xDivisionsSpinBox->setRange(1, 512); - m_xDivisionsSpinBox->setValue(64); - xDivLayout->addWidget(m_xDivisionsSpinBox); - xDivLayout->addStretch(); - divLayout->addLayout(xDivLayout); - - QHBoxLayout *yDivLayout = new QHBoxLayout(); - yDivLayout->addWidget(new QLabel(tr("Y Divisions:"), divisionsTab)); - m_yDivisionsSpinBox = new QSpinBox(divisionsTab); - m_yDivisionsSpinBox->setRange(1, 512); - m_yDivisionsSpinBox->setValue(64); - yDivLayout->addWidget(m_yDivisionsSpinBox); - yDivLayout->addStretch(); - divLayout->addLayout(yDivLayout); - - QHBoxLayout *zDivLayout = new QHBoxLayout(); - zDivLayout->addWidget(new QLabel(tr("Z Divisions:"), divisionsTab)); - m_zDivisionsSpinBox = new QSpinBox(divisionsTab); - m_zDivisionsSpinBox->setRange(1, 512); - m_zDivisionsSpinBox->setValue(64); - zDivLayout->addWidget(m_zDivisionsSpinBox); - zDivLayout->addStretch(); - divLayout->addLayout(zDivLayout); - - divisionsLayout->addWidget(divisionsGroup); - divisionsLayout->addStretch(); - - // === Ticks Tab === - QWidget *ticksTab = new QWidget(); - QVBoxLayout *ticksLayout = new QVBoxLayout(ticksTab); - - QGroupBox *tickGroup = new QGroupBox(tr("Tick Intervals"), ticksTab); - QVBoxLayout *tickLayout = new QVBoxLayout(tickGroup); - - // Show ticks checkbox - QHBoxLayout *showTicksLayout = new QHBoxLayout(); - showTicksLayout->addWidget(new QLabel(tr("Show Ticks:"), ticksTab)); - m_showTicksCheckBox = new QCheckBox(ticksTab); - showTicksLayout->addWidget(m_showTicksCheckBox); - showTicksLayout->addStretch(); - tickLayout->addLayout(showTicksLayout); - - QHBoxLayout *xTickLayout = new QHBoxLayout(); - xTickLayout->addWidget(new QLabel(tr("X Tick Interval:"), ticksTab)); - m_xTickIntervalSpinBox = new QSpinBox(ticksTab); - m_xTickIntervalSpinBox->setRange(1, 256); - m_xTickIntervalSpinBox->setValue(8); - xTickLayout->addWidget(m_xTickIntervalSpinBox); - xTickLayout->addStretch(); - tickLayout->addLayout(xTickLayout); - - QHBoxLayout *yTickLayout = new QHBoxLayout(); - yTickLayout->addWidget(new QLabel(tr("Y Tick Interval:"), ticksTab)); - m_yTickIntervalSpinBox = new QSpinBox(ticksTab); - m_yTickIntervalSpinBox->setRange(1, 256); - m_yTickIntervalSpinBox->setValue(8); - yTickLayout->addWidget(m_yTickIntervalSpinBox); - yTickLayout->addStretch(); - tickLayout->addLayout(yTickLayout); - - QHBoxLayout *zTickLayout = new QHBoxLayout(); - zTickLayout->addWidget(new QLabel(tr("Z Tick Interval:"), ticksTab)); - m_zTickIntervalSpinBox = new QSpinBox(ticksTab); - m_zTickIntervalSpinBox->setRange(1, 256); - m_zTickIntervalSpinBox->setValue(8); - zTickLayout->addWidget(m_zTickIntervalSpinBox); - zTickLayout->addStretch(); - tickLayout->addLayout(zTickLayout); - - ticksLayout->addWidget(tickGroup); - - // Tick Label Properties - QGroupBox *labelGroup = new QGroupBox(tr("Tick Label Properties"), ticksTab); - QVBoxLayout *labelLayout = new QVBoxLayout(labelGroup); - - QHBoxLayout *labelColorLayout = new QHBoxLayout(); - labelColorLayout->addWidget(new QLabel(tr("Label Color:"), ticksTab)); - m_tickLabelColorButton = new QPushButton(tr("Choose..."), ticksTab); - m_tickLabelColorButton->setMinimumWidth(100); - labelColorLayout->addWidget(m_tickLabelColorButton); - labelColorLayout->addStretch(); - labelLayout->addLayout(labelColorLayout); - - QHBoxLayout *fontSizeLayout = new QHBoxLayout(); - fontSizeLayout->addWidget(new QLabel(tr("Font Size:"), ticksTab)); - m_tickLabelFontSizeSpinBox = new QSpinBox(ticksTab); - m_tickLabelFontSizeSpinBox->setRange(6, 72); - m_tickLabelFontSizeSpinBox->setValue(12); - fontSizeLayout->addWidget(m_tickLabelFontSizeSpinBox); - fontSizeLayout->addStretch(); - labelLayout->addLayout(fontSizeLayout); - - ticksLayout->addWidget(labelGroup); - ticksLayout->addStretch(); - - // === Plane Appearance Tab === - QWidget *appearanceTab = new QWidget(); - QVBoxLayout *appearanceLayout = new QVBoxLayout(appearanceTab); - - QGroupBox *colorsGroup = new QGroupBox(tr("Plane Appearance"), appearanceTab); - QVBoxLayout *colorsLayout = new QVBoxLayout(colorsGroup); - - // YZ Plane - colorsLayout->addWidget(new QLabel(tr("YZ Plane (X = 0)"), appearanceTab)); - QHBoxLayout *yzColorLayout = new QHBoxLayout(); - yzColorLayout->addWidget(new QLabel(tr("Color:"), appearanceTab)); - m_yzPlaneColorButton = new QPushButton(tr("Choose..."), appearanceTab); - m_yzPlaneColorButton->setMinimumWidth(100); - yzColorLayout->addWidget(m_yzPlaneColorButton); - yzColorLayout->addStretch(); - colorsLayout->addLayout(yzColorLayout); - - QHBoxLayout *yzLineWidthLayout = new QHBoxLayout(); - yzLineWidthLayout->addWidget(new QLabel(tr("Line Width:"), appearanceTab)); - m_yzLineWidthSpinBox = new QDoubleSpinBox(appearanceTab); - m_yzLineWidthSpinBox->setRange(0.1, 10.0); - m_yzLineWidthSpinBox->setSingleStep(0.1); - m_yzLineWidthSpinBox->setValue(1.0); - m_yzLineWidthSpinBox->setMinimumWidth(100); - yzLineWidthLayout->addWidget(m_yzLineWidthSpinBox); - yzLineWidthLayout->addStretch(); - colorsLayout->addLayout(yzLineWidthLayout); - - QHBoxLayout *yzOpacityLayout = new QHBoxLayout(); - yzOpacityLayout->addWidget(new QLabel(tr("Opacity:"), appearanceTab)); - m_yzOpacitySlider = new QSlider(Qt::Horizontal, appearanceTab); - m_yzOpacitySlider->setRange(0, 100); - m_yzOpacitySlider->setValue(50); - m_yzOpacitySlider->setMinimumWidth(100); - yzOpacityLayout->addWidget(m_yzOpacitySlider); - yzOpacityLayout->addStretch(); - colorsLayout->addLayout(yzOpacityLayout); - - // XZ Plane - colorsLayout->addWidget(new QLabel(tr("XZ Plane (Y = 0)"), appearanceTab)); - QHBoxLayout *xzColorLayout = new QHBoxLayout(); - xzColorLayout->addWidget(new QLabel(tr("Color:"), appearanceTab)); - m_xzPlaneColorButton = new QPushButton(tr("Choose..."), appearanceTab); - m_xzPlaneColorButton->setMinimumWidth(100); - xzColorLayout->addWidget(m_xzPlaneColorButton); - xzColorLayout->addStretch(); - colorsLayout->addLayout(xzColorLayout); - - QHBoxLayout *xzLineWidthLayout = new QHBoxLayout(); - xzLineWidthLayout->addWidget(new QLabel(tr("Line Width:"), appearanceTab)); - m_xzLineWidthSpinBox = new QDoubleSpinBox(appearanceTab); - m_xzLineWidthSpinBox->setRange(0.1, 10.0); - m_xzLineWidthSpinBox->setSingleStep(0.1); - m_xzLineWidthSpinBox->setValue(1.0); - m_xzLineWidthSpinBox->setMinimumWidth(100); - xzLineWidthLayout->addWidget(m_xzLineWidthSpinBox); - xzLineWidthLayout->addStretch(); - colorsLayout->addLayout(xzLineWidthLayout); - - QHBoxLayout *xzOpacityLayout = new QHBoxLayout(); - xzOpacityLayout->addWidget(new QLabel(tr("Opacity:"), appearanceTab)); - m_xzOpacitySlider = new QSlider(Qt::Horizontal, appearanceTab); - m_xzOpacitySlider->setRange(0, 100); - m_xzOpacitySlider->setValue(50); - m_xzOpacitySlider->setMinimumWidth(100); - xzOpacityLayout->addWidget(m_xzOpacitySlider); - xzOpacityLayout->addStretch(); - colorsLayout->addLayout(xzOpacityLayout); - - // XY Plane - colorsLayout->addWidget(new QLabel(tr("XY Plane (Z = 0)"), appearanceTab)); - QHBoxLayout *xyColorLayout = new QHBoxLayout(); - xyColorLayout->addWidget(new QLabel(tr("Color:"), appearanceTab)); - m_xyPlaneColorButton = new QPushButton(tr("Choose..."), appearanceTab); - m_xyPlaneColorButton->setMinimumWidth(100); - xyColorLayout->addWidget(m_xyPlaneColorButton); - xyColorLayout->addStretch(); - colorsLayout->addLayout(xyColorLayout); - - QHBoxLayout *xyLineWidthLayout = new QHBoxLayout(); - xyLineWidthLayout->addWidget(new QLabel(tr("Line Width:"), appearanceTab)); - m_xyLineWidthSpinBox = new QDoubleSpinBox(appearanceTab); - m_xyLineWidthSpinBox->setRange(0.1, 10.0); - m_xyLineWidthSpinBox->setSingleStep(0.1); - m_xyLineWidthSpinBox->setValue(1.0); - m_xyLineWidthSpinBox->setMinimumWidth(100); - xyLineWidthLayout->addWidget(m_xyLineWidthSpinBox); - xyLineWidthLayout->addStretch(); - colorsLayout->addLayout(xyLineWidthLayout); - - QHBoxLayout *xyOpacityLayout = new QHBoxLayout(); - xyOpacityLayout->addWidget(new QLabel(tr("Opacity:"), appearanceTab)); - m_xyOpacitySlider = new QSlider(Qt::Horizontal, appearanceTab); - m_xyOpacitySlider->setRange(0, 100); - m_xyOpacitySlider->setValue(50); - m_xyOpacitySlider->setMinimumWidth(100); - xyOpacityLayout->addWidget(m_xyOpacitySlider); - xyOpacityLayout->addStretch(); - colorsLayout->addLayout(xyOpacityLayout); - - appearanceLayout->addWidget(colorsGroup); - appearanceLayout->addStretch(); - - // Add tabs to tab widget - tabWidget->addTab(visibilityTab, tr("Visibility")); - tabWidget->addTab(divisionsTab, tr("Divisions")); - tabWidget->addTab(ticksTab, tr("Ticks")); - tabWidget->addTab(appearanceTab, tr("Appearance")); - - mainLayout->addWidget(tabWidget); - - setLayout(mainLayout); -} - -void GridOptionsDialog::connectSignals() { - // Apply changes immediately when checkboxes change - connect(m_yzPlaneCheckBox, &QCheckBox::toggled, this, &GridOptionsDialog::applyChanges); - connect(m_xzPlaneCheckBox, &QCheckBox::toggled, this, &GridOptionsDialog::applyChanges); - connect(m_xyPlaneCheckBox, &QCheckBox::toggled, this, &GridOptionsDialog::applyChanges); - connect(m_showTicksCheckBox, &QCheckBox::toggled, this, &GridOptionsDialog::applyChanges); - - // Apply changes when spin boxes change - connect(m_xDivisionsSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_yDivisionsSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_zDivisionsSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - - connect(m_xTickIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_yTickIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_zTickIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - - connect(m_tickLabelFontSizeSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - - // Line width and opacity spin boxes - connect(m_yzLineWidthSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_xzLineWidthSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_xyLineWidthSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &GridOptionsDialog::applyChanges); - connect(m_yzOpacitySlider, &QSlider::valueChanged, this, &GridOptionsDialog::applyChanges); - connect(m_xzOpacitySlider, &QSlider::valueChanged, this, &GridOptionsDialog::applyChanges); - connect(m_xyOpacitySlider, &QSlider::valueChanged, this, &GridOptionsDialog::applyChanges); - - // Color picker buttons - connect(m_yzPlaneColorButton, &QPushButton::clicked, this, - &GridOptionsDialog::chooseYZPlaneColor); - connect(m_xzPlaneColorButton, &QPushButton::clicked, this, - &GridOptionsDialog::chooseXZPlaneColor); - connect(m_xyPlaneColorButton, &QPushButton::clicked, this, - &GridOptionsDialog::chooseXYPlaneColor); - connect(m_tickLabelColorButton, &QPushButton::clicked, this, - &GridOptionsDialog::chooseTickLabelColor); -} - -void GridOptionsDialog::loadFromState() { - if (!m_gridNode) - return; - - m_updatingFromState = true; - - // Block signals while loading to avoid triggering apply - m_yzPlaneCheckBox->blockSignals(true); - m_xzPlaneCheckBox->blockSignals(true); - m_xyPlaneCheckBox->blockSignals(true); - m_showTicksCheckBox->blockSignals(true); - m_xDivisionsSpinBox->blockSignals(true); - m_yDivisionsSpinBox->blockSignals(true); - m_zDivisionsSpinBox->blockSignals(true); - m_xTickIntervalSpinBox->blockSignals(true); - m_yTickIntervalSpinBox->blockSignals(true); - m_zTickIntervalSpinBox->blockSignals(true); - m_tickLabelFontSizeSpinBox->blockSignals(true); - m_yzLineWidthSpinBox->blockSignals(true); - m_xzLineWidthSpinBox->blockSignals(true); - m_xyLineWidthSpinBox->blockSignals(true); - m_yzOpacitySlider->blockSignals(true); - m_xzOpacitySlider->blockSignals(true); - m_xyOpacitySlider->blockSignals(true); - - // Load visibility from GridNode state - m_yzPlaneCheckBox->setChecked(m_gridNode->getState("yz_plane.visible").value()); - m_xzPlaneCheckBox->setChecked(m_gridNode->getState("xz_plane.visible").value()); - m_xyPlaneCheckBox->setChecked(m_gridNode->getState("xy_plane.visible").value()); - - // Load divisions from GridNode state - int x = m_gridNode->getState("divisions_x").value(); - int y = m_gridNode->getState("divisions_y").value(); - int z = m_gridNode->getState("divisions_z").value(); - m_xDivisionsSpinBox->setValue(x); - m_yDivisionsSpinBox->setValue(y); - m_zDivisionsSpinBox->setValue(z); - - // Load tick intervals from GridNode state - m_showTicksCheckBox->setChecked(m_gridNode->getState("tics.visible").value()); - x = m_gridNode->getState("tics.interval_x").value(); - y = m_gridNode->getState("tics.interval_y").value(); - z = m_gridNode->getState("tics.interval_z").value(); - m_xTickIntervalSpinBox->setValue(x); - m_yTickIntervalSpinBox->setValue(y); - m_zTickIntervalSpinBox->setValue(z); - - // Load colors from GridNode state - m_yzPlaneColor[0] = m_gridNode->getState("yz_plane.color_r").value(); - m_yzPlaneColor[1] = m_gridNode->getState("yz_plane.color_g").value(); - m_yzPlaneColor[2] = m_gridNode->getState("yz_plane.color_b").value(); - - m_xzPlaneColor[0] = m_gridNode->getState("xz_plane.color_r").value(); - m_xzPlaneColor[1] = m_gridNode->getState("xz_plane.color_g").value(); - m_xzPlaneColor[2] = m_gridNode->getState("xz_plane.color_b").value(); - - m_xyPlaneColor[0] = m_gridNode->getState("xy_plane.color_r").value(); - m_xyPlaneColor[1] = m_gridNode->getState("xy_plane.color_g").value(); - m_xyPlaneColor[2] = m_gridNode->getState("xy_plane.color_b").value(); - - m_tickLabelColor[0] = m_gridNode->getState("tics.label_color_r").value(); - m_tickLabelColor[1] = m_gridNode->getState("tics.label_color_g").value(); - m_tickLabelColor[2] = m_gridNode->getState("tics.label_color_b").value(); - - updateColorButton(m_yzPlaneColorButton, m_yzPlaneColor[0], m_yzPlaneColor[1], m_yzPlaneColor[2]); - updateColorButton(m_xzPlaneColorButton, m_xzPlaneColor[0], m_xzPlaneColor[1], m_xzPlaneColor[2]); - updateColorButton(m_xyPlaneColorButton, m_xyPlaneColor[0], m_xyPlaneColor[1], m_xyPlaneColor[2]); - updateColorButton(m_tickLabelColorButton, m_tickLabelColor[0], m_tickLabelColor[1], - m_tickLabelColor[2]); - - // Load line width from GridNode state - m_yzLineWidthSpinBox->setValue(m_gridNode->getState("yz_plane.line_width").value()); - m_xzLineWidthSpinBox->setValue(m_gridNode->getState("xz_plane.line_width").value()); - m_xyLineWidthSpinBox->setValue(m_gridNode->getState("xy_plane.line_width").value()); - - // Load opacity from GridNode state - m_yzOpacitySlider->setValue( - static_cast(m_gridNode->getState("yz_plane.opacity").value() * 100)); - m_xzOpacitySlider->setValue( - static_cast(m_gridNode->getState("xz_plane.opacity").value() * 100)); - m_xyOpacitySlider->setValue( - static_cast(m_gridNode->getState("xy_plane.opacity").value() * 100)); - - // Load font size from GridNode state - m_tickLabelFontSizeSpinBox->setValue(m_gridNode->getState("tics.label_font_size").value()); - - // Unblock signals - m_yzPlaneCheckBox->blockSignals(false); - m_xzPlaneCheckBox->blockSignals(false); - m_xyPlaneCheckBox->blockSignals(false); - m_showTicksCheckBox->blockSignals(false); - m_xDivisionsSpinBox->blockSignals(false); - m_yDivisionsSpinBox->blockSignals(false); - m_zDivisionsSpinBox->blockSignals(false); - m_xTickIntervalSpinBox->blockSignals(false); - m_yTickIntervalSpinBox->blockSignals(false); - m_zTickIntervalSpinBox->blockSignals(false); - m_tickLabelFontSizeSpinBox->blockSignals(false); - m_yzLineWidthSpinBox->blockSignals(false); - m_xzLineWidthSpinBox->blockSignals(false); - m_xyLineWidthSpinBox->blockSignals(false); - m_yzOpacitySlider->blockSignals(false); - m_xzOpacitySlider->blockSignals(false); - m_xyOpacitySlider->blockSignals(false); - - m_updatingFromState = false; -} - -void GridOptionsDialog::applyChanges() { - if (!m_gridNode || m_updatingFromState) - return; - - // Apply visibility changes to GridNode state - m_gridNode->getState("yz_plane.visible").value(m_yzPlaneCheckBox->isChecked()); - m_gridNode->getState("xz_plane.visible").value(m_xzPlaneCheckBox->isChecked()); - m_gridNode->getState("xy_plane.visible").value(m_xyPlaneCheckBox->isChecked()); - - // Apply division changes to GridNode state - m_gridNode->getState("divisions_x").value(m_xDivisionsSpinBox->value()); - m_gridNode->getState("divisions_y").value(m_yDivisionsSpinBox->value()); - m_gridNode->getState("divisions_z").value(m_zDivisionsSpinBox->value()); - - // Apply tick interval changes to GridNode state - m_gridNode->getState("tics.visible").value(m_showTicksCheckBox->isChecked()); - m_gridNode->getState("tics.interval_x").value(m_xTickIntervalSpinBox->value()); - m_gridNode->getState("tics.interval_y").value(m_yTickIntervalSpinBox->value()); - m_gridNode->getState("tics.interval_z").value(m_zTickIntervalSpinBox->value()); - - // Apply color changes to GridNode state - m_gridNode->getState("yz_plane.color_r").value(m_yzPlaneColor[0]); - m_gridNode->getState("yz_plane.color_g").value(m_yzPlaneColor[1]); - m_gridNode->getState("yz_plane.color_b").value(m_yzPlaneColor[2]); - - m_gridNode->getState("xz_plane.color_r").value(m_xzPlaneColor[0]); - m_gridNode->getState("xz_plane.color_g").value(m_xzPlaneColor[1]); - m_gridNode->getState("xz_plane.color_b").value(m_xzPlaneColor[2]); - - m_gridNode->getState("xy_plane.color_r").value(m_xyPlaneColor[0]); - m_gridNode->getState("xy_plane.color_g").value(m_xyPlaneColor[1]); - m_gridNode->getState("xy_plane.color_b").value(m_xyPlaneColor[2]); - - m_gridNode->getState("tics.label_color_r").value(m_tickLabelColor[0]); - m_gridNode->getState("tics.label_color_g").value(m_tickLabelColor[1]); - m_gridNode->getState("tics.label_color_b").value(m_tickLabelColor[2]); - - // Apply line width to GridNode state - m_gridNode->getState("yz_plane.line_width").value(m_yzLineWidthSpinBox->value()); - m_gridNode->getState("xz_plane.line_width").value(m_xzLineWidthSpinBox->value()); - m_gridNode->getState("xy_plane.line_width").value(m_xyLineWidthSpinBox->value()); - - // Apply opacity to GridNode state - m_gridNode->getState("yz_plane.opacity").value(m_yzOpacitySlider->value() / 100.0); - m_gridNode->getState("xz_plane.opacity").value(m_xzOpacitySlider->value() / 100.0); - m_gridNode->getState("xy_plane.opacity").value(m_xyOpacitySlider->value() / 100.0); - - // Apply font size to GridNode state - m_gridNode->getState("tics.label_font_size").value(m_tickLabelFontSizeSpinBox->value()); -} - -void GridOptionsDialog::chooseYZPlaneColor() { - QColor current(static_cast(m_yzPlaneColor[0] * 255), - static_cast(m_yzPlaneColor[1] * 255), - static_cast(m_yzPlaneColor[2] * 255)); - QColor color = QColorDialog::getColor(current, this, tr("Choose YZ Plane Color")); - if (color.isValid()) { - m_yzPlaneColor[0] = color.redF(); - m_yzPlaneColor[1] = color.greenF(); - m_yzPlaneColor[2] = color.blueF(); - updateColorButton(m_yzPlaneColorButton, m_yzPlaneColor[0], m_yzPlaneColor[1], - m_yzPlaneColor[2]); - applyChanges(); - } -} - -void GridOptionsDialog::chooseXZPlaneColor() { - QColor current(static_cast(m_xzPlaneColor[0] * 255), - static_cast(m_xzPlaneColor[1] * 255), - static_cast(m_xzPlaneColor[2] * 255)); - QColor color = QColorDialog::getColor(current, this, tr("Choose XZ Plane Color")); - if (color.isValid()) { - m_xzPlaneColor[0] = color.redF(); - m_xzPlaneColor[1] = color.greenF(); - m_xzPlaneColor[2] = color.blueF(); - updateColorButton(m_xzPlaneColorButton, m_xzPlaneColor[0], m_xzPlaneColor[1], - m_xzPlaneColor[2]); - applyChanges(); - } -} - -void GridOptionsDialog::chooseXYPlaneColor() { - QColor current(static_cast(m_xyPlaneColor[0] * 255), - static_cast(m_xyPlaneColor[1] * 255), - static_cast(m_xyPlaneColor[2] * 255)); - QColor color = QColorDialog::getColor(current, this, tr("Choose XY Plane Color")); - if (color.isValid()) { - m_xyPlaneColor[0] = color.redF(); - m_xyPlaneColor[1] = color.greenF(); - m_xyPlaneColor[2] = color.blueF(); - updateColorButton(m_xyPlaneColorButton, m_xyPlaneColor[0], m_xyPlaneColor[1], - m_xyPlaneColor[2]); - applyChanges(); - } -} - -void GridOptionsDialog::chooseTickLabelColor() { - QColor current(static_cast(m_tickLabelColor[0] * 255), - static_cast(m_tickLabelColor[1] * 255), - static_cast(m_tickLabelColor[2] * 255)); - QColor color = QColorDialog::getColor(current, this, tr("Choose Tick Label Color")); - if (color.isValid()) { - m_tickLabelColor[0] = color.redF(); - m_tickLabelColor[1] = color.greenF(); - m_tickLabelColor[2] = color.blueF(); - updateColorButton(m_tickLabelColorButton, m_tickLabelColor[0], m_tickLabelColor[1], - m_tickLabelColor[2]); - applyChanges(); - } -} - -void GridOptionsDialog::updateColorButton(QPushButton *button, double r, double g, double b) { - int red = static_cast(r * 255); - int green = static_cast(g * 255); - int blue = static_cast(b * 255); - - QString styleSheet = - QString("QPushButton { background-color: rgb(%1, %2, %3); }").arg(red).arg(green).arg(blue); - button->setStyleSheet(styleSheet); -} - -void GridOptionsDialog::connectStateMonitoring() { - if (!m_gridNode) - return; - - // Monitor all state changes that affect the UI - std::vector statePaths = { - "yz_plane.visible", "yz_plane.color_r", "yz_plane.color_g", "yz_plane.color_b", - "yz_plane.line_width", "yz_plane.opacity", "xz_plane.visible", "xz_plane.color_r", - "xz_plane.color_g", "xz_plane.color_b", "xz_plane.line_width", "xz_plane.opacity", - "xy_plane.visible", "xy_plane.color_r", "xy_plane.color_g", "xy_plane.color_b", - "xy_plane.line_width", "xy_plane.opacity", "divisions_x", "divisions_y", - "divisions_z", "tics.visible", "tics.interval_x", "tics.interval_y", - "tics.interval_z", "tics.label_color_r", "tics.label_color_g", "tics.label_color_b", - "tics.label_font_size"}; - - for (const auto &path : statePaths) { - auto connection = - m_gridNode->getState(path).valueChanged.connect([this]() { onStateChanged(); }); - m_stateConnections.push_back(connection); - } -} - -void GridOptionsDialog::disconnectStateMonitoring() { m_stateConnections.clear(); } - -void GridOptionsDialog::onStateChanged() { - // Reload UI from state when external changes occur - loadFromState(); -} diff --git a/src/volrover3/IsosurfaceDialog.cpp b/src/volrover3/IsosurfaceDialog.cpp deleted file mode 100644 index 51b3cf59..00000000 --- a/src/volrover3/IsosurfaceDialog.cpp +++ /dev/null @@ -1,392 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -IsosurfaceDialog::IsosurfaceDialog(std::shared_ptr sceneGraph, QWidget *parent) - : QDialog(parent), m_sceneGraph(sceneGraph), m_volumeComboBox(nullptr), - m_isovalueSpinBox(nullptr), m_methodComboBox(nullptr), m_improveIterationsSpinBox(nullptr), - m_normalTypeComboBox(nullptr), m_computeButton(nullptr), m_cancelButton(nullptr), - m_progressBar(nullptr), m_statusLabel(nullptr), m_computing(false) { - setWindowTitle(tr("Isosurface Extraction")); - setMinimumWidth(400); - setupUI(); - connectSignals(); - populateVolumeList(); - - // Connect to SceneGraph signal to monitor for new volumes - if (m_sceneGraph) { - m_graphicsChangedConnection = m_sceneGraph->graphicsChanged.connect([this]() { - QMetaObject::invokeMethod(this, "onGraphicsChildrenChanged", Qt::QueuedConnection); - }); - } -} - -void IsosurfaceDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Volume Selection Group - QGroupBox *volumeGroup = new QGroupBox(tr("Input Volume"), this); - QFormLayout *volumeLayout = new QFormLayout(volumeGroup); - - m_volumeComboBox = new QComboBox(this); - volumeLayout->addRow(tr("Volume:"), m_volumeComboBox); - - mainLayout->addWidget(volumeGroup); - - // Isosurface Parameters Group - QGroupBox *paramGroup = new QGroupBox(tr("Extraction Parameters"), this); - QFormLayout *paramLayout = new QFormLayout(paramGroup); - - m_isovalueSpinBox = new QDoubleSpinBox(this); - m_isovalueSpinBox->setRange(-1e10, 1e10); - m_isovalueSpinBox->setDecimals(6); - m_isovalueSpinBox->setValue(0.0); - paramLayout->addRow(tr("Isovalue:"), m_isovalueSpinBox); - - m_methodComboBox = new QComboBox(this); - m_methodComboBox->addItem(tr("DualLib (Default)"), static_cast(cvc::DUALLIB)); - m_methodComboBox->addItem(tr("Fast Contouring"), static_cast(cvc::FASTCONTOURING)); - m_methodComboBox->addItem(tr("Lib IsoContour"), static_cast(cvc::LIBISOCONTOUR)); - paramLayout->addRow(tr("Method:"), m_methodComboBox); - - m_improveIterationsSpinBox = new QSpinBox(this); - m_improveIterationsSpinBox->setRange(0, 100); - m_improveIterationsSpinBox->setValue(0); - m_improveIterationsSpinBox->setToolTip( - tr("Number of mesh improvement iterations (0 = no improvement)")); - paramLayout->addRow(tr("Improve Iterations:"), m_improveIterationsSpinBox); - - m_normalTypeComboBox = new QComboBox(this); - m_normalTypeComboBox->addItem(tr("B-Spline Convolution"), - static_cast(cvc::BSPLINE_CONVOLUTION)); - m_normalTypeComboBox->addItem(tr("Central Difference"), - static_cast(cvc::CENTRAL_DIFFERENCE)); - m_normalTypeComboBox->addItem(tr("B-Spline Interpolation"), - static_cast(cvc::BSPLINE_INTERPOLATION)); - paramLayout->addRow(tr("Normal Type:"), m_normalTypeComboBox); - - mainLayout->addWidget(paramGroup); - - // Progress Group - QGroupBox *progressGroup = new QGroupBox(tr("Progress"), this); - QVBoxLayout *progressLayout = new QVBoxLayout(progressGroup); - - m_statusLabel = new QLabel(tr("Ready"), this); - progressLayout->addWidget(m_statusLabel); - - m_progressBar = new QProgressBar(this); - m_progressBar->setRange(0, 100); - m_progressBar->setValue(0); - progressLayout->addWidget(m_progressBar); - - mainLayout->addWidget(progressGroup); - - // Button box - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - - m_computeButton = new QPushButton(tr("Extract Isosurface"), this); - m_computeButton->setDefault(true); - buttonLayout->addWidget(m_computeButton); - - m_cancelButton = new QPushButton(tr("Close"), this); - buttonLayout->addWidget(m_cancelButton); - - mainLayout->addLayout(buttonLayout); - - setLayout(mainLayout); -} - -void IsosurfaceDialog::connectSignals() { - connect(m_volumeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &IsosurfaceDialog::onVolumeSelected); - connect(m_computeButton, &QPushButton::clicked, this, &IsosurfaceDialog::onComputeClicked); - connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); -} - -void IsosurfaceDialog::populateVolumeList() { - m_volumeComboBox->clear(); - m_volumePaths.clear(); - - if (!m_sceneGraph) - return; - - // Get all volume nodes recursively - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - - for (const auto &volumeNode : allVolumes) { - if (volumeNode) { - // Use full state tree path for uniqueness - std::string fullPath = volumeNode->getState().fullName(); - m_volumePaths.push_back(fullPath); - - // Display the node name in the combo box - m_volumeComboBox->addItem(QString::fromStdString(volumeNode->getName())); - } - } - - if (m_volumeComboBox->count() == 0) { - m_computeButton->setEnabled(false); - m_statusLabel->setText(tr("No volumes available")); - } else { - onVolumeSelected(0); - } -} - -void IsosurfaceDialog::onGraphicsChildrenChanged() { - if (!m_sceneGraph) - return; - - // Save current selection (by path) - QString currentSelection; - int currentIndex = m_volumeComboBox->currentIndex(); - if (currentIndex >= 0 && currentIndex < static_cast(m_volumePaths.size())) { - currentSelection = QString::fromStdString(m_volumePaths[currentIndex]); - } - - // Refresh the list - populateVolumeList(); - - // Try to restore the previous selection by matching path - bool selectionRestored = false; - if (!currentSelection.isEmpty()) { - for (int i = 0; i < static_cast(m_volumePaths.size()); ++i) { - if (QString::fromStdString(m_volumePaths[i]) == currentSelection) { - m_volumeComboBox->setCurrentIndex(i); - selectionRestored = true; - break; - } - } - } - - // Update UI state based on volume availability - if (m_volumeComboBox->count() > 0 && !m_computing) { - m_computeButton->setEnabled(true); - if (m_statusLabel->text() == tr("No volumes available")) { - m_statusLabel->setText(tr("Ready")); - } - } else if (m_volumeComboBox->count() == 0) { - m_computeButton->setEnabled(false); - if (!m_computing) { - m_statusLabel->setText(tr("No volumes available")); - } - } -} - -void IsosurfaceDialog::onVolumeSelected(int index) { - if (index < 0 || index >= static_cast(m_volumePaths.size())) - return; - - // Could update isovalue based on volume's data range - // For now, just ensure it's a valid selection - const std::string &volumePath = m_volumePaths[index]; - - // Get the volume node to access its data range - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - for (const auto &volumeNode : allVolumes) { - if (volumeNode && volumeNode->getState().fullName() == volumePath) { - // Try to get data range from metadata - auto minVal = volumeNode->getMetadata("data_min"); - auto maxVal = volumeNode->getMetadata("data_max"); - - if (minVal.has_value() && maxVal.has_value()) { - try { - double dataMin = 0.0, dataMax = 1.0; - - if (minVal.type() == typeid(double)) { - dataMin = std::any_cast(minVal); - dataMax = std::any_cast(maxVal); - } else { - dataMin = std::stod(std::any_cast(minVal)); - dataMax = std::stod(std::any_cast(maxVal)); - } - - // Set isovalue to middle of range - m_isovalueSpinBox->setValue((dataMin + dataMax) / 2.0); - m_isovalueSpinBox->setRange(dataMin - (dataMax - dataMin), dataMax + (dataMax - dataMin)); - } catch (...) { - // Ignore conversion errors, keep default range - } - } - break; - } - } -} - -void IsosurfaceDialog::onComputeClicked() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - if (m_computing) { - // Cancel ongoing computation - if (!m_activeThreadKey.empty()) { - volrover3::app().threads(m_activeThreadKey)->interrupt(); - } - setControlsEnabled(true); - m_statusLabel->setText(tr("Cancelled")); - m_progressBar->setValue(0); - m_computing = false; - m_computeButton->setText(tr("Extract Isosurface")); - return; - } - - int currentIndex = m_volumeComboBox->currentIndex(); - if (currentIndex < 0 || currentIndex >= static_cast(m_volumePaths.size())) { - QMessageBox::warning(this, tr("No Volume"), - tr("Please select a volume to extract isosurface.")); - return; - } - - const std::string &volumePath = m_volumePaths[currentIndex]; - - // Find the volume node - std::shared_ptr volumeNode; - std::string volumeName; - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - for (const auto &volNode : allVolumes) { - if (volNode && volNode->getState().fullName() == volumePath) { - volumeNode = volNode; - volumeName = volNode->getName(); - break; - } - } - - if (!volumeNode) { - QMessageBox::critical(this, tr("Error"), tr("Failed to get volume node.")); - return; - } - - // Get the volume data - const cvc::volume *volPtr = volumeNode->getVolume(); - if (!volPtr) { - QMessageBox::critical(this, tr("Error"), tr("Volume has no data loaded.")); - return; - } - - // Get parameters - double isovalue = m_isovalueSpinBox->value(); - cvc::extraction_method method = - static_cast(m_methodComboBox->currentData().toInt()); - int improveIterations = m_improveIterationsSpinBox->value(); - cvc::normal_type normalType = - static_cast(m_normalTypeComboBox->currentData().toInt()); - - // Copy volume for thread safety - cvc::volume vol = *volPtr; - - // Update UI - setControlsEnabled(false); - m_computing = true; - m_computeButton->setText(tr("Cancel")); - m_statusLabel->setText(tr("Extracting isosurface...")); - m_progressBar->setValue(0); - - // Create unique thread key - m_activeThreadKey = "iso_extraction_" + volumeName; - - // Start computation in background thread - volrover3::app().startThread( - m_activeThreadKey, - [this, vol, isovalue, method, improveIterations, normalType, volumeName, volumeNode]() { - cvc::thread_info ti(volrover3::app(), "Isosurface Extraction"); - - try { - // Extract isosurface (thread-safe) - cvc::geometry isoGeom = cvc::iso(vol, isovalue, method, improveIterations, normalType); - - // Extraction complete, now adding to scene - QMetaObject::invokeMethod( - this, [this]() { m_statusLabel->setText(tr("Adding isosurface to scene...")); }, - Qt::QueuedConnection); - - // Post all SceneGraph/VTK operations to main thread via SceneGraph event queue - m_sceneGraph->postEvent([this, isoGeom, volumeName, isovalue, volumeNode]() { - cvc::thread_info ti(volrover3::app(), "Add Isosurface"); - - try { - // Sanitize the name to ensure it's a valid C identifier - std::string rawName = volumeName + "_iso_" + std::to_string(isovalue); - std::string isoName = cvc::state::sanitizeStateName(rawName); - - // Check if isosurface already exists - auto existingNode = m_sceneGraph->getGraphics(isoName); - if (existingNode) { - // Remove existing isosurface - m_sceneGraph->removeGraphics(isoName); - } - - // Add isosurface as child of volume using the template createChild method - auto isoNode = volumeNode->createChild(isoName, isoGeom); - - if (!isoNode) { - throw std::runtime_error("Failed to create isosurface node"); - } - - // Update UI on Qt thread - QMetaObject::invokeMethod( - this, [this]() { onComputeFinished(true, "Isosurface extracted successfully"); }, - Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Failed to create geometry node: ") + e.what(); - QMetaObject::invokeMethod( - this, [this, errorMsg]() { onComputeFinished(false, errorMsg); }, - Qt::QueuedConnection); - } - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { onComputeFinished(false, "Extraction cancelled"); }, - Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error: ") + e.what(); - QMetaObject::invokeMethod( - this, [this, errorMsg]() { onComputeFinished(false, errorMsg); }, - Qt::QueuedConnection); - } - }, - false // Don't wait for existing thread - ); -} - -void IsosurfaceDialog::updateProgress(int value) { m_progressBar->setValue(value); } - -void IsosurfaceDialog::onComputeFinished(bool success, const std::string &message) { - setControlsEnabled(true); - m_computing = false; - m_computeButton->setText(tr("Extract Isosurface")); - m_progressBar->setValue(success ? 100 : 0); - m_statusLabel->setText(QString::fromStdString(message)); - - if (success) { - QMessageBox::information(this, tr("Success"), QString::fromStdString(message)); - } else { - QMessageBox::warning(this, tr("Error"), QString::fromStdString(message)); - } -} - -void IsosurfaceDialog::setControlsEnabled(bool enabled) { - m_volumeComboBox->setEnabled(enabled); - m_isovalueSpinBox->setEnabled(enabled); - m_methodComboBox->setEnabled(enabled); - m_improveIterationsSpinBox->setEnabled(enabled); - m_normalTypeComboBox->setEnabled(enabled); -} diff --git a/src/volrover3/MainWindow.cpp b/src/volrover3/MainWindow.cpp deleted file mode 100644 index fd411cc4..00000000 --- a/src/volrover3/MainWindow.cpp +++ /dev/null @@ -1,1103 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), m_renderWidget(nullptr), m_transferFunctionWidget(nullptr), - m_sceneGraph(nullptr) // Will be created after callback is set - , - m_threadMonitor(nullptr), m_stateTreeWidget(nullptr), m_stateDashboardWidget(nullptr), - m_gridOptionsDialog(nullptr), m_sdfDialog(nullptr), m_isosurfaceDialog(nullptr), - m_geometryDialog(nullptr), m_volumeDialog(nullptr), m_viewerOptionsDialog(nullptr), - m_cameraDialog(nullptr), m_mainToolBar(nullptr), m_threadNameLabel(nullptr), - m_threadInfoLabel(nullptr), m_threadProgressBar(nullptr), m_gridVisible(true), - m_axisVisible(true) { - setWindowTitle("VolRover3 - Volume Rover Version 3"); - resize(1280, 720); - - // Set up thread-safe state change callback for SceneNode hierarchy FIRST - // This MUST be done before creating SceneGraph to avoid VTK calls on background threads - SceneNode::setMainThreadCallback([this](std::function func) { - // Check if we're already on the main thread - if (QThread::currentThread() == this->thread()) { - // We're on the main thread, execute immediately - func(); - } else { - // We're on a worker thread, marshal to main thread - QMetaObject::invokeMethod(this, [func]() { func(); }, Qt::QueuedConnection); - } - }); - - // NOW create the scene graph - state changes will be properly marshaled - m_sceneGraph = std::make_shared(); - - // Create central render widget - m_renderWidget = new VTKRenderWidget(this); - m_renderWidget->setSceneGraph(m_sceneGraph); - setCentralWidget(m_renderWidget); - - createDockWidgets(); - createMenus(); - createToolBar(); - setupStatusBar(); - setupConnections(); - - // GridNode and AxisNode initialize their own visibility state - // Get initial values from their state - m_gridVisible = m_sceneGraph->getGridNode()->isVisible(); - m_axisVisible = true; // AxisNode default - - // Initialize grid with default world bounds - m_sceneGraph->updateGrid(AppState::instance().worldBounds()); - - // Connect to state changes - AppState::instance().onWorldBoundsChanged([this]() { - cvc::bounding_box bounds = AppState::instance().worldBounds(); - - std::cout << "[DEBUG] MainWindow - World bounds changed: [" << bounds[0] << "," << bounds[1] - << "," << bounds[2] << "] to [" << bounds[3] << "," << bounds[4] << "," << bounds[5] - << "]" << std::endl; - - // Always update grid to match new world bounds - m_sceneGraph->updateGrid(bounds); - - // Update camera orbit center to match new bounds center - CameraController *camCtrl = m_renderWidget->getCameraController(); - if (camCtrl) { - cvc::bounding_box bounds = AppState::instance().worldBounds(); - camCtrl->updateOrbitCenterFromBounds(bounds.minx, bounds.miny, bounds.minz, bounds.maxx, - bounds.maxy, bounds.maxz); - } - - m_renderWidget->render(); - }); - - // GridNode and AxisNode handle their own state changes internally via handleStateChanged() - // and updates VTK actors automatically. No MainWindow callbacks needed. - // Grid state is at: volrover3.graphics.root.children.grid.* - - // Initialize camera settings from state tree - initializeCameraFromState(); -} - -MainWindow::~MainWindow() { - // Disconnect all callbacks - for (auto &conn : m_connections) { - conn.disconnect(); - } - m_connections.clear(); -} - -void MainWindow::closeEvent(QCloseEvent *event) { - // Close all child windows when main window is closing - if (m_threadMonitor) { - m_threadMonitor->close(); - } - if (m_stateTreeWidget) { - m_stateTreeWidget->close(); - } - if (m_gridOptionsDialog) { - m_gridOptionsDialog->close(); - } - if (m_sdfDialog) { - m_sdfDialog->close(); - } - if (m_isosurfaceDialog) { - m_isosurfaceDialog->close(); - } - if (m_viewerOptionsDialog) { - m_viewerOptionsDialog->close(); - } - - // Accept the close event - QMainWindow::closeEvent(event); -} - -void MainWindow::createMenus() { - // File menu - QMenu *fileMenu = menuBar()->addMenu(tr("&File")); - - QAction *openFileAction = new QAction(tr("&Open File..."), this); - openFileAction->setShortcut(QKeySequence::Open); - connect(openFileAction, &QAction::triggered, this, &MainWindow::openFile); - fileMenu->addAction(openFileAction); - - fileMenu->addSeparator(); - - QAction *exitAction = new QAction(tr("E&xit"), this); - exitAction->setShortcut(QKeySequence::Quit); - connect(exitAction, &QAction::triggered, this, &QMainWindow::close); - fileMenu->addAction(exitAction); - - // View menu - QMenu *viewMenu = menuBar()->addMenu(tr("&View")); - - QAction *toggleGridAction = new QAction(tr("Show &Grid"), this); - toggleGridAction->setCheckable(true); - toggleGridAction->setChecked(m_gridVisible); - connect(toggleGridAction, &QAction::triggered, this, &MainWindow::toggleGrid); - viewMenu->addAction(toggleGridAction); - - QAction *toggleAxisAction = new QAction(tr("Show &Axis"), this); - toggleAxisAction->setCheckable(true); - toggleAxisAction->setChecked(m_axisVisible); - connect(toggleAxisAction, &QAction::triggered, this, &MainWindow::toggleAxis); - viewMenu->addAction(toggleAxisAction); - - viewMenu->addSeparator(); - - QAction *editBoundsAction = new QAction(tr("Edit &Bounding Box..."), this); - editBoundsAction->setShortcut(tr("Ctrl+B")); - connect(editBoundsAction, &QAction::triggered, this, &MainWindow::editBoundingBox); - viewMenu->addAction(editBoundsAction); - - QAction *editCameraAction = new QAction(tr("&Camera Settings..."), this); - editCameraAction->setShortcut(tr("Ctrl+K")); - connect(editCameraAction, &QAction::triggered, this, &MainWindow::editCameraSettings); - viewMenu->addAction(editCameraAction); - - QAction *gridOptionsAction = new QAction(tr("&Grid Options..."), this); - gridOptionsAction->setShortcut(tr("Ctrl+G")); - connect(gridOptionsAction, &QAction::triggered, this, &MainWindow::showGridOptions); - viewMenu->addAction(gridOptionsAction); - - QAction *viewerOptionsAction = new QAction(tr("&Viewer Options..."), this); - viewerOptionsAction->setShortcut(tr("Ctrl+Shift+V")); - connect(viewerOptionsAction, &QAction::triggered, this, &MainWindow::showViewerOptions); - viewMenu->addAction(viewerOptionsAction); - - viewMenu->addSeparator(); - - QAction *threadMonitorAction = new QAction(tr("&Thread Monitor..."), this); - threadMonitorAction->setShortcut(tr("Ctrl+T")); - connect(threadMonitorAction, &QAction::triggered, this, &MainWindow::showThreadMonitor); - viewMenu->addAction(threadMonitorAction); - - QAction *stateTreeAction = new QAction(tr("&State Tree..."), this); - stateTreeAction->setShortcut(tr("Ctrl+Shift+S")); - connect(stateTreeAction, &QAction::triggered, this, &MainWindow::showStateTree); - viewMenu->addAction(stateTreeAction); - - QAction *stateDashboardAction = new QAction(tr("State &Dashboard..."), this); - stateDashboardAction->setShortcut(tr("Ctrl+Shift+D")); - connect(stateDashboardAction, &QAction::triggered, this, &MainWindow::showStateDashboard); - viewMenu->addAction(stateDashboardAction); - - viewMenu->addSeparator(); - - QAction *geometryAction = new QAction(tr("Geo&metry Properties..."), this); - geometryAction->setShortcut(tr("Ctrl+M")); - connect(geometryAction, &QAction::triggered, this, &MainWindow::showGeometry); - viewMenu->addAction(geometryAction); - - QAction *volumeAction = new QAction(tr("&Volume Properties..."), this); - volumeAction->setShortcut(tr("Ctrl+V")); - connect(volumeAction, &QAction::triggered, this, &MainWindow::showVolume); - viewMenu->addAction(volumeAction); - - // Tools menu - QMenu *toolsMenu = menuBar()->addMenu(tr("&Tools")); - - QAction *sdfAction = new QAction(tr("&Signed Distance Function..."), this); - sdfAction->setShortcut(tr("Ctrl+D")); - connect(sdfAction, &QAction::triggered, this, &MainWindow::showSDF); - toolsMenu->addAction(sdfAction); - - QAction *isoAction = new QAction(tr("&Isosurface Extraction..."), this); - isoAction->setShortcut(tr("Ctrl+I")); - connect(isoAction, &QAction::triggered, this, &MainWindow::showIsosurface); - toolsMenu->addAction(isoAction); - - toolsMenu->addSeparator(); - - // Generate submenu - QMenu *generateMenu = toolsMenu->addMenu(tr("&Generate")); - - // Geometry submenu under Generate - QMenu *generateGeometryMenu = generateMenu->addMenu(tr("&Geometry")); - - QAction *bunnyAction = new QAction(tr("Stanford &Bunny"), this); - bunnyAction->setToolTip(tr("Generate the Stanford Bunny test geometry")); - connect(bunnyAction, &QAction::triggered, this, &MainWindow::generateStanfordBunny); - generateGeometryMenu->addAction(bunnyAction); - - generateGeometryMenu->addSeparator(); - - QAction *sphereAction = new QAction(tr("&Sphere..."), this); - sphereAction->setToolTip(tr("Generate a parametric sphere")); - connect(sphereAction, &QAction::triggered, this, &MainWindow::generateSphere); - generateGeometryMenu->addAction(sphereAction); - - QAction *cubeAction = new QAction(tr("&Cube..."), this); - cubeAction->setToolTip(tr("Generate a parametric cube")); - connect(cubeAction, &QAction::triggered, this, &MainWindow::generateCube); - generateGeometryMenu->addAction(cubeAction); - - QAction *torusAction = new QAction(tr("&Torus..."), this); - torusAction->setToolTip(tr("Generate a parametric torus")); - connect(torusAction, &QAction::triggered, this, &MainWindow::generateTorus); - generateGeometryMenu->addAction(torusAction); - - QAction *coneAction = new QAction(tr("C&one..."), this); - coneAction->setToolTip(tr("Generate a parametric cone")); - connect(coneAction, &QAction::triggered, this, &MainWindow::generateCone); - generateGeometryMenu->addAction(coneAction); - - // Help menu - QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); - - QAction *aboutAction = new QAction(tr("&About VolRover3"), this); - connect(aboutAction, &QAction::triggered, this, &MainWindow::aboutVolRover); - helpMenu->addAction(aboutAction); -} - -void MainWindow::createToolBar() { - m_mainToolBar = addToolBar(tr("Main Toolbar")); - m_mainToolBar->setObjectName("MainToolBar"); - m_mainToolBar->setMovable(true); - - // Reset Camera button - QAction *resetCameraAction = - new QAction(QIcon::fromTheme("view-refresh"), tr("Reset Camera"), this); - resetCameraAction->setToolTip(tr("Reset camera to view all content")); - resetCameraAction->setShortcut(tr("Ctrl+R")); - connect(resetCameraAction, &QAction::triggered, this, &MainWindow::resetCamera); - m_mainToolBar->addAction(resetCameraAction); - - m_mainToolBar->addSeparator(); - - // Axis toggle - QAction *axisAction = new QAction(QIcon::fromTheme("show-axis"), tr("Toggle Axis"), this); - axisAction->setToolTip(tr("Show/hide coordinate axis")); - axisAction->setCheckable(true); - axisAction->setChecked(m_axisVisible); - connect(axisAction, &QAction::triggered, this, &MainWindow::toggleAxis); - m_mainToolBar->addAction(axisAction); -} - -void MainWindow::createDockWidgets() { - // Transfer function dock widget - QDockWidget *tfDock = new QDockWidget(tr("Transfer Function"), this); - tfDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - - m_transferFunctionWidget = new TransferFunctionWidget(tfDock); - m_transferFunctionWidget->setSceneGraph(m_sceneGraph.get()); - tfDock->setWidget(m_transferFunctionWidget); - - addDockWidget(Qt::RightDockWidgetArea, tfDock); -} - -void MainWindow::setupConnections() { - // Connect transfer function changes to update only the selected volume - connect(m_transferFunctionWidget, &TransferFunctionWidget::transferFunctionChanged, [this]() { - // Apply transfer function to the selected volume only - auto selectedVolume = m_transferFunctionWidget->getSelectedVolume(); - if (selectedVolume) { - selectedVolume->setTransferFunction(m_transferFunctionWidget->getColorTable(), - m_transferFunctionWidget->getOpacityTable()); - m_renderWidget->render(); - } - }); -} - -void MainWindow::openFile() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // First, show parent selection dialog - GraphicsParentDialog parentDialog(m_sceneGraph, this); - if (parentDialog.exec() != QDialog::Accepted) { - return; // User cancelled - } - - std::string parentName = parentDialog.getSelectedParentName(); - auto parentNode = parentDialog.getSelectedParent(); - - // Get supported extensions from I/O classes - std::vector geomExts = cvc::geometry_file_io::get_extensions(); - std::vector volExts = cvc::volume_file_io::getExtensions(); - - // Build filter strings (extensions may or may not have leading dots) - QString geomFilter; - for (const auto &ext : geomExts) { - if (!geomFilter.isEmpty()) - geomFilter += " "; - QString extStr = QString::fromStdString(ext); - // Handle extensions with or without leading dot - if (extStr.startsWith('.')) { - geomFilter += "*" + extStr; - } else { - geomFilter += "*." + extStr; - } - } - - QString volFilter; - for (const auto &ext : volExts) { - if (!volFilter.isEmpty()) - volFilter += " "; - QString extStr = QString::fromStdString(ext); - // Handle extensions with or without leading dot - if (extStr.startsWith('.')) { - volFilter += "*" + extStr; - } else { - volFilter += "*." + extStr; - } - } - - QString allFilter = geomFilter; - if (!geomFilter.isEmpty() && !volFilter.isEmpty()) { - allFilter += " "; - } - allFilter += volFilter; - - QString filters = tr("All Graphics Files (%1);;" - "Geometry Files (%2);;" - "Volume Files (%3);;" - "All Files (*)") - .arg(allFilter) - .arg(geomFilter) - .arg(volFilter); - - // Show file selection dialog with both geometry and volume extensions - QStringList fileNames = - QFileDialog::getOpenFileNames(this, tr("Open Graphics File(s)"), QString(), filters); - - if (fileNames.isEmpty()) - return; - - int geomCount = 0, volCount = 0; - int totalVertices = 0, totalTriangles = 0; - cvc::uint64 totalVoxels = 0; - - for (const QString &fileName : fileNames) { - QFileInfo fileInfo(fileName); - - try { - // Extract filename without path for naming - std::string baseName = fileInfo.baseName().toStdString(); - std::string sanitizedName = cvc::state::sanitizeStateName(baseName); - - // Create unique name - std::string graphicsName = sanitizedName; - int counter = 1; - while (m_sceneGraph->getGraphics(graphicsName)) { - graphicsName = sanitizedName + "_" + std::to_string(counter++); - } - - // Try loading as volume first, then geometry if that fails - bool loadedAsVolume = false; - bool loadedAsGeometry = false; - std::string lastError; - - try { - // Try loading as volume - cvc::volume vol(volrover3::app(), fileName.toStdString()); - auto volumeNode = m_sceneGraph->addGraphics(graphicsName, vol); - volumeNode->setMetadata("type", std::string("volume")); - volumeNode->setMetadata("filename", fileName.toStdString()); - - // Set parent if requested - if (parentNode) { - m_sceneGraph->getGraphicsRoot()->removeGraphicsChild(volumeNode); - parentNode->addGraphicsChild(volumeNode); - } - - totalVoxels += vol.XDim() * vol.YDim() * vol.ZDim(); - volCount++; - loadedAsVolume = true; - } catch (const cvc::unsupported_volume_file_type &e) { - // Not a volume file, try geometry - lastError = e.what(); - try { - cvc::geometry geom = cvc::read_geometry(fileName.toStdString()); - - // Create geometry node using parent's factory method (or root if no parent) - // This automatically creates the correct state path - std::shared_ptr graphicsNode; - if (parentNode) { - graphicsNode = parentNode->addGraphicsChild(graphicsName); - m_sceneGraph->registerGraphics(graphicsName, graphicsNode); - } else { - graphicsNode = - m_sceneGraph->getGraphicsRoot()->addGraphicsChild(graphicsName); - m_sceneGraph->registerGraphics(graphicsName, graphicsNode); - } - - // Set geometry and metadata - graphicsNode->setGeometry(geom); - graphicsNode->setMetadata("type", std::string("geometry")); - graphicsNode->setMetadata("filename", fileName.toStdString()); - graphicsNode->setMetadata("num_vertices", static_cast(geom.num_points())); - graphicsNode->setMetadata("num_triangles", static_cast(geom.num_tris())); - - // Node automatically connected to state tree via state_object constructor - // (no manual sync needed) - - totalVertices += geom.num_points(); - totalTriangles += geom.num_tris(); - geomCount++; - loadedAsGeometry = true; - } catch (const cvc::unsupported_geometry_file_type &) { - // Neither volume nor geometry - throw a clear error - throw std::runtime_error( - "File format not supported by any loader (not a recognized volume or geometry file)"); - } catch (const std::exception &e) { - // Other geometry loading error - throw std::runtime_error(std::string("Failed to load as geometry: ") + e.what()); - } - } catch (const std::exception &e) { - // Other volume loading error - still try geometry - lastError = e.what(); - try { - cvc::geometry geom = cvc::read_geometry(fileName.toStdString()); - - // Create geometry node using parent's factory method (or root if no parent) - // This automatically creates the correct state path - std::shared_ptr graphicsNode; - if (parentNode) { - graphicsNode = parentNode->addGraphicsChild(graphicsName); - m_sceneGraph->registerGraphics(graphicsName, graphicsNode); - } else { - graphicsNode = - m_sceneGraph->getGraphicsRoot()->addGraphicsChild(graphicsName); - m_sceneGraph->registerGraphics(graphicsName, graphicsNode); - } - - // Set geometry and metadata - graphicsNode->setGeometry(geom); - graphicsNode->setMetadata("type", std::string("geometry")); - graphicsNode->setMetadata("filename", fileName.toStdString()); - graphicsNode->setMetadata("num_vertices", static_cast(geom.num_points())); - graphicsNode->setMetadata("num_triangles", static_cast(geom.num_tris())); - - // Node automatically connected to state tree via state_object constructor - // (no manual sync needed) - - totalVertices += geom.num_points(); - totalTriangles += geom.num_tris(); - geomCount++; - loadedAsGeometry = true; - } catch (const cvc::unsupported_geometry_file_type &e) { - // Failed both ways - report the original volume error - throw std::runtime_error(lastError); - } - } - - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error Loading File"), - tr("Failed to load %1:\n%2").arg(fileName).arg(e.what())); - } - } - - // Note: World bounds are automatically updated when root NullGraphicNode - // syncs its bounds to children (happens in GeometryNode::setGeometry/VolumeNode::setVolume) - // Grid will update automatically via onWorldBoundsChanged callback - - // Update render - m_renderWidget->render(); - - // Refresh transfer function widget if volumes were loaded - if (volCount > 0) { - m_transferFunctionWidget->refreshVolumeList(); - } - - // Show status message - if (geomCount > 0 || volCount > 0) { - QString parentMsg = parentName.empty() ? tr("root") : QString::fromStdString(parentName); - QString msg; - if (geomCount > 0 && volCount > 0) { - msg = tr("Loaded %1 geometry file(s) (%2 vertices, %3 triangles) and %4 volume file(s) (%5 " - "voxels) under '%6'") - .arg(geomCount) - .arg(totalVertices) - .arg(totalTriangles) - .arg(volCount) - .arg(totalVoxels) - .arg(parentMsg); - } else if (geomCount > 0) { - msg = tr("Loaded %1 geometry file(s) under '%2': %3 vertices, %4 triangles") - .arg(geomCount) - .arg(parentMsg) - .arg(totalVertices) - .arg(totalTriangles); - } else { - msg = tr("Loaded %1 volume file(s) under '%2': %3 voxels") - .arg(volCount) - .arg(parentMsg) - .arg(totalVoxels); - } - statusBar()->showMessage(msg, 5000); - } -} - -void MainWindow::toggleGrid() { - m_gridVisible = !m_gridVisible; - m_sceneGraph->setGridVisible(m_gridVisible); -} - -void MainWindow::toggleAxis() { - m_axisVisible = !m_axisVisible; - m_sceneGraph->setAxisVisible(m_axisVisible); -} - -void MainWindow::editBoundingBox() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - BoundingBoxDialog dialog(m_sceneGraph, this); - dialog.exec(); -} - -void MainWindow::editCameraSettings() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - CameraController *camCtrl = m_renderWidget->getCameraController(); - if (!camCtrl) - return; - - if (!m_cameraDialog) { - // Get current settings from AppState for initial setup - CameraSettingsDialog::CameraSettings settings; - settings.mode = AppState::instance().cameraMode(); - settings.flySpeed = AppState::instance().cameraSpeed(); - settings.mouseSensitivity = AppState::instance().cameraSensitivity(); - settings.invertMouse = AppState::instance().cameraInvertMouse(); - settings.keyForward = AppState::instance().cameraKeyForward(); - settings.keyBackward = AppState::instance().cameraKeyBackward(); - settings.keyStrafeLeft = AppState::instance().cameraKeyLeft(); - settings.keyStrafeRight = AppState::instance().cameraKeyRight(); - settings.keyUp = AppState::instance().cameraKeyUp(); - settings.keyDown = AppState::instance().cameraKeyDown(); - - // Pass camera state tree for live state display (subscribes to childChanged signal) - cvc::state &cameraState = camCtrl->getState(); - - m_cameraDialog = new CameraSettingsDialog(settings, &cameraState, this); - m_cameraDialog->setAttribute(Qt::WA_DeleteOnClose); - - // Connect destroyed signal to reset pointer - connect(m_cameraDialog, &QObject::destroyed, [this]() { m_cameraDialog = nullptr; }); - - // Connect reset view signal - connect(m_cameraDialog, &CameraSettingsDialog::resetViewRequested, [this, camCtrl]() { - cvc::bounding_box bounds = AppState::instance().worldBounds(); - camCtrl->resetView(bounds.minx, bounds.miny, bounds.minz, bounds.maxx, bounds.maxy, - bounds.maxz); - m_renderWidget->render(); - }); - - // Connect settings changed signal for real-time application - connect(m_cameraDialog, &CameraSettingsDialog::settingsChanged, - [this, camCtrl](const CameraSettingsDialog::CameraSettings &newSettings) { - // Save settings to AppState - AppState::instance().setCameraMode(newSettings.mode); - AppState::instance().setCameraSpeed(newSettings.flySpeed); - AppState::instance().setCameraSensitivity(newSettings.mouseSensitivity); - AppState::instance().setCameraInvertMouse(newSettings.invertMouse); - AppState::instance().setCameraKeyForward(newSettings.keyForward); - AppState::instance().setCameraKeyBackward(newSettings.keyBackward); - AppState::instance().setCameraKeyLeft(newSettings.keyStrafeLeft); - AppState::instance().setCameraKeyRight(newSettings.keyStrafeRight); - AppState::instance().setCameraKeyUp(newSettings.keyUp); - AppState::instance().setCameraKeyDown(newSettings.keyDown); - - // Apply settings to controller - camCtrl->setMode(static_cast(newSettings.mode)); - camCtrl->setMovementSpeed(newSettings.flySpeed); - camCtrl->setMouseSensitivity(newSettings.mouseSensitivity); - camCtrl->setInvertMouse(newSettings.invertMouse); - camCtrl->setKeyBindings(newSettings.keyForward, newSettings.keyBackward, - newSettings.keyStrafeLeft, newSettings.keyStrafeRight, - newSettings.keyUp, newSettings.keyDown); - - // Update orbit center to world bounds center when switching to orbit mode - if (newSettings.mode == 0) { - cvc::bounding_box bounds = AppState::instance().worldBounds(); - double cx = (bounds[0] + bounds[3]) * 0.5; - double cy = (bounds[1] + bounds[4]) * 0.5; - double cz = (bounds[2] + bounds[5]) * 0.5; - camCtrl->setOrbitCenter(cx, cy, cz); - } - - m_renderWidget->render(); - }); - } - - m_cameraDialog->show(); - m_cameraDialog->raise(); - m_cameraDialog->activateWindow(); -} - -void MainWindow::showGridOptions() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - if (!m_gridOptionsDialog) { - m_gridOptionsDialog = new GridOptionsDialog(m_sceneGraph->getGridNode()); - m_gridOptionsDialog->setWindowTitle(tr("Grid Options - VolRover3")); - m_gridOptionsDialog->setAttribute(Qt::WA_DeleteOnClose); - m_gridOptionsDialog->resize(450, 600); - - // Reset pointer when dialog is closed - connect(m_gridOptionsDialog, &QObject::destroyed, [this]() { m_gridOptionsDialog = nullptr; }); - } - - m_gridOptionsDialog->show(); - m_gridOptionsDialog->raise(); - m_gridOptionsDialog->activateWindow(); -} - -void MainWindow::showViewerOptions() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - if (!m_viewerOptionsDialog) { - m_viewerOptionsDialog = new ViewerOptionsDialog(m_renderWidget, m_sceneGraph); - m_viewerOptionsDialog->setWindowTitle(tr("Viewer Options - VolRover3")); - m_viewerOptionsDialog->setAttribute(Qt::WA_DeleteOnClose); - - // Reset pointer when dialog is closed - connect(m_viewerOptionsDialog, &QObject::destroyed, - [this]() { m_viewerOptionsDialog = nullptr; }); - } - - m_viewerOptionsDialog->show(); - m_viewerOptionsDialog->raise(); - m_viewerOptionsDialog->activateWindow(); -} - -void MainWindow::showThreadMonitor() { - // Create thread monitor widget as a separate window if not already created - if (!m_threadMonitor) { - m_threadMonitor = new ThreadMonitorWidget(); - m_threadMonitor->setWindowTitle(tr("Thread Monitor - VolRover3")); - m_threadMonitor->setAttribute(Qt::WA_DeleteOnClose); - - // Clean up pointer when window is closed - connect(m_threadMonitor, &QObject::destroyed, [this]() { m_threadMonitor = nullptr; }); - - // Connect to thread completion signal for status bar updates - connect(m_threadMonitor, &ThreadMonitorWidget::threadCompleted, - [this](const QString &threadName, const QString &threadInfo) { - statusBar()->showMessage( - tr("Thread '%1' completed: %2").arg(threadName).arg(threadInfo), - 10000); // Show for 10 seconds - }); - } - - // Show and raise the window - m_threadMonitor->show(); - m_threadMonitor->raise(); - m_threadMonitor->activateWindow(); -} - -void MainWindow::showStateTree() { - // Create state tree widget as a separate window if not already created - if (!m_stateTreeWidget) { - m_stateTreeWidget = new StateTreeWidget(); - m_stateTreeWidget->setWindowTitle(tr("State Tree - VolRover3")); - m_stateTreeWidget->setAttribute(Qt::WA_DeleteOnClose); - m_stateTreeWidget->resize(600, 500); - - // Set root state to the global state singleton - m_stateTreeWidget->setRootState(&cvc::state::instance(volrover3::app())); - - // Clean up pointer when window is closed - connect(m_stateTreeWidget, &QObject::destroyed, [this]() { m_stateTreeWidget = nullptr; }); - - // Connect state tree refresh to trigger graphics updates - connect(m_stateTreeWidget, &StateTreeWidget::stateChanged, this, [this]() { - // Update all graphics nodes - m_sceneGraph->update(); - // Force immediate render - m_renderWidget->render(); - }); - } - - // Refresh to show current state - m_stateTreeWidget->refresh(); - - // Show and raise the window - m_stateTreeWidget->show(); - m_stateTreeWidget->raise(); - m_stateTreeWidget->activateWindow(); -} - -void MainWindow::showStateDashboard() { - if (!m_stateDashboardWidget) { - m_stateDashboardWidget = new StateDashboardWidget(); - m_stateDashboardWidget->setWindowTitle(tr("State Dashboard - VolRover3")); - m_stateDashboardWidget->setAttribute(Qt::WA_DeleteOnClose); - m_stateDashboardWidget->resize(900, 650); - m_stateDashboardWidget->setRootState(&cvc::state::instance(volrover3::app())); - - connect(m_stateDashboardWidget, &QObject::destroyed, - [this]() { m_stateDashboardWidget = nullptr; }); - connect(m_stateDashboardWidget, &StateDashboardWidget::stateChanged, this, [this]() { - m_sceneGraph->update(); - m_renderWidget->render(); - }); - } - - m_stateDashboardWidget->refresh(); - m_stateDashboardWidget->show(); - m_stateDashboardWidget->raise(); - m_stateDashboardWidget->activateWindow(); -} - -void MainWindow::showSDF() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Create SDF dialog as a separate window if not already created - if (!m_sdfDialog) { - m_sdfDialog = new SDFDialog(m_sceneGraph); - m_sdfDialog->setWindowTitle(tr("Signed Distance Function - VolRover3")); - m_sdfDialog->setAttribute(Qt::WA_DeleteOnClose); - - // Clean up pointer when window is closed - connect(m_sdfDialog, &QObject::destroyed, [this]() { m_sdfDialog = nullptr; }); - } - - // Show and raise the window - m_sdfDialog->show(); - m_sdfDialog->raise(); - m_sdfDialog->activateWindow(); -} - -void MainWindow::showIsosurface() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Create Isosurface dialog as a separate window if not already created - if (!m_isosurfaceDialog) { - m_isosurfaceDialog = new IsosurfaceDialog(m_sceneGraph); - m_isosurfaceDialog->setWindowTitle(tr("Isosurface Extraction - VolRover3")); - m_isosurfaceDialog->setAttribute(Qt::WA_DeleteOnClose); - - // Clean up pointer when window is closed - connect(m_isosurfaceDialog, &QObject::destroyed, [this]() { m_isosurfaceDialog = nullptr; }); - } - - // Show and raise the window - m_isosurfaceDialog->show(); - m_isosurfaceDialog->raise(); - m_isosurfaceDialog->activateWindow(); -} - -void MainWindow::showGeometry() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Create Geometry dialog as a separate window if not already created - if (!m_geometryDialog) { - m_geometryDialog = new GeometryDialog(m_sceneGraph, this); - m_geometryDialog->setWindowTitle(tr("Geometry Properties - VolRover3")); - m_geometryDialog->setAttribute(Qt::WA_DeleteOnClose); - - // Clean up pointer when window is closed - connect(m_geometryDialog, &QObject::destroyed, [this]() { m_geometryDialog = nullptr; }); - } - - // Show and raise the window - m_geometryDialog->show(); - m_geometryDialog->raise(); - m_geometryDialog->activateWindow(); -} - -void MainWindow::showVolume() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Create Volume dialog as a separate window if not already created - if (!m_volumeDialog) { - m_volumeDialog = new VolumeDialog(m_sceneGraph, this); - m_volumeDialog->setWindowTitle(tr("Volume Properties - VolRover3")); - m_volumeDialog->setAttribute(Qt::WA_DeleteOnClose); - - // Clean up pointer when window is closed - connect(m_volumeDialog, &QObject::destroyed, [this]() { m_volumeDialog = nullptr; }); - } - - // Show and raise the window - m_volumeDialog->show(); - m_volumeDialog->raise(); - m_volumeDialog->activateWindow(); -} - -void MainWindow::aboutVolRover() { - QMessageBox::about(this, tr("About VolRover3"), - tr("

VolRover3

" - "

Volume Rover Version 3.0

" - "

A prototype visualization application built on libcvc

" - "

Features:

" - "
    " - "
  • Volume rendering with transfer functions
  • " - "
  • Surface and volumetric mesh visualization
  • " - "
  • Isosurface extraction and rendering
  • " - "
  • Quake-style camera controls
  • " - "
" - "

Copyright © 2025 CVC

")); -} - -void MainWindow::resetCamera() { - CameraController *camCtrl = m_renderWidget->getCameraController(); - if (camCtrl) { - // Use CameraController's resetView to properly update state tree - cvc::bounding_box bounds = AppState::instance().worldBounds(); - camCtrl->resetView(bounds.minx, bounds.miny, bounds.minz, bounds.maxx, bounds.maxy, - bounds.maxz); - m_renderWidget->render(); - } -} - -void MainWindow::generateStanfordBunny() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - try { - // Load the built-in Stanford Bunny using the .bunny extension - // The bunny_io class handles this special extension and returns the embedded mesh - cvc::geometry geom = cvc::read_geometry("stanford.bunny"); - - // Create unique name for the geometry - std::string baseName = "StanfordBunny"; - std::string graphicsName = baseName; - int counter = 1; - while (m_sceneGraph->getGraphics(graphicsName)) { - graphicsName = baseName + "_" + std::to_string(counter++); - } - - // Create geometry node under root - auto graphicsNode = - m_sceneGraph->getGraphicsRoot()->addGraphicsChild(graphicsName); - m_sceneGraph->registerGraphics(graphicsName, graphicsNode); - - // Set geometry and metadata - graphicsNode->setGeometry(geom); - graphicsNode->setMetadata("type", std::string("geometry")); - graphicsNode->setMetadata("filename", std::string("stanford.bunny")); - graphicsNode->setMetadata("num_vertices", static_cast(geom.num_points())); - graphicsNode->setMetadata("num_triangles", static_cast(geom.num_tris())); - graphicsNode->setMetadata("source", std::string("built-in")); - - // Update world bounds and reset camera to show new geometry - AppState::instance().setWorldBounds(geom.extents()); - resetCamera(); - - statusBar()->showMessage(tr("Generated Stanford Bunny: %1 vertices, %2 triangles") - .arg(geom.num_points()) - .arg(geom.num_tris()), - 5000); - - } catch (const std::exception &e) { - QMessageBox::critical(this, tr("Generation Error"), - tr("Failed to generate Stanford Bunny:\n%1").arg(e.what())); - } -} - -void MainWindow::generateSphere() { - ProceduralGeometryDialog dialog(ProceduralGeometryType::Sphere, m_sceneGraph, this); - if (dialog.exec() == QDialog::Accepted) { - resetCamera(); - statusBar()->showMessage(tr("Generated Sphere"), 3000); - } -} - -void MainWindow::generateCube() { - ProceduralGeometryDialog dialog(ProceduralGeometryType::Cube, m_sceneGraph, this); - if (dialog.exec() == QDialog::Accepted) { - resetCamera(); - statusBar()->showMessage(tr("Generated Cube"), 3000); - } -} - -void MainWindow::generateTorus() { - ProceduralGeometryDialog dialog(ProceduralGeometryType::Torus, m_sceneGraph, this); - if (dialog.exec() == QDialog::Accepted) { - resetCamera(); - statusBar()->showMessage(tr("Generated Torus"), 3000); - } -} - -void MainWindow::generateCone() { - ProceduralGeometryDialog dialog(ProceduralGeometryType::Cone, m_sceneGraph, this); - if (dialog.exec() == QDialog::Accepted) { - resetCamera(); - statusBar()->showMessage(tr("Generated Cone"), 3000); - } -} - -void MainWindow::setupStatusBar() { - // Create status bar widgets for thread monitoring - m_threadNameLabel = new QLabel(this); - m_threadNameLabel->setMinimumWidth(150); - m_threadNameLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken); - - m_threadInfoLabel = new QLabel(this); - m_threadInfoLabel->setMinimumWidth(200); - m_threadInfoLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken); - - m_threadProgressBar = new QProgressBar(this); - m_threadProgressBar->setMinimumWidth(150); - m_threadProgressBar->setMaximumWidth(200); - m_threadProgressBar->setTextVisible(true); - m_threadProgressBar->setRange(0, 100); - m_threadProgressBar->setValue(0); - - // Add widgets to status bar - statusBar()->addPermanentWidget(m_threadNameLabel); - statusBar()->addPermanentWidget(m_threadInfoLabel); - statusBar()->addPermanentWidget(m_threadProgressBar); - - // Initially hidden - m_threadNameLabel->hide(); - m_threadInfoLabel->hide(); - m_threadProgressBar->hide(); - - // Register callback for thread changes - // Use QMetaObject::invokeMethod to ensure UI updates happen on the main thread - m_connections.push_back(volrover3::app().threadsChanged.connect([this](const std::string &) { - QMetaObject::invokeMethod(this, "updateThreadStatus", Qt::QueuedConnection); - })); - - // Do initial update - updateThreadStatus(); -} - -void MainWindow::updateThreadStatus() { - // Get all threads - auto threads = volrover3::app().threads(); - - if (threads.empty()) { - // No threads active - hide widgets - m_threadNameLabel->hide(); - m_threadInfoLabel->hide(); - m_threadProgressBar->hide(); - // Don't clear message - let completion messages persist - } else { - // Find the best thread to display: - // 1. Prefer running threads (progress < 1.0) - // 2. Otherwise show the most recent thread - std::string displayThreadKey; - double displayProgress = -1.0; - bool hasRunningThread = false; - - for (const auto &entry : threads) { - const std::string &threadKey = entry.first; - const cvc::thread_ptr &threadPtr = entry.second; - - if (!threadPtr) - continue; - - double progress = volrover3::app().threadProgress(threadKey); - bool isComplete = (progress >= 1.0); - - if (!isComplete) { - // Found a running thread - prefer this - if (!hasRunningThread || progress > displayProgress) { - displayThreadKey = threadKey; - displayProgress = progress; - hasRunningThread = true; - } - } else if (!hasRunningThread) { - // No running threads yet, track completed ones - displayThreadKey = threadKey; - displayProgress = progress; - } - } - - if (displayThreadKey.empty()) { - // Fallback to first thread - displayThreadKey = threads.begin()->first; - displayProgress = volrover3::app().threadProgress(displayThreadKey); - } - - // Get thread info - std::string info = volrover3::app().threadInfo(displayThreadKey); - - // Add status indicator - bool isComplete = (displayProgress >= 1.0); - if (isComplete) { - if (info.empty()) - info = "completed"; - else - info += " (completed)"; - } else if (info.empty()) { - info = "running..."; - } - - // Update status bar widgets - m_threadNameLabel->setText(QString::fromStdString(displayThreadKey)); - m_threadInfoLabel->setText(QString::fromStdString(info)); - m_threadProgressBar->setValue(static_cast(displayProgress * 100.0)); - - // Color the progress bar based on status - if (isComplete) { - m_threadProgressBar->setStyleSheet("QProgressBar::chunk { background-color: #4CAF50; }"); - } else { - m_threadProgressBar->setStyleSheet(""); // Default color - } - - // Show widgets - m_threadNameLabel->show(); - m_threadInfoLabel->show(); - m_threadProgressBar->show(); - } -} - -void MainWindow::initializeCameraFromState() { - CameraController *camCtrl = m_renderWidget->getCameraController(); - if (!camCtrl) - return; - - // Load all camera settings from AppState - camCtrl->setMode(static_cast(AppState::instance().cameraMode())); - camCtrl->setMovementSpeed(AppState::instance().cameraSpeed()); - camCtrl->setMouseSensitivity(AppState::instance().cameraSensitivity()); - camCtrl->setInvertMouse(AppState::instance().cameraInvertMouse()); - camCtrl->setKeyBindings( - AppState::instance().cameraKeyForward(), AppState::instance().cameraKeyBackward(), - AppState::instance().cameraKeyLeft(), AppState::instance().cameraKeyRight(), - AppState::instance().cameraKeyUp(), AppState::instance().cameraKeyDown()); - - // Camera state is now managed entirely through CameraController's state tree - // No need to load from AppState - - // Set orbit center to world bounds center - cvc::bounding_box bounds = AppState::instance().worldBounds(); - double cx = (bounds[0] + bounds[3]) / 2.0; - double cy = (bounds[1] + bounds[4]) / 2.0; - double cz = (bounds[2] + bounds[5]) / 2.0; - camCtrl->setOrbitCenter(cx, cy, cz); -} diff --git a/src/volrover3/NullGraphicNode.cpp b/src/volrover3/NullGraphicNode.cpp deleted file mode 100644 index 57414952..00000000 --- a/src/volrover3/NullGraphicNode.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -NullGraphicNode::NullGraphicNode(cvc::app &ctx, const std::string &statePath, - const std::string &name) - : GraphicsNode(ctx, statePath, name), - m_bounds(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5) // Default 1x1x1 box centered at origin - , - m_dummyActor(vtkSmartPointer::New()), - m_includeOwnBounds(false) // Don't include own bounds by default (typical for root nodes) - , - m_syncBoundsWithChildren(true) // By default, sync bounds to encompass children -{ - // Dummy actor has no mapper, won't render anything - // This node exists only to provide bounding box extents - - // Initialize bounds in state tree - if (!statePath.empty()) { - std::ostringstream oss; - oss << m_bounds.minx << "," << m_bounds.miny << "," << m_bounds.minz << "," << m_bounds.maxx - << "," << m_bounds.maxy << "," << m_bounds.maxz; - getState("bounds").value(oss.str()); - getState("include_own_bounds").value(m_includeOwnBounds ? 1 : 0); - getState("sync_bounds_with_children").value(m_syncBoundsWithChildren ? 1 : 0); - } -} - -NullGraphicNode::~NullGraphicNode() {} - -vtkProp *NullGraphicNode::getProp() { - // Return dummy actor that won't render anything - return m_dummyActor; -} - -void NullGraphicNode::setBounds(const cvc::bounding_box &bbox) { - m_bounds = bbox; - - // Update state tree - std::ostringstream oss; - oss << bbox[0] << "," << bbox[1] << "," << bbox[2] << "," << bbox[3] << "," << bbox[4] << "," - << bbox[5]; - getState("bounds").value(oss.str()); - - updateBoundingBoxNode(); -} - -void NullGraphicNode::setBounds(double minX, double minY, double minZ, double maxX, double maxY, - double maxZ) { - m_bounds = cvc::bounding_box(minX, minY, minZ, maxX, maxY, maxZ); - - // Update state tree - std::ostringstream oss; - oss << minX << "," << minY << "," << minZ << "," << maxX << "," << maxY << "," << maxZ; - getState("bounds").value(oss.str()); - - updateBoundingBoxNode(); -} - -cvc::bounding_box NullGraphicNode::getBoundingBox() const { - // Return this node's own bounds - // Note: If we have children, getCombinedBoundingBox() (inherited from GraphicsNode) - // will handle merging children's transformed bboxes with our bounds - return m_bounds; -} - -void NullGraphicNode::setIncludeOwnBounds(bool include) { - if (m_includeOwnBounds == include) - return; - - m_includeOwnBounds = include; - - // Update state tree - getState("include_own_bounds").value(include ? 1 : 0); - - // Update bbox visualization since combined bounds may have changed - updateBoundingBoxNode(); -} - -void NullGraphicNode::setSyncBoundsWithChildren(bool sync) { - if (m_syncBoundsWithChildren == sync) - return; - - m_syncBoundsWithChildren = sync; - - // Update state tree - getState("sync_bounds_with_children").value(sync ? 1 : 0); - - // If enabling sync, update bounds immediately to match children - if (sync) { - syncBoundsToChildren(); - } -} - -void NullGraphicNode::addGraphicsChild(std::shared_ptr child) { - // Call parent implementation first - GraphicsNode::addGraphicsChild(child); - - // Auto-sync bounds to encompass new child - if (m_syncBoundsWithChildren) { - syncBoundsToChildren(); - } -} - -void NullGraphicNode::removeGraphicsChild(std::shared_ptr child) { - // Call parent implementation first - GraphicsNode::removeGraphicsChild(child); - - // Auto-sync bounds after removing child - if (m_syncBoundsWithChildren) { - syncBoundsToChildren(); - } -} - -void NullGraphicNode::syncBoundsToChildren() { - if (!m_syncBoundsWithChildren) - return; - - std::cout << "[DEBUG] NullGraphicNode::syncBoundsToChildren - Syncing bounds for node '" - << getName() << "', children count: " << m_graphicsChildren.size() << std::endl; - - // Calculate combined bounds of all children (without including our own bounds) - double acc_minx = std::numeric_limits::max(); - double acc_miny = std::numeric_limits::max(); - double acc_minz = std::numeric_limits::max(); - double acc_maxx = std::numeric_limits::lowest(); - double acc_maxy = std::numeric_limits::lowest(); - double acc_maxz = std::numeric_limits::lowest(); - - bool hasChildren = false; - - for (const auto &child : m_graphicsChildren) { - if (!child) - continue; - - // Skip grid and axis nodes - they don't contribute to scene bounds - if (dynamic_cast(child.get()) || dynamic_cast(child.get())) { - continue; - } - - cvc::bounding_box childBBox = child->getCombinedBoundingBox(); - - // Skip invalid bounding boxes - if (childBBox[0] > childBBox[3] || childBBox[1] > childBBox[4] || childBBox[2] > childBBox[5]) { - continue; - } - - // Transform child's bbox by child's local transform - vtkMatrix4x4 *childTransform = child->getTransform(); - - double corners[8][3] = { - {childBBox[0], childBBox[1], childBBox[2]}, {childBBox[3], childBBox[1], childBBox[2]}, - {childBBox[0], childBBox[4], childBBox[2]}, {childBBox[3], childBBox[4], childBBox[2]}, - {childBBox[0], childBBox[1], childBBox[5]}, {childBBox[3], childBBox[1], childBBox[5]}, - {childBBox[0], childBBox[4], childBBox[5]}, {childBBox[3], childBBox[4], childBBox[5]}}; - - for (int i = 0; i < 8; ++i) { - double in[4] = {corners[i][0], corners[i][1], corners[i][2], 1.0}; - double out[4]; - childTransform->MultiplyPoint(in, out); - - acc_minx = std::min(acc_minx, out[0]); - acc_miny = std::min(acc_miny, out[1]); - acc_minz = std::min(acc_minz, out[2]); - acc_maxx = std::max(acc_maxx, out[0]); - acc_maxy = std::max(acc_maxy, out[1]); - acc_maxz = std::max(acc_maxz, out[2]); - } - - hasChildren = true; - } - - // Update bounds to match children (if we have any valid children) - if (hasChildren && acc_minx <= acc_maxx && acc_miny <= acc_maxy && acc_minz <= acc_maxz) { - m_bounds = cvc::bounding_box(acc_minx, acc_miny, acc_minz, acc_maxx, acc_maxy, acc_maxz); - - std::cout << "[DEBUG] NullGraphicNode::syncBoundsToChildren - Updated bounds to [" << acc_minx - << "," << acc_miny << "," << acc_minz << "] to [" << acc_maxx << "," << acc_maxy - << "," << acc_maxz << "]" << std::endl; - - // Update state tree - std::ostringstream oss; - oss << m_bounds.minx << "," << m_bounds.miny << "," << m_bounds.minz << "," << m_bounds.maxx - << "," << m_bounds.maxy << "," << m_bounds.maxz; - getState("bounds").value(oss.str()); - - // Update visualization - updateBoundingBoxNode(); - } -} - -void NullGraphicNode::handleStateChanged(const std::string &childState) { - // Handle bounds state changes (no VTK calls, so no runOnMainThread needed) - if (childState == "bounds") { - std::string boundsStr = getState("bounds").value(); - std::istringstream iss(boundsStr); - double minX, minY, minZ, maxX, maxY, maxZ; - char comma; - if (iss >> minX >> comma >> minY >> comma >> minZ >> comma >> maxX >> comma >> maxY >> comma >> - maxZ) { - setBounds(minX, minY, minZ, maxX, maxY, maxZ); - } - } else if (childState == "include_own_bounds") { - int includeOwn = getState("include_own_bounds").value(); - m_includeOwnBounds = (includeOwn != 0); - // Update bbox visualization since combined bounds may have changed - updateBoundingBoxNode(); - } else if (childState == "sync_bounds_with_children") { - int syncBounds = getState("sync_bounds_with_children").value(); - m_syncBoundsWithChildren = (syncBounds != 0); - // If enabling sync, update bounds immediately - if (m_syncBoundsWithChildren) { - syncBoundsToChildren(); - } - } else { - // Delegate to parent for common graphics fields - // Parent will handle its own runOnMainThread wrapping - GraphicsNode::handleStateChanged(childState); - } -} diff --git a/src/volrover3/ProceduralGeometryDialog.cpp b/src/volrover3/ProceduralGeometryDialog.cpp deleted file mode 100644 index e54ba54d..00000000 --- a/src/volrover3/ProceduralGeometryDialog.cpp +++ /dev/null @@ -1,349 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif - -ProceduralGeometryDialog::ProceduralGeometryDialog(ProceduralGeometryType type, - std::shared_ptr sceneGraph, - QWidget *parent) - : QDialog(parent), m_type(type), m_sceneGraph(sceneGraph), m_centerXSpinBox(nullptr), - m_centerYSpinBox(nullptr), m_centerZSpinBox(nullptr), m_radiusSpinBox(nullptr), - m_thetaResSpinBox(nullptr), m_phiResSpinBox(nullptr), m_sizeXSpinBox(nullptr), - m_sizeYSpinBox(nullptr), m_sizeZSpinBox(nullptr), m_majorRadiusSpinBox(nullptr), - m_minorRadiusSpinBox(nullptr), m_majorResSpinBox(nullptr), m_minorResSpinBox(nullptr), - m_coneRadiusSpinBox(nullptr), m_coneHeightSpinBox(nullptr), m_coneResSpinBox(nullptr), - m_coneCapResSpinBox(nullptr), m_buttonBox(nullptr) { - setupUI(); -} - -void ProceduralGeometryDialog::setupUI() { - QString title; - switch (m_type) { - case ProceduralGeometryType::Sphere: - title = tr("Generate Sphere"); - break; - case ProceduralGeometryType::Cube: - title = tr("Generate Cube"); - break; - case ProceduralGeometryType::Torus: - title = tr("Generate Torus"); - break; - case ProceduralGeometryType::Cone: - title = tr("Generate Cone"); - break; - } - setWindowTitle(title); - - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Center position group - QGroupBox *centerGroup = new QGroupBox(tr("Center Position"), this); - QFormLayout *centerLayout = new QFormLayout(centerGroup); - - m_centerXSpinBox = new QDoubleSpinBox(this); - m_centerXSpinBox->setRange(-1000.0, 1000.0); - m_centerXSpinBox->setValue(0.0); - m_centerXSpinBox->setDecimals(3); - centerLayout->addRow(tr("X:"), m_centerXSpinBox); - - m_centerYSpinBox = new QDoubleSpinBox(this); - m_centerYSpinBox->setRange(-1000.0, 1000.0); - m_centerYSpinBox->setValue(0.0); - m_centerYSpinBox->setDecimals(3); - centerLayout->addRow(tr("Y:"), m_centerYSpinBox); - - m_centerZSpinBox = new QDoubleSpinBox(this); - m_centerZSpinBox->setRange(-1000.0, 1000.0); - m_centerZSpinBox->setValue(0.0); - m_centerZSpinBox->setDecimals(3); - centerLayout->addRow(tr("Z:"), m_centerZSpinBox); - - mainLayout->addWidget(centerGroup); - - // Parameters group - QGroupBox *paramsGroup = new QGroupBox(tr("Parameters"), this); - QFormLayout *paramsLayout = new QFormLayout(paramsGroup); - - switch (m_type) { - case ProceduralGeometryType::Sphere: - setupSphereUI(paramsLayout); - break; - case ProceduralGeometryType::Cube: - setupCubeUI(paramsLayout); - break; - case ProceduralGeometryType::Torus: - setupTorusUI(paramsLayout); - break; - case ProceduralGeometryType::Cone: - setupConeUI(paramsLayout); - break; - } - - mainLayout->addWidget(paramsGroup); - - // Buttons - m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Generate")); - connect(m_buttonBox, &QDialogButtonBox::accepted, this, &ProceduralGeometryDialog::onGenerate); - connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - mainLayout->addWidget(m_buttonBox); - - setMinimumWidth(300); -} - -void ProceduralGeometryDialog::setupSphereUI(QFormLayout *formLayout) { - m_radiusSpinBox = new QDoubleSpinBox(this); - m_radiusSpinBox->setRange(0.001, 1000.0); - m_radiusSpinBox->setValue(1.0); - m_radiusSpinBox->setDecimals(3); - m_radiusSpinBox->setToolTip(tr("Radius of the sphere")); - formLayout->addRow(tr("Radius:"), m_radiusSpinBox); - - m_thetaResSpinBox = new QSpinBox(this); - m_thetaResSpinBox->setRange(3, 256); - m_thetaResSpinBox->setValue(32); - m_thetaResSpinBox->setToolTip(tr("Number of segments around the equator")); - formLayout->addRow(tr("Theta Resolution:"), m_thetaResSpinBox); - - m_phiResSpinBox = new QSpinBox(this); - m_phiResSpinBox->setRange(3, 256); - m_phiResSpinBox->setValue(16); - m_phiResSpinBox->setToolTip(tr("Number of segments from pole to pole")); - formLayout->addRow(tr("Phi Resolution:"), m_phiResSpinBox); -} - -void ProceduralGeometryDialog::setupCubeUI(QFormLayout *formLayout) { - m_sizeXSpinBox = new QDoubleSpinBox(this); - m_sizeXSpinBox->setRange(0.001, 1000.0); - m_sizeXSpinBox->setValue(1.0); - m_sizeXSpinBox->setDecimals(3); - m_sizeXSpinBox->setToolTip(tr("Size along X axis")); - formLayout->addRow(tr("Width (X):"), m_sizeXSpinBox); - - m_sizeYSpinBox = new QDoubleSpinBox(this); - m_sizeYSpinBox->setRange(0.001, 1000.0); - m_sizeYSpinBox->setValue(1.0); - m_sizeYSpinBox->setDecimals(3); - m_sizeYSpinBox->setToolTip(tr("Size along Y axis")); - formLayout->addRow(tr("Height (Y):"), m_sizeYSpinBox); - - m_sizeZSpinBox = new QDoubleSpinBox(this); - m_sizeZSpinBox->setRange(0.001, 1000.0); - m_sizeZSpinBox->setValue(1.0); - m_sizeZSpinBox->setDecimals(3); - m_sizeZSpinBox->setToolTip(tr("Size along Z axis")); - formLayout->addRow(tr("Depth (Z):"), m_sizeZSpinBox); -} - -void ProceduralGeometryDialog::setupTorusUI(QFormLayout *formLayout) { - m_majorRadiusSpinBox = new QDoubleSpinBox(this); - m_majorRadiusSpinBox->setRange(0.001, 1000.0); - m_majorRadiusSpinBox->setValue(1.0); - m_majorRadiusSpinBox->setDecimals(3); - m_majorRadiusSpinBox->setToolTip(tr("Distance from center to tube center")); - formLayout->addRow(tr("Major Radius:"), m_majorRadiusSpinBox); - - m_minorRadiusSpinBox = new QDoubleSpinBox(this); - m_minorRadiusSpinBox->setRange(0.001, 1000.0); - m_minorRadiusSpinBox->setValue(0.25); - m_minorRadiusSpinBox->setDecimals(3); - m_minorRadiusSpinBox->setToolTip(tr("Radius of the tube")); - formLayout->addRow(tr("Minor Radius:"), m_minorRadiusSpinBox); - - m_majorResSpinBox = new QSpinBox(this); - m_majorResSpinBox->setRange(3, 256); - m_majorResSpinBox->setValue(32); - m_majorResSpinBox->setToolTip(tr("Number of segments around the torus")); - formLayout->addRow(tr("Major Resolution:"), m_majorResSpinBox); - - m_minorResSpinBox = new QSpinBox(this); - m_minorResSpinBox->setRange(3, 256); - m_minorResSpinBox->setValue(16); - m_minorResSpinBox->setToolTip(tr("Number of segments around the tube")); - formLayout->addRow(tr("Minor Resolution:"), m_minorResSpinBox); -} - -void ProceduralGeometryDialog::setupConeUI(QFormLayout *formLayout) { - m_coneRadiusSpinBox = new QDoubleSpinBox(this); - m_coneRadiusSpinBox->setRange(0.001, 1000.0); - m_coneRadiusSpinBox->setValue(0.5); - m_coneRadiusSpinBox->setDecimals(3); - m_coneRadiusSpinBox->setToolTip(tr("Radius of the cone base")); - formLayout->addRow(tr("Base Radius:"), m_coneRadiusSpinBox); - - m_coneHeightSpinBox = new QDoubleSpinBox(this); - m_coneHeightSpinBox->setRange(0.001, 1000.0); - m_coneHeightSpinBox->setValue(1.0); - m_coneHeightSpinBox->setDecimals(3); - m_coneHeightSpinBox->setToolTip(tr("Height of the cone")); - formLayout->addRow(tr("Height:"), m_coneHeightSpinBox); - - m_coneResSpinBox = new QSpinBox(this); - m_coneResSpinBox->setRange(3, 256); - m_coneResSpinBox->setValue(32); - m_coneResSpinBox->setToolTip(tr("Number of segments around the cone")); - formLayout->addRow(tr("Resolution:"), m_coneResSpinBox); - - m_coneCapResSpinBox = new QSpinBox(this); - m_coneCapResSpinBox->setRange(1, 64); - m_coneCapResSpinBox->setValue(1); - m_coneCapResSpinBox->setToolTip(tr("Number of rings on the base cap")); - formLayout->addRow(tr("Cap Resolution:"), m_coneCapResSpinBox); -} - -void ProceduralGeometryDialog::onGenerate() { - try { - switch (m_type) { - case ProceduralGeometryType::Sphere: - generateSphere(); - break; - case ProceduralGeometryType::Cube: - generateCube(); - break; - case ProceduralGeometryType::Torus: - generateTorus(); - break; - case ProceduralGeometryType::Cone: - generateCone(); - break; - } - accept(); - } catch (const std::exception &e) { - QMessageBox::critical(this, tr("Generation Error"), - tr("Failed to generate geometry:\n%1").arg(e.what())); - } -} - -std::string ProceduralGeometryDialog::getUniqueName(const std::string &baseName) { - std::string name = baseName; - int counter = 1; - while (m_sceneGraph->getGraphics(name)) { - name = baseName + "_" + std::to_string(counter++); - } - return name; -} - -void ProceduralGeometryDialog::generateSphere() { - double cx = m_centerXSpinBox->value(); - double cy = m_centerYSpinBox->value(); - double cz = m_centerZSpinBox->value(); - double radius = m_radiusSpinBox->value(); - int thetaRes = m_thetaResSpinBox->value(); - int phiRes = m_phiResSpinBox->value(); - - // Use the algorithm function to generate the geometry - cvc::geometry geom = cvc::generate_sphere(cx, cy, cz, radius, thetaRes, phiRes); - - // Create geometry node - std::string name = getUniqueName("Sphere"); - auto node = m_sceneGraph->getGraphicsRoot()->addGraphicsChild(name); - m_sceneGraph->registerGraphics(name, node); - - node->setGeometry(geom); - node->setMetadata("type", std::string("geometry")); - node->setMetadata("source", std::string("procedural")); - node->setMetadata("primitive", std::string("sphere")); - node->setMetadata("num_vertices", static_cast(geom.num_points())); - node->setMetadata("num_triangles", static_cast(geom.num_tris())); - - AppState::instance().setWorldBounds(geom.extents()); -} - -void ProceduralGeometryDialog::generateCube() { - double cx = m_centerXSpinBox->value(); - double cy = m_centerYSpinBox->value(); - double cz = m_centerZSpinBox->value(); - double sx = m_sizeXSpinBox->value(); - double sy = m_sizeYSpinBox->value(); - double sz = m_sizeZSpinBox->value(); - - // Use the algorithm function to generate the geometry - cvc::geometry geom = cvc::generate_cube(cx, cy, cz, sx, sy, sz); - - // Create geometry node - std::string name = getUniqueName("Cube"); - auto node = m_sceneGraph->getGraphicsRoot()->addGraphicsChild(name); - m_sceneGraph->registerGraphics(name, node); - - node->setGeometry(geom); - node->setMetadata("type", std::string("geometry")); - node->setMetadata("source", std::string("procedural")); - node->setMetadata("primitive", std::string("cube")); - node->setMetadata("num_vertices", static_cast(geom.num_points())); - node->setMetadata("num_triangles", static_cast(geom.num_tris())); - - AppState::instance().setWorldBounds(geom.extents()); -} - -void ProceduralGeometryDialog::generateTorus() { - double cx = m_centerXSpinBox->value(); - double cy = m_centerYSpinBox->value(); - double cz = m_centerZSpinBox->value(); - double majorRadius = m_majorRadiusSpinBox->value(); - double minorRadius = m_minorRadiusSpinBox->value(); - int majorRes = m_majorResSpinBox->value(); - int minorRes = m_minorResSpinBox->value(); - - // Use the algorithm function to generate the geometry - cvc::geometry geom = - cvc::generate_torus(cx, cy, cz, majorRadius, minorRadius, majorRes, minorRes); - - // Create geometry node - std::string name = getUniqueName("Torus"); - auto node = m_sceneGraph->getGraphicsRoot()->addGraphicsChild(name); - m_sceneGraph->registerGraphics(name, node); - - node->setGeometry(geom); - node->setMetadata("type", std::string("geometry")); - node->setMetadata("source", std::string("procedural")); - node->setMetadata("primitive", std::string("torus")); - node->setMetadata("num_vertices", static_cast(geom.num_points())); - node->setMetadata("num_triangles", static_cast(geom.num_tris())); - - AppState::instance().setWorldBounds(geom.extents()); -} - -void ProceduralGeometryDialog::generateCone() { - double cx = m_centerXSpinBox->value(); - double cy = m_centerYSpinBox->value(); - double cz = m_centerZSpinBox->value(); - double radius = m_coneRadiusSpinBox->value(); - double height = m_coneHeightSpinBox->value(); - int res = m_coneResSpinBox->value(); - - // Use the algorithm function to generate the geometry - // Note: The algorithm function doesn't use capRes parameter - cvc::geometry geom = cvc::generate_cone(cx, cy, cz, radius, height, res); - - // Create geometry node - std::string name = getUniqueName("Cone"); - auto node = m_sceneGraph->getGraphicsRoot()->addGraphicsChild(name); - m_sceneGraph->registerGraphics(name, node); - - node->setGeometry(geom); - node->setMetadata("type", std::string("geometry")); - node->setMetadata("source", std::string("procedural")); - node->setMetadata("primitive", std::string("cone")); - node->setMetadata("num_vertices", static_cast(geom.num_points())); - node->setMetadata("num_triangles", static_cast(geom.num_tris())); - - AppState::instance().setWorldBounds(geom.extents()); -} diff --git a/src/volrover3/README.md b/src/volrover3/README.md deleted file mode 100644 index 45727676..00000000 --- a/src/volrover3/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# VolRover3 - Volume Rover Version 3 - -A prototype visualization application built on libcvc for rendering volumetric data, surface meshes, and volumetric meshes. - -## Features - -- **Volume Rendering**: 3D texture-based volume rendering with GPU acceleration via VTK -- **Surface Mesh Visualization**: Triangle mesh rendering with normals and colors -- **Volumetric Mesh Support**: Tetrahedral and hexahedral mesh visualization -- **Transfer Functions**: Interactive color and opacity mapping for volume data -- **Quake-Style Camera**: First-person camera controls for intuitive navigation -- **Scene Elements**: Toggleable grid and coordinate axis for reference -- **File I/O Integration**: Support for CVC geometry and volume formats - -## Building - -VolRover3 requires: -- Qt6 (Core, Widgets, OpenGL, OpenGLWidgets) -- VTK (Visualization Toolkit) 9.0+ -- libcvc (built from this project) - -### Build Steps - -```bash -mkdir build && cd build -cmake .. -DCVC_BUILD_VOLROVER3=ON -make volrover3 -``` - -### Optional: Disable VolRover3 Build - -If Qt6 or VTK are not available: - -```bash -cmake .. -DCVC_BUILD_VOLROVER3=OFF -``` - -## Usage - -### Launch - -```bash -./bin/volrover3 -``` - -### Controls - -**Camera Movement (Quake-Style)**: -- `W` - Move forward -- `S` - Move backward -- `A` - Strafe left -- `D` - Strafe right -- `E` or `Space` - Move up -- `Q` or `Ctrl` - Move down -- `Mouse drag` (left button) - Look around -- `Mouse wheel` - Zoom in/out - -### Menu Options - -**File Menu**: -- `Open Geometry...` - Load surface meshes (.off, .raw, .obj, etc.) -- `Open Volume...` - Load volume data (.rawiv, .mrc, .ccp4) - -**View Menu**: -- `Show Grid` - Toggle ground grid display -- `Show Axis` - Toggle coordinate axis display - -## Supported File Formats - -**Geometry**: -- `.off` - Object File Format -- `.raw`, `.rawn`, `.rawc`, `.rawnc` - CVC raw formats -- `.obj` - Wavefront OBJ (experimental via SDF) - -**Volume**: -- `.rawiv` - RAWIV format -- `.mrc` - MRC/CCP4 format -- Other formats supported by libcvc - -## Architecture - -### Components - -- **MainWindow**: Qt6 main application window with menus and docking -- **VTKRenderWidget**: VTK/OpenGL rendering widget with event handling -- **SceneGraph**: Scene management and traversal -- **SceneNode**: Base class for renderable objects - - **GeometryNode**: Surface mesh rendering - - **VolumeNode**: Volume rendering with transfer functions - - **GridNode**: Reference grid - - **AxisNode**: Coordinate axis -- **CameraController**: Quake-style first-person camera -- **TransferFunctionWidget**: Color and opacity mapping UI - -### Rendering Pipeline - -1. Load geometry/volume via libcvc file I/O -2. Convert to VTK data structures (vtkPolyData, vtkImageData) -3. Create appropriate mappers (vtkPolyDataMapper, vtkSmartVolumeMapper) -4. Add actors/volumes to VTK renderer -5. Scene graph manages visibility and updates -6. Camera controller handles user input -7. Transfer function widget controls volume appearance - -### State Management - -VolRover3 uses a reactive state management system built on `cvc::state`: - -- **AppState**: Singleton managing application-wide state - - Camera position, view direction, FOV - - Geometry and volume data - - World bounds and visibility flags - - Transfer function parameters - -- **State Tree**: All state stored in hierarchical tree at `volrover3.*` - - Direct access: `cvc::state::instance()("volrover3")("camera_position_x")` - - Bidirectional synchronization with AppState methods - -- **Change Notifications**: Register callbacks for reactive updates - - All callback methods return `boost::signals2::connection` - - Disconnect when no longer needed for proper lifecycle management - - See `docs/APPSTATE_CALLBACKS.md` for detailed API documentation - -## API Documentation - -- [AppState Callback System](../../docs/APPSTATE_CALLBACKS.md) - Reactive state change notifications -- [Testing Guide](../../docs/TESTING.md) - Unit and integration test documentation - -## Future Enhancements - -- [ ] Isosurface extraction and rendering -- [ ] Multiple geometry/volume layers -- [ ] Advanced transfer function editor with histogram -- [ ] Screenshot and animation export -- [ ] Property inspector for loaded data -- [ ] Clipping planes -- [ ] Lighting controls -- [ ] Material editor -- [ ] Measurements and annotations - -## License - -Copyright © 2025 CVC (Computational Visualization Center) - -See main project LICENSE for details. diff --git a/src/volrover3/SDFDialog.cpp b/src/volrover3/SDFDialog.cpp deleted file mode 100644 index eb498230..00000000 --- a/src/volrover3/SDFDialog.cpp +++ /dev/null @@ -1,492 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -SDFDialog::SDFDialog(std::shared_ptr sceneGraph, QWidget *parent) - : QDialog(parent), m_sceneGraph(sceneGraph), m_geometryComboBox(nullptr), - m_dimXSpinBox(nullptr), m_dimYSpinBox(nullptr), m_dimZSpinBox(nullptr), - m_algorithmComboBox(nullptr), m_flipNormalsCheckBox(nullptr), m_useBoundsCheckBox(nullptr), - m_minXSpinBox(nullptr), m_minYSpinBox(nullptr), m_minZSpinBox(nullptr), - m_maxXSpinBox(nullptr), m_maxYSpinBox(nullptr), m_maxZSpinBox(nullptr), - m_computeButton(nullptr), m_cancelButton(nullptr), m_progressBar(nullptr), - m_statusLabel(nullptr), m_computing(false) { - setWindowTitle(tr("Signed Distance Function")); - setMinimumWidth(400); - setupUI(); - connectSignals(); - populateGeometryList(); - - // Connect to state tree to monitor for new geometry - // Listen to graphics root's children changes - if (m_sceneGraph) { - std::string statePrefix = m_sceneGraph->getStatePrefix(); - std::string graphicsRootPath = statePrefix + ".graphics.root.children"; - - m_graphicsChildrenConnection = - cvc::state::instance(volrover3::app())(graphicsRootPath) - .childChanged.connect([this](const std::string &) { - // Post to Qt event loop to ensure thread safety - QMetaObject::invokeMethod(this, "onGraphicsChildrenChanged", Qt::QueuedConnection); - }); - } -} - -void SDFDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Geometry Selection Group - QGroupBox *geomGroup = new QGroupBox(tr("Input Geometry"), this); - QFormLayout *geomLayout = new QFormLayout(geomGroup); - - m_geometryComboBox = new QComboBox(this); - geomLayout->addRow(tr("Geometry:"), m_geometryComboBox); - - mainLayout->addWidget(geomGroup); - - // Grid Dimensions Group - QGroupBox *dimGroup = new QGroupBox(tr("Grid Dimensions"), this); - QFormLayout *dimLayout = new QFormLayout(dimGroup); - - m_dimXSpinBox = new QSpinBox(this); - m_dimXSpinBox->setRange(8, 1024); - m_dimXSpinBox->setValue(128); - dimLayout->addRow(tr("X Dimension:"), m_dimXSpinBox); - - m_dimYSpinBox = new QSpinBox(this); - m_dimYSpinBox->setRange(8, 1024); - m_dimYSpinBox->setValue(128); - dimLayout->addRow(tr("Y Dimension:"), m_dimYSpinBox); - - m_dimZSpinBox = new QSpinBox(this); - m_dimZSpinBox->setRange(8, 1024); - m_dimZSpinBox->setValue(128); - dimLayout->addRow(tr("Z Dimension:"), m_dimZSpinBox); - - mainLayout->addWidget(dimGroup); - - // Algorithm Options Group - QGroupBox *algoGroup = new QGroupBox(tr("Algorithm Options"), this); - QFormLayout *algoLayout = new QFormLayout(algoGroup); - - m_algorithmComboBox = new QComboBox(this); - m_algorithmComboBox->addItem(tr("SDF v1 (Default)"), static_cast(cvc::SDF_V1)); - m_algorithmComboBox->addItem(tr("SDF v2 (Faster)"), static_cast(cvc::SDF_V2)); - algoLayout->addRow(tr("Algorithm:"), m_algorithmComboBox); - - m_flipNormalsCheckBox = new QCheckBox(tr("Flip normals (invert inside/outside)"), this); - algoLayout->addRow(m_flipNormalsCheckBox); - - mainLayout->addWidget(algoGroup); - - // Bounding Box Group - QGroupBox *bboxGroup = new QGroupBox(tr("Bounding Box"), this); - QVBoxLayout *bboxLayout = new QVBoxLayout(bboxGroup); - - m_useBoundsCheckBox = - new QCheckBox(tr("Use custom bounding box (unchecked = use geometry extents)"), this); - bboxLayout->addWidget(m_useBoundsCheckBox); - - QFormLayout *boundsLayout = new QFormLayout(); - - QHBoxLayout *minLayout = new QHBoxLayout(); - m_minXSpinBox = new QDoubleSpinBox(this); - m_minXSpinBox->setRange(-10000.0, 10000.0); - m_minXSpinBox->setValue(0.0); - m_minXSpinBox->setEnabled(false); - minLayout->addWidget(new QLabel(tr("X:"), this)); - minLayout->addWidget(m_minXSpinBox); - - m_minYSpinBox = new QDoubleSpinBox(this); - m_minYSpinBox->setRange(-10000.0, 10000.0); - m_minYSpinBox->setValue(0.0); - m_minYSpinBox->setEnabled(false); - minLayout->addWidget(new QLabel(tr("Y:"), this)); - minLayout->addWidget(m_minYSpinBox); - - m_minZSpinBox = new QDoubleSpinBox(this); - m_minZSpinBox->setRange(-10000.0, 10000.0); - m_minZSpinBox->setValue(0.0); - m_minZSpinBox->setEnabled(false); - minLayout->addWidget(new QLabel(tr("Z:"), this)); - minLayout->addWidget(m_minZSpinBox); - - boundsLayout->addRow(tr("Min:"), minLayout); - - QHBoxLayout *maxLayout = new QHBoxLayout(); - m_maxXSpinBox = new QDoubleSpinBox(this); - m_maxXSpinBox->setRange(-10000.0, 10000.0); - m_maxXSpinBox->setValue(1.0); - m_maxXSpinBox->setEnabled(false); - maxLayout->addWidget(new QLabel(tr("X:"), this)); - maxLayout->addWidget(m_maxXSpinBox); - - m_maxYSpinBox = new QDoubleSpinBox(this); - m_maxYSpinBox->setRange(-10000.0, 10000.0); - m_maxYSpinBox->setValue(1.0); - m_maxYSpinBox->setEnabled(false); - maxLayout->addWidget(new QLabel(tr("Y:"), this)); - maxLayout->addWidget(m_maxYSpinBox); - - m_maxZSpinBox = new QDoubleSpinBox(this); - m_maxZSpinBox->setRange(-10000.0, 10000.0); - m_maxZSpinBox->setValue(1.0); - m_maxZSpinBox->setEnabled(false); - maxLayout->addWidget(new QLabel(tr("Z:"), this)); - maxLayout->addWidget(m_maxZSpinBox); - - boundsLayout->addRow(tr("Max:"), maxLayout); - - bboxLayout->addLayout(boundsLayout); - mainLayout->addWidget(bboxGroup); - - // Progress Group - QGroupBox *progressGroup = new QGroupBox(tr("Progress"), this); - QVBoxLayout *progressLayout = new QVBoxLayout(progressGroup); - - m_statusLabel = new QLabel(tr("Ready"), this); - progressLayout->addWidget(m_statusLabel); - - m_progressBar = new QProgressBar(this); - m_progressBar->setRange(0, 100); - m_progressBar->setValue(0); - progressLayout->addWidget(m_progressBar); - - mainLayout->addWidget(progressGroup); - - // Button box - QHBoxLayout *buttonLayout = new QHBoxLayout(); - buttonLayout->addStretch(); - - m_computeButton = new QPushButton(tr("Compute SDF"), this); - m_computeButton->setDefault(true); - buttonLayout->addWidget(m_computeButton); - - m_cancelButton = new QPushButton(tr("Close"), this); - buttonLayout->addWidget(m_cancelButton); - - mainLayout->addLayout(buttonLayout); - - setLayout(mainLayout); -} - -void SDFDialog::connectSignals() { - connect(m_geometryComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &SDFDialog::onGeometrySelected); - connect(m_computeButton, &QPushButton::clicked, this, &SDFDialog::onComputeClicked); - connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - connect(m_useBoundsCheckBox, &QCheckBox::toggled, [this](bool checked) { - m_minXSpinBox->setEnabled(checked); - m_minYSpinBox->setEnabled(checked); - m_minZSpinBox->setEnabled(checked); - m_maxXSpinBox->setEnabled(checked); - m_maxYSpinBox->setEnabled(checked); - m_maxZSpinBox->setEnabled(checked); - }); -} - -void SDFDialog::populateGeometryList() { - m_geometryComboBox->clear(); - m_geometryNames.clear(); - - if (!m_sceneGraph) - return; - - // Get all geometry nodes recursively - auto allGeometries = m_sceneGraph->getAllGeometryGraphics(); - - for (const auto &geomNode : allGeometries) { - if (geomNode && geomNode->getGeometry() && !geomNode->getGeometry()->empty()) { - std::string name = geomNode->getName(); - m_geometryNames.push_back(name); - m_geometryComboBox->addItem(QString::fromStdString(name)); - } - } - - if (m_geometryComboBox->count() == 0) { - m_computeButton->setEnabled(false); - m_statusLabel->setText(tr("No geometry available")); - } else { - onGeometrySelected(0); - } -} - -void SDFDialog::onGraphicsChildrenChanged() { - if (!m_sceneGraph) - return; - - // Get current geometry count - size_t currentCount = m_geometryNames.size(); - - // Count geometry nodes in scene graph recursively - size_t sceneGeomCount = 0; - auto allGeometries = m_sceneGraph->getAllGeometryGraphics(); - for (const auto &geomNode : allGeometries) { - if (geomNode && geomNode->getGeometry() && !geomNode->getGeometry()->empty()) { - sceneGeomCount++; - } - } - - // If counts differ, refresh the list - if (sceneGeomCount != currentCount) { - // Save current selection - QString currentSelection; - int currentIndex = m_geometryComboBox->currentIndex(); - if (currentIndex >= 0 && currentIndex < static_cast(m_geometryNames.size())) { - currentSelection = QString::fromStdString(m_geometryNames[currentIndex]); - } - - // Refresh the list - populateGeometryList(); - - // Try to restore the previous selection - if (!currentSelection.isEmpty()) { - int newIndex = m_geometryComboBox->findText(currentSelection); - if (newIndex >= 0) { - m_geometryComboBox->setCurrentIndex(newIndex); - } - } - - // Update status if geometry is now available - if (m_geometryComboBox->count() > 0 && !m_computing) { - m_computeButton->setEnabled(true); - if (m_statusLabel->text() == tr("No geometry available")) { - m_statusLabel->setText(tr("Ready")); - } - } - } -} - -void SDFDialog::onGeometrySelected(int index) { - if (index < 0 || index >= static_cast(m_geometryNames.size())) - return; - - const std::string &geomName = m_geometryNames[index]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) - return; - - // Update bounding box fields with geometry extents - const cvc::geometry *geom = geomNode->getGeometry(); - if (!geom) - return; - - cvc::bounding_box bbox = geom->extents(); - - m_minXSpinBox->setValue(bbox.minx); - m_minYSpinBox->setValue(bbox.miny); - m_minZSpinBox->setValue(bbox.minz); - m_maxXSpinBox->setValue(bbox.maxx); - m_maxYSpinBox->setValue(bbox.maxy); - m_maxZSpinBox->setValue(bbox.maxz); -} - -void SDFDialog::onComputeClicked() { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - if (m_computing) { - // Cancel ongoing computation - if (!m_activeThreadKey.empty()) { - volrover3::app().threads(m_activeThreadKey)->interrupt(); - } - setControlsEnabled(true); - m_statusLabel->setText(tr("Cancelled")); - m_progressBar->setValue(0); - m_computing = false; - m_computeButton->setText(tr("Compute SDF")); - return; - } - - if (m_geometryComboBox->currentIndex() < 0) { - QMessageBox::warning(this, tr("No Geometry"), tr("Please select a geometry to compute SDF.")); - return; - } - - const std::string &geomName = m_geometryNames[m_geometryComboBox->currentIndex()]; - auto graphicsNode = m_sceneGraph->getGraphics(geomName); - auto geomNode = std::dynamic_pointer_cast(graphicsNode); - - if (!geomNode) { - QMessageBox::critical(this, tr("Error"), tr("Failed to get geometry node.")); - return; - } - - // Get parameters - cvc::dimension dim(m_dimXSpinBox->value(), m_dimYSpinBox->value(), m_dimZSpinBox->value()); - - cvc::bounding_box bbox; - if (m_useBoundsCheckBox->isChecked()) { - bbox = - cvc::bounding_box(m_minXSpinBox->value(), m_minYSpinBox->value(), m_minZSpinBox->value(), - m_maxXSpinBox->value(), m_maxYSpinBox->value(), m_maxZSpinBox->value()); - } else { - const cvc::geometry *geomPtr = geomNode->getGeometry(); - if (!geomPtr) { - QMessageBox::critical(this, tr("Error"), tr("Failed to get geometry.")); - return; - } - bbox = geomPtr->extents(); - } - - cvc::sdf_algorithm algorithm = - static_cast(m_algorithmComboBox->currentData().toInt()); - - bool flipNormals = m_flipNormalsCheckBox->isChecked(); - - // Copy geometry for thread safety - const cvc::geometry *geomPtr = geomNode->getGeometry(); - if (!geomPtr) { - QMessageBox::critical(this, tr("Error"), tr("Failed to get geometry.")); - return; - } - cvc::geometry geom = *geomPtr; - - // Update UI - setControlsEnabled(false); - m_computing = true; - m_computeButton->setText(tr("Cancel")); - m_statusLabel->setText(tr("Computing SDF...")); - m_progressBar->setValue(0); - - // Create unique thread key - m_activeThreadKey = "sdf_computation_" + geomName; - - // Start computation in background thread - volrover3::app().startThread( - m_activeThreadKey, - [this, geom, dim, bbox, algorithm, flipNormals, geomName, activeKey = m_activeThreadKey]() { - // Use thread_feedback for proper progress tracking (must be at thread entry point) - cvc::app::thread_feedback feedback(volrover3::app(), activeKey); - - try { - // Update progress to indicate we've started - volrover3::app().threadProgress(activeKey, 0.1); - volrover3::app().threadInfo(activeKey, "Computing SDF..."); - - // Compute SDF (this is safe to do in background thread) - cvc::volume sdfVol = cvc::sdf(volrover3::app(), geom, dim, bbox, algorithm, flipNormals); - - // Update progress - volrover3::app().threadProgress(activeKey, 0.9); - volrover3::app().threadInfo(activeKey, "Adding volume to scene..."); - - // SDF computation complete, now adding to scene - QMetaObject::invokeMethod( - this, [this]() { m_statusLabel->setText(tr("Adding SDF volume to scene...")); }, - Qt::QueuedConnection); - - // Post all SceneGraph/VTK operations to main thread via SceneGraph event queue - // Capture activeKey for finish call - m_sceneGraph->postEvent([this, sdfVol, geomName, activeKey]() { - try { - // Sanitize the name to ensure it's a valid C identifier - std::string rawName = geomName + "_sdf"; - std::string sdfName = cvc::state::sanitizeStateName(rawName); - - // Check if SDF volume already exists - auto existingNode = m_sceneGraph->getGraphics(sdfName); - if (existingNode) { - // Remove existing SDF volume - m_sceneGraph->removeGraphics(sdfName); - } - - // Get the parent geometry node first - auto geomNode = m_sceneGraph->getGraphics(geomName); - if (!geomNode) { - throw std::runtime_error("Geometry node not found"); - } - - // Add SDF volume as child of geometry using the template createChild method - auto sdfNode = geomNode->createChild(sdfName, sdfVol); - - if (!sdfNode) { - throw std::runtime_error("Failed to create SDF volume node"); - } - - // Mark thread as finished - volrover3::app().finishThreadProgress(activeKey); - - // Update UI on Qt thread - QMetaObject::invokeMethod( - this, [this]() { onComputeFinished(true, "SDF computed successfully"); }, - Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Failed to create volume node: ") + e.what(); - volrover3::app().finishThreadProgress(activeKey); - QMetaObject::invokeMethod( - this, [this, errorMsg]() { onComputeFinished(false, errorMsg); }, - Qt::QueuedConnection); - } - }); - - } catch (const boost::thread_interrupted &) { - QMetaObject::invokeMethod( - this, [this]() { onComputeFinished(false, "Computation cancelled"); }, - Qt::QueuedConnection); - } catch (const std::exception &e) { - std::string errorMsg = std::string("Error: ") + e.what(); - QMetaObject::invokeMethod( - this, [this, errorMsg]() { onComputeFinished(false, errorMsg); }, - Qt::QueuedConnection); - } - }, - false // Don't wait for existing thread - ); -} - -void SDFDialog::updateProgress(int value) { m_progressBar->setValue(value); } - -void SDFDialog::onComputeFinished(bool success, const std::string &message) { - setControlsEnabled(true); - m_computing = false; - m_computeButton->setText(tr("Compute SDF")); - m_progressBar->setValue(success ? 100 : 0); - m_statusLabel->setText(QString::fromStdString(message)); - - if (success) { - QMessageBox::information(this, tr("Success"), QString::fromStdString(message)); - } else { - QMessageBox::warning(this, tr("Error"), QString::fromStdString(message)); - } -} - -void SDFDialog::setControlsEnabled(bool enabled) { - m_geometryComboBox->setEnabled(enabled); - m_dimXSpinBox->setEnabled(enabled); - m_dimYSpinBox->setEnabled(enabled); - m_dimZSpinBox->setEnabled(enabled); - m_algorithmComboBox->setEnabled(enabled); - m_flipNormalsCheckBox->setEnabled(enabled); - m_useBoundsCheckBox->setEnabled(enabled); - - bool boundsEnabled = enabled && m_useBoundsCheckBox->isChecked(); - m_minXSpinBox->setEnabled(boundsEnabled); - m_minYSpinBox->setEnabled(boundsEnabled); - m_minZSpinBox->setEnabled(boundsEnabled); - m_maxXSpinBox->setEnabled(boundsEnabled); - m_maxYSpinBox->setEnabled(boundsEnabled); - m_maxZSpinBox->setEnabled(boundsEnabled); -} diff --git a/src/volrover3/SceneGraph.cpp b/src/volrover3/SceneGraph.cpp deleted file mode 100644 index 1d4b8a4e..00000000 --- a/src/volrover3/SceneGraph.cpp +++ /dev/null @@ -1,579 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -SceneGraph::SceneGraph(const std::string &statePrefix) - : m_renderer(nullptr), m_statePrefix(statePrefix), m_gridNode(nullptr), m_axisNode(nullptr), - m_graphicsRoot(nullptr), m_nullGraphic(nullptr), m_multiVolumeRenderingEnabled(false), - m_renderNeeded(false) { - // Create null graphic as THE root graphics node (all graphics go under this) - // State path: {statePrefix}.graphics.root - std::string rootStatePath = statePrefix + ".graphics.root"; - m_nullGraphic = std::make_shared(volrover3::app(), rootStatePath, "root"); - - // Set SceneGraph reference IMMEDIATELY after construction - // This enables threading for event posting (nodes disable threading in constructor) - m_nullGraphic->setSceneGraph(this); - - m_nullGraphic->setShowBBox(true); // Show bbox by default - m_nullGraphic->setBounds(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5); // Default unit cube when empty - m_nullGraphic->setIncludeOwnBounds( - true); // Include root bounds in visualization (will change to false when children are added) - m_graphicsRoot = m_nullGraphic; // NullGraphic IS the graphics root - m_rootNodes.push_back(m_graphicsRoot); - - // Create grid and axis as graphics children of the root null graphic - // They will live in the null graphic's coordinate system and state tree - m_gridNode = m_nullGraphic->template addGraphicsChild("grid"); - m_axisNode = m_nullGraphic->template addGraphicsChild("axis"); - - // GridNode and AxisNode initialize their own default state and colors - - // Subscribe to root node bounds changes to update world bounds in AppState - m_rootBoundsConnection = m_nullGraphic->getState("bounds").valueChanged.connect([this]() { - cvc::bounding_box bounds = m_nullGraphic->getBoundingBox(); - std::cout << "[DEBUG] SceneGraph - Root node bounds changed, updating world bounds to [" - << bounds[0] << "," << bounds[1] << "," << bounds[2] << "] to [" << bounds[3] << "," - << bounds[4] << "," << bounds[5] << "]" << std::endl; - AppState::instance().setWorldBounds(bounds); - }); -} - -SceneGraph::~SceneGraph() { - // Disconnect root bounds subscription - m_rootBoundsConnection.disconnect(); - - // Process any remaining events before shutdown - processEvents(); - - if (m_renderer) { - for (auto &node : m_rootNodes) { - node->removeFromRenderer(m_renderer); - } - } - - // Clear SceneGraph reference from all nodes - for (auto &node : m_rootNodes) { - node->setSceneGraph(nullptr); - } -} - -void SceneGraph::postEvent(std::function callback) { - std::lock_guard lock(m_eventQueueMutex); - m_eventQueue.push(std::move(callback)); - m_renderNeeded = true; -} - -void SceneGraph::processEvents() { - // Process all pending events on the main thread - // Extract all events while holding the lock, then execute without lock - std::queue> events; - { - std::lock_guard lock(m_eventQueueMutex); - std::swap(events, m_eventQueue); - } - - // Execute all events on the main thread - while (!events.empty()) { - auto &callback = events.front(); - if (callback) { - callback(); - } - events.pop(); - } -} - -bool SceneGraph::checkAndResetRenderNeeded() { - std::lock_guard lock(m_eventQueueMutex); - bool needed = m_renderNeeded; - m_renderNeeded = false; - return needed; -} - -void SceneGraph::setRenderer(vtkRenderer *renderer) { - if (m_renderer) { - for (auto &node : m_rootNodes) { - node->removeFromRenderer(m_renderer); - } - } - - m_renderer = renderer; - - if (m_renderer) { - for (auto &node : m_rootNodes) { - node->addToRenderer(m_renderer); - } - } -} - -void SceneGraph::update() { - for (auto &node : m_rootNodes) { - node->update(); - } -} - -void SceneGraph::setGridVisible(bool visible) { m_gridNode->setVisible(visible); } - -void SceneGraph::setAxisVisible(bool visible) { m_axisNode->setVisible(visible); } - -void SceneGraph::setGridColor(double r, double g, double b) { m_gridNode->setColor(r, g, b); } - -void SceneGraph::updateGrid(const cvc::bounding_box &bounds) { - std::cout << "[DEBUG] SceneGraph::updateGrid - Input bounds: [" << bounds[0] << "," << bounds[1] - << "," << bounds[2] << "] to [" << bounds[3] << "," << bounds[4] << "," << bounds[5] - << "]" << std::endl; - - // Update the null graphic's own bounds - m_nullGraphic->setBounds(bounds); - - // Get combined bounds of null graphic (respecting children's local coordinate systems) - // This automatically excludes grid and axis as they're just visualization helpers - cvc::bounding_box combinedBounds = m_nullGraphic->getCombinedBoundingBox(); - - std::cout << "[DEBUG] SceneGraph::updateGrid - Combined bounds: [" << combinedBounds[0] << "," - << combinedBounds[1] << "," << combinedBounds[2] << "] to [" << combinedBounds[3] << "," - << combinedBounds[4] << "," << combinedBounds[5] << "]" << std::endl; - - // Update grid to match combined bounds - m_gridNode->setBounds(combinedBounds); - - // Scale axis length to be proportional to combined bounding box size - double spanX = combinedBounds[3] - combinedBounds[0]; - double spanY = combinedBounds[4] - combinedBounds[1]; - double spanZ = combinedBounds[5] - combinedBounds[2]; - double maxSpan = std::max({spanX, spanY, spanZ}); - - // Set axis to be about 20% of the maximum span - double axisLength = maxSpan * 0.2; - if (axisLength > 0.0) { - m_axisNode->setAxisLength(axisLength); - } -} - -void SceneGraph::setGridPlaneVisibility(bool yz, bool xz, bool xy) { - m_gridNode->setYZPlaneVisible(yz); - m_gridNode->setXZPlaneVisible(xz); - m_gridNode->setXYPlaneVisible(xy); -} - -void SceneGraph::setGridDivisions(int x, int y, int z) { m_gridNode->setGridDivisions(x, y, z); } - -void SceneGraph::setGridTickIntervals(int x, int y, int z) { - m_gridNode->setTickIntervals(x, y, z); -} - -void SceneGraph::setGridPlaneColors(double yzR, double yzG, double yzB, double xzR, double xzG, - double xzB, double xyR, double xyG, double xyB) { - m_gridNode->setYZPlaneColor(yzR, yzG, yzB); - m_gridNode->setXZPlaneColor(xzR, xzG, xzB); - m_gridNode->setXYPlaneColor(xyR, xyG, xyB); -} - -void SceneGraph::setGridTickLabelProperties(double r, double g, double b, int fontSize) { - m_gridNode->setTickLabelColor(r, g, b); - m_gridNode->setTickLabelFontSize(fontSize); -} - -void SceneGraph::updateTransferFunction(const std::vector &colorTable, - const std::vector &opacityTable) { - // Apply transfer function to all volume nodes - auto volumes = getAllVolumeGraphics(); - for (auto &volNode : volumes) { - volNode->setTransferFunction(colorTable, opacityTable); - } -} - -cvc::bounding_box SceneGraph::computeGraphicsBounds() const { - cvc::bounding_box combinedBounds; - bool first = true; - - // Process each direct child of the graphics root - // Each child's getCombinedBoundingBox() already includes its descendants recursively - if (m_graphicsRoot) { - for (const auto &child : m_graphicsRoot->getGraphicsChildren()) { - if (!child) - continue; - - // Skip grid and axis nodes - they don't contribute to scene bounds - if (child.get() == m_gridNode.get() || child.get() == m_axisNode.get()) { - continue; - } - - // Get combined bbox of this child (includes all its descendants in local space) - cvc::bounding_box childBBox = child->getCombinedBoundingBox(); - - // Skip invalid bounding boxes - if (childBBox[0] > childBBox[3] || childBBox[1] > childBBox[4] || - childBBox[2] > childBBox[5]) { - continue; - } - - // Apply world transform to the bounding box by transforming all 8 corners - vtkSmartPointer worldTransform = child->getWorldTransform(); - - double corners[8][3] = { - {childBBox[0], childBBox[1], childBBox[2]}, // min, min, min - {childBBox[3], childBBox[1], childBBox[2]}, // max, min, min - {childBBox[0], childBBox[4], childBBox[2]}, // min, max, min - {childBBox[3], childBBox[4], childBBox[2]}, // max, max, min - {childBBox[0], childBBox[1], childBBox[5]}, // min, min, max - {childBBox[3], childBBox[1], childBBox[5]}, // max, min, max - {childBBox[0], childBBox[4], childBBox[5]}, // min, max, max - {childBBox[3], childBBox[4], childBBox[5]} // max, max, max - }; - - // Transform all corners and find new axis-aligned bounds - double minx = std::numeric_limits::max(); - double miny = std::numeric_limits::max(); - double minz = std::numeric_limits::max(); - double maxx = std::numeric_limits::lowest(); - double maxy = std::numeric_limits::lowest(); - double maxz = std::numeric_limits::lowest(); - - for (int i = 0; i < 8; ++i) { - double in[4] = {corners[i][0], corners[i][1], corners[i][2], 1.0}; - double out[4]; - worldTransform->MultiplyPoint(in, out); - - minx = std::min(minx, out[0]); - miny = std::min(miny, out[1]); - minz = std::min(minz, out[2]); - maxx = std::max(maxx, out[0]); - maxy = std::max(maxy, out[1]); - maxz = std::max(maxz, out[2]); - } - - // Merge with combined bounds - if (first) { - combinedBounds = cvc::bounding_box(minx, miny, minz, maxx, maxy, maxz); - first = false; - } else { - combinedBounds[0] = std::min(combinedBounds[0], minx); - combinedBounds[1] = std::min(combinedBounds[1], miny); - combinedBounds[2] = std::min(combinedBounds[2], minz); - combinedBounds[3] = std::max(combinedBounds[3], maxx); - combinedBounds[4] = std::max(combinedBounds[4], maxy); - combinedBounds[5] = std::max(combinedBounds[5], maxz); - } - } - } - - return combinedBounds; -} - -// Multi-object graphics management -std::shared_ptr SceneGraph::addGraphics(const std::string &name, - const cvc::geometry &geom) { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Check if name already exists - if (m_graphicsNodes.find(name) != m_graphicsNodes.end()) { - volrover3::app().log(0, "SceneGraph::addGraphics: Graphics object '" + name + - "' already exists, replacing"); - removeGraphics(name); - } - - // Create new geometry node using template factory (automatically creates proper state path) - auto graphicsNode = m_graphicsRoot->addGraphicsChild(name); - graphicsNode->setGeometry(geom); - - // Add to lookup map - m_graphicsNodes[name] = graphicsNode; - - // Remove null graphic since we now have real graphics - removeNullGraphicIfPresent(); - - // Notify dialogs that children collection has changed - try { - m_graphicsRoot->getState("children").touch(); - } catch (...) { - // State might not exist yet - } - - // Emit signal for dialogs - graphicsChanged(); - - return graphicsNode; -} - -std::shared_ptr SceneGraph::addGraphics(const std::string &name) { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Check if name already exists - if (m_graphicsNodes.find(name) != m_graphicsNodes.end()) { - volrover3::app().log(0, "SceneGraph::addGraphics: Graphics object '" + name + - "' already exists, replacing"); - removeGraphics(name); - } - - // Create new empty geometry node using template factory (automatically creates proper state path) - auto graphicsNode = m_graphicsRoot->addGraphicsChild(name); - - // Add to lookup map - m_graphicsNodes[name] = graphicsNode; - - // Remove null graphic since we now have real graphics - removeNullGraphicIfPresent(); - - // Notify dialogs that children collection has changed - try { - m_graphicsRoot->getState("children").touch(); - } catch (...) { - // State might not exist yet - } - - // Emit signal for dialogs - graphicsChanged(); - - return graphicsNode; -} - -bool SceneGraph::hasGraphics(const std::string &name) const { - return m_graphicsNodes.find(name) != m_graphicsNodes.end(); -} - -void SceneGraph::removeGraphics(const std::string &name) { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - auto it = m_graphicsNodes.find(name); - if (it == m_graphicsNodes.end()) { - volrover3::app().log(0, "SceneGraph::removeGraphics: Graphics object '" + name + "' not found"); - return; - } - - auto graphicsNode = it->second; - - // Remove from graphics root - m_graphicsRoot->removeGraphicsChild(graphicsNode); - - // Remove from lookup map - m_graphicsNodes.erase(it); - - // Explicitly notify state tree that children have changed - // This triggers dataChanged signals that dialogs are listening to - try { - m_graphicsRoot->getState("children").touch(); - } catch (...) { - // State might not exist yet during initialization - } - - // Emit signal for dialogs - graphicsChanged(); - - // If scene is now empty, add null graphic back - ensureNullGraphicIfEmpty(); - - // Note: No manual sync needed - state_object handles state tree automatically -} - -std::shared_ptr SceneGraph::getGraphics(const std::string &name) { - auto it = m_graphicsNodes.find(name); - if (it != m_graphicsNodes.end()) { - return it->second; - } - return nullptr; -} - -void SceneGraph::registerGraphics(const std::string &name, std::shared_ptr node) { - if (node) { - m_graphicsNodes[name] = node; - } -} - -// Volume graphics management -std::shared_ptr SceneGraph::addGraphics(const std::string &name, - const cvc::volume &vol) { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - // Check if name already exists - if (m_graphicsNodes.find(name) != m_graphicsNodes.end()) { - volrover3::app().log(0, "SceneGraph::addGraphics: Volume '" + name + - "' already exists, replacing"); - removeGraphics(name); - } - - // Create new volume node using template factory (automatically creates proper state path) - auto volumeNode = m_graphicsRoot->addGraphicsChild(name); - volumeNode->setVolume(vol); - - // Add to lookup map - m_graphicsNodes[name] = volumeNode; - - // Remove null graphic since we now have real graphics - removeNullGraphicIfPresent(); - - // Update multi-volume rendering if needed - updateVolumeRendering(); - - // Notify dialogs that children collection has changed - try { - m_graphicsRoot->getState("children").touch(); - } catch (...) { - // State might not exist yet - } - - // Emit signal for dialogs - graphicsChanged(); - - return volumeNode; -} - -cvc::bounding_box SceneGraph::computeVolumeBounds() const { - cvc::bounding_box combinedBounds; - bool first = true; - - // Helper function to process volume graphics nodes recursively - std::function &)> processBounds = - [&](const std::shared_ptr &node) { - if (!node) - return; - - // Check if this is a VolumeNode - if (auto volNode = std::dynamic_pointer_cast(node)) { - // Get volume if available - if (volNode->hasVolume() && volNode->getVolume()) { - cvc::bounding_box volBounds = volNode->getVolume()->boundingBox(); - - if (first) { - combinedBounds = volBounds; - first = false; - } else { - // Expand to include this volume - combinedBounds[0] = std::min(combinedBounds[0], volBounds[0]); - combinedBounds[1] = std::min(combinedBounds[1], volBounds[1]); - combinedBounds[2] = std::min(combinedBounds[2], volBounds[2]); - combinedBounds[3] = std::max(combinedBounds[3], volBounds[3]); - combinedBounds[4] = std::max(combinedBounds[4], volBounds[4]); - combinedBounds[5] = std::max(combinedBounds[5], volBounds[5]); - } - } - } - - // Process children recursively - for (const auto &child : node->getGraphicsChildren()) { - processBounds(child); - } - }; - - // Start from unified graphics root (includes volumes) - if (m_graphicsRoot) { - processBounds(m_graphicsRoot); - } - - return combinedBounds; -} - -void SceneGraph::enableMultiVolumeRendering(bool enable) { - if (m_multiVolumeRenderingEnabled == enable) { - return; // No change - } - - m_multiVolumeRenderingEnabled = enable; - - if (enable) { - setupMultiVolumeRendering(); - } else { - teardownMultiVolumeRendering(); - } -} - -bool SceneGraph::isMultiVolumeRenderingEnabled() const { return m_multiVolumeRenderingEnabled; } - -void SceneGraph::setupMultiVolumeRendering() { - if (!m_renderer) { - return; - } - - // Create multi-volume if not already created - if (!m_multiVolume) { - m_multiVolume = vtkSmartPointer::New(); - } - - // Collect all volume graphics nodes - auto allVolumes = getAllVolumeGraphics(); - - if (allVolumes.size() <= 1) { - return; // No need for multi-volume rendering with 0 or 1 volume - } - - // TODO: Implement proper multi-volume rendering with GraphicsNode architecture - // For now, individual volumes are rendered separately - // Remove individual volume props from renderer - /* - for (const auto& volNode : allVolumes) { - volNode->removeFromRenderer(m_renderer); - } - - // Add all volumes to the multi-volume - int port = 0; - for (const auto& volNode : allVolumes) { - // Note: vtkMultiVolume SetVolume takes a port number, not a transform - // Transforms should be already applied to individual vtkVolume actors - m_multiVolume->SetVolume(vol, port++); - } - - // Add multi-volume to renderer - m_renderer->AddViewProp(m_multiVolume); - */ -} - -void SceneGraph::teardownMultiVolumeRendering() { - if (!m_renderer || !m_multiVolume) { - return; - } - - // TODO: Implement proper multi-volume teardown with GraphicsNode architecture - // For now, individual volumes are rendered separately - /* - // Remove multi-volume from renderer - m_renderer->RemoveViewProp(m_multiVolume); - - // Re-add individual volume props - auto allVolumes = getAllVolumeGraphics(); - for (const auto& volNode : allVolumes) { - volNode->addToRenderer(m_renderer); - } - */ -} - -void SceneGraph::updateVolumeRendering() { - if (!m_renderer) { - return; - } - - size_t volumeCount = getVolumeGraphicsCount(); - - // Enable multi-volume rendering if we have more than 1 volume - if (volumeCount > 1 && !m_multiVolumeRenderingEnabled) { - enableMultiVolumeRendering(true); - } else if (volumeCount <= 1 && m_multiVolumeRenderingEnabled) { - enableMultiVolumeRendering(false); - } -} - -void SceneGraph::ensureNullGraphicIfEmpty() { - // With new architecture: NullGraphicNode IS the graphics root, always present - // No need to add/remove it -} - -void SceneGraph::removeNullGraphicIfPresent() { - // With new architecture: NullGraphicNode IS the graphics root, always present - // No need to add/remove it -} diff --git a/src/volrover3/SceneNode.cpp b/src/volrover3/SceneNode.cpp deleted file mode 100644 index b8beacc6..00000000 --- a/src/volrover3/SceneNode.cpp +++ /dev/null @@ -1,162 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -// Static member for main thread callback (DEPRECATED - use SceneGraph event queue) -SceneNode::MainThreadCallback SceneNode::s_mainThreadCallback; - -void SceneNode::setMainThreadCallback(MainThreadCallback callback) { - s_mainThreadCallback = callback; -} - -void SceneNode::setSceneGraph(SceneGraph *sceneGraph) { - m_sceneGraph = sceneGraph; - - // Enable threading now that we have a SceneGraph for event posting - // If sceneGraph is null (cleanup), use static threading setting - if (sceneGraph) { - setInstanceThreading(state_object::getUseThreading()); - } else { - clearInstanceThreading(); - } - - // Propagate to all children - for (auto &child : m_children) { - child->setSceneGraph(sceneGraph); - } -} - -void SceneNode::runOnMainThread(std::function func) { - // If threading is disabled (tests or during construction), execute immediately - if (!getInstanceThreading()) { - func(); - return; - } - - // Try node's SceneGraph event queue first (production with threading) - if (m_sceneGraph) { - m_sceneGraph->postEvent(std::move(func)); - return; - } - - // Fallback to old callback system (for Qt-based scenarios without SceneGraph) - if (s_mainThreadCallback) { - s_mainThreadCallback(func); - return; - } - - // No queue or callback available - execute immediately as last resort - // This should rarely happen in production - func(); -} - -SceneNode::SceneNode(cvc::app &ctx, const std::string &statePath) - : state_object(ctx, statePath), m_visible(true), m_renderer(nullptr), - m_sceneGraph(nullptr) { - // Disable threading for this instance during construction - // Will be enabled when SceneGraph reference is set - setInstanceThreading(false); - // Initialize visible state - if (!statePath.empty()) { - getState("visible").value(1); // Default to visible - } -} - -SceneNode::~SceneNode() { - // Disconnect from state tree before derived class destructor completes - // to prevent pure virtual method calls during destruction - disconnectState(); -} - -void SceneNode::addToRenderer(vtkRenderer *renderer) { - m_renderer = renderer; - // Capture the prop pointer before queuing the lambda - vtkProp *prop = m_visible ? getProp() : nullptr; - if (prop) { - // Wrap VTK operation in runOnMainThread - runOnMainThread([prop, renderer]() { renderer->AddViewProp(prop); }); - } - - for (auto &child : m_children) { - child->addToRenderer(renderer); - } -} - -void SceneNode::removeFromRenderer(vtkRenderer *renderer) { - // Capture the prop pointer before queuing the lambda to avoid accessing 'this' - // after the node might be deleted - vtkProp *prop = getProp(); - if (prop) { - // Wrap VTK operation in runOnMainThread - runOnMainThread([prop, renderer]() { renderer->RemoveViewProp(prop); }); - } - - for (auto &child : m_children) { - child->removeFromRenderer(renderer); - } - - m_renderer = nullptr; -} - -void SceneNode::update() { - for (auto &child : m_children) { - child->update(); - } -} - -void SceneNode::setVisible(bool visible) { - if (m_visible == visible) - return; - - m_visible = visible; - - if (m_renderer && getProp()) { - // Wrap VTK operations in runOnMainThread for thread safety - runOnMainThread([this, visible]() { - vtkProp *prop = getProp(); - if (m_renderer && prop) { - if (visible) { - m_renderer->AddViewProp(prop); - } else { - m_renderer->RemoveViewProp(prop); - } - } - }); - } - - for (auto &child : m_children) { - child->setVisible(visible); - } -} - -void SceneNode::addChild(std::shared_ptr child) { - m_children.push_back(child); - if (m_renderer) { - child->addToRenderer(m_renderer); - } -} - -void SceneNode::removeChild(std::shared_ptr child) { - auto it = std::find(m_children.begin(), m_children.end(), child); - if (it != m_children.end()) { - if (m_renderer) { - (*it)->removeFromRenderer(m_renderer); - } - m_children.erase(it); - } -} - -void SceneNode::handleStateChanged(const std::string &childState) { - // Marshal to main thread via event queue - runOnMainThread([this, childState]() { - // Handle visible state changes - if (childState == "visible") { - int visible = getState("visible").value(); - setVisible(visible != 0); - } - }); -} diff --git a/src/volrover3/StateDashboardWidget.cpp b/src/volrover3/StateDashboardWidget.cpp deleted file mode 100644 index e3304557..00000000 --- a/src/volrover3/StateDashboardWidget.cpp +++ /dev/null @@ -1,810 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cvc; -using namespace cvc::state_exec; - -// ═══════════════════════════════════════════════════════════════════════════ -// Construction -// ═══════════════════════════════════════════════════════════════════════════ - -StateDashboardWidget::StateDashboardWidget(QWidget *parent) : QWidget(parent) { - auto *layout = new QVBoxLayout(this); - layout->setContentsMargins(4, 4, 4, 4); - - auto *tabs = new QTabWidget(this); - buildStateTreeTab(tabs); - buildExecConsoleTab(tabs); - buildClusterTab(tabs); - layout->addWidget(tabs); -} - -StateDashboardWidget::~StateDashboardWidget() { - m_valueConn.disconnect(); - m_treeConn.disconnect(); - m_destroyConn.disconnect(); - - if (m_processRefreshTimer) - m_processRefreshTimer->stop(); - if (m_clusterRefreshTimer) - m_clusterRefreshTimer->stop(); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Public setters -// ═══════════════════════════════════════════════════════════════════════════ - -void StateDashboardWidget::setRootState(state *root) { - m_valueConn.disconnect(); - m_treeConn.disconnect(); - m_destroyConn.disconnect(); - m_currentState = nullptr; - - m_rootState = root; - if (m_rootState) { - m_treeConn = m_rootState->childChanged.connect([this](const std::string &) { - QMetaObject::invokeMethod(this, "onTreeStructureChanged", Qt::QueuedConnection); - }); - } - refreshStateTree(); -} - -void StateDashboardWidget::setScheduler(scheduler *sched) { - m_scheduler = sched; - refreshProcessList(); -} - -void StateDashboardWidget::setShard(state_cluster_shard *shard) { m_shard = shard; } - -void StateDashboardWidget::setMembership(state_cluster_membership *membership) { - m_membership = membership; - refreshClusterInfo(); -} - -void StateDashboardWidget::setCoordinator(exec_coordinator *coord) { m_coordinator = coord; } - -void StateDashboardWidget::setTelemetryAggregator(state_telemetry_aggregator *agg) { - m_telemetryAgg = agg; -} - -void StateDashboardWidget::refresh() { - refreshStateTree(); - refreshProcessList(); - refreshClusterInfo(); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Tab 1 — State Tree -// ═══════════════════════════════════════════════════════════════════════════ - -void StateDashboardWidget::buildStateTreeTab(QTabWidget *tabs) { - auto *page = new QWidget; - auto *layout = new QVBoxLayout(page); - layout->setContentsMargins(2, 2, 2, 2); - - // Search bar - m_treeSearch = new QLineEdit; - m_treeSearch->setPlaceholderText(tr("Filter state tree...")); - connect(m_treeSearch, &QLineEdit::textChanged, this, - &StateDashboardWidget::onTreeSearchTextChanged); - layout->addWidget(m_treeSearch); - - // Splitter: tree left, properties right - auto *splitter = new QSplitter(Qt::Horizontal); - - // Tree widget - m_treeWidget = new QTreeWidget; - m_treeWidget->setHeaderLabel(tr("State Tree")); - m_treeWidget->setSelectionMode(QAbstractItemView::SingleSelection); - connect(m_treeWidget, &QTreeWidget::itemSelectionChanged, this, - &StateDashboardWidget::onTreeItemSelected); - splitter->addWidget(m_treeWidget); - - // Right side: property table + buttons - auto *rightPanel = new QWidget; - auto *rightLayout = new QVBoxLayout(rightPanel); - rightLayout->setContentsMargins(0, 0, 0, 0); - - m_propertyTable = new QTableWidget; - m_propertyTable->setColumnCount(2); - m_propertyTable->setHorizontalHeaderLabels({tr("Property"), tr("Value")}); - m_propertyTable->horizontalHeader()->setStretchLastSection(true); - m_propertyTable->verticalHeader()->hide(); - m_propertyTable->setEditTriggers(QAbstractItemView::DoubleClicked | - QAbstractItemView::SelectedClicked); - connect(m_propertyTable, &QTableWidget::cellChanged, this, - &StateDashboardWidget::onPropertyValueChanged); - rightLayout->addWidget(m_propertyTable); - - auto *btnBar = new QHBoxLayout; - m_addStateBtn = new QPushButton(tr("Add State")); - m_deleteStateBtn = new QPushButton(tr("Delete State")); - m_deleteStateBtn->setEnabled(false); - connect(m_addStateBtn, &QPushButton::clicked, this, &StateDashboardWidget::onAddStateClicked); - connect(m_deleteStateBtn, &QPushButton::clicked, this, - &StateDashboardWidget::onDeleteStateClicked); - btnBar->addWidget(m_addStateBtn); - btnBar->addWidget(m_deleteStateBtn); - btnBar->addStretch(); - rightLayout->addLayout(btnBar); - - splitter->addWidget(rightPanel); - splitter->setStretchFactor(0, 1); - splitter->setStretchFactor(1, 1); - layout->addWidget(splitter); - - tabs->addTab(page, tr("State Tree")); -} - -void StateDashboardWidget::refreshStateTree() { - m_treeWidget->clear(); - if (!m_rootState) - return; - - auto *root = new QTreeWidgetItem(m_treeWidget); - root->setText(0, QString::fromStdString(m_rootState->name())); - root->setData(0, Qt::UserRole, QVariant::fromValue(static_cast(m_rootState))); - populateTree(root, m_rootState); - root->setExpanded(true); -} - -void StateDashboardWidget::populateTree(QTreeWidgetItem *parentItem, state *s) { - auto childPaths = s->children(); - std::string parentFull = s->fullName(); - - for (auto &childFullName : childPaths) { - // Only immediate children: relative path has no dots - std::string rel = childFullName.substr(parentFull.size()); - if (!rel.empty() && rel[0] == '.') - rel = rel.substr(1); - if (rel.empty() || rel.find('.') != std::string::npos) - continue; - - auto &child = (*s)(rel); - if (!child.initialized()) - continue; - - auto *item = new QTreeWidgetItem(parentItem); - item->setText(0, QString::fromStdString(rel)); - item->setData(0, Qt::UserRole, QVariant::fromValue(static_cast(&child))); - populateTree(item, &child); - } -} - -void StateDashboardWidget::onTreeItemSelected() { - m_valueConn.disconnect(); - m_destroyConn.disconnect(); - m_currentState = nullptr; - - auto items = m_treeWidget->selectedItems(); - if (items.isEmpty()) { - m_propertyTable->setRowCount(0); - m_deleteStateBtn->setEnabled(false); - return; - } - - auto *ptr = static_cast(items[0]->data(0, Qt::UserRole).value()); - if (!ptr) - return; - - m_currentState = ptr; - m_deleteStateBtn->setEnabled(true); - - // Listen for value changes on the selected node - m_valueConn = m_currentState->valueChanged.connect( - [this]() { QMetaObject::invokeMethod(this, "onCurrentStateChanged", Qt::QueuedConnection); }); - m_destroyConn = m_currentState->destroyed.connect([this]() { - QMetaObject::invokeMethod(this, "onCurrentStateDestroyed", Qt::QueuedConnection); - }); - - populateProperties(m_currentState); -} - -void StateDashboardWidget::populateProperties(state *s) { - m_propertyTable->blockSignals(true); - m_propertyTable->setRowCount(0); - - auto addRow = [this](const QString &key, const QString &val, bool editable = false) { - int row = m_propertyTable->rowCount(); - m_propertyTable->insertRow(row); - auto *keyItem = new QTableWidgetItem(key); - keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); - m_propertyTable->setItem(row, 0, keyItem); - auto *valItem = new QTableWidgetItem(val); - if (!editable) - valItem->setFlags(valItem->flags() & ~Qt::ItemIsEditable); - m_propertyTable->setItem(row, 1, valItem); - }; - - addRow(tr("Name"), QString::fromStdString(s->name())); - addRow(tr("Full Path"), QString::fromStdString(s->fullName())); - addRow(tr("Value"), QString::fromStdString(s->value()), !s->readOnly()); - addRow(tr("Value Type"), QString::fromStdString(s->valueTypeName())); - addRow(tr("Data Type"), QString::fromStdString(s->dataTypeName())); - addRow(tr("Read Only"), s->readOnly() ? tr("true") : tr("false")); - addRow(tr("Hidden"), s->hidden() ? tr("true") : tr("false")); - addRow(tr("Comment"), QString::fromStdString(s->comment()), true); - - // Last modified - auto lastMod = s->lastMod(); - if (!lastMod.is_not_a_date_time()) - addRow(tr("Last Modified"), - QString::fromStdString(boost::posix_time::to_simple_string(lastMod))); - else - addRow(tr("Last Modified"), tr("(unknown)")); - - addRow(tr("Children"), QString::number(s->numChildren())); - addRow(tr("Initialized"), s->initialized() ? tr("true") : tr("false")); - - // Link info - addRow(tr("Is Link"), s->isLink() ? tr("true") : tr("false")); - if (s->isLink()) { - addRow(tr("Link Target"), QString::fromStdString(s->linkTarget())); - addRow(tr("Link Mode"), - s->linkMode() == state::link_mode::transparent ? tr("transparent") : tr("opaque")); - addRow(tr("Link Writable"), s->linkWritable() ? tr("true") : tr("false")); - - auto res = s->resolveLink(); - QString kindStr; - switch (res.kind) { - case state::link_resolution_kind::resolved: - kindStr = tr("resolved"); - break; - case state::link_resolution_kind::cycle_detected: - kindStr = tr("cycle"); - break; - case state::link_resolution_kind::budget_exhausted: - kindStr = tr("budget exhausted"); - break; - case state::link_resolution_kind::broken: - kindStr = tr("broken"); - break; - case state::link_resolution_kind::none: - kindStr = tr("none"); - break; - } - addRow(tr("Link Resolution"), kindStr); - addRow(tr("Resolved Value"), QString::fromStdString(s->resolvedValue())); - } - - // Expiry info - addRow(tr("Has Expiry"), s->hasExpiry() ? tr("true") : tr("false")); - if (s->hasExpiry()) { - auto exp = s->expiryTime(); - addRow(tr("Expiry Time"), QString::fromStdString(boost::posix_time::to_simple_string(exp))); - addRow(tr("Expired"), s->isExpired() ? tr("true") : tr("false")); - } - - m_propertyTable->blockSignals(false); -} - -void StateDashboardWidget::onPropertyValueChanged(int row, int column) { - if (column != 1 || !m_currentState) - return; - - auto *keyItem = m_propertyTable->item(row, 0); - if (!keyItem) - return; - - QString key = keyItem->text(); - QString val = m_propertyTable->item(row, 1)->text(); - - if (key == tr("Value") && !m_currentState->readOnly()) { - setStateValue(m_currentState, val); - emit stateChanged(); - } else if (key == tr("Comment")) { - m_currentState->comment(val.toStdString()); - } -} - -void StateDashboardWidget::onTreeSearchTextChanged(const QString &text) { - // Show/hide tree items based on filter text - std::function filterItem = [&](QTreeWidgetItem *item) -> bool { - bool childVisible = false; - for (int i = 0; i < item->childCount(); ++i) { - if (filterItem(item->child(i))) - childVisible = true; - } - bool selfMatch = text.isEmpty() || item->text(0).contains(text, Qt::CaseInsensitive); - bool visible = selfMatch || childVisible; - item->setHidden(!visible); - if (visible && !text.isEmpty()) - item->setExpanded(true); - return visible; - }; - - for (int i = 0; i < m_treeWidget->topLevelItemCount(); ++i) - filterItem(m_treeWidget->topLevelItem(i)); -} - -void StateDashboardWidget::onAddStateClicked() { - bool ok; - QString path = QInputDialog::getText(this, tr("Add State"), tr("State path (dot-separated):"), - QLineEdit::Normal, QString(), &ok); - if (!ok || path.isEmpty()) - return; - - std::string pathStr = path.toStdString(); - if (!state::isValidStateName(pathStr)) { - std::string sanitized = state::sanitizeStateName(pathStr); - if (sanitized.empty()) { - QMessageBox::warning(this, tr("Invalid Path"), tr("The path is not valid.")); - return; - } - pathStr = sanitized; - } - - state *parent = m_currentState ? m_currentState : m_rootState; - if (!parent) - return; - - (*parent)(pathStr).value(std::string("")); - emit stateChanged(); -} - -void StateDashboardWidget::onDeleteStateClicked() { - if (!m_currentState || m_currentState == m_rootState) - return; - - auto reply = QMessageBox::question(this, tr("Delete State"), - tr("Reset state '%1' and all children?") - .arg(QString::fromStdString(m_currentState->fullName())), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - m_currentState->reset(); - emit stateChanged(); - } -} - -void StateDashboardWidget::onTreeStructureChanged() { refreshStateTree(); } - -void StateDashboardWidget::onCurrentStateChanged() { - if (m_currentState) - populateProperties(m_currentState); -} - -void StateDashboardWidget::onCurrentStateDestroyed() { - m_valueConn.disconnect(); - m_destroyConn.disconnect(); - m_currentState = nullptr; - m_propertyTable->setRowCount(0); - m_deleteStateBtn->setEnabled(false); - refreshStateTree(); -} - -std::string StateDashboardWidget::getStateValue(state *s) { return s->value(); } - -void StateDashboardWidget::setStateValue(state *s, const QString &valueStr) { - std::string typeName = s->valueTypeName(); - std::string v = valueStr.toStdString(); - - if (typeName == "double") - s->value(valueStr.toDouble()); - else if (typeName == "float") - s->value(valueStr.toFloat()); - else if (typeName == "int") - s->value(valueStr.toInt()); - else if (typeName == "unsigned int") - s->value(static_cast(valueStr.toUInt())); - else if (typeName == "bool") - s->value(v == "true" || v == "1"); - else if (typeName == "long") - s->value(valueStr.toLong()); - else if (typeName == "unsigned long" || typeName == "size_t") - s->value(static_cast(valueStr.toULong())); - else - s->value(v); -} - -QTreeWidgetItem *StateDashboardWidget::findTreeItem(QTreeWidgetItem *parent, state *target) { - for (int i = 0; i < parent->childCount(); ++i) { - auto *child = parent->child(i); - auto *ptr = static_cast(child->data(0, Qt::UserRole).value()); - if (ptr == target) - return child; - auto *found = findTreeItem(child, target); - if (found) - return found; - } - return nullptr; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Tab 2 — State Exec Console -// ═══════════════════════════════════════════════════════════════════════════ - -void StateDashboardWidget::buildExecConsoleTab(QTabWidget *tabs) { - auto *page = new QWidget; - auto *layout = new QVBoxLayout(page); - layout->setContentsMargins(2, 2, 2, 2); - - auto *splitter = new QSplitter(Qt::Vertical); - - // Top: script editor + run button - auto *editorGroup = new QGroupBox(tr("S-Expression Script")); - auto *editorLayout = new QVBoxLayout(editorGroup); - - m_scriptEditor = new QPlainTextEdit; - m_scriptEditor->setPlaceholderText(tr("Enter state_exec program...\ne.g. (+ 1 2 3)")); - QFont mono("monospace"); - mono.setStyleHint(QFont::Monospace); - m_scriptEditor->setFont(mono); - m_scriptEditor->setTabStopDistance(QFontMetrics(mono).horizontalAdvance(' ') * 2); - editorLayout->addWidget(m_scriptEditor); - - auto *editorBtnBar = new QHBoxLayout; - m_runBtn = new QPushButton(tr("Run")); - m_clearOutputBtn = new QPushButton(tr("Clear Output")); - connect(m_runBtn, &QPushButton::clicked, this, &StateDashboardWidget::onRunScriptClicked); - connect(m_clearOutputBtn, &QPushButton::clicked, this, - &StateDashboardWidget::onClearOutputClicked); - editorBtnBar->addWidget(m_runBtn); - editorBtnBar->addWidget(m_clearOutputBtn); - editorBtnBar->addStretch(); - editorLayout->addLayout(editorBtnBar); - splitter->addWidget(editorGroup); - - // Middle: output panel - auto *outputGroup = new QGroupBox(tr("Output")); - auto *outputLayout = new QVBoxLayout(outputGroup); - m_outputPanel = new QPlainTextEdit; - m_outputPanel->setReadOnly(true); - m_outputPanel->setFont(mono); - outputLayout->addWidget(m_outputPanel); - splitter->addWidget(outputGroup); - - // Bottom: process list - auto *processGroup = new QGroupBox(tr("Processes")); - auto *processLayout = new QVBoxLayout(processGroup); - - m_processTable = new QTableWidget; - m_processTable->setColumnCount(8); - m_processTable->setHorizontalHeaderLabels({tr("PID"), tr("Name"), tr("Status"), tr("Priority"), - tr("UID"), tr("Steps"), tr("Elapsed"), tr("Memory")}); - m_processTable->horizontalHeader()->setStretchLastSection(true); - m_processTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_processTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - connect(m_processTable, &QTableWidget::itemSelectionChanged, this, - &StateDashboardWidget::onProcessTableSelectionChanged); - processLayout->addWidget(m_processTable); - - auto *procBtnBar = new QHBoxLayout; - m_pauseBtn = new QPushButton(tr("Pause")); - m_resumeBtn = new QPushButton(tr("Resume")); - m_killBtn = new QPushButton(tr("Kill")); - m_pauseBtn->setEnabled(false); - m_resumeBtn->setEnabled(false); - m_killBtn->setEnabled(false); - connect(m_pauseBtn, &QPushButton::clicked, this, &StateDashboardWidget::onPauseProcessClicked); - connect(m_resumeBtn, &QPushButton::clicked, this, &StateDashboardWidget::onResumeProcessClicked); - connect(m_killBtn, &QPushButton::clicked, this, &StateDashboardWidget::onKillProcessClicked); - procBtnBar->addWidget(m_pauseBtn); - procBtnBar->addWidget(m_resumeBtn); - procBtnBar->addWidget(m_killBtn); - procBtnBar->addStretch(); - processLayout->addLayout(procBtnBar); - - splitter->addWidget(processGroup); - splitter->setStretchFactor(0, 2); - splitter->setStretchFactor(1, 1); - splitter->setStretchFactor(2, 2); - layout->addWidget(splitter); - - // Timer for auto-refreshing process list - m_processRefreshTimer = new QTimer(this); - m_processRefreshTimer->setInterval(1000); - connect(m_processRefreshTimer, &QTimer::timeout, this, &StateDashboardWidget::refreshProcessList); - m_processRefreshTimer->start(); - - tabs->addTab(page, tr("Exec Console")); -} - -void StateDashboardWidget::onRunScriptClicked() { - QString script = m_scriptEditor->toPlainText().trimmed(); - if (script.isEmpty()) - return; - - m_outputPanel->appendPlainText(QStringLiteral("> ") + script); - - try { - auto env = builtins::make_default_environment(); - evaluator ev(env); - auto result = ev.evaluate_script(script.toStdString()); - m_outputPanel->appendPlainText(QString::fromStdString(to_string(result))); - } catch (const std::exception &e) { - m_outputPanel->appendPlainText(QStringLiteral("ERROR: ") + QString::fromUtf8(e.what())); - } - - // Also submit to scheduler if available - if (m_scheduler) { - refreshProcessList(); - } -} - -void StateDashboardWidget::onClearOutputClicked() { m_outputPanel->clear(); } - -void StateDashboardWidget::refreshProcessList() { - if (!m_scheduler) { - m_processTable->setRowCount(0); - return; - } - - auto procs = m_scheduler->list_processes(); - m_processTable->setRowCount(static_cast(procs.size())); - - for (int i = 0; i < static_cast(procs.size()); ++i) { - auto &p = procs[static_cast(i)]; - auto setCell = [this, i](int col, const QString &text) { - auto *item = new QTableWidgetItem(text); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - m_processTable->setItem(i, col, item); - }; - - setCell(0, QString::number(p.pid)); - setCell(1, QString::fromStdString(p.name)); - - QString statusStr; - switch (p.status) { - case process_status::ready: - statusStr = tr("ready"); - break; - case process_status::running: - statusStr = tr("running"); - break; - case process_status::paused: - statusStr = tr("paused"); - break; - case process_status::waiting: - statusStr = tr("waiting"); - break; - case process_status::terminated: - statusStr = tr("terminated"); - break; - case process_status::killed: - statusStr = tr("killed"); - break; - } - setCell(2, statusStr); - setCell(3, QString::number(p.priority)); - setCell(4, QString::fromStdString(p.uid)); - setCell(5, QString::number(p.step_count)); - setCell(6, QString::number(p.elapsed_time, 'f', 3) + tr("s")); - - // Memory in human-readable format - QString memStr; - if (p.current_memory >= 1024 * 1024) - memStr = QString::number(p.current_memory / (1024.0 * 1024.0), 'f', 1) + tr(" MB"); - else if (p.current_memory >= 1024) - memStr = QString::number(p.current_memory / 1024.0, 'f', 1) + tr(" KB"); - else - memStr = QString::number(p.current_memory) + tr(" B"); - setCell(7, memStr); - } -} - -void StateDashboardWidget::onProcessTableSelectionChanged() { - bool hasSelection = !m_processTable->selectedItems().isEmpty(); - m_pauseBtn->setEnabled(hasSelection); - m_resumeBtn->setEnabled(hasSelection); - m_killBtn->setEnabled(hasSelection); -} - -int getSelectedPid(QTableWidget *table) { - auto items = table->selectedItems(); - if (items.isEmpty()) - return -1; - return table->item(items[0]->row(), 0)->text().toInt(); -} - -void StateDashboardWidget::onPauseProcessClicked() { - if (!m_scheduler) - return; - int pid = getSelectedPid(m_processTable); - if (pid >= 0) { - m_scheduler->pause(pid); - refreshProcessList(); - } -} - -void StateDashboardWidget::onResumeProcessClicked() { - if (!m_scheduler) - return; - int pid = getSelectedPid(m_processTable); - if (pid >= 0) { - m_scheduler->resume(pid); - refreshProcessList(); - } -} - -void StateDashboardWidget::onKillProcessClicked() { - if (!m_scheduler) - return; - int pid = getSelectedPid(m_processTable); - if (pid >= 0) { - m_scheduler->kill(pid); - refreshProcessList(); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Tab 3 — Cluster & Networking -// ═══════════════════════════════════════════════════════════════════════════ - -void StateDashboardWidget::buildClusterTab(QTabWidget *tabs) { - auto *page = new QWidget; - auto *layout = new QVBoxLayout(page); - layout->setContentsMargins(2, 2, 2, 2); - - // Node identity - auto *idGroup = new QGroupBox(tr("Local Node")); - auto *idLayout = new QVBoxLayout(idGroup); - m_nodeIdLabel = new QLabel(tr("Node ID: (not connected)")); - m_clusterIdLabel = new QLabel(tr("Cluster ID: (none)")); - m_leaderLabel = new QLabel(tr("Leader: (unknown)")); - idLayout->addWidget(m_nodeIdLabel); - idLayout->addWidget(m_clusterIdLabel); - idLayout->addWidget(m_leaderLabel); - layout->addWidget(idGroup); - - // Peer list - auto *peerGroup = new QGroupBox(tr("Peers")); - auto *peerLayout = new QVBoxLayout(peerGroup); - - m_peerTable = new QTableWidget; - m_peerTable->setColumnCount(4); - m_peerTable->setHorizontalHeaderLabels( - {tr("Node ID"), tr("Endpoint"), tr("State"), tr("Last Heartbeat")}); - m_peerTable->horizontalHeader()->setStretchLastSection(true); - m_peerTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_peerTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - peerLayout->addWidget(m_peerTable); - - // Connect to peer - auto *connectBar = new QHBoxLayout; - m_peerEndpointInput = new QLineEdit; - m_peerEndpointInput->setPlaceholderText(tr("host:port")); - m_connectPeerBtn = new QPushButton(tr("Connect")); - connect(m_connectPeerBtn, &QPushButton::clicked, this, - &StateDashboardWidget::onConnectPeerClicked); - connectBar->addWidget(m_peerEndpointInput); - connectBar->addWidget(m_connectPeerBtn); - peerLayout->addLayout(connectBar); - layout->addWidget(peerGroup); - - // Stats - auto *statsGroup = new QGroupBox(tr("Cluster Statistics")); - auto *statsLayout = new QVBoxLayout(statsGroup); - m_busStatsLabel = new QLabel(tr("Message Bus: (no shard)")); - m_shardStatsLabel = new QLabel(tr("Shard: (no shard)")); - m_telemetryLabel = new QLabel(tr("Telemetry: (none)")); - m_busStatsLabel->setWordWrap(true); - m_shardStatsLabel->setWordWrap(true); - m_telemetryLabel->setWordWrap(true); - statsLayout->addWidget(m_busStatsLabel); - statsLayout->addWidget(m_shardStatsLabel); - statsLayout->addWidget(m_telemetryLabel); - layout->addWidget(statsGroup); - - layout->addStretch(); - - // Timer for auto-refreshing cluster info - m_clusterRefreshTimer = new QTimer(this); - m_clusterRefreshTimer->setInterval(2000); - connect(m_clusterRefreshTimer, &QTimer::timeout, this, &StateDashboardWidget::refreshClusterInfo); - m_clusterRefreshTimer->start(); - - tabs->addTab(page, tr("Cluster")); -} - -void StateDashboardWidget::onConnectPeerClicked() { - QString endpoint = m_peerEndpointInput->text().trimmed(); - if (endpoint.isEmpty()) - return; - - if (!m_membership) { - QMessageBox::information(this, tr("Not Connected"), - tr("No cluster membership manager configured.")); - return; - } - - // Register the peer with the endpoint; the membership manager will - // initiate heartbeats and the peer will be integrated into the cluster. - std::string nodeId = "peer-" + endpoint.toStdString(); - m_membership->register_peer(nodeId, m_membership->cluster_id(), endpoint.toStdString()); - m_peerEndpointInput->clear(); - refreshClusterInfo(); -} - -void StateDashboardWidget::refreshClusterInfo() { - // Identity - if (m_membership) { - m_nodeIdLabel->setText( - tr("Node ID: %1").arg(QString::fromStdString(m_membership->local_node_id()))); - m_clusterIdLabel->setText( - tr("Cluster ID: %1").arg(QString::fromStdString(m_membership->cluster_id()))); - } else if (m_shard) { - m_nodeIdLabel->setText(tr("Node ID: %1").arg(QString::fromStdString(m_shard->local_node_id()))); - m_clusterIdLabel->setText( - tr("Cluster ID: %1").arg(QString::fromStdString(m_shard->cluster_id()))); - } - - // Peer table - if (m_membership) { - auto peers = m_membership->peer_snapshot(); - m_peerTable->setRowCount(static_cast(peers.size())); - for (int i = 0; i < static_cast(peers.size()); ++i) { - auto &p = peers[static_cast(i)]; - auto setCell = [this, i](int col, const QString &text) { - auto *item = new QTableWidgetItem(text); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - m_peerTable->setItem(i, col, item); - }; - - setCell(0, QString::fromStdString(p.node_id)); - setCell(1, QString::fromStdString(p.endpoint)); - - QString stateStr; - switch (p.state) { - case state_cluster_membership::peer_state::alive: - stateStr = tr("alive"); - break; - case state_cluster_membership::peer_state::suspect: - stateStr = tr("suspect"); - break; - case state_cluster_membership::peer_state::dead: - stateStr = tr("dead"); - break; - } - setCell(2, stateStr); - setCell(3, QString::number(p.last_heartbeat_ns)); - } - } - - // Message bus stats - if (m_shard) { - auto &bus = m_shard->message_bus(); - m_busStatsLabel->setText(tr("Message Bus — admitted: %1, dispatched: %2, duplicates: %3, " - "dropped: %4, subscribers: %5, dedup size: %6") - .arg(bus.total_admitted()) - .arg(bus.total_dispatched()) - .arg(bus.total_duplicates()) - .arg(bus.total_dropped()) - .arg(bus.subscriber_count()) - .arg(bus.dedup_size())); - - m_shardStatsLabel->setText(tr("Shard — attached: %1, remote applied: %2, rejected: %3, " - "conflicts: %4, duplicates: %5") - .arg(m_shard->is_attached() ? tr("yes") : tr("no")) - .arg(m_shard->total_remote_applied()) - .arg(m_shard->total_remote_rejected()) - .arg(m_shard->total_conflicts_detected()) - .arg(m_shard->total_remote_duplicates())); - } - - // Telemetry - if (m_telemetryAgg) { - auto summary = m_telemetryAgg->summarize(); - m_telemetryLabel->setText(tr("Telemetry — nodes: %1, stale: %2, mutations published: %3, " - "applied: %4, messages admitted: %5") - .arg(summary.node_count) - .arg(summary.stale_count) - .arg(summary.total_mutations_published) - .arg(summary.total_mutations_applied) - .arg(summary.total_messages_admitted)); - } -} diff --git a/src/volrover3/StateTreeWidget.cpp b/src/volrover3/StateTreeWidget.cpp deleted file mode 100644 index 58d9e340..00000000 --- a/src/volrover3/StateTreeWidget.cpp +++ /dev/null @@ -1,733 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -StateTreeWidget::StateTreeWidget(QWidget *parent) - : QWidget(parent), m_rootState(nullptr), m_currentState(nullptr) { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Create splitter for tree and table - QSplitter *splitter = new QSplitter(Qt::Vertical, this); - - // Create tree widget for state hierarchy - m_treeWidget = new QTreeWidget(this); - m_treeWidget->setHeaderLabel(tr("State Tree")); - m_treeWidget->setMinimumHeight(200); - connect(m_treeWidget, &QTreeWidget::itemSelectionChanged, this, - &StateTreeWidget::onTreeItemSelected); - splitter->addWidget(m_treeWidget); - - // Create table widget for state properties - m_tableWidget = new QTableWidget(this); - m_tableWidget->setColumnCount(2); - m_tableWidget->setHorizontalHeaderLabels({tr("Property"), tr("Value")}); - m_tableWidget->horizontalHeader()->setStretchLastSection(true); - m_tableWidget->setMinimumHeight(150); - connect(m_tableWidget, &QTableWidget::cellChanged, this, &StateTreeWidget::onTableValueChanged); - splitter->addWidget(m_tableWidget); - - mainLayout->addWidget(splitter); - - // Create button bar - QHBoxLayout *buttonLayout = new QHBoxLayout(); - - m_addButton = new QPushButton(tr("Add State..."), this); - m_deleteButton = new QPushButton(tr("Delete State"), this); - m_deleteButton->setEnabled(false); - - buttonLayout->addWidget(m_addButton); - buttonLayout->addWidget(m_deleteButton); - buttonLayout->addStretch(); - - mainLayout->addLayout(buttonLayout); - - // Connect button signals - connect(m_addButton, &QPushButton::clicked, this, &StateTreeWidget::onAddStateClicked); - connect(m_deleteButton, &QPushButton::clicked, this, &StateTreeWidget::onDeleteStateClicked); - - // Set splitter sizes - splitter->setSizes({300, 200}); -} - -StateTreeWidget::~StateTreeWidget() { - // Disconnect all signals - m_stateChangeConnection.disconnect(); - m_treeChangeConnection.disconnect(); - m_currentStateDestroyedConnection.disconnect(); -} - -void StateTreeWidget::setRootState(cvc::state *root) { - // Disconnect from previous root's signals - m_treeChangeConnection.disconnect(); - - m_rootState = root; - - // Connect to root state's childChanged signal to detect additions/deletions - if (m_rootState) { - m_treeChangeConnection = m_rootState->childChanged.connect([this](const std::string &) { - QMetaObject::invokeMethod(this, "onTreeStructureChanged", Qt::QueuedConnection); - }); - } - - refresh(); -} - -void StateTreeWidget::refresh() { - // Save currently selected state's full name to restore after refresh - std::string previousSelectionName; - if (m_currentState && m_currentState->initialized()) { - previousSelectionName = m_currentState->fullName(); - } - - m_treeWidget->clear(); - m_tableWidget->setRowCount(0); - - if (!m_rootState) - return; - - // Create root item - QTreeWidgetItem *rootItem = new QTreeWidgetItem(m_treeWidget); - rootItem->setText(0, QString::fromStdString(m_rootState->name())); - rootItem->setData(0, Qt::UserRole, QVariant::fromValue(static_cast(m_rootState))); - - // Populate tree recursively - populateTree(rootItem, m_rootState, ""); - - rootItem->setExpanded(true); - - // Restore selection if the previously selected state still exists and is initialized - if (!previousSelectionName.empty()) { - try { - // Try to get the state from the root using the saved full name - // We need to navigate from root to the state - cvc::state *restoredState = m_rootState; - std::string remainingPath = previousSelectionName; - - // Remove root name prefix if present - std::string rootName = m_rootState->fullName(); - if (remainingPath.find(rootName) == 0) { - remainingPath = remainingPath.substr(rootName.length()); - if (!remainingPath.empty() && remainingPath[0] == '.') { - remainingPath = remainingPath.substr(1); - } - } - - // Navigate to the state if path is not empty - if (!remainingPath.empty()) { - restoredState = &((*m_rootState)(remainingPath)); - } - - // Only restore selection if state is still initialized - if (restoredState && restoredState->initialized()) { - QTreeWidgetItem *itemToSelect = findTreeItem(rootItem, restoredState); - if (itemToSelect) { - m_treeWidget->setCurrentItem(itemToSelect); - // Note: setCurrentItem will trigger onTreeItemSelected, which will update m_currentState - } - } - } catch (const std::exception &) { - // State no longer exists, selection will remain cleared - } - } -} - -void StateTreeWidget::populateTree(QTreeWidgetItem *parentItem, cvc::state *state, - const std::string &path) { - if (!state) - return; - - try { - // Get all descendant children (children() returns full paths and is recursive) - std::vector allChildren = state->children(); - - // Get the parent's full name to filter for immediate children only - std::string parentFullName = state->fullName(); - - // Track immediate children (just the names, not full paths) - std::set immediateChildNames; - - for (const auto &childFullName : allChildren) { - // Check if this is an immediate child by comparing paths - // Immediate children will have parentFullName + "." + childName format - // with no additional dots in childName - - if (childFullName.find(parentFullName) == 0) { - // This child is under our parent node - std::string relativePath = childFullName.substr(parentFullName.length()); - - // Remove leading separator if present - if (!relativePath.empty() && relativePath[0] == '.') { - relativePath = relativePath.substr(1); - } - - // Check if this is an immediate child (no dots in relative path) - if (!relativePath.empty() && relativePath.find('.') == std::string::npos) { - immediateChildNames.insert(relativePath); - } - } - } - - // Now create tree items for each immediate child - for (const auto &childName : immediateChildNames) { - try { - cvc::state &child = (*state)(childName); - - // Skip uninitialized states - if (!child.initialized()) { - continue; - } - - QTreeWidgetItem *childItem = new QTreeWidgetItem(parentItem); - childItem->setText(0, QString::fromStdString(childName)); - childItem->setData(0, Qt::UserRole, QVariant::fromValue(static_cast(&child))); - - // Recursively populate this child's children - populateTree(childItem, &child, childName); - } catch (const std::exception &e) { - // Child not accessible, skip - } - } - } catch (const std::exception &e) { - // No children or error getting children - } -} - -void StateTreeWidget::onTreeItemSelected() { - // Disconnect from previous state's signals - m_stateChangeConnection.disconnect(); - m_currentStateDestroyedConnection.disconnect(); - - QList selected = m_treeWidget->selectedItems(); - if (selected.isEmpty()) { - m_tableWidget->setRowCount(0); - m_currentState = nullptr; - m_deleteButton->setEnabled(false); - return; - } - - QTreeWidgetItem *item = selected.first(); - void *statePtr = item->data(0, Qt::UserRole).value(); - m_currentState = static_cast(statePtr); - - // Enable delete button for non-root items - m_deleteButton->setEnabled(m_currentState != m_rootState); - - populateTable(m_currentState); - - // Connect to new state's valueChanged signal to update UI when value changes - if (m_currentState) { - m_stateChangeConnection = m_currentState->valueChanged.connect([this]() { - // Use Qt's queued connection to update UI from signal thread - QMetaObject::invokeMethod(this, "onCurrentStateChanged", Qt::QueuedConnection); - }); - - // Connect to destroyed signal to handle deletion of current state - m_currentStateDestroyedConnection = m_currentState->destroyed.connect([this]() { - QMetaObject::invokeMethod(this, "onCurrentStateDestroyed", Qt::QueuedConnection); - }); - } -} - -void StateTreeWidget::onCurrentStateChanged() { - // Re-populate table with updated values from current state - if (m_currentState) { - populateTable(m_currentState); - } -} - -void StateTreeWidget::onTreeStructureChanged() { - // The tree structure changed (child added or removed) - // Refresh the entire tree to show the changes - // The refresh() method will preserve the current selection if it still exists - refresh(); -} - -void StateTreeWidget::onCurrentStateDestroyed() { - // The currently selected state was deleted - // Clear the selection and show empty state - m_currentState = nullptr; - m_treeWidget->clearSelection(); - m_tableWidget->setRowCount(0); - m_deleteButton->setEnabled(false); - - // Refresh the tree to remove the deleted state from the UI - refresh(); -} - -QTreeWidgetItem *StateTreeWidget::findTreeItem(QTreeWidgetItem *parent, cvc::state *state) { - if (!parent || !state) - return nullptr; - - // Check if this item matches the state we're looking for - void *itemStatePtr = parent->data(0, Qt::UserRole).value(); - if (itemStatePtr == static_cast(state)) { - return parent; - } - - // Recursively search children - for (int i = 0; i < parent->childCount(); ++i) { - QTreeWidgetItem *found = findTreeItem(parent->child(i), state); - if (found) { - return found; - } - } - - return nullptr; -} - -void StateTreeWidget::populateTable(cvc::state *state) { - if (!state) { - m_tableWidget->setRowCount(0); - return; - } - - // Block signals while populating to avoid triggering cellChanged - m_tableWidget->blockSignals(true); - - // Clear existing rows - m_tableWidget->setRowCount(0); - - int row = 0; - - // Add "Name" row - m_tableWidget->insertRow(row); - QTableWidgetItem *nameLabel = new QTableWidgetItem(tr("name")); - nameLabel->setFlags(nameLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, nameLabel); - - QTableWidgetItem *nameItem = new QTableWidgetItem(QString::fromStdString(state->name())); - nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); - nameItem->setForeground(QBrush(QColor(128, 128, 128))); - m_tableWidget->setItem(row, 1, nameItem); - row++; - - // Add "Full Path" row - m_tableWidget->insertRow(row); - QTableWidgetItem *pathLabel = new QTableWidgetItem(tr("full path")); - pathLabel->setFlags(pathLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, pathLabel); - - QTableWidgetItem *pathItem = new QTableWidgetItem(QString::fromStdString(state->fullName())); - pathItem->setFlags(pathItem->flags() & ~Qt::ItemIsEditable); - pathItem->setForeground(QBrush(QColor(128, 128, 128))); - m_tableWidget->setItem(row, 1, pathItem); - row++; - - // Add "Value" row - m_tableWidget->insertRow(row); - QTableWidgetItem *valueLabel = new QTableWidgetItem(tr("value")); - valueLabel->setFlags(valueLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, valueLabel); - - std::string valueStr = getStateValue(state); - QTableWidgetItem *valueItem = new QTableWidgetItem(QString::fromStdString(valueStr)); - - // Check if state is read-only and mark accordingly - if (state->readOnly()) { - // Make non-editable and add visual indicator - valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); - valueItem->setForeground(QBrush(QColor(100, 100, 100))); - valueItem->setToolTip(tr("This value is read-only (computed/generated)")); - // Add lock emoji/icon to indicate read-only - QString displayValue = QString::fromStdString(valueStr) + " 🔒"; - valueItem->setText(displayValue); - } - - m_tableWidget->setItem(row, 1, valueItem); - row++; - - // Add "Read-Only" status row - m_tableWidget->insertRow(row); - QTableWidgetItem *readOnlyLabel = new QTableWidgetItem(tr("read-only")); - readOnlyLabel->setFlags(readOnlyLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, readOnlyLabel); - - QString readOnlyStatus = state->readOnly() ? tr("Yes 🔒") : tr("No"); - QTableWidgetItem *readOnlyItem = new QTableWidgetItem(readOnlyStatus); - readOnlyItem->setFlags(readOnlyItem->flags() & ~Qt::ItemIsEditable); - if (state->readOnly()) { - readOnlyItem->setForeground(QBrush(QColor(200, 100, 50))); - readOnlyItem->setToolTip(tr("This state is read-only and cannot be modified")); - } else { - readOnlyItem->setForeground(QBrush(QColor(100, 150, 100))); - } - m_tableWidget->setItem(row, 1, readOnlyItem); - row++; - - // Add "Comment" row if comment is set - try { - std::string commentStr = state->comment(); - if (!commentStr.empty()) { - m_tableWidget->insertRow(row); - QTableWidgetItem *commentLabel = new QTableWidgetItem(tr("comment")); - commentLabel->setFlags(commentLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, commentLabel); - - QTableWidgetItem *commentItem = new QTableWidgetItem(QString::fromStdString(commentStr)); - commentItem->setFlags(commentItem->flags() & ~Qt::ItemIsEditable); - commentItem->setForeground(QBrush(QColor(80, 120, 180))); // Blue color for comments - commentItem->setFont(QFont("", -1, QFont::Normal, true)); // Italic - commentItem->setToolTip(QString::fromStdString(commentStr)); // Show full comment on hover - m_tableWidget->setItem(row, 1, commentItem); - row++; - } - } catch (...) { - } - - // Add "Value Type" row if value type is set - try { - std::string valueTypeName = state->valueTypeName(); - if (!valueTypeName.empty()) { - m_tableWidget->insertRow(row); - QTableWidgetItem *valueTypeLabel = new QTableWidgetItem(tr("value type")); - valueTypeLabel->setFlags(valueTypeLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, valueTypeLabel); - - QTableWidgetItem *valueTypeItem = new QTableWidgetItem(QString::fromStdString(valueTypeName)); - valueTypeItem->setFlags(valueTypeItem->flags() & ~Qt::ItemIsEditable); - valueTypeItem->setForeground(QBrush(QColor(128, 128, 128))); - m_tableWidget->setItem(row, 1, valueTypeItem); - row++; - } - } catch (...) { - } - - // Add "Data Type" row only if data exists - try { - boost::any anyData = state->data(); - if (!anyData.empty()) { - m_tableWidget->insertRow(row); - QTableWidgetItem *dataLabel = new QTableWidgetItem(tr("data (type)")); - dataLabel->setFlags(dataLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, dataLabel); - - std::string dataType = getStateDataType(state); - QTableWidgetItem *dataItem = new QTableWidgetItem(QString::fromStdString(dataType)); - dataItem->setFlags(dataItem->flags() & ~Qt::ItemIsEditable); - dataItem->setForeground(QBrush(QColor(128, 128, 128))); - m_tableWidget->setItem(row, 1, dataItem); - row++; - } - } catch (...) { - } - - // Add "Last Modified" row - try { - boost::posix_time::ptime lastMod = state->lastMod(); - if (!lastMod.is_not_a_date_time()) { - m_tableWidget->insertRow(row); - QTableWidgetItem *lastModLabel = new QTableWidgetItem(tr("last modified")); - lastModLabel->setFlags(lastModLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, lastModLabel); - - std::string timeStr = boost::posix_time::to_simple_string(lastMod); - QTableWidgetItem *lastModItem = new QTableWidgetItem(QString::fromStdString(timeStr)); - lastModItem->setFlags(lastModItem->flags() & ~Qt::ItemIsEditable); - lastModItem->setForeground(QBrush(QColor(128, 128, 128))); - m_tableWidget->setItem(row, 1, lastModItem); - row++; - } - } catch (...) { - } - - // Add "Children Count" row - m_tableWidget->insertRow(row); - QTableWidgetItem *childrenLabel = new QTableWidgetItem(tr("children")); - childrenLabel->setFlags(childrenLabel->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 0, childrenLabel); - - try { - std::vector children = state->children(); - QTableWidgetItem *childrenItem = new QTableWidgetItem(QString::number(children.size())); - childrenItem->setFlags(childrenItem->flags() & ~Qt::ItemIsEditable); - childrenItem->setForeground(QBrush(QColor(128, 128, 128))); - m_tableWidget->setItem(row, 1, childrenItem); - } catch (...) { - QTableWidgetItem *childrenItem = new QTableWidgetItem(tr("0")); - childrenItem->setFlags(childrenItem->flags() & ~Qt::ItemIsEditable); - m_tableWidget->setItem(row, 1, childrenItem); - } - - m_tableWidget->resizeColumnsToContents(); - m_tableWidget->blockSignals(false); -} - -std::string StateTreeWidget::getStateValue(cvc::state *state) { - if (!state) - return ""; - - try { - // state::value() returns std::string directly - return state->value(); - } catch (const std::exception &e) { - return std::string(""; - } -} - -std::string StateTreeWidget::getStateDataType(cvc::state *state) { - if (!state) - return "unknown"; - - try { - // Get the boost::any data and use volrover3::app() to get the registered type name - boost::any anyData = state->data(); - if (anyData.empty()) { - return ""; - } - - // Use volrover3::app()'s registered type names - std::string typeName = volrover3::app().dataTypeName(anyData); - return typeName; - } catch (const std::exception &e) { - return ""; - } -} - -void StateTreeWidget::setStateValue(cvc::state *state, const QString &valueStr) { - if (!state) - return; - - try { - std::string typeName = state->valueTypeName(); - - // Try to set value based on detected type - try { - if (typeName.find("string") != std::string::npos) { - state->value(valueStr.toStdString()); - return; - } - } catch (...) { - } - - try { - if (typeName.find("double") != std::string::npos || typeName == "d") { - state->value(valueStr.toDouble()); - return; - } - } catch (...) { - } - - try { - if (typeName.find("float") != std::string::npos || typeName == "f") { - state->value(valueStr.toFloat()); - return; - } - } catch (...) { - } - - try { - if (typeName.find("int") != std::string::npos || typeName == "i") { - state->value(valueStr.toInt()); - return; - } - } catch (...) { - } - - try { - if (typeName == "j") { // unsigned int - state->value(static_cast(valueStr.toUInt())); - return; - } - } catch (...) { - } - - try { - if (typeName.find("bool") != std::string::npos || typeName == "b") { - QString lower = valueStr.toLower(); - bool boolValue = (lower == "true" || lower == "1" || lower == "yes"); - state->value(boolValue); - return; - } - } catch (...) { - } - - try { - if (typeName == "l") { // long - state->value(valueStr.toLong()); - return; - } - } catch (...) { - } - - try { - if (typeName == "m") { // size_t - state->value(static_cast(valueStr.toULongLong())); - return; - } - } catch (...) { - } - - // Default: try string - state->value(valueStr.toStdString()); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error Setting Value"), - tr("Failed to set value: %1").arg(e.what())); - } -} - -void StateTreeWidget::onTableValueChanged(int row, int column) { - if (!m_currentState || column != 1) - return; - - // Only the value row (row 2) is editable (after name, full path) - QTableWidgetItem *labelItem = m_tableWidget->item(row, 0); - if (!labelItem || labelItem->text() != tr("value")) - return; - - // Check if state is read-only before attempting to change - if (m_currentState->readOnly()) { - m_tableWidget->blockSignals(true); - QMessageBox::information(this, tr("Read-Only State"), - tr("This state is read-only and cannot be modified.\n" - "It contains computed or generated values.")); - // Revert to original value - populateTable(m_currentState); - m_tableWidget->blockSignals(false); - return; - } - - QTableWidgetItem *item = m_tableWidget->item(row, column); - if (!item) - return; - - QString newValue = item->text(); - - // Block signals to prevent recursion - m_tableWidget->blockSignals(true); - - try { - setStateValue(m_currentState, newValue); - // Value was set successfully, emit state changed signal - emit stateChanged(); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), tr("Failed to update state value: %1").arg(e.what())); - // Revert to old value - populateTable(m_currentState); - } - - m_tableWidget->blockSignals(false); -} - -void StateTreeWidget::onAddStateClicked() { - // Pre-fill the path with the selected node's full name (if not root) - QString prefill = ""; - if (m_currentState && m_currentState != m_rootState) { - prefill = QString::fromStdString(m_currentState->fullName()) + "."; - } - - bool ok; - QString path = QInputDialog::getText(this, tr("Add State"), - tr("Enter full path for new state:\ne.g., " - "'volrover3.my_setting' or 'myapp.nested.child.value'"), - QLineEdit::Normal, prefill, &ok); - - if (!ok || path.isEmpty()) - return; - - // Validate the path using state's validation function - // Split path into components and validate each - QStringList components = path.split('.'); - bool needsSanitization = false; - QStringList sanitizedComponents; - - for (const QString &component : components) { - std::string comp = component.toStdString(); - if (!cvc::state::isValidStateName(comp)) { - needsSanitization = true; - std::string sanitized = cvc::state::sanitizeStateName(comp); - sanitizedComponents.append(QString::fromStdString(sanitized)); - } else { - sanitizedComponents.append(component); - } - } - - // If path needs sanitization, offer to use sanitized version - if (needsSanitization) { - QString sanitizedPath = sanitizedComponents.join('.'); - QMessageBox::StandardButton reply = - QMessageBox::question(this, tr("Invalid State Name"), - tr("The path contains invalid characters.\n\n" - "Original: %1\n" - "Suggested: %2\n\n" - "State names must follow C identifier rules:\n" - "- Start with letter or underscore\n" - "- Contain only letters, digits, and underscores\n" - "- No spaces, dashes, or special characters\n\n" - "Use sanitized version?") - .arg(path) - .arg(sanitizedPath), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - path = sanitizedPath; - } else { - return; - } - } - - QString value = QInputDialog::getText(this, tr("Add State"), tr("Enter initial value:"), - QLineEdit::Normal, "", &ok); - - if (!ok) - return; - - try { - // Access the state using the full path from the global state singleton - cvc::state &newState = cvc::state::instance(volrover3::app())(path.toStdString()); - newState.value(value.toStdString()); - - // Refresh the tree to show the new state - refresh(); - - QString fullPath = QString::fromStdString(newState.fullName()); - QMessageBox::information(this, tr("Success"), - tr("State '%1' created successfully").arg(fullPath)); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), tr("Failed to create state: %1").arg(e.what())); - } -} - -void StateTreeWidget::onDeleteStateClicked() { - if (!m_currentState || m_currentState == m_rootState) { - QMessageBox::warning(this, tr("Error"), tr("Cannot delete root state")); - return; - } - - QString stateName = QString::fromStdString(m_currentState->name()); - QString fullPath = QString::fromStdString(m_currentState->fullName()); - - auto reply = QMessageBox::question(this, tr("Delete State"), - tr("Are you sure you want to reset state '%1'?\n\nThis will " - "clear its value, data, and mark it as uninitialized.") - .arg(fullPath), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - try { - // Reset the state (clears value, data, and sets initialized to false) - m_currentState->reset(); - - // Refresh the entire tree since the node should now be hidden - refresh(); - - // Emit state changed signal - emit stateChanged(); - - QMessageBox::information(this, tr("Success"), - tr("State '%1' cleared successfully").arg(fullPath)); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), tr("Failed to clear state: %1").arg(e.what())); - } - } -} diff --git a/src/volrover3/ThreadMonitorWidget.cpp b/src/volrover3/ThreadMonitorWidget.cpp deleted file mode 100644 index 38dcee22..00000000 --- a/src/volrover3/ThreadMonitorWidget.cpp +++ /dev/null @@ -1,316 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -ThreadMonitorWidget::ThreadMonitorWidget(QWidget *parent) - : QWidget(parent), m_threadTable(nullptr), m_updateTimer(nullptr), m_cleanupTimer(nullptr), - m_updatePending(false) { - setupUI(); - - // Set up rate-limiting timer (minimum 50ms between updates) - m_updateTimer = new QTimer(this); - m_updateTimer->setSingleShot(true); - connect(m_updateTimer, &QTimer::timeout, this, &ThreadMonitorWidget::performUpdate); - - // Set up cleanup timer to remove completed threads after delay - m_cleanupTimer = new QTimer(this); - m_cleanupTimer->setInterval(10000); // Check every 10 seconds - connect(m_cleanupTimer, &QTimer::timeout, this, &ThreadMonitorWidget::cleanupCompletedThreads); - m_cleanupTimer->start(); - - // Start tracking time for rate limiting - m_lastUpdateTime.start(); - - // Register callbacks to be notified of thread changes - registerCallbacks(); - - // Initial population - updateThreadTable(); -} - -ThreadMonitorWidget::~ThreadMonitorWidget() { - disconnectCallbacks(); - - if (m_updateTimer) { - m_updateTimer->stop(); - } - if (m_cleanupTimer) { - m_cleanupTimer->stop(); - } -} - -void ThreadMonitorWidget::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Title label - QLabel *titleLabel = new QLabel(tr("Active Threads"), this); - QFont titleFont = titleLabel->font(); - titleFont.setPointSize(titleFont.pointSize() + 2); - titleFont.setBold(true); - titleLabel->setFont(titleFont); - mainLayout->addWidget(titleLabel); - - // Thread table - m_threadTable = new QTableWidget(0, COL_COUNT, this); - m_threadTable->setHorizontalHeaderLabels( - {tr("Thread Name"), tr("Status"), tr("Progress"), tr("Progress Bar"), tr("Action")}); - - // Configure table - m_threadTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_threadTable->setSelectionMode(QAbstractItemView::SingleSelection); - m_threadTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - m_threadTable->verticalHeader()->setVisible(false); - m_threadTable->setAlternatingRowColors(true); - - // Set column resize modes - QHeaderView *header = m_threadTable->horizontalHeader(); - header->setSectionResizeMode(COL_NAME, QHeaderView::Stretch); - header->setSectionResizeMode(COL_STATUS, QHeaderView::ResizeToContents); - header->setSectionResizeMode(COL_PROGRESS, QHeaderView::ResizeToContents); - header->setSectionResizeMode(COL_PROGRESS_BAR, QHeaderView::Fixed); - header->setSectionResizeMode(COL_CANCEL, QHeaderView::ResizeToContents); - header->resizeSection(COL_PROGRESS_BAR, 150); - - mainLayout->addWidget(m_threadTable); - - // Bottom info label - QLabel *infoLabel = - new QLabel(tr("Updates automatically on thread changes (rate limited to 50ms)"), this); - infoLabel->setStyleSheet("color: gray; font-style: italic;"); - mainLayout->addWidget(infoLabel); - - setLayout(mainLayout); - setMinimumSize(600, 300); -} - -void ThreadMonitorWidget::registerCallbacks() { - // Connect to the app's thread map changes signal - // This fires whenever a thread is added, removed, or its state changes - // Use QMetaObject::invokeMethod to ensure UI updates happen on the main thread - auto connection = volrover3::app().threadsChanged.connect([this](const std::string &) { - QMetaObject::invokeMethod(this, "requestUpdate", Qt::QueuedConnection); - }); - m_connections.push_back(connection); -} - -void ThreadMonitorWidget::disconnectCallbacks() { - for (auto &conn : m_connections) { - conn.disconnect(); - } - m_connections.clear(); -} - -void ThreadMonitorWidget::requestUpdate() { - // Rate limiting: only update if at least 50ms has passed since last update - const qint64 minUpdateInterval = 50; // milliseconds - - qint64 elapsed = m_lastUpdateTime.elapsed(); - - if (elapsed >= minUpdateInterval) { - // Enough time has passed, update immediately - updateThreadTable(); - m_lastUpdateTime.restart(); - m_updatePending = false; - } else { - // Too soon, schedule an update for later if not already pending - if (!m_updatePending) { - m_updatePending = true; - qint64 delay = minUpdateInterval - elapsed; - m_updateTimer->start(static_cast(delay)); - } - } -} - -void ThreadMonitorWidget::performUpdate() { - m_updatePending = false; - updateThreadTable(); - m_lastUpdateTime.restart(); -} - -void ThreadMonitorWidget::updateThreadTable() { - // Get current threads from cvc::app - cvc::thread_map threads = volrover3::app().threads(); - - // Track which threads are completed (100% progress) - // Note: We avoid calling thread->joinable() frequently as it can be expensive - qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); - for (const auto &entry : threads) { - const std::string &threadKey = entry.first; - const cvc::thread_ptr &thread = entry.second; - - if (!thread) - continue; - - double progress = volrover3::app().threadProgress(threadKey); - bool isComplete = (progress >= 1.0); - - if (isComplete) { - // Mark thread as completed if not already tracked - if (m_completedThreads.find(threadKey) == m_completedThreads.end()) { - m_completedThreads[threadKey] = currentTime; - - // Emit signal for status bar update - std::string info = volrover3::app().threadInfo(threadKey); - emit threadCompleted(QString::fromStdString(threadKey), - QString::fromStdString(info.empty() ? "completed" : info)); - } - } else { - // Thread is running again (restarted?), remove from completed tracking - m_completedThreads.erase(threadKey); - } - } - - // Clear existing rows - m_threadTable->setRowCount(0); - - // Add a row for each thread - int row = 0; - for (const auto &entry : threads) { - const std::string &threadKey = entry.first; - const cvc::thread_ptr &thread = entry.second; - - // Skip null threads - if (!thread) - continue; - - m_threadTable->insertRow(row); - - // Column 0: Thread name - QTableWidgetItem *nameItem = new QTableWidgetItem(QString::fromStdString(threadKey)); - m_threadTable->setItem(row, COL_NAME, nameItem); - - // Column 1: Status (thread info) - std::string statusInfo = volrover3::app().threadInfo(threadKey); - double progress = volrover3::app().threadProgress(threadKey); - bool isComplete = (progress >= 1.0); - - if (isComplete) { - statusInfo = statusInfo.empty() ? "completed" : statusInfo + " (completed)"; - } else if (statusInfo.empty()) { - statusInfo = "running"; - } - QTableWidgetItem *statusItem = new QTableWidgetItem(QString::fromStdString(statusInfo)); - - // Color completed threads differently - if (isComplete) { - statusItem->setForeground(QColor(0, 128, 0)); // Green for completed - } - m_threadTable->setItem(row, COL_STATUS, statusItem); - - // Column 2: Progress percentage - QString progressText = formatProgress(progress); - QTableWidgetItem *progressItem = new QTableWidgetItem(progressText); - progressItem->setTextAlignment(Qt::AlignCenter); - if (isComplete) { - progressItem->setForeground(QColor(0, 128, 0)); - } - m_threadTable->setItem(row, COL_PROGRESS, progressItem); - - // Column 3: Progress bar - QProgressBar *progressBar = new QProgressBar(); - progressBar->setRange(0, 100); - progressBar->setValue(static_cast(progress * 100)); - progressBar->setTextVisible(false); - progressBar->setMaximumHeight(20); - if (isComplete) { - progressBar->setStyleSheet("QProgressBar::chunk { background-color: #4CAF50; }"); - } - m_threadTable->setCellWidget(row, COL_PROGRESS_BAR, progressBar); - - // Column 4: Cancel button (disabled for completed threads) - QPushButton *cancelBtn = new QPushButton(isComplete ? tr("Done") : tr("Cancel")); - cancelBtn->setMaximumWidth(80); - cancelBtn->setEnabled(!isComplete); - - // Capture threadKey by value for the lambda - if (!isComplete) { - connect(cancelBtn, &QPushButton::clicked, [this, threadKey]() { cancelThread(threadKey); }); - } - - m_threadTable->setCellWidget(row, COL_CANCEL, cancelBtn); - - row++; - } - - // Adjust row heights - for (int i = 0; i < m_threadTable->rowCount(); ++i) { - m_threadTable->setRowHeight(i, 30); - } -} - -QString ThreadMonitorWidget::formatProgress(double progress) { - if (progress < 0.0) - return tr("N/A"); - if (progress > 1.0) - progress = 1.0; - - int percentage = static_cast(progress * 100); - return QString("%1%").arg(percentage); -} - -void ThreadMonitorWidget::cancelThread(const std::string &threadKey) { - // Confirm cancellation - int reply = QMessageBox::question( - this, tr("Cancel Thread"), - tr("Are you sure you want to cancel thread:\n%1?").arg(QString::fromStdString(threadKey)), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (reply != QMessageBox::Yes) { - return; - } - - // Get the thread and interrupt it - cvc::thread_ptr thread = volrover3::app().threads(threadKey); - if (thread) { - thread->interrupt(); - - QMessageBox::information(this, tr("Thread Cancelled"), - tr("Cancellation request sent to thread:\n%1\n\n" - "The thread will stop at its next interruption point.") - .arg(QString::fromStdString(threadKey))); - } else { - QMessageBox::warning( - this, tr("Thread Not Found"), - tr("Thread %1 is no longer active.").arg(QString::fromStdString(threadKey))); - } - - // Request update (will be rate-limited) - requestUpdate(); -} - -void ThreadMonitorWidget::cleanupCompletedThreads() { - qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); - std::vector threadsToRemove; - - // Find threads that have been completed for longer than the delay - for (const auto &entry : m_completedThreads) { - const std::string &threadKey = entry.first; - qint64 completionTime = entry.second; - - if (currentTime - completionTime >= CLEANUP_DELAY_MS) { - threadsToRemove.push_back(threadKey); - } - } - - // Remove the completed threads from the app's thread map - for (const std::string &threadKey : threadsToRemove) { - // Remove from our tracking - m_completedThreads.erase(threadKey); - - // Remove from the app's thread map - volrover3::app().removeThread(threadKey); - } - - // Request UI update if we removed any threads - if (!threadsToRemove.empty()) { - requestUpdate(); - } -} diff --git a/src/volrover3/TransferFunctionWidget.cpp b/src/volrover3/TransferFunctionWidget.cpp deleted file mode 100644 index f3f5c47c..00000000 --- a/src/volrover3/TransferFunctionWidget.cpp +++ /dev/null @@ -1,699 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Simple color bar widget -class ColorBarWidget : public QWidget { -public: - ColorBarWidget(QWidget *parent = nullptr) : QWidget(parent) { - setMinimumHeight(40); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - } - - void setColorPoints(const std::vector &points) { - m_colorPoints = points; - update(); - } - -protected: - void paintEvent(QPaintEvent *) override { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - if (m_colorPoints.empty()) { - painter.fillRect(rect(), Qt::gray); - return; - } - - // Draw gradient - int w = width(); - for (int x = 0; x < w; ++x) { - double t = static_cast(x) / (w - 1); - QColor color = interpolateColor(t); - painter.setPen(color); - painter.drawLine(x, 0, x, height()); - } - } - -private: - QColor interpolateColor(double t) const { - if (m_colorPoints.empty()) - return Qt::white; - if (m_colorPoints.size() == 1) - return m_colorPoints[0].color; - - // Find surrounding color points - for (size_t i = 0; i < m_colorPoints.size() - 1; ++i) { - if (t >= m_colorPoints[i].value && t <= m_colorPoints[i + 1].value) { - double localT = - (t - m_colorPoints[i].value) / (m_colorPoints[i + 1].value - m_colorPoints[i].value); - - QColor c1 = m_colorPoints[i].color; - QColor c2 = m_colorPoints[i + 1].color; - - int r = c1.red() + localT * (c2.red() - c1.red()); - int g = c1.green() + localT * (c2.green() - c1.green()); - int b = c1.blue() + localT * (c2.blue() - c1.blue()); - - return QColor(r, g, b); - } - } - - return m_colorPoints.back().color; - } - - std::vector m_colorPoints; -}; - -// Simple opacity graph widget -class OpacityGraphWidget : public QWidget { - Q_OBJECT -public: - OpacityGraphWidget(QWidget *parent = nullptr) - : QWidget(parent), m_selectedPoint(-1), m_dragging(false) { - setMinimumHeight(100); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - setMouseTracking(true); - } - - void setOpacityPoints(const std::vector &points) { - m_opacityPoints = points; - update(); - } - - const std::vector &getOpacityPoints() const { - return m_opacityPoints; - } - -signals: - void opacityChanged(); - -protected: - void paintEvent(QPaintEvent *) override { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // Draw background - painter.fillRect(rect(), QColor(240, 240, 240)); - - // Draw grid - painter.setPen(QColor(200, 200, 200)); - for (int i = 0; i <= 4; ++i) { - int y = i * height() / 4; - painter.drawLine(0, y, width(), y); - } - - if (m_opacityPoints.empty()) - return; - - // Draw opacity curve - painter.setPen(QPen(Qt::blue, 2)); - for (size_t i = 0; i < m_opacityPoints.size() - 1; ++i) { - int x1 = m_opacityPoints[i].value * width(); - int y1 = (1.0 - m_opacityPoints[i].opacity) * height(); - int x2 = m_opacityPoints[i + 1].value * width(); - int y2 = (1.0 - m_opacityPoints[i + 1].opacity) * height(); - painter.drawLine(x1, y1, x2, y2); - } - - // Draw control points - painter.setBrush(Qt::red); - for (const auto &pt : m_opacityPoints) { - int x = pt.value * width(); - int y = (1.0 - pt.opacity) * height(); - painter.drawEllipse(QPoint(x, y), 5, 5); - } - } - - void mousePressEvent(QMouseEvent *event) override { - if (event->button() == Qt::LeftButton) { - // Find nearest control point - int nearestIdx = findNearestPoint(event->pos()); - if (nearestIdx >= 0 && nearestIdx < static_cast(m_opacityPoints.size())) { - double dist = pointDistance(event->pos(), nearestIdx); - if (dist < 10.0) { - m_selectedPoint = nearestIdx; - m_dragging = true; - return; - } - } - - // Add new point if clicked away from existing points - double x = static_cast(event->pos().x()) / width(); - double y = 1.0 - static_cast(event->pos().y()) / height(); - x = std::max(0.0, std::min(1.0, x)); - y = std::max(0.0, std::min(1.0, y)); - - // Insert point in sorted order - TransferFunctionWidget::OpacityPoint newPt{x, y}; - auto it = std::lower_bound( - m_opacityPoints.begin(), m_opacityPoints.end(), newPt, - [](const TransferFunctionWidget::OpacityPoint &a, - const TransferFunctionWidget::OpacityPoint &b) { return a.value < b.value; }); - m_selectedPoint = std::distance(m_opacityPoints.begin(), it); - m_opacityPoints.insert(it, newPt); - m_dragging = true; - update(); - emit opacityChanged(); - } else if (event->button() == Qt::RightButton) { - // Remove point on right-click (but keep at least 2 points) - if (m_opacityPoints.size() > 2) { - int nearestIdx = findNearestPoint(event->pos()); - if (nearestIdx >= 0 && nearestIdx < static_cast(m_opacityPoints.size())) { - double dist = pointDistance(event->pos(), nearestIdx); - if (dist < 10.0) { - m_opacityPoints.erase(m_opacityPoints.begin() + nearestIdx); - update(); - emit opacityChanged(); - } - } - } - } - } - - void mouseMoveEvent(QMouseEvent *event) override { - if (m_dragging && m_selectedPoint >= 0 && - m_selectedPoint < static_cast(m_opacityPoints.size())) { - double x = static_cast(event->pos().x()) / width(); - double y = 1.0 - static_cast(event->pos().y()) / height(); - - // Clamp to valid range - y = std::max(0.0, std::min(1.0, y)); - - // Don't allow moving endpoints horizontally, only vertically - if (m_selectedPoint == 0) { - x = 0.0; - } else if (m_selectedPoint == static_cast(m_opacityPoints.size()) - 1) { - x = 1.0; - } else { - // Constrain x between neighbors - double minX = m_opacityPoints[m_selectedPoint - 1].value + 0.01; - double maxX = m_opacityPoints[m_selectedPoint + 1].value - 0.01; - x = std::max(minX, std::min(maxX, x)); - } - - m_opacityPoints[m_selectedPoint].value = x; - m_opacityPoints[m_selectedPoint].opacity = y; - update(); - emit opacityChanged(); - } - } - - void mouseReleaseEvent(QMouseEvent *event) override { - if (event->button() == Qt::LeftButton) { - m_dragging = false; - m_selectedPoint = -1; - } - } - -private: - int findNearestPoint(const QPoint &pos) const { - if (m_opacityPoints.empty()) - return -1; - - int nearest = 0; - double minDist = pointDistance(pos, 0); - - for (size_t i = 1; i < m_opacityPoints.size(); ++i) { - double dist = pointDistance(pos, i); - if (dist < minDist) { - minDist = dist; - nearest = i; - } - } - - return nearest; - } - - double pointDistance(const QPoint &pos, int idx) const { - if (idx < 0 || idx >= static_cast(m_opacityPoints.size())) - return 1e9; - - int x = m_opacityPoints[idx].value * width(); - int y = (1.0 - m_opacityPoints[idx].opacity) * height(); - - int dx = pos.x() - x; - int dy = pos.y() - y; - - return std::sqrt(dx * dx + dy * dy); - } - - std::vector m_opacityPoints; - int m_selectedPoint; - bool m_dragging; -}; - -TransferFunctionWidget::TransferFunctionWidget(QWidget *parent) - : QWidget(parent), m_presetCombo(nullptr), m_colorBarWidget(nullptr), m_opacityWidget(nullptr), - m_volumeCombo(nullptr), m_sceneGraph(nullptr), m_dataMin(0.0), m_dataMax(1.0), - m_updatingFromState(false) { - setupUI(); - createDefaultTransferFunction(); -} - -TransferFunctionWidget::~TransferFunctionWidget() {} - -void TransferFunctionWidget::setupUI() { - QVBoxLayout *layout = new QVBoxLayout(this); - - // Volume selector - QHBoxLayout *volumeLayout = new QHBoxLayout(); - volumeLayout->addWidget(new QLabel("Volume:")); - m_volumeCombo = new QComboBox(); - connect(m_volumeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - &TransferFunctionWidget::onVolumeSelected); - volumeLayout->addWidget(m_volumeCombo); - layout->addLayout(volumeLayout); - - // Preset selector - QHBoxLayout *presetLayout = new QHBoxLayout(); - presetLayout->addWidget(new QLabel("Preset:")); - m_presetCombo = new QComboBox(); - m_presetCombo->addItem("Grayscale"); - m_presetCombo->addItem("Rainbow"); - m_presetCombo->addItem("Hot"); - m_presetCombo->addItem("Cool"); - m_presetCombo->addItem("X-Ray"); - connect(m_presetCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - &TransferFunctionWidget::onPresetChanged); - presetLayout->addWidget(m_presetCombo); - layout->addLayout(presetLayout); - - // Color bar - layout->addWidget(new QLabel("Color Map:")); - m_colorBarWidget = new ColorBarWidget(this); - layout->addWidget(m_colorBarWidget); - - // Opacity graph - layout->addWidget(new QLabel("Opacity:")); - m_opacityWidget = new OpacityGraphWidget(this); - connect(static_cast(m_opacityWidget), &OpacityGraphWidget::opacityChanged, - this, &TransferFunctionWidget::onOpacityGraphChanged); - layout->addWidget(m_opacityWidget); - - // Add instructions - QLabel *instructions = new QLabel("Left-click to add/drag points, Right-click to remove"); - instructions->setStyleSheet("QLabel { color: gray; font-size: 9pt; }"); - layout->addWidget(instructions); - - layout->addStretch(); -} - -void TransferFunctionWidget::createDefaultTransferFunction() { - applyPreset("Grayscale"); - - // Initialize default opacity points if empty - if (m_opacityPoints.empty() && m_opacityWidget != nullptr) { - m_opacityPoints.push_back({0.0, 0.0}); - m_opacityPoints.push_back({1.0, 1.0}); - - // Update the opacity widget - auto opacityWidget = static_cast(m_opacityWidget); - opacityWidget->setOpacityPoints(m_opacityPoints); - } -} - -void TransferFunctionWidget::setSceneGraph(SceneGraph *sceneGraph) { - m_sceneGraph = sceneGraph; - refreshVolumeList(); - - // Connect to state tree to monitor for new volumes - // Listen to graphics root's children changes - if (m_sceneGraph) { - std::string statePrefix = m_sceneGraph->getStatePrefix(); - std::string graphicsRootPath = statePrefix + ".graphics.root.children"; - - m_graphicsChildrenConnection = - cvc::state::instance(volrover3::app())(graphicsRootPath) - .childChanged.connect([this](const std::string &) { - // Post to Qt event loop to ensure thread safety - QMetaObject::invokeMethod(this, "onGraphicsChildrenChanged", Qt::QueuedConnection); - }); - } -} - -void TransferFunctionWidget::onGraphicsChildrenChanged() { - if (!m_sceneGraph || !m_volumeCombo) { - return; - } - - // Get current volume list from scene graph - auto volumeGraphics = m_sceneGraph->getAllVolumeGraphics(); - - // Check if count changed - if (volumeGraphics.size() != m_volumes.size()) { - refreshVolumeList(); - return; - } - - // Check if the set of volume names changed (handles reordering and replacement) - std::set newNames; - for (const auto &vol : volumeGraphics) { - if (vol) { - newNames.insert(vol->getName()); - } - } - - std::set currentNames; - for (const auto &vol : m_volumes) { - if (vol) { - currentNames.insert(vol->getName()); - } - } - - if (newNames != currentNames) { - refreshVolumeList(); - return; - } -} - -void TransferFunctionWidget::refreshVolumeList() { - if (!m_sceneGraph || !m_volumeCombo) { - return; - } - - // Store current selection - QString currentText = m_volumeCombo->currentText(); - - // Clear and repopulate - m_volumeCombo->clear(); - m_volumes.clear(); - - auto volumeGraphics = m_sceneGraph->getAllVolumeGraphics(); - for (const auto &volNode : volumeGraphics) { - if (volNode) { - QString name = QString::fromStdString(volNode->getName()); - m_volumeCombo->addItem(name); - m_volumes.push_back(volNode); - } - } - - // Restore selection if possible - int index = m_volumeCombo->findText(currentText); - if (index >= 0) { - m_volumeCombo->setCurrentIndex(index); - } else if (m_volumeCombo->count() > 0) { - m_volumeCombo->setCurrentIndex(0); - if (!m_volumes.empty()) { - loadTransferFunctionFromVolume(m_volumes[0]); - } - } -} - -std::shared_ptr TransferFunctionWidget::getSelectedVolume() const { - int index = m_volumeCombo ? m_volumeCombo->currentIndex() : -1; - if (index >= 0 && index < static_cast(m_volumes.size())) { - return m_volumes[index]; - } - return nullptr; -} - -void TransferFunctionWidget::onVolumeSelected(int index) { - if (index >= 0 && index < static_cast(m_volumes.size())) { - auto volume = m_volumes[index]; - emit selectedVolumeChanged(volume); - connectToVolumeState(volume); - loadTransferFunctionFromVolume(volume); - } else { - disconnectFromVolumeState(); - } -} - -void TransferFunctionWidget::loadTransferFunctionFromVolume(std::shared_ptr volume) { - if (!volume || !volume->hasVolume()) { - volrover3::app().log( - 0, - "TransferFunctionWidget::loadTransferFunctionFromVolume: No volume or volume not loaded"); - return; - } - - // Update data range from volume's metadata - auto minVal = volume->getMetadata("data_min"); - auto maxVal = volume->getMetadata("data_max"); - - volrover3::app().log(0, "\nTransferFunctionWidget::loadTransferFunctionFromVolume[" + - volume->getName() + "]: Getting metadata"); - - if (minVal.has_value() && maxVal.has_value()) { - try { - // Try double first, then string - if (minVal.type() == typeid(double)) { - m_dataMin = std::any_cast(minVal); - m_dataMax = std::any_cast(maxVal); - } else { - std::string minStr = std::any_cast(minVal); - std::string maxStr = std::any_cast(maxVal); - m_dataMin = std::stod(minStr); - m_dataMax = std::stod(maxStr); - } - volrover3::app().log(0, " Set data range to: [" + std::to_string(m_dataMin) + ", " + - std::to_string(m_dataMax) + "]\n"); - } catch (const std::exception &e) { - volrover3::app().log(0, "TransferFunctionWidget::loadTransferFunctionFromVolume[" + - volume->getName() + "]: Failed to convert metadata (" + - std::string(e.what()) + "), using defaults [0.0, 1.0]"); - m_dataMin = 0.0; - m_dataMax = 1.0; - } - } else { - volrover3::app().log(0, "TransferFunctionWidget::loadTransferFunctionFromVolume[" + - volume->getName() + - "]: No metadata found, using defaults [0.0, 1.0]"); - m_dataMin = 0.0; - m_dataMax = 1.0; - } - - // Load transfer function from volume's state tree - std::vector colorTable = volume->getTransferFunctionColorTable(); - std::vector opacityTable = volume->getTransferFunctionOpacityTable(); - - volrover3::app().log(0, " Raw from state: " + std::to_string(colorTable.size()) + - " color values, " + std::to_string(opacityTable.size()) + - " opacity values"); - - if (!colorTable.empty() && !opacityTable.empty()) { - // Parse color table into color points (format: scalar, r, g, b, ...) - m_colorPoints.clear(); - - volrover3::app().log(0, " Parsing color table: " + std::to_string(colorTable.size()) + - " values = " + std::to_string(colorTable.size() / 4) + " points"); - - for (size_t i = 0; i + 3 < colorTable.size(); i += 4) { - double scalar = colorTable[i]; - double r = colorTable[i + 1]; - double g = colorTable[i + 2]; - double b = colorTable[i + 3]; - - // Convert to normalized value [0, 1] - double normalizedValue = - (m_dataMax > m_dataMin) ? (scalar - m_dataMin) / (m_dataMax - m_dataMin) : 0.0; - - m_colorPoints.push_back({normalizedValue, QColor::fromRgbF(r, g, b)}); - } - - // Parse opacity table into opacity points (format: scalar, opacity, ...) - m_opacityPoints.clear(); - for (size_t i = 0; i + 1 < opacityTable.size(); i += 2) { - double scalar = opacityTable[i]; - double opacity = opacityTable[i + 1]; - - // Convert to normalized value [0, 1] - double normalizedValue = - (m_dataMax > m_dataMin) ? (scalar - m_dataMin) / (m_dataMax - m_dataMin) : 0.0; - - m_opacityPoints.push_back({normalizedValue, opacity}); - } - - // Update UI - updateColorBar(); - if (m_opacityWidget) { - auto opacityWidget = static_cast(m_opacityWidget); - opacityWidget->setOpacityPoints(m_opacityPoints); - } - - volrover3::app().log(0, " Loaded TF: " + std::to_string(m_colorPoints.size()) + - " color pts, " + std::to_string(m_opacityPoints.size()) + - " opacity pts"); - } else { - volrover3::app().log(0, " No transfer function in state, keeping current widget TF"); - } -} - -void TransferFunctionWidget::applyPreset(const QString &presetName) { - cvc::thread_info ti(volrover3::app(), "Applying transfer function preset"); - - m_colorPoints.clear(); - // Don't clear opacity points - keep them independent! - - if (presetName == "Grayscale") { - m_colorPoints.push_back({0.0, QColor(0, 0, 0)}); - m_colorPoints.push_back({1.0, QColor(255, 255, 255)}); - } else if (presetName == "Rainbow") { - m_colorPoints.push_back({0.0, QColor(0, 0, 255)}); // Blue - m_colorPoints.push_back({0.25, QColor(0, 255, 255)}); // Cyan - m_colorPoints.push_back({0.5, QColor(0, 255, 0)}); // Green - m_colorPoints.push_back({0.75, QColor(255, 255, 0)}); // Yellow - m_colorPoints.push_back({1.0, QColor(255, 0, 0)}); // Red - } else if (presetName == "Hot") { - m_colorPoints.push_back({0.0, QColor(0, 0, 0)}); - m_colorPoints.push_back({0.33, QColor(255, 0, 0)}); - m_colorPoints.push_back({0.66, QColor(255, 255, 0)}); - m_colorPoints.push_back({1.0, QColor(255, 255, 255)}); - } else if (presetName == "Cool") { - m_colorPoints.push_back({0.0, QColor(0, 255, 255)}); - m_colorPoints.push_back({1.0, QColor(255, 0, 255)}); - } else if (presetName == "X-Ray") { - m_colorPoints.push_back({0.0, QColor(0, 0, 0)}); - m_colorPoints.push_back({1.0, QColor(255, 255, 255)}); - } - - // Only initialize opacity points if they're empty - if (m_opacityPoints.empty()) { - m_opacityPoints.push_back({0.0, 0.0}); - m_opacityPoints.push_back({0.5, 0.5}); - m_opacityPoints.push_back({1.0, 1.0}); - } - - updateColorBar(); - - // Apply to selected volume - if (m_updatingFromState == 0) { - auto selectedVolume = getSelectedVolume(); - if (selectedVolume) { - m_updatingFromState++; // Increment before calling to prevent feedback - selectedVolume->setTransferFunction(getColorTable(), getOpacityTable()); - m_updatingFromState--; // Decrement after - } - } - - emit transferFunctionChanged(); -} - -void TransferFunctionWidget::setDataRange(double min, double max) { - m_dataMin = min; - m_dataMax = max; -} - -void TransferFunctionWidget::onPresetChanged(int index) { - applyPreset(m_presetCombo->currentText()); -} - -void TransferFunctionWidget::onColorMapClicked(double x, double y) { - // Placeholder for adding color control points -} - -void TransferFunctionWidget::onOpacityGraphChanged() { - if (m_updatingFromState == 0) { - // Apply to selected volume - auto selectedVolume = getSelectedVolume(); - if (selectedVolume) { - m_updatingFromState++; // Increment before calling to prevent feedback - selectedVolume->setTransferFunction(getColorTable(), getOpacityTable()); - m_updatingFromState--; // Decrement after - } - emit transferFunctionChanged(); - } -} - -void TransferFunctionWidget::updateColorBar() { - static_cast(m_colorBarWidget)->setColorPoints(m_colorPoints); - // Don't reset opacity points - they're managed independently by the opacity widget -} - -std::vector TransferFunctionWidget::getColorTable() const { - std::vector table; - - volrover3::app().log(0, "TransferFunctionWidget::getColorTable() - m_colorPoints.size() = " + - std::to_string(m_colorPoints.size())); - - for (const auto &pt : m_colorPoints) { - double scalar = m_dataMin + pt.value * (m_dataMax - m_dataMin); - table.push_back(scalar); - table.push_back(pt.color.redF()); - table.push_back(pt.color.greenF()); - table.push_back(pt.color.blueF()); - } - - volrover3::app().log(0, " Returning color table with " + std::to_string(table.size()) + - " values (" + std::to_string(table.size() / 4) + " points)"); - - return table; -} - -std::vector TransferFunctionWidget::getOpacityTable() const { - std::vector table; - - // Get the current opacity points from the opacity widget - const auto &opacityPoints = - static_cast(m_opacityWidget)->getOpacityPoints(); - - for (const auto &pt : opacityPoints) { - double scalar = m_dataMin + pt.value * (m_dataMax - m_dataMin); - table.push_back(scalar); - table.push_back(pt.opacity); - } - - return table; -} - -void TransferFunctionWidget::connectToVolumeState(std::shared_ptr volume) { - disconnectFromVolumeState(); - - if (!volume) { - return; - } - - // Connect to transfer function state changes - std::string statePath = volume->getState().fullName(); - - auto colorTFPath = statePath + ".transfer_function.color"; - auto opacityTFPath = statePath + ".transfer_function.opacity"; - - // Use DirectConnection instead of QueuedConnection to ensure m_updatingFromState flag works - // correctly - m_colorTFConnection = - cvc::state::instance(volrover3::app())(colorTFPath).valueChanged.connect([this]() { - // Only reload if we're not currently updating state from widget - if (m_updatingFromState == 0) { - onVolumeTransferFunctionChanged(); - } - }); - - m_opacityTFConnection = - cvc::state::instance(volrover3::app())(opacityTFPath).valueChanged.connect([this]() { - // Only reload if we're not currently updating state from widget - if (m_updatingFromState == 0) { - onVolumeTransferFunctionChanged(); - } - }); -} - -void TransferFunctionWidget::disconnectFromVolumeState() { - m_colorTFConnection.disconnect(); - m_opacityTFConnection.disconnect(); -} - -void TransferFunctionWidget::onVolumeTransferFunctionChanged() { - // Reload transfer function from selected volume's state - auto volume = getSelectedVolume(); - if (volume && m_updatingFromState == 0) { - m_updatingFromState++; - loadTransferFunctionFromVolume(volume); - m_updatingFromState--; - } -} - -#include "TransferFunctionWidget.moc" diff --git a/src/volrover3/VTKRenderWidget.cpp b/src/volrover3/VTKRenderWidget.cpp deleted file mode 100644 index adaf4157..00000000 --- a/src/volrover3/VTKRenderWidget.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -VTKRenderWidget::VTKRenderWidget(QWidget *parent) - : QVTK_WIDGET_BASE(parent), - m_renderWindow(vtkSmartPointer::New()), - m_renderer(vtkSmartPointer::New()), - m_cameraController(std::make_unique(volrover3::app())), - m_fpsAnnotation(vtkSmartPointer::New()), m_showFPS(false) { - initializeVTK(); - - // Set up timer to process SceneGraph events on main thread - connect(&m_eventTimer, &QTimer::timeout, this, &VTKRenderWidget::processSceneGraphEvents); - m_eventTimer.start(16); // ~60fps event processing - - // Set up timer to update FPS display (every 500ms) - connect(&m_fpsTimer, &QTimer::timeout, this, &VTKRenderWidget::updateFPSDisplay); - - // Load FPS display setting from state - m_showFPS = AppState::instance().showFPS(); - if (m_showFPS) { - m_fpsAnnotation->SetVisibility(true); - m_fpsTimer.start(500); - } -} - -VTKRenderWidget::~VTKRenderWidget() {} - -void VTKRenderWidget::initializeVTK() { - // Set up render window - setRenderWindow(m_renderWindow); - m_renderWindow->AddRenderer(m_renderer); - - // Set background color (dark gray) - m_renderer->SetBackground(0.2, 0.2, 0.2); - - // Set up camera - vtkCamera *camera = m_renderer->GetActiveCamera(); - camera->SetPosition(0, 0, 10); - camera->SetFocalPoint(0, 0, 0); - camera->SetViewUp(0, 1, 0); - - // Initialize camera controller - m_cameraController->setCamera(camera); - - // Set up FPS annotation in top-left corner - m_fpsAnnotation->SetText(2, "FPS: --"); // Position 2 = upper left - m_fpsAnnotation->GetTextProperty()->SetColor(1.0, 1.0, 0.0); // Yellow - m_fpsAnnotation->GetTextProperty()->SetFontSize(14); - m_fpsAnnotation->SetVisibility(false); // Hidden by default - m_renderer->AddViewProp(m_fpsAnnotation); - - // Enable focus for keyboard input - setFocusPolicy(Qt::StrongFocus); -} - -void VTKRenderWidget::setSceneGraph(std::shared_ptr sceneGraph) { - m_sceneGraph = sceneGraph; - if (m_sceneGraph) { - m_sceneGraph->setRenderer(m_renderer); - } -} - -void VTKRenderWidget::keyPressEvent(QKeyEvent *event) { - if (m_cameraController) { - m_cameraController->handleKeyPress(event->key()); - updateCamera(); - renderWindow()->Render(); - } - QVTK_WIDGET_BASE::keyPressEvent(event); -} - -void VTKRenderWidget::keyReleaseEvent(QKeyEvent *event) { - if (m_cameraController) { - m_cameraController->handleKeyRelease(event->key()); - } - QVTK_WIDGET_BASE::keyReleaseEvent(event); -} - -void VTKRenderWidget::mousePressEvent(QMouseEvent *event) { - m_lastMousePos = event->pos(); - if (m_cameraController) { - m_cameraController->handleMousePress(event->button()); - } - // Don't pass middle mouse to VTK to avoid conflicts with our - // CameraController - if (event->button() != Qt::MiddleButton) { - QVTK_WIDGET_BASE::mousePressEvent(event); - } -} - -void VTKRenderWidget::mouseReleaseEvent(QMouseEvent *event) { - if (m_cameraController) { - m_cameraController->handleMouseRelease(event->button()); - } - // Don't pass middle mouse to VTK to avoid conflicts with our - // CameraController - if (event->button() != Qt::MiddleButton) { - QVTK_WIDGET_BASE::mouseReleaseEvent(event); - } -} - -void VTKRenderWidget::mouseMoveEvent(QMouseEvent *event) { - if (m_cameraController) { - QPoint delta = event->pos() - m_lastMousePos; - m_cameraController->handleMouseMove(delta.x(), delta.y()); - updateCamera(); - renderWindow()->Render(); - } - m_lastMousePos = event->pos(); - // Don't pass middle mouse moves to VTK when middle button is pressed - bool isMiddlePressed = (event->buttons() & Qt::MiddleButton); - if (!isMiddlePressed) { - QVTK_WIDGET_BASE::mouseMoveEvent(event); - } -} - -void VTKRenderWidget::wheelEvent(QWheelEvent *event) { - if (m_cameraController) { - m_cameraController->handleMouseWheel(event->angleDelta().y()); - updateCamera(); - renderWindow()->Render(); - } - QVTK_WIDGET_BASE::wheelEvent(event); -} - -void VTKRenderWidget::updateCamera() { - if (m_cameraController) { - m_cameraController->update(); - } -} - -void VTKRenderWidget::resetCamera() { - if (m_renderer) { - m_renderer->ResetCamera(); - renderWindow()->Render(); - } -} - -void VTKRenderWidget::render() { - if (m_renderWindow) { - m_renderWindow->Render(); - } -} - -void VTKRenderWidget::processSceneGraphEvents() { - if (m_sceneGraph) { - m_sceneGraph->processEvents(); - // Trigger render if any events modified the scene - if (m_sceneGraph->checkAndResetRenderNeeded()) { - render(); - } - } -} - -void VTKRenderWidget::setShowFPS(bool show) { - m_showFPS = show; - - if (m_fpsAnnotation) { - m_fpsAnnotation->SetVisibility(show ? 1 : 0); - } - - if (show) { - // Start timer to update FPS display - m_fpsTimer.start(500); // Update every 500ms - updateFPSDisplay(); - } else { - m_fpsTimer.stop(); - } - - // Trigger a render to show/hide the annotation - if (m_renderWindow) { - m_renderWindow->Render(); - } -} - -void VTKRenderWidget::updateFPSDisplay() { - if (!m_showFPS || !m_renderer || !m_fpsAnnotation) { - return; - } - - // Get the last render time from the renderer - double renderTime = m_renderer->GetLastRenderTimeInSeconds(); - - QString fpsText; - if (renderTime > 0.0) { - double fps = 1.0 / renderTime; - fpsText = QString("FPS: %1").arg(fps, 0, 'f', 1); - } else { - fpsText = "FPS: --"; - } - - m_fpsAnnotation->SetText(2, fpsText.toStdString().c_str()); - - // Trigger render to update the display - if (m_renderWindow) { - m_renderWindow->Render(); - } -} diff --git a/src/volrover3/ViewerOptionsDialog.cpp b/src/volrover3/ViewerOptionsDialog.cpp deleted file mode 100644 index fa824483..00000000 --- a/src/volrover3/ViewerOptionsDialog.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -ViewerOptionsDialog::ViewerOptionsDialog(VTKRenderWidget *renderWidget, - std::shared_ptr sceneGraph, QWidget *parent) - : QWidget(parent, Qt::Window), m_renderWidget(renderWidget), m_sceneGraph(sceneGraph), - m_showFPSCheckBox(nullptr), m_graphicsRootComboBox(nullptr), m_refreshRootsButton(nullptr), - m_cameraComboBox(nullptr), m_refreshCamerasButton(nullptr) { - setupUI(); - connectSignals(); - loadFromState(); -} - -ViewerOptionsDialog::~ViewerOptionsDialog() { - for (auto &conn : m_connections) { - conn.disconnect(); - } -} - -void ViewerOptionsDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Display Options Group - QGroupBox *displayGroup = new QGroupBox(tr("Display Options")); - QFormLayout *displayLayout = new QFormLayout(displayGroup); - - m_showFPSCheckBox = new QCheckBox(tr("Show FPS Counter")); - m_showFPSCheckBox->setToolTip( - tr("Display frames per second in the top-left corner of the viewer")); - displayLayout->addRow(m_showFPSCheckBox); - - mainLayout->addWidget(displayGroup); - - // Scene Selection Group - QGroupBox *sceneGroup = new QGroupBox(tr("Scene Selection")); - QFormLayout *sceneLayout = new QFormLayout(sceneGroup); - - QHBoxLayout *rootsLayout = new QHBoxLayout(); - m_graphicsRootComboBox = new QComboBox(); - m_graphicsRootComboBox->setToolTip( - tr("Select which graphics scene to render (for future multi-scene support)")); - m_graphicsRootComboBox->setMinimumWidth(200); - rootsLayout->addWidget(m_graphicsRootComboBox); - m_refreshRootsButton = new QPushButton(tr("↻")); - m_refreshRootsButton->setToolTip(tr("Refresh the list of available graphics roots")); - m_refreshRootsButton->setMaximumWidth(30); - rootsLayout->addWidget(m_refreshRootsButton); - sceneLayout->addRow(tr("Graphics Root:"), rootsLayout); - - QHBoxLayout *cameraLayout = new QHBoxLayout(); - m_cameraComboBox = new QComboBox(); - m_cameraComboBox->setToolTip(tr("Select which camera to use (for future multi-camera support)")); - m_cameraComboBox->setMinimumWidth(200); - cameraLayout->addWidget(m_cameraComboBox); - m_refreshCamerasButton = new QPushButton(tr("↻")); - m_refreshCamerasButton->setToolTip(tr("Refresh the list of available cameras")); - m_refreshCamerasButton->setMaximumWidth(30); - cameraLayout->addWidget(m_refreshCamerasButton); - sceneLayout->addRow(tr("Camera:"), cameraLayout); - - mainLayout->addWidget(sceneGroup); - - // Add stretch to push everything to the top - mainLayout->addStretch(); - - // Set window properties - setWindowTitle(tr("Viewer Options")); - resize(350, 200); -} - -void ViewerOptionsDialog::connectSignals() { - connect(m_showFPSCheckBox, &QCheckBox::toggled, this, &ViewerOptionsDialog::onShowFPSChanged); - - connect(m_graphicsRootComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ViewerOptionsDialog::onGraphicsRootChanged); - - connect(m_cameraComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ViewerOptionsDialog::onCameraChanged); - - connect(m_refreshRootsButton, &QPushButton::clicked, this, - &ViewerOptionsDialog::refreshGraphicsRoots); - - connect(m_refreshCamerasButton, &QPushButton::clicked, this, - &ViewerOptionsDialog::refreshCameras); -} - -void ViewerOptionsDialog::loadFromState() { - // Load FPS display setting from AppState - bool showFPS = AppState::instance().showFPS(); - m_showFPSCheckBox->setChecked(showFPS); - - // Populate combo boxes - refreshGraphicsRoots(); - refreshCameras(); -} - -void ViewerOptionsDialog::showEvent(QShowEvent *event) { - loadFromState(); - QWidget::showEvent(event); -} - -void ViewerOptionsDialog::closeEvent(QCloseEvent *event) { QWidget::closeEvent(event); } - -void ViewerOptionsDialog::onShowFPSChanged(bool checked) { - AppState::instance().setShowFPS(checked); - - // Update the render widget to show/hide FPS display - if (m_renderWidget) { - m_renderWidget->setShowFPS(checked); - } -} - -void ViewerOptionsDialog::onGraphicsRootChanged(int index) { - Q_UNUSED(index); - // Currently only one graphics root is supported - // This is a placeholder for future multi-scene support -} - -void ViewerOptionsDialog::onCameraChanged(int index) { - Q_UNUSED(index); - // Currently only one camera is supported - // This is a placeholder for future multi-camera support -} - -void ViewerOptionsDialog::refreshGraphicsRoots() { - m_graphicsRootComboBox->clear(); - - if (m_sceneGraph) { - std::string statePrefix = m_sceneGraph->getStatePrefix(); - // Currently only one graphics root is supported - m_graphicsRootComboBox->addItem(QString::fromStdString(statePrefix + ".graphics.root")); - } - - // Disable combo box if only one option - m_graphicsRootComboBox->setEnabled(m_graphicsRootComboBox->count() > 1); -} - -void ViewerOptionsDialog::refreshCameras() { - m_cameraComboBox->clear(); - - if (m_sceneGraph) { - std::string statePrefix = m_sceneGraph->getStatePrefix(); - // Currently only one camera is supported - m_cameraComboBox->addItem(QString::fromStdString(statePrefix + ".camera")); - } - - // Disable combo box if only one option - m_cameraComboBox->setEnabled(m_cameraComboBox->count() > 1); -} diff --git a/src/volrover3/VolumeDialog.cpp b/src/volrover3/VolumeDialog.cpp deleted file mode 100644 index 6a075f17..00000000 --- a/src/volrover3/VolumeDialog.cpp +++ /dev/null @@ -1,350 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -VolumeDialog::VolumeDialog(std::shared_ptr sceneGraph, QWidget *parent) - : QDialog(parent), m_sceneGraph(sceneGraph), m_volumeComboBox(nullptr), - m_shadingCheckBox(nullptr), m_ambientSpinBox(nullptr), m_diffuseSpinBox(nullptr), - m_specularSpinBox(nullptr), m_specularPowerSpinBox(nullptr), - m_scalarOpacityUnitDistanceSpinBox(nullptr), m_sampleDistanceSpinBox(nullptr), - m_autoAdjustSampleDistancesCheckBox(nullptr), m_updating(false) { - setWindowTitle(tr("Volume Properties")); - setMinimumWidth(400); - setupUI(); - connectSignals(); - populateVolumeList(); - - // Connect to SceneGraph signal to monitor for new/removed volumes - if (m_sceneGraph) { - m_graphicsChangedConnection = m_sceneGraph->graphicsChanged.connect([this]() { - QMetaObject::invokeMethod(this, "onGraphicsChildrenChanged", Qt::QueuedConnection); - }); - } -} - -void VolumeDialog::setupUI() { - QVBoxLayout *mainLayout = new QVBoxLayout(this); - - // Volume Selection Group - QGroupBox *selectionGroup = new QGroupBox(tr("Volume Selection"), this); - QVBoxLayout *selectionVLayout = new QVBoxLayout(selectionGroup); - - // Combo box and delete button in horizontal layout - QHBoxLayout *comboLayout = new QHBoxLayout(); - m_volumeComboBox = new QComboBox(this); - m_deleteButton = new QPushButton(tr("Delete"), this); - m_deleteButton->setToolTip(tr("Remove selected volume from scene")); - comboLayout->addWidget(new QLabel(tr("Volume:"), this)); - comboLayout->addWidget(m_volumeComboBox, 1); - comboLayout->addWidget(m_deleteButton); - selectionVLayout->addLayout(comboLayout); - - mainLayout->addWidget(selectionGroup); - - // Rendering Properties Group - QGroupBox *renderGroup = new QGroupBox(tr("Rendering Properties"), this); - QFormLayout *renderLayout = new QFormLayout(renderGroup); - - m_shadingCheckBox = new QCheckBox(tr("Enable Shading"), this); - renderLayout->addRow(m_shadingCheckBox); - - m_ambientSpinBox = new QDoubleSpinBox(this); - m_ambientSpinBox->setRange(0.0, 1.0); - m_ambientSpinBox->setSingleStep(0.01); - m_ambientSpinBox->setDecimals(3); - renderLayout->addRow(tr("Ambient:"), m_ambientSpinBox); - - m_diffuseSpinBox = new QDoubleSpinBox(this); - m_diffuseSpinBox->setRange(0.0, 1.0); - m_diffuseSpinBox->setSingleStep(0.01); - m_diffuseSpinBox->setDecimals(3); - renderLayout->addRow(tr("Diffuse:"), m_diffuseSpinBox); - - m_specularSpinBox = new QDoubleSpinBox(this); - m_specularSpinBox->setRange(0.0, 1.0); - m_specularSpinBox->setSingleStep(0.01); - m_specularSpinBox->setDecimals(3); - renderLayout->addRow(tr("Specular:"), m_specularSpinBox); - - m_specularPowerSpinBox = new QDoubleSpinBox(this); - m_specularPowerSpinBox->setRange(0.0, 128.0); - m_specularPowerSpinBox->setSingleStep(1.0); - m_specularPowerSpinBox->setDecimals(1); - renderLayout->addRow(tr("Specular Power:"), m_specularPowerSpinBox); - - mainLayout->addWidget(renderGroup); - - // Advanced Properties Group - QGroupBox *advancedGroup = new QGroupBox(tr("Advanced Properties"), this); - QFormLayout *advancedLayout = new QFormLayout(advancedGroup); - - m_scalarOpacityUnitDistanceSpinBox = new QDoubleSpinBox(this); - m_scalarOpacityUnitDistanceSpinBox->setRange(0.001, 100.0); - m_scalarOpacityUnitDistanceSpinBox->setSingleStep(0.1); - m_scalarOpacityUnitDistanceSpinBox->setDecimals(3); - m_scalarOpacityUnitDistanceSpinBox->setToolTip(tr("Distance over which opacity is evaluated")); - advancedLayout->addRow(tr("Scalar Opacity Unit Distance:"), m_scalarOpacityUnitDistanceSpinBox); - - m_sampleDistanceSpinBox = new QDoubleSpinBox(this); - m_sampleDistanceSpinBox->setRange(0.001, 10.0); - m_sampleDistanceSpinBox->setSingleStep(0.01); - m_sampleDistanceSpinBox->setDecimals(3); - m_sampleDistanceSpinBox->setToolTip(tr("Distance between samples during ray casting")); - advancedLayout->addRow(tr("Sample Distance:"), m_sampleDistanceSpinBox); - - m_autoAdjustSampleDistancesCheckBox = new QCheckBox(tr("Auto-adjust Sample Distances"), this); - m_autoAdjustSampleDistancesCheckBox->setToolTip( - tr("Automatically adjust sample distances based on volume size")); - advancedLayout->addRow(m_autoAdjustSampleDistancesCheckBox); - - mainLayout->addWidget(advancedGroup); - - setLayout(mainLayout); -} - -void VolumeDialog::connectSignals() { - connect(m_volumeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &VolumeDialog::onVolumeSelected); - connect(m_deleteButton, &QPushButton::clicked, this, &VolumeDialog::onDeleteButtonClicked); - - // Rendering property signals - connect(m_shadingCheckBox, &QCheckBox::toggled, this, &VolumeDialog::onMaterialPropertyChanged); - connect(m_ambientSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &VolumeDialog::onMaterialPropertyChanged); - connect(m_diffuseSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &VolumeDialog::onMaterialPropertyChanged); - connect(m_specularSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &VolumeDialog::onMaterialPropertyChanged); - connect(m_specularPowerSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &VolumeDialog::onMaterialPropertyChanged); - connect(m_scalarOpacityUnitDistanceSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), - this, &VolumeDialog::onMaterialPropertyChanged); - connect(m_sampleDistanceSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), this, - &VolumeDialog::onMaterialPropertyChanged); - connect(m_autoAdjustSampleDistancesCheckBox, &QCheckBox::toggled, this, - &VolumeDialog::onMaterialPropertyChanged); -} - -void VolumeDialog::populateVolumeList() { - m_volumeComboBox->clear(); - m_volumePaths.clear(); - - if (!m_sceneGraph) - return; - - // Get all volume nodes recursively - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - - for (const auto &volumeNode : allVolumes) { - if (volumeNode) { - // Use full state tree path for uniqueness - std::string fullPath = volumeNode->getState().fullName(); - m_volumePaths.push_back(fullPath); - - // Display the node name in the combo box - m_volumeComboBox->addItem(QString::fromStdString(volumeNode->getName())); - } - } - - if (m_volumeComboBox->count() == 0) { - setPropertiesEnabled(false); - } else { - setPropertiesEnabled(true); - onVolumeSelected(0); - } -} - -void VolumeDialog::onGraphicsChildrenChanged() { - if (!m_sceneGraph) - return; - - // Save current selection (by path) - QString currentSelection; - int currentIndex = m_volumeComboBox->currentIndex(); - if (currentIndex >= 0 && currentIndex < static_cast(m_volumePaths.size())) { - currentSelection = QString::fromStdString(m_volumePaths[currentIndex]); - } - - // Refresh the list - populateVolumeList(); - - // Try to restore the previous selection by matching path - bool selectionRestored = false; - if (!currentSelection.isEmpty()) { - for (int i = 0; i < static_cast(m_volumePaths.size()); ++i) { - if (QString::fromStdString(m_volumePaths[i]) == currentSelection) { - m_volumeComboBox->setCurrentIndex(i); - selectionRestored = true; - break; - } - } - } - - // If selection couldn't be restored (volume was deleted), disable controls if no volumes - if (!selectionRestored && m_volumeComboBox->count() == 0) { - setPropertiesEnabled(false); - } -} - -void VolumeDialog::onVolumeSelected(int index) { - if (m_updating) - return; - - // Disconnect from previous node's state changes - m_nodeStateConnection.disconnect(); - - if (index < 0 || index >= static_cast(m_volumePaths.size())) { - setPropertiesEnabled(false); - return; - } - - // Connect to selected node's state changes - const std::string &volumePath = m_volumePaths[index]; - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - for (const auto &volumeNode : allVolumes) { - if (volumeNode && volumeNode->getState().fullName() == volumePath) { - // Connect to the node's childChanged signal (fires when any child state changes) - m_nodeStateConnection = - volumeNode->getState().childChanged.connect([this](const std::string &) { - // Use Qt's queued connection for thread-safe UI updates - QMetaObject::invokeMethod(this, "onNodeStateChanged", Qt::QueuedConnection); - }); - break; - } - } - - setPropertiesEnabled(true); - updatePropertiesFromNode(); -} - -void VolumeDialog::updatePropertiesFromNode() { - if (m_updating) - return; - - int index = m_volumeComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_volumePaths.size())) - return; - - const std::string &volumePath = m_volumePaths[index]; - - // Get the volume node - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - for (const auto &volumeNode : allVolumes) { - if (volumeNode && volumeNode->getState().fullName() == volumePath) { - m_updating = true; - - // Update rendering properties - m_shadingCheckBox->setChecked(volumeNode->getShading()); - m_ambientSpinBox->setValue(volumeNode->getAmbient()); - m_diffuseSpinBox->setValue(volumeNode->getDiffuse()); - m_specularSpinBox->setValue(volumeNode->getSpecular()); - m_specularPowerSpinBox->setValue(volumeNode->getSpecularPower()); - m_scalarOpacityUnitDistanceSpinBox->setValue(volumeNode->getScalarOpacityUnitDistance()); - m_sampleDistanceSpinBox->setValue(volumeNode->getSampleDistance()); - m_autoAdjustSampleDistancesCheckBox->setChecked(volumeNode->getAutoAdjustSampleDistances()); - - m_updating = false; - break; - } - } -} - -void VolumeDialog::onNodeStateChanged() { - // Update UI from state tree when node state changes - updatePropertiesFromNode(); -} - -void VolumeDialog::onMaterialPropertyChanged() { - if (m_updating) - return; - - int index = m_volumeComboBox->currentIndex(); - if (index < 0 || index >= static_cast(m_volumePaths.size())) { - setPropertiesEnabled(false); - return; - } - - const std::string &volumePath = m_volumePaths[index]; - - // Get the volume node - auto allVolumes = m_sceneGraph->getAllVolumeGraphics(); - for (const auto &volumeNode : allVolumes) { - if (volumeNode && volumeNode->getState().fullName() == volumePath) { - // Determine which property changed and update it - QObject *sender = QObject::sender(); - - if (sender == m_shadingCheckBox) { - volumeNode->setShading(m_shadingCheckBox->isChecked()); - } else if (sender == m_ambientSpinBox) { - volumeNode->setAmbient(m_ambientSpinBox->value()); - } else if (sender == m_diffuseSpinBox) { - volumeNode->setDiffuse(m_diffuseSpinBox->value()); - } else if (sender == m_specularSpinBox) { - volumeNode->setSpecular(m_specularSpinBox->value()); - } else if (sender == m_specularPowerSpinBox) { - volumeNode->setSpecularPower(m_specularPowerSpinBox->value()); - } else if (sender == m_scalarOpacityUnitDistanceSpinBox) { - volumeNode->setScalarOpacityUnitDistance(m_scalarOpacityUnitDistanceSpinBox->value()); - } else if (sender == m_sampleDistanceSpinBox) { - volumeNode->setSampleDistance(m_sampleDistanceSpinBox->value()); - } else if (sender == m_autoAdjustSampleDistancesCheckBox) { - volumeNode->setAutoAdjustSampleDistances(m_autoAdjustSampleDistancesCheckBox->isChecked()); - } - - break; - } - } -} - -void VolumeDialog::onDeleteButtonClicked() { - if (!m_sceneGraph) - return; - - int currentIndex = m_volumeComboBox->currentIndex(); - if (currentIndex < 0 || currentIndex >= static_cast(m_volumePaths.size())) { - return; - } - - // Extract the volume name from the full path (last component after the last dot) - std::string volumePath = m_volumePaths[currentIndex]; - size_t lastDot = volumePath.find_last_of('.'); - std::string volumeName = - (lastDot != std::string::npos) ? volumePath.substr(lastDot + 1) : volumePath; - - // Confirm deletion - QMessageBox::StandardButton reply; - reply = QMessageBox::question( - this, tr("Delete Volume"), - tr("Are you sure you want to delete '%1'?").arg(QString::fromStdString(volumeName)), - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - m_sceneGraph->removeGraphics(volumeName); - // The combo box will update automatically via the state tree signal - } -} - -void VolumeDialog::setPropertiesEnabled(bool enabled) { - m_deleteButton->setEnabled(enabled); - m_shadingCheckBox->setEnabled(enabled); - m_ambientSpinBox->setEnabled(enabled); - m_diffuseSpinBox->setEnabled(enabled); - m_specularSpinBox->setEnabled(enabled); - m_specularPowerSpinBox->setEnabled(enabled); - m_scalarOpacityUnitDistanceSpinBox->setEnabled(enabled); - m_sampleDistanceSpinBox->setEnabled(enabled); - m_autoAdjustSampleDistancesCheckBox->setEnabled(enabled); -} diff --git a/src/volrover3/VolumeNode.cpp b/src/volrover3/VolumeNode.cpp deleted file mode 100644 index cfba3f03..00000000 --- a/src/volrover3/VolumeNode.cpp +++ /dev/null @@ -1,704 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -VolumeNode::VolumeNode(cvc::app &ctx, const std::string &statePath, const std::string &name) - : GraphicsNode(ctx, statePath, name), m_hasVolume(false), - m_vtkVolume(vtkSmartPointer::New()), - m_mapper(vtkSmartPointer::New()), - m_imageData(vtkSmartPointer::New()), - m_colorFunc(vtkSmartPointer::New()), - m_opacityFunc(vtkSmartPointer::New()), - m_volumeProperty(vtkSmartPointer::New()), m_dataMin(0.0), m_dataMax(1.0), - m_shading(true), m_ambient(0.3), m_diffuse(0.6), m_specular(0.2), m_specularPower(10.0), - m_scalarOpacityUnitDistance(1.0), m_sampleDistance(0.5), m_autoAdjustSampleDistances(true) { - // Initialize with empty 1x1x1 volume to avoid VTK errors before data is loaded - m_imageData->SetDimensions(1, 1, 1); - m_imageData->AllocateScalars(VTK_UNSIGNED_CHAR, 1); - unsigned char *ptr = static_cast(m_imageData->GetScalarPointer()); - ptr[0] = 0; - - m_mapper->SetInputData(m_imageData); - m_vtkVolume->SetMapper(m_mapper); - - // Set up volume property - m_volumeProperty->SetColor(m_colorFunc); - m_volumeProperty->SetScalarOpacity(m_opacityFunc); - m_volumeProperty->SetShade(m_shading ? 1 : 0); - m_volumeProperty->SetInterpolationTypeToLinear(); - - // Set lighting properties - m_volumeProperty->SetAmbient(m_ambient); - m_volumeProperty->SetDiffuse(m_diffuse); - m_volumeProperty->SetSpecular(m_specular); - m_volumeProperty->SetSpecularPower(m_specularPower); - - // Set scalar opacity unit distance - m_volumeProperty->SetScalarOpacityUnitDistance(m_scalarOpacityUnitDistance); - - // Use composite blending for proper opacity - m_mapper->SetBlendModeToComposite(); - - // Configure the smart volume mapper - m_mapper->SetAutoAdjustSampleDistances(m_autoAdjustSampleDistances ? 1 : 0); - m_mapper->SetSampleDistance(m_sampleDistance); - - m_vtkVolume->SetProperty(m_volumeProperty); - - // Initialize state tree with all rendering attributes - if (!statePath.empty()) { - getState("visible").value(1); // Visible by default - - // Shading properties - getState("shading").value(m_shading ? 1 : 0); - getState("ambient").value(m_ambient); - getState("diffuse").value(m_diffuse); - getState("specular").value(m_specular); - getState("specular_power").value(m_specularPower); - - // Sampling properties - getState("scalar_opacity_unit_distance").value(m_scalarOpacityUnitDistance); - getState("sample_distance").value(m_sampleDistance); - getState("auto_adjust_sample_distances").value(m_autoAdjustSampleDistances ? 1 : 0); - - // Data range (will be updated when volume is loaded) - getState("data_min").value(m_dataMin); - getState("data_max").value(m_dataMax); - - // Transfer function state (stored as serialized arrays) - getState("transfer_function.color").value(""); - getState("transfer_function.opacity").value(""); - } - - // Initialize with default transfer function - setDefaultTransferFunction(); -} - -VolumeNode::~VolumeNode() { - // Disconnect all signal connections to prevent new handlers from queuing - m_dataConnection.disconnect(); - m_shadingConnection.disconnect(); - m_ambientConnection.disconnect(); - m_diffuseConnection.disconnect(); - m_specularConnection.disconnect(); - m_specularPowerConnection.disconnect(); - m_scalarOpacityUnitDistanceConnection.disconnect(); - m_sampleDistanceConnection.disconnect(); - m_autoAdjustSampleDistancesConnection.disconnect(); - - // Note: Do NOT call waitForHandlers() here! - // The base class state_object destructor will handle it, - // but only AFTER our VTK members are destroyed. Calling it here would - // allow handlers to access destroyed VTK objects. -} - -vtkProp *VolumeNode::getProp() { return m_vtkVolume; } - -void VolumeNode::applyTransformToVTK() { - // Use generic helper to apply world transform - applyWorldTransformToProps({m_vtkVolume}); -} - -void VolumeNode::applyClipPlanes(vtkPlaneCollection *planes) { - if (m_mapper) { - if (planes && planes->GetNumberOfItems() > 0) { - m_mapper->SetClippingPlanes(planes); - } else { - m_mapper->RemoveAllClippingPlanes(); - } - } -} - -void VolumeNode::addToRenderer(vtkRenderer *renderer) { - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Adding to renderer"); - GraphicsNode::addToRenderer(renderer); - - // Verify it was actually added and log detailed info - if (renderer && renderer->GetVolumes()->IsItemPresent(m_vtkVolume)) { - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + - "]: CONFIRMED - Volume is in renderer"); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + - "]: Total volumes in renderer: " + - std::to_string(renderer->GetVolumes()->GetNumberOfItems())); - - // Log volume property details - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Volume visibility: " + - std::to_string(m_vtkVolume->GetVisibility())); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Volume pickable: " + - std::to_string(m_vtkVolume->GetPickable())); - - // Log image data details - int dims[3]; - m_imageData->GetDimensions(dims); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + - "]: Image data dimensions: [" + std::to_string(dims[0]) + ", " + - std::to_string(dims[1]) + ", " + std::to_string(dims[2]) + "]"); - - double *bounds = m_vtkVolume->GetBounds(); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Volume bounds: [" + - std::to_string(bounds[0]) + ", " + std::to_string(bounds[1]) + - ", " + std::to_string(bounds[2]) + ", " + - std::to_string(bounds[3]) + ", " + std::to_string(bounds[4]) + - ", " + std::to_string(bounds[5]) + "]"); - - // Log transfer function ranges - double colorRange[2], opacityRange[2]; - m_colorFunc->GetRange(colorRange); - m_opacityFunc->GetRange(opacityRange); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Color TF range: [" + - std::to_string(colorRange[0]) + ", " + - std::to_string(colorRange[1]) + "]"); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Opacity TF range: [" + - std::to_string(opacityRange[0]) + ", " + - std::to_string(opacityRange[1]) + "]"); - - // Log opacity at a few sample points - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Opacity at dataMin(" + - std::to_string(m_dataMin) + - "): " + std::to_string(m_opacityFunc->GetValue(m_dataMin))); - volrover3::app().log( - 0, "VolumeNode::addToRenderer[" + getName() + "]: Opacity at dataMid: " + - std::to_string(m_opacityFunc->GetValue((m_dataMin + m_dataMax) / 2.0))); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + "]: Opacity at dataMax(" + - std::to_string(m_dataMax) + - "): " + std::to_string(m_opacityFunc->GetValue(m_dataMax))); - - // Log scalar range from image data - double *scalarRange = m_imageData->GetScalarRange(); - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + - "]: Image data scalar range: [" + std::to_string(scalarRange[0]) + - ", " + std::to_string(scalarRange[1]) + "]"); - } else { - volrover3::app().log(0, "VolumeNode::addToRenderer[" + getName() + - "]: WARNING - Volume NOT in renderer!"); - } -} - -void VolumeNode::setVolume(const cvc::volume &vol) { - cvc::thread_info ti(volrover3::app(), BOOST_CURRENT_FUNCTION); - - volrover3::app().log(0, "\n=== VolumeNode::setVolume[" + getName() + "] ==="); - - // Store the volume object - m_volume = std::make_shared(vol); - - updateImageData(vol); - m_dataMin = vol.min(); - m_dataMax = vol.max(); - - // Update state tree with data range - getState("data_min").value(m_dataMin); - getState("data_max").value(m_dataMax); - - volrover3::app().log(0, " Data range: [" + std::to_string(m_dataMin) + ", " + - std::to_string(m_dataMax) + "]"); - volrover3::app().log(0, " Dimensions: [" + std::to_string(vol.XDim()) + ", " + - std::to_string(vol.YDim()) + ", " + std::to_string(vol.ZDim()) + "]"); - volrover3::app().log(0, " Bounding box: [" + std::to_string(vol.XMin()) + "," + - std::to_string(vol.XMax()) + "], [" + std::to_string(vol.YMin()) + - "," + std::to_string(vol.YMax()) + "], [" + - std::to_string(vol.ZMin()) + "," + std::to_string(vol.ZMax()) + "]"); - volrover3::app().log(0, " Spans: [" + std::to_string(vol.XSpan()) + ", " + - std::to_string(vol.YSpan()) + ", " + std::to_string(vol.ZSpan()) + - "]"); - - // Calculate appropriate scalar opacity unit distance based on volume diagonal - double dx = vol.XSpan(); - double dy = vol.YSpan(); - double dz = vol.ZSpan(); - double diagonal = std::sqrt(dx * dx + dy * dy + dz * dz); - volrover3::app().log(0, " Diagonal: " + std::to_string(diagonal) + - ", ScalarOpacityUnitDistance: " + std::to_string(diagonal / 100.0)); - m_volumeProperty->SetScalarOpacityUnitDistance(diagonal / 100.0); - - // Set transfer function using actual data range - volrover3::app().log(0, " Setting default transfer function..."); - setDefaultTransferFunction(); - updateTransferFunctions(); - updateMetadata(vol); - m_hasVolume = true; - - // Update bbox to match volume bounds - updateBoundingBoxNode(); - - // Notify parent to resync bounds if it's a NullGraphicNode with auto-sync enabled - if (m_parent) { - auto nullParent = dynamic_cast(m_parent); - if (nullParent) { - nullParent->syncBoundsToChildren(); - } - } - - volrover3::app().log(0, "=================================\n"); -} - -void VolumeNode::updateImageData(const cvc::volume &vol) { - volrover3::app().log(0, "\n VolumeNode::updateImageData - Copying volume data to VTK..."); - - // Get dimensions - int dims[3] = {static_cast(vol.XDim()), static_cast(vol.YDim()), - static_cast(vol.ZDim())}; - - volrover3::app().log(0, " CVC Volume bounds: X=[" + std::to_string(vol.XMin()) + ", " + - std::to_string(vol.XMax()) + "]"); - volrover3::app().log(0, " Y=[" + std::to_string(vol.YMin()) + ", " + - std::to_string(vol.YMax()) + "]"); - volrover3::app().log(0, " Z=[" + std::to_string(vol.ZMin()) + ", " + - std::to_string(vol.ZMax()) + "]"); - volrover3::app().log(0, " CVC XSpan/YSpan/ZSpan: [" + std::to_string(vol.XSpan()) + ", " + - std::to_string(vol.YSpan()) + ", " + std::to_string(vol.ZSpan()) + - "]"); - - // CRITICAL FIX: Calculate spacing directly from bounding box, not from Span() methods - // The Span() methods appear to return incorrect values for some volumes - double spacing[3] = {(vol.XMax() - vol.XMin()) / vol.XDim(), - (vol.YMax() - vol.YMin()) / vol.YDim(), - (vol.ZMax() - vol.ZMin()) / vol.ZDim()}; - - volrover3::app().log(0, " Calculated spacing: [" + std::to_string(spacing[0]) + ", " + - std::to_string(spacing[1]) + ", " + std::to_string(spacing[2]) + "]"); - - // Get origin - double origin[3] = {vol.XMin(), vol.YMin(), vol.ZMin()}; - - volrover3::app().log(0, " Origin: [" + std::to_string(origin[0]) + ", " + - std::to_string(origin[1]) + ", " + std::to_string(origin[2]) + "]"); - - // Determine VTK scalar type - int scalarType; - std::string scalarTypeName; - switch (vol.voxelType()) { - case cvc::UChar: - scalarType = VTK_UNSIGNED_CHAR; - scalarTypeName = "UChar"; - break; - case cvc::UShort: - scalarType = VTK_UNSIGNED_SHORT; - scalarTypeName = "UShort"; - break; - case cvc::UInt: - scalarType = VTK_UNSIGNED_INT; - scalarTypeName = "UInt"; - break; - case cvc::Float: - scalarType = VTK_FLOAT; - scalarTypeName = "Float"; - break; - case cvc::Double: - scalarType = VTK_DOUBLE; - scalarTypeName = "Double"; - break; - default: - scalarType = VTK_FLOAT; - scalarTypeName = "Float (default)"; - break; - } - - volrover3::app().log(0, " Voxel type: " + scalarTypeName); - - // Set up image data - m_imageData->SetDimensions(dims); - m_imageData->SetSpacing(spacing); - m_imageData->SetOrigin(origin); - m_imageData->AllocateScalars(scalarType, 1); - - // Copy voxel data - void *vtkPtr = m_imageData->GetScalarPointer(); - const unsigned char *cvcPtr = *vol; - - size_t numVoxels = vol.XDim() * vol.YDim() * vol.ZDim(); - size_t bytesPerVoxel = vol.voxelSize(); - size_t totalBytes = numVoxels * bytesPerVoxel; - - volrover3::app().log(0, " Total voxels: " + std::to_string(numVoxels) + - ", bytes per voxel: " + std::to_string(bytesPerVoxel) + - ", total bytes: " + std::to_string(totalBytes)); - volrover3::app().log(0, " CVC data pointer: " + std::string(cvcPtr ? "VALID" : "NULL")); - volrover3::app().log(0, " VTK data pointer: " + std::string(vtkPtr ? "VALID" : "NULL")); - - if (cvcPtr && vtkPtr) { - std::memcpy(vtkPtr, cvcPtr, totalBytes); - volrover3::app().log(0, " \u2713 Data copied successfully"); - } else { - volrover3::app().log(0, " \u2717 ERROR: Cannot copy data - null pointer!"); - } - - m_imageData->Modified(); -} - -void VolumeNode::setTransferFunction(const std::vector &colorTable, - const std::vector &opacityTable) { - volrover3::app().log(0, "\nVolumeNode::setTransferFunction[" + getName() + - "]: " + std::to_string(colorTable.size() / 4) + " color pts, " + - std::to_string(opacityTable.size() / 2) + " opacity pts"); - - // DEBUG: Log first few color values to see what we're getting - if (colorTable.size() >= 8) { - volrover3::app().log(0, " First 2 color points:"); - volrover3::app().log(0, " [0]: scalar=" + std::to_string(colorTable[0]) + ", rgb=(" + - std::to_string(colorTable[1]) + "," + - std::to_string(colorTable[2]) + "," + - std::to_string(colorTable[3]) + ")"); - volrover3::app().log(0, " [1]: scalar=" + std::to_string(colorTable[4]) + ", rgb=(" + - std::to_string(colorTable[5]) + "," + - std::to_string(colorTable[6]) + "," + - std::to_string(colorTable[7]) + ")"); - } - - // Clear existing functions - m_colorFunc->RemoveAllPoints(); - m_opacityFunc->RemoveAllPoints(); - - // Add color points (RGB triplets) - for (size_t i = 0; i < colorTable.size() / 4; ++i) { - double scalar = colorTable[i * 4 + 0]; - double r = colorTable[i * 4 + 1]; - double g = colorTable[i * 4 + 2]; - double b = colorTable[i * 4 + 3]; - m_colorFunc->AddRGBPoint(scalar, r, g, b); - } - - // Add opacity points - for (size_t i = 0; i < opacityTable.size() / 2; ++i) { - double scalar = opacityTable[i * 2 + 0]; - double opacity = opacityTable[i * 2 + 1]; - m_opacityFunc->AddPoint(scalar, opacity); - - if (i < 3) { // Log first few points - volrover3::app().log(0, " Opacity[" + std::to_string(i) + "]: scalar=" + - std::to_string(scalar) + ", opacity=" + std::to_string(opacity)); - } - } - - updateTransferFunctions(); - - // Save to state tree only if values changed - // Build strings for comparison - std::ostringstream colorStr, opacityStr; - for (size_t i = 0; i < colorTable.size(); ++i) { - if (i > 0) - colorStr << ","; - colorStr << std::fixed << std::setprecision(6) << colorTable[i]; - } - for (size_t i = 0; i < opacityTable.size(); ++i) { - if (i > 0) - opacityStr << ","; - opacityStr << std::fixed << std::setprecision(6) << opacityTable[i]; - } - - // Only update state if values actually changed - std::string currentColorStr = getState("transfer_function.color").value(); - std::string currentOpacityStr = getState("transfer_function.opacity").value(); - - if (colorStr.str() != currentColorStr) { - getState("transfer_function.color").value(colorStr.str()); - } - if (opacityStr.str() != currentOpacityStr) { - getState("transfer_function.opacity").value(opacityStr.str()); - } -} - -void VolumeNode::setDefaultTransferFunction() { - m_colorFunc->RemoveAllPoints(); - m_opacityFunc->RemoveAllPoints(); - - // Default grayscale color map using actual data range - m_colorFunc->AddRGBPoint(m_dataMin, 0.0, 0.0, 0.0); - m_colorFunc->AddRGBPoint(m_dataMax, 1.0, 1.0, 1.0); - - // Default opacity ramp using actual data range - m_opacityFunc->AddPoint(m_dataMin, 0.0); - m_opacityFunc->AddPoint(m_dataMax, 1.0); - - // Save to state tree - std::ostringstream colorStr, opacityStr; - colorStr << m_dataMin << ",0,0,0," << m_dataMax << ",1,1,1"; - opacityStr << m_dataMin << ",0," << m_dataMax << ",1"; - getState("transfer_function.color").value(colorStr.str()); - getState("transfer_function.opacity").value(opacityStr.str()); -} - -std::vector VolumeNode::getTransferFunctionColorTable() const { - std::string tableStr = getState("transfer_function.color").value(); - std::vector table; - - if (tableStr.empty()) { - return table; - } - - std::istringstream iss(tableStr); - std::string value; - while (std::getline(iss, value, ',')) { - table.push_back(std::stod(value)); - } - - return table; -} - -std::vector VolumeNode::getTransferFunctionOpacityTable() const { - std::string tableStr = getState("transfer_function.opacity").value(); - std::vector table; - - if (tableStr.empty()) { - return table; - } - - std::istringstream iss(tableStr); - std::string value; - while (std::getline(iss, value, ',')) { - table.push_back(std::stod(value)); - } - - return table; -} - -void VolumeNode::setShading(bool enabled) { getState("shading").value(enabled ? 1 : 0); } - -void VolumeNode::setAmbient(double value) { getState("ambient").value(value); } - -void VolumeNode::setDiffuse(double value) { getState("diffuse").value(value); } - -void VolumeNode::setSpecular(double value) { getState("specular").value(value); } - -void VolumeNode::setSpecularPower(double value) { getState("specular_power").value(value); } - -void VolumeNode::setScalarOpacityUnitDistance(double value) { - getState("scalar_opacity_unit_distance").value(value); -} - -void VolumeNode::setSampleDistance(double value) { getState("sample_distance").value(value); } - -void VolumeNode::setAutoAdjustSampleDistances(bool enabled) { - getState("auto_adjust_sample_distances").value(enabled ? 1 : 0); -} - -void VolumeNode::updateTransferFunctions() { - static int callCount = 0; - if (callCount++ == 0) { - volrover3::app().log(0, "\nVolumeNode::updateTransferFunctions[" + getName() + "]: First call"); - volrover3::app().log(0, " Data range: [" + std::to_string(m_dataMin) + ", " + - std::to_string(m_dataMax) + "]"); - } - - m_colorFunc->Modified(); - m_opacityFunc->Modified(); - m_volumeProperty->Modified(); - m_vtkVolume->Modified(); - m_mapper->Modified(); - m_imageData->Modified(); -} - -cvc::bounding_box VolumeNode::getBoundingBox() const { - if (m_volume) { - try { - return cvc::bounding_box(m_volume->XMin(), m_volume->YMin(), m_volume->ZMin(), - m_volume->XMax(), m_volume->YMax(), m_volume->ZMax()); - } catch (...) { - // Bounding box calculations can throw for empty/invalid volumes - return cvc::bounding_box(0, 0, 0, 0, 0, 0); - } - } - // Return empty bounding box - return cvc::bounding_box(0, 0, 0, 0, 0, 0); -} - -// Note: syncToState and syncFromState removed - state_object handles state synchronization -// automatically - -void VolumeNode::handleStateChanged(const std::string &childState) { - volrover3::app().log(2, str(boost::format("VolumeNode::handleStateChanged(%s) for '%s'") % - childState % getName())); - - // Handle volume-specific state changes - if (childState == "shading") { - runOnMainThread([this]() { - m_shading = getState("shading").value(); - if (m_volumeProperty) { - m_volumeProperty->SetShade(m_shading ? 1 : 0); - } - }); - } else if (childState == "ambient") { - runOnMainThread([this]() { - m_ambient = getState("ambient").value(); - if (m_volumeProperty) { - m_volumeProperty->SetAmbient(m_ambient); - } - }); - } else if (childState == "diffuse") { - runOnMainThread([this]() { - m_diffuse = getState("diffuse").value(); - if (m_volumeProperty) { - m_volumeProperty->SetDiffuse(m_diffuse); - } - }); - } else if (childState == "specular") { - runOnMainThread([this]() { - m_specular = getState("specular").value(); - if (m_volumeProperty) { - m_volumeProperty->SetSpecular(m_specular); - } - }); - } else if (childState == "specular_power") { - runOnMainThread([this]() { - m_specularPower = getState("specular_power").value(); - if (m_volumeProperty) { - m_volumeProperty->SetSpecularPower(m_specularPower); - } - }); - } else if (childState == "scalar_opacity_unit_distance") { - runOnMainThread([this]() { - m_scalarOpacityUnitDistance = getState("scalar_opacity_unit_distance").value(); - if (m_volumeProperty) { - m_volumeProperty->SetScalarOpacityUnitDistance(m_scalarOpacityUnitDistance); - } - }); - } else if (childState == "sample_distance") { - runOnMainThread([this]() { - m_sampleDistance = getState("sample_distance").value(); - if (m_mapper) { - m_mapper->SetSampleDistance(m_sampleDistance); - } - }); - } else if (childState == "auto_adjust_sample_distances") { - runOnMainThread([this]() { - m_autoAdjustSampleDistances = getState("auto_adjust_sample_distances").value(); - if (m_mapper) { - m_mapper->SetAutoAdjustSampleDistances(m_autoAdjustSampleDistances ? 1 : 0); - } - }); - } else if (childState == "data_min" || childState == "data_max") { - runOnMainThread([this]() { - try { - // Data range changed - update transfer functions - m_dataMin = getState("data_min").value(); - m_dataMax = getState("data_max").value(); - setDefaultTransferFunction(); - updateTransferFunctions(); - } catch (const boost::bad_lexical_cast &) { - // Ignore - state initialization may trigger before all components are set - } - }); - } else { - // Delegate to parent for common graphics fields - GraphicsNode::handleStateChanged(childState); - } -} - -bool VolumeNode::isComputedMetadata(const std::string &key) { - // These metadata keys are computed from volume data and should be read-only - static const std::set computedKeys = {"dim_x", - "dim_y", - "dim_z", - "bbox_min_x", - "bbox_min_y", - "bbox_min_z", - "bbox_max_x", - "bbox_max_y", - "bbox_max_z", - "spacing_x", - "spacing_y", - "spacing_z", - "data_range_min", - "data_range_max", - "voxel_type", - "bounding_box", - "filename", - "combined_bbox_min_x", - "combined_bbox_min_y", - "combined_bbox_min_z", - "combined_bbox_max_x", - "combined_bbox_max_y", - "combined_bbox_max_z", - "combined_extent_x", - "combined_extent_y", - "combined_extent_z", - "combined_center_x", - "combined_center_y", - "combined_center_z"}; - - return computedKeys.find(key) != computedKeys.end(); -} - -void VolumeNode::updateMetadata(const cvc::volume &vol) { - // Store volume dimensions - setMetadata("dim_x", static_cast(vol.XDim())); - setMetadata("dim_y", static_cast(vol.YDim())); - setMetadata("dim_z", static_cast(vol.ZDim())); - - // Store bounding box - setMetadata("bbox_min_x", vol.XMin()); - setMetadata("bbox_min_y", vol.YMin()); - setMetadata("bbox_min_z", vol.ZMin()); - setMetadata("bbox_max_x", vol.XMax()); - setMetadata("bbox_max_y", vol.YMax()); - setMetadata("bbox_max_z", vol.ZMax()); - - // Store combined bounding box string for computeGraphicsBounds() - std::string bboxStr = std::to_string(vol.XMin()) + "," + std::to_string(vol.YMin()) + "," + - std::to_string(vol.ZMin()) + "," + std::to_string(vol.XMax()) + "," + - std::to_string(vol.YMax()) + "," + std::to_string(vol.ZMax()); - setMetadata("bounding_box", bboxStr); - - // Store spacing - setMetadata("spacing_x", vol.XSpan() / vol.XDim()); - setMetadata("spacing_y", vol.YSpan() / vol.YDim()); - setMetadata("spacing_z", vol.ZSpan() / vol.ZDim()); - - // Store data range - setMetadata("data_min", vol.min()); - setMetadata("data_max", vol.max()); - - // Store volume type - std::string typeStr; - switch (vol.voxelType()) { - case cvc::UChar: - typeStr = "unsigned_char"; - break; - case cvc::UShort: - typeStr = "unsigned_short"; - break; - case cvc::UInt: - typeStr = "unsigned_int"; - break; - case cvc::Float: - typeStr = "float"; - break; - case cvc::Double: - typeStr = "double"; - break; - default: - typeStr = "unknown"; - break; - } - setMetadata("voxel_type", typeStr); -} - -void VolumeNode::onDataChanged() { - // Called when state data changes - reload volume from state - // Note: With state_object, we access state via getState() instead of m_stateNode - if (getState().isData()) { - try { - const cvc::volume &vol = boost::any_cast(getState().data()); - setVolume(vol); - } catch (...) { - // Failed to load volume from state - } - } -} diff --git a/src/volrover3/main.cpp b/src/volrover3/main.cpp deleted file mode 100644 index c9b84ad5..00000000 --- a/src/volrover3/main.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include -#include -#include - -// VTK module initialization -VTK_MODULE_INIT(vtkRenderingOpenGL2); -VTK_MODULE_INIT(vtkInteractionStyle); -VTK_MODULE_INIT(vtkRenderingFreeType); -VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2); - -int main(int argc, char *argv[]) { - // Set up OpenGL format - QSurfaceFormat format; - format.setDepthBufferSize(24); - format.setStencilBufferSize(8); - format.setVersion(3, 3); - format.setProfile(QSurfaceFormat::CoreProfile); - QSurfaceFormat::setDefaultFormat(format); - - QApplication app(argc, argv); - app.setApplicationName("VolRover3"); - app.setApplicationVersion("3.0.0"); - app.setOrganizationName("CVC"); - - MainWindow mainWindow; - mainWindow.show(); - - return app.exec(); -} diff --git a/src/volrover3/tests/AppStateTest.cpp b/src/volrover3/tests/AppStateTest.cpp deleted file mode 100644 index 4bc617af..00000000 --- a/src/volrover3/tests/AppStateTest.cpp +++ /dev/null @@ -1,413 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -// Need QApplication for Qt types -class AppStateTest : public ::testing::Test { -protected: - static void SetUpTestSuite() { - if (!QApplication::instance()) { - int argc = 0; - char **argv = nullptr; - app = new QApplication(argc, argv); - } - // Disable threading for state_object to avoid destruction race conditions - cvc::state_object::setUseThreading(false); - } - - void SetUp() override { - // Create AppState with unique prefix for test isolation - // Each test instance gets its own state subtree - m_statePrefix = "appstate_test_" + std::to_string(testCounter++); - state = std::make_unique(m_statePrefix); - } - - void TearDown() override { - // Clean up test-specific state instance - // No need to reset state tree - unique prefixes provide isolation - state.reset(); - } - - static QApplication *app; - static int testCounter; - std::string m_statePrefix; - std::unique_ptr state; -}; - -QApplication *AppStateTest::app = nullptr; -int AppStateTest::testCounter = 0; - -TEST_F(AppStateTest, SingletonInstance) { - // Verify that the singleton instance is different from our test instance - // (they use different state prefixes) - AppState &singleton = AppState::instance(); - EXPECT_NE(state->getStatePrefix(), singleton.getStatePrefix()); - EXPECT_EQ(singleton.getStatePrefix(), "volrover3"); - // Our test instance uses unique prefix - EXPECT_EQ(state->getStatePrefix(), m_statePrefix); -} - -TEST_F(AppStateTest, CameraPosition) { - state->setCameraPosition(1.0, 2.0, 3.0); - - double x, y, z; - state->getCameraPosition(x, y, z); - EXPECT_DOUBLE_EQ(x, 1.0); - EXPECT_DOUBLE_EQ(y, 2.0); - EXPECT_DOUBLE_EQ(z, 3.0); -} - -TEST_F(AppStateTest, CameraSensitivity) { - state->setCameraSensitivity(0.5); - EXPECT_DOUBLE_EQ(state->cameraSensitivity(), 0.5); - - state->setCameraSensitivity(1.5); - EXPECT_DOUBLE_EQ(state->cameraSensitivity(), 1.5); -} - -TEST_F(AppStateTest, CameraSpeed) { - state->setCameraSpeed(2.0); - EXPECT_DOUBLE_EQ(state->cameraSpeed(), 2.0); - - state->setCameraSpeed(5.0); - EXPECT_DOUBLE_EQ(state->cameraSpeed(), 5.0); -} - -TEST_F(AppStateTest, KeyBindings) { - state->setCameraKeyForward(Qt::Key_W); - EXPECT_EQ(state->cameraKeyForward(), Qt::Key_W); - - state->setCameraKeyBackward(Qt::Key_S); - EXPECT_EQ(state->cameraKeyBackward(), Qt::Key_S); - - state->setCameraKeyLeft(Qt::Key_A); - EXPECT_EQ(state->cameraKeyLeft(), Qt::Key_A); - - state->setCameraKeyRight(Qt::Key_D); - EXPECT_EQ(state->cameraKeyRight(), Qt::Key_D); - - state->setCameraKeyUp(Qt::Key_E); - EXPECT_EQ(state->cameraKeyUp(), Qt::Key_E); - - state->setCameraKeyDown(Qt::Key_Q); - EXPECT_EQ(state->cameraKeyDown(), Qt::Key_Q); -} - -// NOTE: Transfer function storage moved to per-volume state in VolumeNode -// These tests are commented out as AppState no longer has global transfer function storage -/* -TEST_F(AppStateTest, TransferFunctionColorTable) { - std::vector colorTable = {0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0}; - state->setTransferFunctionColorTable(colorTable); - - auto retrieved = state->transferFunctionColorTable(); - ASSERT_EQ(retrieved.size(), colorTable.size()); - for (size_t i = 0; i < colorTable.size(); ++i) { - EXPECT_DOUBLE_EQ(retrieved[i], colorTable[i]); - } -} - -TEST_F(AppStateTest, TransferFunctionOpacityTable) { - std::vector opacityTable = {0.0, 0.0, 0.5, 0.5, 1.0, 1.0}; - state->setTransferFunctionOpacityTable(opacityTable); - - auto retrieved = state->transferFunctionOpacityTable(); - ASSERT_EQ(retrieved.size(), opacityTable.size()); - for (size_t i = 0; i < opacityTable.size(); ++i) { - EXPECT_DOUBLE_EQ(retrieved[i], opacityTable[i]); - } -} -*/ - -// =========================== -// State Tree Tests -// =========================== - -TEST_F(AppStateTest, StateTreeCameraPosition) { - // Set camera position via AppState - state->setCameraPosition(10.0, 20.0, 30.0); - - // Verify values are stored in state tree - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.position.x").value(), 10.0); - EXPECT_DOUBLE_EQ(stateTree("camera.position.y").value(), 20.0); - EXPECT_DOUBLE_EQ(stateTree("camera.position.z").value(), 30.0); - - // Verify getters match state tree values - double x, y, z; - state->getCameraPosition(x, y, z); - EXPECT_DOUBLE_EQ(x, 10.0); - EXPECT_DOUBLE_EQ(y, 20.0); - EXPECT_DOUBLE_EQ(z, 30.0); -} - -TEST_F(AppStateTest, StateTreeCameraViewDirection) { - state->setCameraViewDirection(1.0, 0.0, 0.0); - - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.view_direction.x").value(), 1.0); - EXPECT_DOUBLE_EQ(stateTree("camera.view_direction.y").value(), 0.0); - EXPECT_DOUBLE_EQ(stateTree("camera.view_direction.z").value(), 0.0); - - double x, y, z; - state->getCameraViewDirection(x, y, z); - EXPECT_DOUBLE_EQ(x, 1.0); - EXPECT_DOUBLE_EQ(y, 0.0); - EXPECT_DOUBLE_EQ(z, 0.0); -} - -TEST_F(AppStateTest, StateTreeCameraUpVector) { - state->setCameraUpVector(0.0, 1.0, 0.0); - - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.up_vector.x").value(), 0.0); - EXPECT_DOUBLE_EQ(stateTree("camera.up_vector.y").value(), 1.0); - EXPECT_DOUBLE_EQ(stateTree("camera.up_vector.z").value(), 0.0); - - double x, y, z; - state->getCameraUpVector(x, y, z); - EXPECT_DOUBLE_EQ(x, 0.0); - EXPECT_DOUBLE_EQ(y, 1.0); - EXPECT_DOUBLE_EQ(z, 0.0); -} - -TEST_F(AppStateTest, StateTreeCameraFOV) { - state->setCameraFieldOfView(60.0); - - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.fov").value(), 60.0); - EXPECT_DOUBLE_EQ(state->cameraFieldOfView(), 60.0); -} - -TEST_F(AppStateTest, StateTreeCameraSpeed) { - state->setCameraSpeed(3.5); - - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.speed").value(), 3.5); - EXPECT_DOUBLE_EQ(state->cameraSpeed(), 3.5); -} - -TEST_F(AppStateTest, StateTreeCameraSensitivity) { - state->setCameraSensitivity(0.75); - - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.sensitivity").value(), 0.75); - EXPECT_DOUBLE_EQ(state->cameraSensitivity(), 0.75); -} - -TEST_F(AppStateTest, StateTreeWorldBounds) { - cvc::bounding_box bounds(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); - state->setWorldBounds(bounds); - - auto &stateTree = state->getRootState(); - std::vector values = stateTree("world_bounds").values(); - ASSERT_EQ(values.size(), size_t(6)); - - auto retrieved = state->worldBounds(); - EXPECT_DOUBLE_EQ(retrieved[0], 1.0); - EXPECT_DOUBLE_EQ(retrieved[1], 2.0); - EXPECT_DOUBLE_EQ(retrieved[2], 3.0); - EXPECT_DOUBLE_EQ(retrieved[3], 4.0); - EXPECT_DOUBLE_EQ(retrieved[4], 5.0); - EXPECT_DOUBLE_EQ(retrieved[5], 6.0); -} - -// Grid and axis visibility now managed by GraphicsNode state tree -// See GridNodeTest.cpp for grid visibility tests - -TEST_F(AppStateTest, StateTreeKeyBindings) { - state->setCameraKeyForward(Qt::Key_W); - state->setCameraKeyBackward(Qt::Key_S); - state->setCameraKeyLeft(Qt::Key_A); - state->setCameraKeyRight(Qt::Key_D); - state->setCameraKeyUp(Qt::Key_E); - state->setCameraKeyDown(Qt::Key_Q); - - auto &stateTree = state->getRootState(); - EXPECT_EQ(stateTree("camera.key_forward").value(), Qt::Key_W); - EXPECT_EQ(stateTree("camera.key_backward").value(), Qt::Key_S); - EXPECT_EQ(stateTree("camera.key_left").value(), Qt::Key_A); - EXPECT_EQ(stateTree("camera.key_right").value(), Qt::Key_D); - EXPECT_EQ(stateTree("camera.key_up").value(), Qt::Key_E); - EXPECT_EQ(stateTree("camera.key_down").value(), Qt::Key_Q); -} - -TEST_F(AppStateTest, StateTreeDirectUpdate) { - // Set values directly in state tree (simulating external update) - auto &stateTree = state->getRootState(); - stateTree("camera.position.x").value(100.0); - stateTree("camera.position.y").value(200.0); - stateTree("camera.position.z").value(300.0); - - // Verify AppState reads from state tree - double x, y, z; - state->getCameraPosition(x, y, z); - EXPECT_DOUBLE_EQ(x, 100.0); - EXPECT_DOUBLE_EQ(y, 200.0); - EXPECT_DOUBLE_EQ(z, 300.0); -} - -// =========================== -// Callback Tests -// =========================== - -TEST_F(AppStateTest, CameraChangedCallback) { - int callback_count = 0; - - auto connection = state->onCameraChanged([&callback_count]() { callback_count++; }); - - // Trigger camera changes - state->setCameraPosition(1.0, 2.0, 3.0); - EXPECT_GT(callback_count, 0); - - int prev_count = callback_count; - state->setCameraViewDirection(0.0, 0.0, 1.0); - EXPECT_GT(callback_count, prev_count); - - prev_count = callback_count; - state->setCameraUpVector(0.0, 1.0, 0.0); - EXPECT_GT(callback_count, prev_count); - - prev_count = callback_count; - state->setCameraFieldOfView(45.0); - EXPECT_GT(callback_count, prev_count); - - // Disconnect and verify no more callbacks - connection.disconnect(); - prev_count = callback_count; - state->setCameraPosition(99.0, 99.0, 99.0); - EXPECT_EQ(callback_count, prev_count); // Should not have incremented -} - -TEST_F(AppStateTest, WorldBoundsChangedCallback) { - int callback_count = 0; - - auto connection = state->onWorldBoundsChanged([&callback_count]() { callback_count++; }); - - cvc::bounding_box bounds(0.0, 0.0, 0.0, 1.0, 1.0, 1.0); - state->setWorldBounds(bounds); - - EXPECT_GT(callback_count, 0); - - connection.disconnect(); -} - -// Grid and axis visibility callback tests removed - GraphicsNode manages its own state -// See GridNodeTest.cpp for grid-related tests - -TEST_F(AppStateTest, MultipleCallbacksForSameState) { - int callback1_count = 0; - int callback2_count = 0; - int callback3_count = 0; - - auto conn1 = state->onCameraChanged([&callback1_count]() { callback1_count++; }); - auto conn2 = state->onCameraChanged([&callback2_count]() { callback2_count++; }); - auto conn3 = state->onCameraChanged([&callback3_count]() { callback3_count++; }); - - state->setCameraPosition(5.0, 5.0, 5.0); - - EXPECT_GT(callback1_count, 0); - EXPECT_GT(callback2_count, 0); - EXPECT_GT(callback3_count, 0); - - conn1.disconnect(); - conn2.disconnect(); - conn3.disconnect(); -} - -TEST_F(AppStateTest, CallbackReceivesCorrectValue) { - double captured_x = 0.0; - double captured_y = 0.0; - double captured_z = 0.0; - - auto connection = state->onCameraChanged([this, &captured_x, &captured_y, &captured_z]() { - state->getCameraPosition(captured_x, captured_y, captured_z); - }); - - state->setCameraPosition(7.0, 8.0, 9.0); - - EXPECT_DOUBLE_EQ(captured_x, 7.0); - EXPECT_DOUBLE_EQ(captured_y, 8.0); - EXPECT_DOUBLE_EQ(captured_z, 9.0); - - connection.disconnect(); -} - -TEST_F(AppStateTest, StateTreeTriggerCallback) { - // This test demonstrates that callbacks can be triggered by directly - // setting the state tree value. - int callback_count = 0; - - auto connection = state->onCameraChanged([&callback_count]() { callback_count++; }); - - auto &stateTree = state->getRootState(); - int before_count = callback_count; - - // Trigger callback by setting camera position in state tree directly - stateTree("camera.position.x").value(123.0); - - EXPECT_GT(callback_count, before_count); - - connection.disconnect(); -} - -TEST_F(AppStateTest, CallbackDisconnection) { - int callback_count = 0; - - auto connection = state->onCameraModeChanged([&callback_count]() { callback_count++; }); - - // Trigger callback (default is ORBIT_MODE, so change to FLY_MODE) - state->setCameraMode(FLY_MODE); - EXPECT_EQ(callback_count, 1); - - // Disconnect - connection.disconnect(); - - // Trigger again - should not fire - state->setCameraMode(ORBIT_MODE); - EXPECT_EQ(callback_count, 1); // Should still be 1 -} - -// =========================== -// State Persistence Tests -// =========================== - -TEST_F(AppStateTest, StateTreeInitialized) { - auto &stateTree = state->getRootState(); - - // Verify default state values are initialized - EXPECT_TRUE(stateTree("camera.position.x").initialized()); - EXPECT_TRUE(stateTree("camera.position.y").initialized()); - EXPECT_TRUE(stateTree("camera.position.z").initialized()); - EXPECT_TRUE(stateTree("camera.speed").initialized()); - EXPECT_TRUE(stateTree("camera.sensitivity").initialized()); - EXPECT_TRUE(stateTree("camera.fov").initialized()); -} - -TEST_F(AppStateTest, StateTreePersistence) { - // Set values - state->setCameraPosition(11.0, 22.0, 33.0); - state->setCameraSpeed(5.5); - - // Verify values persist in state tree - auto &stateTree = state->getRootState(); - EXPECT_DOUBLE_EQ(stateTree("camera.position.x").value(), 11.0); - EXPECT_DOUBLE_EQ(stateTree("camera.position.y").value(), 22.0); - EXPECT_DOUBLE_EQ(stateTree("camera.position.z").value(), 33.0); - EXPECT_DOUBLE_EQ(stateTree("camera.speed").value(), 5.5); - - // Values should persist across multiple reads - EXPECT_DOUBLE_EQ(stateTree("camera.position.x").value(), 11.0); - EXPECT_DOUBLE_EQ(stateTree("camera.speed").value(), 5.5); -} - -// =========================== -// Note: Grid-specific tests removed -// =========================== -// Grid visibility and properties are now managed by GridNode at -// volrover3.graphics.root.children.grid See GridNodeTest.cpp for grid-related tests diff --git a/src/volrover3/tests/BBoxNodeTest.cpp b/src/volrover3/tests/BBoxNodeTest.cpp deleted file mode 100644 index 1373f8df..00000000 --- a/src/volrover3/tests/BBoxNodeTest.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include -#include -#include -#include -#include - -class BBoxNodeTest : public ::testing::Test { -protected: - void SetUp() override { testBBox = cvc::bounding_box(0.0, 0.0, 0.0, 10.0, 20.0, 30.0); } - - void TearDown() override {} - - cvc::bounding_box testBBox; -}; - -// ============================================================================ -// Coordinate Label Tests (2 corners instead of 8) -// ============================================================================ - -TEST_F(BBoxNodeTest, CoordinateLabelsDefaultVisible) { - BBoxNode node; - - // Coordinate labels should be visible by default - EXPECT_TRUE(node.getCoordinatesVisible()); -} - -TEST_F(BBoxNodeTest, SetCoordinatesVisible) { - BBoxNode node; - - node.setCoordinatesVisible(false); - EXPECT_FALSE(node.getCoordinatesVisible()); - - node.setCoordinatesVisible(true); - EXPECT_TRUE(node.getCoordinatesVisible()); -} - -TEST_F(BBoxNodeTest, CoordinateLabelCount) { - BBoxNode node; - - // Set coordinates visible first - node.setCoordinatesVisible(true); - - // Then set bounding box (this will create the labels) - node.setBoundingBox(testBBox); - - // After the fix, we should have exactly 2 labels (min and max corners) - // instead of 8 labels (all corners) - // This is verified by the internal implementation in createCoordinateLabels() - - // We can verify the labels are created by checking visibility - EXPECT_TRUE(node.getCoordinatesVisible()); -} - -TEST_F(BBoxNodeTest, CoordinateLabelColor) { - BBoxNode node; - - // Set label color - node.setCoordinateLabelColor(1.0, 0.5, 0.0); - - // Get label color - double r, g, b; - node.getCoordinateLabelColor(r, g, b); - - EXPECT_DOUBLE_EQ(r, 1.0); - EXPECT_DOUBLE_EQ(g, 0.5); - EXPECT_DOUBLE_EQ(b, 0.0); -} - -TEST_F(BBoxNodeTest, CoordinateLabelFontSize) { - BBoxNode node; - - // Default font size should be 12 - EXPECT_EQ(node.getCoordinateLabelFontSize(), 12); - - // Set new font size - node.setCoordinateLabelFontSize(18); - EXPECT_EQ(node.getCoordinateLabelFontSize(), 18); - - // Should clamp to minimum 1 - node.setCoordinateLabelFontSize(0); - EXPECT_EQ(node.getCoordinateLabelFontSize(), 1); - - node.setCoordinateLabelFontSize(-5); - EXPECT_EQ(node.getCoordinateLabelFontSize(), 1); -} - -// ============================================================================ -// Bounding Box Tests -// ============================================================================ - -TEST_F(BBoxNodeTest, DefaultConstruction) { - BBoxNode node; - - // Default bbox should be (-1, -1, -1) to (1, 1, 1) - cvc::bounding_box bbox = node.getBoundingBox(); - - EXPECT_DOUBLE_EQ(bbox[0], -1.0); - EXPECT_DOUBLE_EQ(bbox[1], -1.0); - EXPECT_DOUBLE_EQ(bbox[2], -1.0); - EXPECT_DOUBLE_EQ(bbox[3], 1.0); - EXPECT_DOUBLE_EQ(bbox[4], 1.0); - EXPECT_DOUBLE_EQ(bbox[5], 1.0); -} - -TEST_F(BBoxNodeTest, SetBoundingBox) { - BBoxNode node; - - node.setBoundingBox(testBBox); - - cvc::bounding_box bbox = node.getBoundingBox(); - - EXPECT_DOUBLE_EQ(bbox[0], 0.0); - EXPECT_DOUBLE_EQ(bbox[1], 0.0); - EXPECT_DOUBLE_EQ(bbox[2], 0.0); - EXPECT_DOUBLE_EQ(bbox[3], 10.0); - EXPECT_DOUBLE_EQ(bbox[4], 20.0); - EXPECT_DOUBLE_EQ(bbox[5], 30.0); -} - -TEST_F(BBoxNodeTest, SetColor) { - BBoxNode node; - - // Set yellow color (default is yellow anyway) - node.setColor(1.0, 1.0, 0.0); - - // We can't directly verify the color is applied to the actor, - // but we can verify the method doesn't crash - EXPECT_NO_THROW(node.setColor(0.5, 0.5, 0.5)); -} - -TEST_F(BBoxNodeTest, LargeBoundingBox) { - BBoxNode node; - - // Test with a large bounding box (like the 945x945x945 volume) - cvc::bounding_box largeBBox(0.0, 0.0, 0.0, 945.0, 945.0, 945.0); - node.setBoundingBox(largeBBox); - - cvc::bounding_box bbox = node.getBoundingBox(); - - EXPECT_DOUBLE_EQ(bbox[0], 0.0); - EXPECT_DOUBLE_EQ(bbox[1], 0.0); - EXPECT_DOUBLE_EQ(bbox[2], 0.0); - EXPECT_DOUBLE_EQ(bbox[3], 945.0); - EXPECT_DOUBLE_EQ(bbox[4], 945.0); - EXPECT_DOUBLE_EQ(bbox[5], 945.0); - - // Verify coordinates are visible by default - EXPECT_TRUE(node.getCoordinatesVisible()); -} - -TEST_F(BBoxNodeTest, NonUniformBoundingBox) { - BBoxNode node; - - // Test with non-uniform bounds - cvc::bounding_box nonUniform(-10.0, -5.0, 0.0, 100.0, 50.0, 25.0); - node.setBoundingBox(nonUniform); - - cvc::bounding_box bbox = node.getBoundingBox(); - - EXPECT_DOUBLE_EQ(bbox[0], -10.0); - EXPECT_DOUBLE_EQ(bbox[1], -5.0); - EXPECT_DOUBLE_EQ(bbox[2], 0.0); - EXPECT_DOUBLE_EQ(bbox[3], 100.0); - EXPECT_DOUBLE_EQ(bbox[4], 50.0); - EXPECT_DOUBLE_EQ(bbox[5], 25.0); -} - -TEST_F(BBoxNodeTest, UpdateBoundingBox) { - BBoxNode node; - - // Set initial bbox - node.setBoundingBox(testBBox); - - cvc::bounding_box bbox1 = node.getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox1[3], 10.0); - - // Update to different bbox - cvc::bounding_box newBBox(5.0, 5.0, 5.0, 15.0, 15.0, 15.0); - node.setBoundingBox(newBBox); - - cvc::bounding_box bbox2 = node.getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox2[0], 5.0); - EXPECT_DOUBLE_EQ(bbox2[3], 15.0); -} - -TEST_F(BBoxNodeTest, SetTransform) { - BBoxNode node; - - // Set a simple bounding box - cvc::bounding_box bbox(0.0, 0.0, 0.0, 1.0, 1.0, 1.0); - node.setBoundingBox(bbox); - - // Create a transform (translation) - vtkSmartPointer transform = vtkSmartPointer::New(); - transform->Identity(); - transform->SetElement(0, 3, 10.0); // Translate X by 10 - transform->SetElement(1, 3, 20.0); // Translate Y by 20 - transform->SetElement(2, 3, 30.0); // Translate Z by 30 - - // Apply transform - this should work without error - node.setTransform(transform); - - // The bounding box coordinates should remain in local space - cvc::bounding_box localBBox = node.getBoundingBox(); - EXPECT_DOUBLE_EQ(localBBox[0], 0.0); - EXPECT_DOUBLE_EQ(localBBox[3], 1.0); - - // The transform is applied to the VTK actor, not the bbox coordinates - // This test verifies the method doesn't crash and the bbox stays in local space -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/tests/BoundingBoxSemanticsTest.cpp b/src/volrover3/tests/BoundingBoxSemanticsTest.cpp deleted file mode 100644 index ba39e194..00000000 --- a/src/volrover3/tests/BoundingBoxSemanticsTest.cpp +++ /dev/null @@ -1,304 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -class BoundingBoxSemanticsTest : public ::testing::Test { -protected: - cvc::app ctx; - void SetUp() override { m_statePrefix = "test_bbox_semantics_" + std::to_string(testCounter++); } - - // Helper to create geometry with specific bounds - cvc::geometry createGeometry(double minX, double minY, double minZ, double maxX, double maxY, - double maxZ) { - cvc::geometry geom; - geom.points().resize(2); - geom.points()[0][0] = minX; - geom.points()[0][1] = minY; - geom.points()[0][2] = minZ; - geom.points()[1][0] = maxX; - geom.points()[1][1] = maxY; - geom.points()[1][2] = maxZ; - return geom; - } - - std::string m_statePrefix; - static int testCounter; -}; - -int BoundingBoxSemanticsTest::testCounter = 0; - -// Test parent node's own bounding box -TEST_F(BoundingBoxSemanticsTest, ParentOwnBoundingBox) { - auto parent = std::make_shared(ctx, "parent"); - auto geom = createGeometry(0, 0, 0, 10, 10, 10); - parent->setGeometry(geom); - - // Parent's own bounding box should be its geometry bounds - auto bbox = parent->getBoundingBox(); - EXPECT_NEAR(bbox[0], 0.0, 1e-6); - EXPECT_NEAR(bbox[1], 0.0, 1e-6); - EXPECT_NEAR(bbox[2], 0.0, 1e-6); - EXPECT_NEAR(bbox[3], 10.0, 1e-6); - EXPECT_NEAR(bbox[4], 10.0, 1e-6); - EXPECT_NEAR(bbox[5], 10.0, 1e-6); -} - -// Test combined bounding box with no children equals own bounding box -TEST_F(BoundingBoxSemanticsTest, CombinedBBoxNoChildren) { - auto parent = std::make_shared(ctx, "parent"); - auto geom = createGeometry(5, 5, 5, 15, 15, 15); - parent->setGeometry(geom); - - auto ownBBox = parent->getBoundingBox(); - auto combinedBBox = parent->getCombinedBoundingBox(); - - // Should be identical when no children - EXPECT_NEAR(ownBBox[0], combinedBBox[0], 1e-6); - EXPECT_NEAR(ownBBox[1], combinedBBox[1], 1e-6); - EXPECT_NEAR(ownBBox[2], combinedBBox[2], 1e-6); - EXPECT_NEAR(ownBBox[3], combinedBBox[3], 1e-6); - EXPECT_NEAR(ownBBox[4], combinedBBox[4], 1e-6); - EXPECT_NEAR(ownBBox[5], combinedBBox[5], 1e-6); -} - -// Test combined bounding box includes child -TEST_F(BoundingBoxSemanticsTest, CombinedBBoxIncludesChild) { - auto parent = std::make_shared(ctx, "parent"); - auto parentGeom = createGeometry(0, 0, 0, 10, 10, 10); - parent->setGeometry(parentGeom); - - auto child = std::make_shared(ctx, "child"); - auto childGeom = createGeometry(15, 15, 15, 25, 25, 25); - child->setGeometry(childGeom); - - parent->addGraphicsChild(child); - - // Parent's own bbox should still be its geometry - auto ownBBox = parent->getBoundingBox(); - EXPECT_NEAR(ownBBox[0], 0.0, 1e-6); - EXPECT_NEAR(ownBBox[3], 10.0, 1e-6); - - // Combined bbox should include both parent and child - auto combinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBox[0], 0.0, 1e-6); // min from parent - EXPECT_NEAR(combinedBBox[1], 0.0, 1e-6); - EXPECT_NEAR(combinedBBox[2], 0.0, 1e-6); - EXPECT_NEAR(combinedBBox[3], 25.0, 1e-6); // max from child - EXPECT_NEAR(combinedBBox[4], 25.0, 1e-6); - EXPECT_NEAR(combinedBBox[5], 25.0, 1e-6); -} - -// Test combined bounding box with multiple children -TEST_F(BoundingBoxSemanticsTest, CombinedBBoxMultipleChildren) { - auto parent = std::make_shared(ctx, "parent"); - auto parentGeom = createGeometry(10, 10, 10, 20, 20, 20); - parent->setGeometry(parentGeom); - - auto child1 = std::make_shared(ctx, "child1"); - auto child1Geom = createGeometry(0, 0, 0, 5, 5, 5); - child1->setGeometry(child1Geom); - parent->addGraphicsChild(child1); - - auto child2 = std::make_shared(ctx, "child2"); - auto child2Geom = createGeometry(30, 30, 30, 40, 40, 40); - child2->setGeometry(child2Geom); - parent->addGraphicsChild(child2); - - // Parent's own bbox unchanged - auto ownBBox = parent->getBoundingBox(); - EXPECT_NEAR(ownBBox[0], 10.0, 1e-6); - EXPECT_NEAR(ownBBox[3], 20.0, 1e-6); - - // Combined bbox should span all three - auto combinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBox[0], 0.0, 1e-6); // min from child1 - EXPECT_NEAR(combinedBBox[1], 0.0, 1e-6); - EXPECT_NEAR(combinedBBox[2], 0.0, 1e-6); - EXPECT_NEAR(combinedBBox[3], 40.0, 1e-6); // max from child2 - EXPECT_NEAR(combinedBBox[4], 40.0, 1e-6); - EXPECT_NEAR(combinedBBox[5], 40.0, 1e-6); -} - -// Test combined bounding box with nested children (grandchildren) -TEST_F(BoundingBoxSemanticsTest, CombinedBBoxNestedChildren) { - auto grandparent = std::make_shared(ctx, "grandparent"); - auto grandparentGeom = createGeometry(50, 50, 50, 60, 60, 60); - grandparent->setGeometry(grandparentGeom); - - auto parent = std::make_shared(ctx, "parent"); - auto parentGeom = createGeometry(0, 0, 0, 10, 10, 10); - parent->setGeometry(parentGeom); - grandparent->addGraphicsChild(parent); - - auto child = std::make_shared(ctx, "child"); - auto childGeom = createGeometry(100, 100, 100, 110, 110, 110); - child->setGeometry(childGeom); - parent->addGraphicsChild(child); - - // Grandparent's own bbox is just its geometry - auto grandparentOwnBBox = grandparent->getBoundingBox(); - EXPECT_NEAR(grandparentOwnBBox[0], 50.0, 1e-6); - EXPECT_NEAR(grandparentOwnBBox[3], 60.0, 1e-6); - - // Parent's combined bbox includes parent + child - auto parentCombinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(parentCombinedBBox[0], 0.0, 1e-6); - EXPECT_NEAR(parentCombinedBBox[3], 110.0, 1e-6); - - // Grandparent's combined bbox includes all three levels - auto grandparentCombinedBBox = grandparent->getCombinedBoundingBox(); - EXPECT_NEAR(grandparentCombinedBBox[0], 0.0, 1e-6); // min from parent - EXPECT_NEAR(grandparentCombinedBBox[3], 110.0, 1e-6); // max from child -} - -// Test parent with no geometry but has children -TEST_F(BoundingBoxSemanticsTest, ParentNoGeometryHasChildren) { - auto parent = std::make_shared(ctx, "parent"); - // No geometry set on parent - - auto child = std::make_shared(ctx, "child"); - auto childGeom = createGeometry(10, 20, 30, 40, 50, 60); - child->setGeometry(childGeom); - parent->addGraphicsChild(child); - - // Parent's own bbox should be empty/default (0,0,0 to 0,0,0) - auto ownBBox = parent->getBoundingBox(); - EXPECT_DOUBLE_EQ(ownBBox[0], 0.0); - EXPECT_DOUBLE_EQ(ownBBox[3], 0.0); - - // Combined bbox starts with parent's bbox (0,0,0) and expands to include child - // So combined is min(0, 10) to max(0, 40) = 0 to 40 (includes origin) - auto combinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBox[0], 0.0, 1e-6); // min(0, 10) - EXPECT_NEAR(combinedBBox[1], 0.0, 1e-6); // min(0, 20) - EXPECT_NEAR(combinedBBox[2], 0.0, 1e-6); // min(0, 30) - EXPECT_NEAR(combinedBBox[3], 40.0, 1e-6); // max(0, 40) - EXPECT_NEAR(combinedBBox[4], 50.0, 1e-6); // max(0, 50) - EXPECT_NEAR(combinedBBox[5], 60.0, 1e-6); // max(0, 60) -} - -// Test volume node bounding box semantics - -// Test combined bbox with volume and geometry children - -// Test child removed updates combined bbox -TEST_F(BoundingBoxSemanticsTest, ChildRemovedUpdatesCombinedBBox) { - auto parent = std::make_shared(ctx, "parent"); - auto parentGeom = createGeometry(10, 10, 10, 20, 20, 20); - parent->setGeometry(parentGeom); - - auto child = std::make_shared(ctx, "child"); - auto childGeom = createGeometry(50, 50, 50, 100, 100, 100); - child->setGeometry(childGeom); - parent->addGraphicsChild(child); - - // Combined bbox with child - auto combinedBBoxWithChild = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBoxWithChild[0], 10.0, 1e-6); - EXPECT_NEAR(combinedBBoxWithChild[3], 100.0, 1e-6); - - // Remove child - parent->removeGraphicsChild(child); - - // Combined bbox should now be same as own bbox - auto combinedBBoxNoChild = parent->getCombinedBoundingBox(); - auto ownBBox = parent->getBoundingBox(); - EXPECT_NEAR(combinedBBoxNoChild[0], ownBBox[0], 1e-6); - EXPECT_NEAR(combinedBBoxNoChild[3], ownBBox[3], 1e-6); -} - -// Test non-overlapping children -TEST_F(BoundingBoxSemanticsTest, NonOverlappingChildren) { - auto parent = std::make_shared(ctx, "parent"); - auto parentGeom = createGeometry(0, 0, 0, 1, 1, 1); - parent->setGeometry(parentGeom); - - auto child1 = std::make_shared(ctx, "child1"); - auto child1Geom = createGeometry(-100, -100, -100, -90, -90, -90); - child1->setGeometry(child1Geom); - parent->addGraphicsChild(child1); - - auto child2 = std::make_shared(ctx, "child2"); - auto child2Geom = createGeometry(200, 200, 200, 210, 210, 210); - child2->setGeometry(child2Geom); - parent->addGraphicsChild(child2); - - // Combined bbox should span entire range - auto combinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBox[0], -100.0, 1e-6); - EXPECT_NEAR(combinedBBox[1], -100.0, 1e-6); - EXPECT_NEAR(combinedBBox[2], -100.0, 1e-6); - EXPECT_NEAR(combinedBBox[3], 210.0, 1e-6); - EXPECT_NEAR(combinedBBox[4], 210.0, 1e-6); - EXPECT_NEAR(combinedBBox[5], 210.0, 1e-6); -} - -// Test negative coordinates -TEST_F(BoundingBoxSemanticsTest, NegativeCoordinates) { - auto parent = std::make_shared(ctx, "parent"); - auto parentGeom = createGeometry(-50, -60, -70, -10, -20, -30); - parent->setGeometry(parentGeom); - - auto child = std::make_shared(ctx, "child"); - auto childGeom = createGeometry(-200, -150, -100, -180, -130, -80); - child->setGeometry(childGeom); - parent->addGraphicsChild(child); - - // Parent's own bbox - auto ownBBox = parent->getBoundingBox(); - EXPECT_NEAR(ownBBox[0], -50.0, 1e-6); - EXPECT_NEAR(ownBBox[3], -10.0, 1e-6); - - // Combined bbox with negative values - auto combinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBox[0], -200.0, 1e-6); - EXPECT_NEAR(combinedBBox[1], -150.0, 1e-6); - EXPECT_NEAR(combinedBBox[2], -100.0, 1e-6); - EXPECT_NEAR(combinedBBox[3], -10.0, 1e-6); - EXPECT_NEAR(combinedBBox[4], -20.0, 1e-6); - EXPECT_NEAR(combinedBBox[5], -30.0, 1e-6); -} - -// Test empty parent with empty child -TEST_F(BoundingBoxSemanticsTest, EmptyParentEmptyChild) { - auto parent = std::make_shared(ctx, "parent"); - auto child = std::make_shared(ctx, "child"); - parent->addGraphicsChild(child); - - // Both should have default/empty bboxes - auto ownBBox = parent->getBoundingBox(); - auto combinedBBox = parent->getCombinedBoundingBox(); - - // Combined should equal own when both empty - EXPECT_DOUBLE_EQ(ownBBox[0], combinedBBox[0]); - EXPECT_DOUBLE_EQ(ownBBox[3], combinedBBox[3]); -} - -// Test single point geometry -TEST_F(BoundingBoxSemanticsTest, SinglePointGeometry) { - auto parent = std::make_shared(ctx, "parent"); - cvc::geometry geom; - geom.points().resize(1); - geom.points()[0][0] = 5.5; - geom.points()[0][1] = 6.6; - geom.points()[0][2] = 7.7; - parent->setGeometry(geom); - - auto bbox = parent->getBoundingBox(); - // Single point creates a bbox with min == max - EXPECT_NEAR(bbox[0], 5.5, 1e-6); - EXPECT_NEAR(bbox[1], 6.6, 1e-6); - EXPECT_NEAR(bbox[2], 7.7, 1e-6); - EXPECT_NEAR(bbox[3], 5.5, 1e-6); - EXPECT_NEAR(bbox[4], 6.6, 1e-6); - EXPECT_NEAR(bbox[5], 7.7, 1e-6); - - // Combined should be same - auto combinedBBox = parent->getCombinedBoundingBox(); - EXPECT_NEAR(combinedBBox[0], 5.5, 1e-6); - EXPECT_NEAR(combinedBBox[3], 5.5, 1e-6); -} diff --git a/src/volrover3/tests/CameraControllerTest.cpp b/src/volrover3/tests/CameraControllerTest.cpp deleted file mode 100644 index abde7d5d..00000000 --- a/src/volrover3/tests/CameraControllerTest.cpp +++ /dev/null @@ -1,330 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class CameraControllerTest : public ::testing::Test { -protected: - cvc::app ctx; - static void SetUpTestSuite() { - if (!QApplication::instance()) { - int argc = 0; - char **argv = nullptr; - app = new QApplication(argc, argv); - } - testCounter = 0; - } - - void SetUp() override { - renderer = vtkSmartPointer::New(); - camera = vtkSmartPointer::New(); - renderer->SetActiveCamera(camera); - - // Create unique state path for each test instance - std::stringstream ss; - ss << "volrover3.camera.test" << testCounter++; - controller = new CameraController(ctx, ss.str()); - controller->setCamera(camera); - - // Get AppState singleton - appState = &AppState::instance(); - } - - void TearDown() override { delete controller; } - - static QApplication *app; - static int testCounter; - vtkSmartPointer renderer; - vtkSmartPointer camera; - CameraController *controller; - AppState *appState; -}; - -QApplication *CameraControllerTest::app = nullptr; -int CameraControllerTest::testCounter = 0; - -TEST_F(CameraControllerTest, InitialState) { - EXPECT_NE(controller, nullptr); - EXPECT_EQ(controller->getMode(), ORBIT_MODE); -} - -TEST_F(CameraControllerTest, ModeSwitch) { - controller->setMode(FLY_MODE); - EXPECT_EQ(controller->getMode(), FLY_MODE); - - controller->setMode(ORBIT_MODE); - EXPECT_EQ(controller->getMode(), ORBIT_MODE); -} - -TEST_F(CameraControllerTest, MouseSensitivity) { - controller->setMouseSensitivity(0.5); - // No getter available, just verify setter doesn't crash - controller->setMouseSensitivity(1.5); - SUCCEED(); -} - -TEST_F(CameraControllerTest, MouseInversion) { - controller->setInvertMouse(true); - // No getter available, just verify setter doesn't crash - controller->setInvertMouse(false); - SUCCEED(); -} - -TEST_F(CameraControllerTest, MovementSpeed) { - controller->setMovementSpeed(2.0); - // No getter available, just verify setter doesn't crash - controller->setMovementSpeed(5.0); - SUCCEED(); -} - -TEST_F(CameraControllerTest, KeyBindings) { - controller->setKeyBindings(Qt::Key_W, Qt::Key_S, Qt::Key_A, Qt::Key_D, Qt::Key_E, Qt::Key_Q); - - // Verify bindings were set (movement will be tested in integration tests) - SUCCEED(); -} - -TEST_F(CameraControllerTest, OrbitRotation) { - controller->setMode(ORBIT_MODE); - - // Get initial camera position - double initialPos[3]; - camera->GetPosition(initialPos); - - // Simulate mouse drag - controller->handleMousePress(1); // 1 = left button - controller->handleMouseMove(50, 50); // dx, dy - controller->handleMouseRelease(1); - - // Camera position should have changed - double newPos[3]; - camera->GetPosition(newPos); - - // In orbit mode, camera should have moved - bool positionChanged = - (initialPos[0] != newPos[0]) || (initialPos[1] != newPos[1]) || (initialPos[2] != newPos[2]); - EXPECT_TRUE(positionChanged); -} - -TEST_F(CameraControllerTest, OrbitPan) { - controller->setMode(ORBIT_MODE); - - // Get initial focal point - double initialFocus[3]; - camera->GetFocalPoint(initialFocus); - - // Simulate middle mouse drag - controller->handleMousePress(2); // 2 = middle button - controller->handleMouseMove(50, 50); // dx, dy - controller->handleMouseRelease(2); - - // Note: Middle button (pan) is not currently implemented in CameraController - // This test just verifies the API doesn't crash - SUCCEED(); -} - -TEST_F(CameraControllerTest, Zoom) { - // Set initial distance - camera->SetPosition(0, 0, 10); - camera->SetFocalPoint(0, 0, 0); - - double initialDistance = camera->GetDistance(); - - // Simulate scroll (zoom in) - controller->handleMouseWheel(120); // Positive delta = zoom in - - double newDistance = camera->GetDistance(); - - // Distance should have decreased (zoomed in) - EXPECT_LT(newDistance, initialDistance); -} - -TEST_F(CameraControllerTest, FlyMode) { - controller->setMode(FLY_MODE); - - // Get initial camera position - double initialPos[3]; - camera->GetPosition(initialPos); - - // Simulate mouse drag (should change view direction) - controller->handleMousePress(1); // 1 = left button - controller->handleMouseMove(50, 0); // dx, dy - controller->handleMouseRelease(1); - - // View direction should have changed (tested via focal point relative to position) - SUCCEED(); -} - -TEST_F(CameraControllerTest, KeyboardMovement) { - controller->setMode(FLY_MODE); - controller->setMovementSpeed(1.0); - controller->setKeyBindings(Qt::Key_W, Qt::Key_S, Qt::Key_A, Qt::Key_D, Qt::Key_E, Qt::Key_Q); - - // Get initial position - double initialPos[3]; - camera->GetPosition(initialPos); - - // Simulate key press for moving forward - controller->handleKeyPress(Qt::Key_W); - - // Update camera (simulate time passing) - controller->update(); - - // Position should have changed - double newPos[3]; - camera->GetPosition(newPos); - - bool moved = - (initialPos[0] != newPos[0]) || (initialPos[1] != newPos[1]) || (initialPos[2] != newPos[2]); - - // Release key - controller->handleKeyRelease(Qt::Key_W); - - EXPECT_TRUE(moved); -} - -TEST_F(CameraControllerTest, GetSetCameraState) { - // Set specific camera state - double position[3] = {5.0, 3.0, 8.0}; - double direction[3] = {0.0, 0.0, -1.0}; - double up[3] = {0.0, 1.0, 0.0}; - double fov = 45.0; - - controller->setCameraState(position, direction, up, fov); - - // Verify camera state was set - double pos[3], focal[3], upVec[3]; - camera->GetPosition(pos); - camera->GetFocalPoint(focal); - camera->GetViewUp(upVec); - - EXPECT_NEAR(pos[0], position[0], 0.001); - EXPECT_NEAR(pos[1], position[1], 0.001); - EXPECT_NEAR(pos[2], position[2], 0.001); - EXPECT_NEAR(upVec[0], up[0], 0.001); - EXPECT_NEAR(upVec[1], up[1], 0.001); - EXPECT_NEAR(upVec[2], up[2], 0.001); - EXPECT_NEAR(camera->GetViewAngle(), fov, 0.001); -} - -TEST_F(CameraControllerTest, ResetCamera) { - // Move camera to a specific position - camera->SetPosition(100, 200, 300); - camera->SetFocalPoint(50, 50, 50); - - // CameraController doesn't have resetCamera, skip this test - // The renderer can reset camera using renderer->ResetCamera() - SUCCEED(); -} - -TEST_F(CameraControllerTest, UpdateWithNoMovement) { - // Get initial position - double initialPos[3]; - camera->GetPosition(initialPos); - - // Update without any key presses - controller->update(); - - // Position should not change - double newPos[3]; - camera->GetPosition(newPos); - - EXPECT_DOUBLE_EQ(initialPos[0], newPos[0]); - EXPECT_DOUBLE_EQ(initialPos[1], newPos[1]); - EXPECT_DOUBLE_EQ(initialPos[2], newPos[2]); -} - -// =========================== -// State Tree Integration Tests -// =========================== - -TEST_F(CameraControllerTest, StateTreeCameraPosition) { - // Set camera position via controller - double testPos[3] = {10.0, 20.0, 30.0}; - double testDir[3] = {0.0, 0.0, -1.0}; - double testUp[3] = {0.0, 1.0, 0.0}; - double testFov = 60.0; - - controller->setCameraState(testPos, testDir, testUp, testFov); - - // Verify state tree contains the values using controller's state path - auto &stateTree = cvc::state::instance(ctx)(controller->stateName()); - EXPECT_NEAR(stateTree("position.x").value(), 10.0, 0.01); - EXPECT_NEAR(stateTree("position.y").value(), 20.0, 0.01); - EXPECT_NEAR(stateTree("position.z").value(), 30.0, 0.01); - EXPECT_NEAR(stateTree("fov").value(), 60.0, 0.01); -} - -TEST_F(CameraControllerTest, StateTreeCameraUpdate) { - // Move camera through controller - controller->setMode(FLY_MODE); - controller->setMovementSpeed(1.0); - controller->setKeyBindings(Qt::Key_W, Qt::Key_S, Qt::Key_A, Qt::Key_D, Qt::Key_E, Qt::Key_Q); - - // Get initial position from state tree using controller's state path - auto &stateTree = cvc::state::instance(ctx)(controller->stateName()); - double initialX = stateTree("position.x").value(); - double initialY = stateTree("position.y").value(); - double initialZ = stateTree("position.z").value(); - - // Move forward - controller->handleKeyPress(Qt::Key_W); - controller->update(); - controller->handleKeyRelease(Qt::Key_W); - - // State tree should be updated - double newX = stateTree("position.x").value(); - double newY = stateTree("position.y").value(); - double newZ = stateTree("position.z").value(); - - bool moved = (initialX != newX) || (initialY != newY) || (initialZ != newZ); - EXPECT_TRUE(moved); -} - -TEST_F(CameraControllerTest, CameraChangeCallback) { - // Change camera via controller - double testPos[3] = {5.0, 5.0, 5.0}; - double testDir[3] = {0.0, 0.0, -1.0}; - double testUp[3] = {0.0, 1.0, 0.0}; - double testFov = 45.0; - - controller->setCameraState(testPos, testDir, testUp, testFov); - - // Verify state tree was updated - auto &stateTree = cvc::state::instance(ctx)(controller->stateName()); - EXPECT_NEAR(stateTree("position.x").value(), 5.0, 0.01); - EXPECT_NEAR(stateTree("fov").value(), 45.0, 0.01); -} - -TEST_F(CameraControllerTest, CameraStateSymmetry) { - // Set via controller - double setPos[3] = {7.0, 8.0, 9.0}; - double setDir[3] = {1.0, 0.0, 0.0}; - double setUp[3] = {0.0, 0.0, 1.0}; - double setFov = 70.0; - - controller->setCameraState(setPos, setDir, setUp, setFov); - - // Get via controller's getCameraState - double getPos[3], getDir[3], getUp[3], getFov; - controller->getCameraState(getPos, getDir, getUp, getFov); - - // Values should match - EXPECT_NEAR(setPos[0], getPos[0], 0.01); - EXPECT_NEAR(setPos[1], getPos[1], 0.01); - EXPECT_NEAR(setPos[2], getPos[2], 0.01); - EXPECT_NEAR(setFov, getFov, 0.01); - - // Also verify state tree has correct values - auto &stateTree = cvc::state::instance(ctx)(controller->stateName()); - EXPECT_NEAR(stateTree("position.x").value(), 7.0, 0.01); - EXPECT_NEAR(stateTree("position.y").value(), 8.0, 0.01); - EXPECT_NEAR(stateTree("position.z").value(), 9.0, 0.01); - EXPECT_NEAR(stateTree("fov").value(), 70.0, 0.01); -} diff --git a/src/volrover3/tests/GeometryDialogTest.cpp b/src/volrover3/tests/GeometryDialogTest.cpp deleted file mode 100644 index 09c3061c..00000000 --- a/src/volrover3/tests/GeometryDialogTest.cpp +++ /dev/null @@ -1,395 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class GeometryDialogTest : public ::testing::Test { -protected: - static void SetUpTestSuite() { - // Disable threading for state_object to avoid race conditions - cvc::state_object::setUseThreading(false); - - // Initialize Qt if not already initialized - if (!QApplication::instance()) { - // Qt requires valid argc/argv pointers, otherwise X11 backend crashes - // when trying to access QCoreApplication::arguments() - static int argc = 1; - static char appName[] = "test"; - static char *argv[] = {appName, nullptr}; - static QApplication app(argc, argv); - - // Disable session management to prevent X11 session manager crashes - app.setProperty("sessionManagement", false); - } - } - - void SetUp() override { - sceneGraph = std::make_shared(); - dialog = nullptr; - } - - void TearDown() override { - if (dialog) { - delete dialog; - dialog = nullptr; - } - sceneGraph.reset(); - } - - cvc::geometry createTestGeometry() { - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - geom.tris().push_back({0, 1, 2}); - return geom; - } - - std::shared_ptr sceneGraph; - GeometryDialog *dialog; -}; - -TEST_F(GeometryDialogTest, DialogCreation) { - dialog = new GeometryDialog(sceneGraph); - EXPECT_NE(dialog, nullptr); - EXPECT_EQ(dialog->windowTitle(), "Geometry Properties"); -} - -TEST_F(GeometryDialogTest, EmptySceneGraph) { - dialog = new GeometryDialog(sceneGraph); - - // Dialog should be created but properties should be disabled - EXPECT_NE(dialog, nullptr); - - // Access the combo box through findChild - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 0); -} - -TEST_F(GeometryDialogTest, SingleGeometry) { - cvc::geometry geom = createTestGeometry(); - sceneGraph->addGraphics("test_geom", geom); - - dialog = new GeometryDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 1); - EXPECT_EQ(comboBox->itemText(0).toStdString(), "test_geom"); -} - -TEST_F(GeometryDialogTest, MultipleGeometries) { - cvc::geometry geom1 = createTestGeometry(); - cvc::geometry geom2 = createTestGeometry(); - - sceneGraph->addGraphics("geom1", geom1); - sceneGraph->addGraphics("geom2", geom2); - - dialog = new GeometryDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 2); -} - -TEST_F(GeometryDialogTest, GeometrySelectionUpdatesUI) { - cvc::geometry geom = createTestGeometry(); - auto node = sceneGraph->addGraphics("test_geom", geom); - auto geomNode = std::dynamic_pointer_cast(node); - ASSERT_NE(geomNode, nullptr); - - // Set some properties - geomNode->setColor(0.5, 0.6, 0.7); - geomNode->setAmbient(0.3); - geomNode->setDiffuse(0.8); - - dialog = new GeometryDialog(sceneGraph); - - // Find the color spin boxes - QList spinBoxes = dialog->findChildren(); - EXPECT_GT(spinBoxes.size(), 0); - - // Check that UI reflects the geometry properties - // Look for the specific color values with proper tolerance - bool foundColorValue = false; - for (auto *spinBox : spinBoxes) { - double value = spinBox->value(); - if (qAbs(value - 0.5) < 0.001 || qAbs(value - 0.6) < 0.001 || qAbs(value - 0.7) < 0.001) { - foundColorValue = true; - break; - } - } - EXPECT_TRUE(foundColorValue); -} - -TEST_F(GeometryDialogTest, RenderModeChange) { - cvc::geometry geom = createTestGeometry(); - auto node = sceneGraph->addGraphics("test_geom", geom); - auto geomNode = std::dynamic_pointer_cast(node); - ASSERT_NE(geomNode, nullptr); - - dialog = new GeometryDialog(sceneGraph); - - // Find render mode combo box by object name - QComboBox *renderModeCombo = dialog->findChild("renderModeComboBox"); - ASSERT_NE(renderModeCombo, nullptr); - - // Change render mode - int wireframeIndex = renderModeCombo->findText("Wireframe"); - ASSERT_GE(wireframeIndex, 0); - - renderModeCombo->setCurrentIndex(wireframeIndex); - QCoreApplication::processEvents(); - - // Verify the node's render mode changed - EXPECT_EQ(geomNode->getRenderMode(), GeometryRenderMode::LINES); -} - -TEST_F(GeometryDialogTest, ColorPropertyChange) { - cvc::geometry geom = createTestGeometry(); - auto node = sceneGraph->addGraphics("test_geom", geom); - auto geomNode = std::dynamic_pointer_cast(node); - ASSERT_NE(geomNode, nullptr); - - dialog = new GeometryDialog(sceneGraph); - - // Find color spin boxes by object name - auto colorRSpinBox = dialog->findChild("colorRSpinBox"); - auto colorGSpinBox = dialog->findChild("colorGSpinBox"); - auto colorBSpinBox = dialog->findChild("colorBSpinBox"); - ASSERT_NE(colorRSpinBox, nullptr); - ASSERT_NE(colorGSpinBox, nullptr); - ASSERT_NE(colorBSpinBox, nullptr); - - // Set color values - colorRSpinBox->setValue(1.0); - colorGSpinBox->setValue(0.0); - colorBSpinBox->setValue(0.0); - - // Verify the UI values were set - EXPECT_DOUBLE_EQ(colorRSpinBox->value(), 1.0); - EXPECT_DOUBLE_EQ(colorGSpinBox->value(), 0.0); - EXPECT_DOUBLE_EQ(colorBSpinBox->value(), 0.0); -} - -TEST_F(GeometryDialogTest, DynamicGeometryAddition) { - dialog = new GeometryDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 0); - - // Add geometry after dialog creation - cvc::geometry geom = createTestGeometry(); - sceneGraph->addGraphics("new_geom", geom); - - // Wait for state tree signals to propagate - QCoreApplication::processEvents(); - - // The combo box should update automatically - EXPECT_EQ(comboBox->count(), 1); -} - -TEST_F(GeometryDialogTest, DynamicGeometryRemoval) { - cvc::geometry geom = createTestGeometry(); - sceneGraph->addGraphics("test_geom", geom); - - dialog = new GeometryDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 1); - - // Remove geometry - sceneGraph->removeGraphics("test_geom"); - - // Wait for state tree signal and Qt signal processing - QCoreApplication::processEvents(); - - // The combo box should update automatically - EXPECT_EQ(comboBox->count(), 0); -} - -TEST_F(GeometryDialogTest, OpacityChange) { - cvc::geometry geom = createTestGeometry(); - auto node = sceneGraph->addGraphics("test_geom", geom); - auto geomNode = std::dynamic_pointer_cast(node); - ASSERT_NE(geomNode, nullptr); - - dialog = new GeometryDialog(sceneGraph); - - // Find all double spin boxes - QList spinBoxes = dialog->findChildren(); - - // Look for opacity spin box (should have range 0-1) - for (auto *spinBox : spinBoxes) { - if (spinBox->minimum() == 0.0 && spinBox->maximum() == 1.0) { - spinBox->setValue(0.5); - QCoreApplication::processEvents(); - - // Check if it could be the opacity - auto opacity = geomNode->getMetadata("opacity"); - if (opacity.has_value()) { - double opacityValue = std::any_cast(opacity); - if (qFuzzyCompare(opacityValue, 0.5)) { - SUCCEED(); - return; - } - } - } - } -} - -TEST_F(GeometryDialogTest, EmptyGeometryNotListed) { - cvc::geometry emptyGeom; // Empty geometry - sceneGraph->addGraphics("empty_geom", emptyGeom); - - cvc::geometry validGeom = createTestGeometry(); - sceneGraph->addGraphics("valid_geom", validGeom); - - dialog = new GeometryDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - - // Only the valid geometry should be listed - EXPECT_EQ(comboBox->count(), 1); - EXPECT_EQ(comboBox->itemText(0).toStdString(), "valid_geom"); -} - -TEST_F(GeometryDialogTest, NestedGeometries) { - cvc::geometry geom1 = createTestGeometry(); - cvc::geometry geom2 = createTestGeometry(); - - auto parent = sceneGraph->addGraphics("parent_geom", geom1); - ASSERT_NE(parent, nullptr); - - // Add child geometry - auto child = parent->createChild("child_geom", geom2); - ASSERT_NE(child, nullptr); - - dialog = new GeometryDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - - // Both parent and child should be listed - EXPECT_EQ(comboBox->count(), 2); -} - -// Test safe deletion of geometry from state tree -TEST_F(GeometryDialogTest, SafeGeometryDeletion) { - cvc::geometry geom = createTestGeometry(); - auto node = sceneGraph->addGraphics("test_geom", geom); - ASSERT_NE(node, nullptr); - - // Verify geometry is in scene graph - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 1); - - // Get weak pointer to track object lifetime - std::weak_ptr weakNode = node; - node.reset(); // Release our reference - - // Object should still exist (held by scene graph) - EXPECT_FALSE(weakNode.expired()); - - // Remove geometry - should not crash - sceneGraph->removeGraphics("test_geom"); - - // Verify removal was clean - C++ object should be destroyed - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 0); - - // The GraphicsNode object should now be destroyed (no more references) - EXPECT_TRUE(weakNode.expired()); - - // State tree should still be accessible without crashes (even if nodes remain) - std::string statePrefix = sceneGraph->getStatePrefix(); - EXPECT_NO_THROW({ - auto &state = cvc::state::instance(volrover3::app())(statePrefix + ".graphics.root.children"); - // State tree nodes may persist, but accessing them shouldn't crash - size_t childCount = state.numChildren(); - EXPECT_GE(childCount, 0); // Just verify we can read without crashing - }); -} - -// Test multiple additions and removals -TEST_F(GeometryDialogTest, MultipleAddRemoveCycles) { - cvc::geometry geom = createTestGeometry(); - - // Perform multiple add/remove cycles - for (int i = 0; i < 5; ++i) { - auto node = sceneGraph->addGraphics("cycle_geom", geom); - ASSERT_NE(node, nullptr); - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 1); - - sceneGraph->removeGraphics("cycle_geom"); - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 0); - } - - // State tree should still be valid - std::string statePrefix = sceneGraph->getStatePrefix(); - EXPECT_NO_THROW({ - auto &state = cvc::state::instance(volrover3::app())(statePrefix + ".graphics.root"); - EXPECT_TRUE(true); // Just verify no crash accessing state - }); -} - -// Test removal of nested geometries -TEST_F(GeometryDialogTest, SafeNestedGeometryDeletion) { - cvc::geometry geom1 = createTestGeometry(); - cvc::geometry geom2 = createTestGeometry(); - - // Create parent-child hierarchy - auto parent = sceneGraph->addGraphics("parent", geom1); - ASSERT_NE(parent, nullptr); - - auto child = parent->createChild("child", geom2); - ASSERT_NE(child, nullptr); - - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 2); - - // Remove parent (should handle child cleanup) - sceneGraph->removeGraphics("parent"); - - // Should not crash and should clean up properly - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 0); -} - -// Test repeated add/remove cycles for memory safety -TEST_F(GeometryDialogTest, RepeatedAddRemoveSafety) { - cvc::geometry geom = createTestGeometry(); - - // Perform multiple add/remove cycles - for (int i = 0; i < 10; ++i) { - auto node = sceneGraph->addGraphics("test_geom_" + std::to_string(i % 3), geom); - ASSERT_NE(node, nullptr); - - // Immediately remove it - sceneGraph->removeGraphics("test_geom_" + std::to_string(i % 3)); - } - - // Should complete without crashes or memory issues - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 0); -} - -// Test removal of non-existent geometry (error handling) -TEST_F(GeometryDialogTest, RemoveNonExistentGeometry) { - // Should not crash when removing non-existent geometry - EXPECT_NO_THROW({ sceneGraph->removeGraphics("does_not_exist"); }); - - EXPECT_EQ(sceneGraph->getAllGeometryGraphics().size(), 0); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/tests/GraphicsNodeTest.cpp b/src/volrover3/tests/GraphicsNodeTest.cpp deleted file mode 100644 index 97695d80..00000000 --- a/src/volrover3/tests/GraphicsNodeTest.cpp +++ /dev/null @@ -1,1433 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class GraphicsNodeTest : public ::testing::Test { -protected: - void SetUp() override { - // Disable threading for state_object to avoid destruction race conditions - cvc::state_object::setUseThreading(false); - - // Each test uses its own state subtree - m_statePrefix = "graphics_test_" + std::to_string(testCounter++); - - // Create a simple test geometry (triangle) - testGeom.points().push_back({0.0, 0.0, 0.0}); - testGeom.points().push_back({1.0, 0.0, 0.0}); - testGeom.points().push_back({0.0, 1.0, 0.0}); - - testGeom.tris().push_back({0, 1, 2}); - } - - void TearDown() override { - // disconnectState() in SceneNode destructor prevents callbacks during destruction - } - - static int testCounter; - std::string m_statePrefix; - cvc::app ctx; - cvc::geometry testGeom; -}; - -int GraphicsNodeTest::testCounter = 0; - -// Test basic GeometryNode creation (GraphicsNode is abstract) -TEST_F(GraphicsNodeTest, Creation) { - GeometryNode node(ctx, "test", "test_node"); - EXPECT_EQ(node.getName(), "test_node"); - EXPECT_FALSE(node.hasGeometry()); -} - -// Test setting geometry -TEST_F(GraphicsNodeTest, SetGeometry) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - EXPECT_TRUE(node.hasGeometry()); - ASSERT_NE(node.getGeometry(), nullptr); - EXPECT_EQ(node.getGeometry()->num_points(), 3); - EXPECT_EQ(node.getGeometry()->num_tris(), 1); -} - -// Test geometry storage in node -TEST_F(GraphicsNodeTest, GeometryRetrieval) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - const cvc::geometry *geom = node.getGeometry(); - ASSERT_NE(geom, nullptr); - - // Verify geometry data is correct - EXPECT_EQ(geom->points().size(), 3); - EXPECT_EQ(geom->tris().size(), 1); - EXPECT_DOUBLE_EQ(geom->points()[0][0], 0.0); - EXPECT_DOUBLE_EQ(geom->points()[1][0], 1.0); -} - -// Test default transform is identity -TEST_F(GraphicsNodeTest, DefaultTransformIsIdentity) { - GeometryNode node(ctx, "test", "test_node"); - vtkMatrix4x4 *transform = node.getTransform(); - - ASSERT_NE(transform, nullptr); - - // Check identity matrix - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - if (i == j) { - EXPECT_DOUBLE_EQ(transform->GetElement(i, j), 1.0); - } else { - EXPECT_DOUBLE_EQ(transform->GetElement(i, j), 0.0); - } - } - } -} - -// Test setting position -TEST_F(GraphicsNodeTest, SetPosition) { - GeometryNode node(ctx, "test", "test_node"); - node.setPosition(1.0, 2.0, 3.0); - - vtkMatrix4x4 *transform = node.getTransform(); - EXPECT_DOUBLE_EQ(transform->GetElement(0, 3), 1.0); - EXPECT_DOUBLE_EQ(transform->GetElement(1, 3), 2.0); - EXPECT_DOUBLE_EQ(transform->GetElement(2, 3), 3.0); -} - -// Test setting scale -TEST_F(GraphicsNodeTest, SetScale) { - GeometryNode node(ctx, "test", "test_node"); - node.setScale(2.0, 3.0, 4.0); - - vtkMatrix4x4 *transform = node.getTransform(); - EXPECT_DOUBLE_EQ(transform->GetElement(0, 0), 2.0); - EXPECT_DOUBLE_EQ(transform->GetElement(1, 1), 3.0); - EXPECT_DOUBLE_EQ(transform->GetElement(2, 2), 4.0); -} - -// Test reset transform -TEST_F(GraphicsNodeTest, ResetTransform) { - GeometryNode node(ctx, "test", "test_node"); - node.setPosition(1.0, 2.0, 3.0); - node.resetTransform(); - - vtkMatrix4x4 *transform = node.getTransform(); - - // Should be identity again - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - if (i == j) { - EXPECT_DOUBLE_EQ(transform->GetElement(i, j), 1.0); - } else { - EXPECT_DOUBLE_EQ(transform->GetElement(i, j), 0.0); - } - } - } -} - -// Test metadata storage and retrieval -TEST_F(GraphicsNodeTest, MetadataStorage) { - GeometryNode node(ctx, "test", "test_node"); - - node.setMetadata("test_string", std::string("hello")); - node.setMetadata("test_int", 42); - node.setMetadata("test_double", 3.14); - node.setMetadata("test_bool", true); - - EXPECT_TRUE(node.hasMetadata("test_string")); - EXPECT_TRUE(node.hasMetadata("test_int")); - EXPECT_TRUE(node.hasMetadata("test_double")); - EXPECT_TRUE(node.hasMetadata("test_bool")); - EXPECT_FALSE(node.hasMetadata("nonexistent")); - - // Retrieve and verify - EXPECT_EQ(std::any_cast(node.getMetadata("test_string")), "hello"); - EXPECT_EQ(std::any_cast(node.getMetadata("test_int")), 42); - EXPECT_DOUBLE_EQ(std::any_cast(node.getMetadata("test_double")), 3.14); - EXPECT_EQ(std::any_cast(node.getMetadata("test_bool")), true); -} - -// Test default visible metadata -TEST_F(GraphicsNodeTest, DefaultVisibleMetadata) { - GeometryNode node(ctx, "test", "test_node"); - - EXPECT_TRUE(node.isVisible()); -} - -// Test setVisible updates metadata -TEST_F(GraphicsNodeTest, SetVisibleUpdatesMetadata) { - GeometryNode node(ctx, "test", "test_node"); - - node.setVisible(false); - EXPECT_FALSE(node.isVisible()); - - node.setVisible(true); - EXPECT_TRUE(node.isVisible()); -} - -// Test type metadata for geometry -TEST_F(GraphicsNodeTest, TypeMetadata) { - GeometryNode node(ctx, "test", "test_node"); - node.setMetadata("type", std::string("geometry")); - - EXPECT_TRUE(node.hasMetadata("type")); - EXPECT_EQ(std::any_cast(node.getMetadata("type")), "geometry"); -} - -// Test hierarchical structure - adding children -TEST_F(GraphicsNodeTest, AddChild) { - auto parent = std::make_shared(ctx, "test", "parent"); - auto child1 = std::make_shared(ctx, "test", "child1"); - auto child2 = std::make_shared(ctx, "test", "child2"); - - parent->addGraphicsChild(child1); - parent->addGraphicsChild(child2); - - EXPECT_EQ(parent->getGraphicsChildren().size(), 2); -} - -// Test finding child by name -TEST_F(GraphicsNodeTest, FindChildByName) { - auto parent = std::make_shared(ctx, "test", "parent"); - auto child1 = std::make_shared(ctx, "test", "child1"); - auto child2 = std::make_shared(ctx, "test", "child2"); - - parent->addGraphicsChild(child1); - parent->addGraphicsChild(child2); - - auto found = parent->findChildByName("child1"); - ASSERT_NE(found, nullptr); - EXPECT_EQ(found->getName(), "child1"); - - auto notFound = parent->findChildByName("nonexistent"); - EXPECT_EQ(notFound, nullptr); -} - -// Test removing children -TEST_F(GraphicsNodeTest, RemoveChild) { - auto parent = std::make_shared(ctx, "test", "parent"); - auto child1 = std::make_shared(ctx, "test", "child1"); - auto child2 = std::make_shared(ctx, "test", "child2"); - - parent->addGraphicsChild(child1); - parent->addGraphicsChild(child2); - - parent->removeGraphicsChild(child1); - EXPECT_EQ(parent->getGraphicsChildren().size(), 1); - - auto found = parent->findChildByName("child1"); - EXPECT_EQ(found, nullptr); -} - -// Test SceneGraph graphics management -TEST_F(GraphicsNodeTest, SceneGraphAddGraphics) { - SceneGraph sceneGraph(m_statePrefix); - - auto node = sceneGraph.addGraphics("test_graphics", testGeom); - - ASSERT_NE(node, nullptr); - EXPECT_EQ(node->getName(), "test_graphics"); - - // Cast to GeometryNode to check geometry - auto geomNode = std::dynamic_pointer_cast(node); - ASSERT_NE(geomNode, nullptr); - EXPECT_TRUE(geomNode->hasGeometry()); - - // Verify we can retrieve it - auto retrieved = sceneGraph.getGraphics("test_graphics"); - EXPECT_EQ(retrieved, node); -} - -// Test SceneGraph empty graphics node -TEST_F(GraphicsNodeTest, SceneGraphAddEmptyGraphics) { - SceneGraph sceneGraph(m_statePrefix); - - auto node = sceneGraph.addGraphics("empty_node"); - - ASSERT_NE(node, nullptr); - EXPECT_EQ(node->getName(), "empty_node"); - - // Cast to GeometryNode to check geometry - auto geomNode = std::dynamic_pointer_cast(node); - ASSERT_NE(geomNode, nullptr); - EXPECT_FALSE(geomNode->hasGeometry()); -} - -// Test SceneGraph remove graphics -TEST_F(GraphicsNodeTest, SceneGraphRemoveGraphics) { - SceneGraph sceneGraph(m_statePrefix); - - sceneGraph.addGraphics("test_graphics", testGeom); - sceneGraph.removeGraphics("test_graphics"); - - auto retrieved = sceneGraph.getGraphics("test_graphics"); - EXPECT_EQ(retrieved, nullptr); -} - -// Test SceneGraph graphics root -TEST_F(GraphicsNodeTest, SceneGraphGraphicsRoot) { - SceneGraph sceneGraph(m_statePrefix); - - auto root = sceneGraph.getGraphicsRoot(); - ASSERT_NE(root, nullptr); - // Graphics root is now the NullGraphicNode named "root" - EXPECT_EQ(root->getName(), "root"); -} - -// Test SceneGraph register graphics -TEST_F(GraphicsNodeTest, SceneGraphRegisterGraphics) { - SceneGraph sceneGraph(m_statePrefix); - - auto customNode = std::make_shared(ctx, "test", "custom"); - sceneGraph.registerGraphics("custom", customNode); - - auto retrieved = sceneGraph.getGraphics("custom"); - EXPECT_EQ(retrieved, customNode); -} -// Test SceneGraph compute bounds -TEST_F(GraphicsNodeTest, SceneGraphComputeBounds) { - SceneGraph sceneGraph(m_statePrefix); - - // Create geometry with known bounds - cvc::geometry geom1; - geom1.points().push_back({0.0, 0.0, 0.0}); - geom1.points().push_back({1.0, 1.0, 1.0}); - - cvc::geometry geom2; - geom2.points().push_back({-1.0, -1.0, -1.0}); - geom2.points().push_back({2.0, 2.0, 2.0}); - - sceneGraph.addGraphics("geom1", geom1); - sceneGraph.addGraphics("geom2", geom2); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // Bounds should encompass both geometries - EXPECT_LE(bounds[0], -1.0); - EXPECT_LE(bounds[1], -1.0); - EXPECT_LE(bounds[2], -1.0); - EXPECT_GE(bounds[3], 2.0); - EXPECT_GE(bounds[4], 2.0); - EXPECT_GE(bounds[5], 2.0); -} - -// Test SceneGraph compute bounds with no graphics -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsEmpty) { - SceneGraph sceneGraph(m_statePrefix); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // Should return invalid/empty bounds - EXPECT_TRUE(bounds[0] > bounds[3] || bounds[0] == 0.0); -} - -// Test world transform calculation for nested nodes -TEST_F(GraphicsNodeTest, WorldTransformHierarchy) { - auto parent = std::make_shared(ctx, "test.parent", "parent"); - auto child = std::make_shared(ctx, "test.child", "child"); - - // Set parent position - parent->setPosition(10.0, 0.0, 0.0); - - // Add child and set its position - parent->addGraphicsChild(child); - child->setPosition(5.0, 0.0, 0.0); - - // Get child's world transform - vtkSmartPointer worldTransform = child->getWorldTransform(); - - // Child's world position should be parent + child = 15.0, 0.0, 0.0 - EXPECT_DOUBLE_EQ(worldTransform->GetElement(0, 3), 15.0); - EXPECT_DOUBLE_EQ(worldTransform->GetElement(1, 3), 0.0); - EXPECT_DOUBLE_EQ(worldTransform->GetElement(2, 3), 0.0); -} - -// Test that metadata is computed from geometry -TEST_F(GraphicsNodeTest, MetadataFromGeometry) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - // Check that basic stats are computed - EXPECT_TRUE(node.hasMetadata("num_vertices")); - EXPECT_TRUE(node.hasMetadata("num_triangles")); - EXPECT_TRUE(node.hasMetadata("type")); - - // Verify values - int numVerts = std::any_cast(node.getMetadata("num_vertices")); - int numTris = std::any_cast(node.getMetadata("num_triangles")); - - EXPECT_EQ(numVerts, 3); - EXPECT_EQ(numTris, 1); - - std::string type = std::any_cast(node.getMetadata("type")); - EXPECT_EQ(type, "triangle_mesh"); -} - -// Test that bounding box metadata is computed -TEST_F(GraphicsNodeTest, BoundingBoxMetadata) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - // Check bounding box metadata exists - EXPECT_TRUE(node.hasMetadata("bbox_min_x")); - EXPECT_TRUE(node.hasMetadata("bbox_min_y")); - EXPECT_TRUE(node.hasMetadata("bbox_min_z")); - EXPECT_TRUE(node.hasMetadata("bbox_max_x")); - EXPECT_TRUE(node.hasMetadata("bbox_max_y")); - EXPECT_TRUE(node.hasMetadata("bbox_max_z")); - - // Verify bounding box values - double minX = std::any_cast(node.getMetadata("bbox_min_x")); - double minY = std::any_cast(node.getMetadata("bbox_min_y")); - double maxX = std::any_cast(node.getMetadata("bbox_max_x")); - double maxY = std::any_cast(node.getMetadata("bbox_max_y")); - - EXPECT_DOUBLE_EQ(minX, 0.0); - EXPECT_DOUBLE_EQ(minY, 0.0); - EXPECT_DOUBLE_EQ(maxX, 1.0); - EXPECT_DOUBLE_EQ(maxY, 1.0); -} - -// Test that extent metadata is computed -TEST_F(GraphicsNodeTest, ExtentMetadata) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - // Check extent metadata exists - EXPECT_TRUE(node.hasMetadata("extent_x")); - EXPECT_TRUE(node.hasMetadata("extent_y")); - EXPECT_TRUE(node.hasMetadata("extent_z")); - - // Verify extent values - double extentX = std::any_cast(node.getMetadata("extent_x")); - double extentY = std::any_cast(node.getMetadata("extent_y")); - double extentZ = std::any_cast(node.getMetadata("extent_z")); - - EXPECT_DOUBLE_EQ(extentX, 1.0); - EXPECT_DOUBLE_EQ(extentY, 1.0); - EXPECT_DOUBLE_EQ(extentZ, 0.0); -} - -// Test that center metadata is computed -TEST_F(GraphicsNodeTest, CenterMetadata) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - // Check center metadata exists - EXPECT_TRUE(node.hasMetadata("center_x")); - EXPECT_TRUE(node.hasMetadata("center_y")); - EXPECT_TRUE(node.hasMetadata("center_z")); - - // Verify center values - double centerX = std::any_cast(node.getMetadata("center_x")); - double centerY = std::any_cast(node.getMetadata("center_y")); - double centerZ = std::any_cast(node.getMetadata("center_z")); - - EXPECT_DOUBLE_EQ(centerX, 0.5); - EXPECT_DOUBLE_EQ(centerY, 0.5); - EXPECT_DOUBLE_EQ(centerZ, 0.0); -} -// Test geometry type detection -TEST_F(GraphicsNodeTest, GeometryTypeDetection) { - // Test triangle mesh - { - GeometryNode node(ctx, "test", "tri_mesh"); - cvc::geometry triGeom; - triGeom.points().push_back({0.0, 0.0, 0.0}); - triGeom.points().push_back({1.0, 0.0, 0.0}); - triGeom.points().push_back({0.0, 1.0, 0.0}); - triGeom.tris().push_back({0, 1, 2}); - - node.setGeometry(triGeom); - std::string type = std::any_cast(node.getMetadata("type")); - EXPECT_EQ(type, "triangle_mesh"); - } - - // Test quad mesh - { - GeometryNode node(ctx, "test", "quad_mesh"); - cvc::geometry quadGeom; - quadGeom.points().push_back({0.0, 0.0, 0.0}); - quadGeom.points().push_back({1.0, 0.0, 0.0}); - quadGeom.points().push_back({1.0, 1.0, 0.0}); - quadGeom.points().push_back({0.0, 1.0, 0.0}); - quadGeom.quads().push_back({0, 1, 2, 3}); - - node.setGeometry(quadGeom); - std::string type = std::any_cast(node.getMetadata("type")); - EXPECT_EQ(type, "quad_mesh"); - } - - // Test mixed mesh - { - GeometryNode node(ctx, "test", "mixed_mesh"); - cvc::geometry mixedGeom; - mixedGeom.points().push_back({0.0, 0.0, 0.0}); - mixedGeom.points().push_back({1.0, 0.0, 0.0}); - mixedGeom.points().push_back({1.0, 1.0, 0.0}); - mixedGeom.points().push_back({0.0, 1.0, 0.0}); - mixedGeom.tris().push_back({0, 1, 2}); - mixedGeom.quads().push_back({0, 1, 2, 3}); - - node.setGeometry(mixedGeom); - std::string type = std::any_cast(node.getMetadata("type")); - EXPECT_EQ(type, "mixed_mesh"); - } -} - -// Test metadata updates when geometry changes -TEST_F(GraphicsNodeTest, MetadataUpdatesOnGeometryChange) { - GeometryNode node(ctx, "test", "test_node"); - - // Set initial geometry - node.setGeometry(testGeom); - int initialVerts = std::any_cast(node.getMetadata("num_vertices")); - EXPECT_EQ(initialVerts, 3); - - // Change geometry - cvc::geometry newGeom; - for (int i = 0; i < 10; ++i) { - newGeom.points().push_back({static_cast(i), 0.0, 0.0}); - } - newGeom.tris().push_back({0, 1, 2}); - newGeom.tris().push_back({3, 4, 5}); - - node.setGeometry(newGeom); - - // Verify metadata was updated - int newVerts = std::any_cast(node.getMetadata("num_vertices")); - int newTris = std::any_cast(node.getMetadata("num_triangles")); - - EXPECT_EQ(newVerts, 10); - EXPECT_EQ(newTris, 2); -} - -// Test that empty geometry produces zero metadata -TEST_F(GraphicsNodeTest, EmptyGeometryMetadata) { - GeometryNode node(ctx, "test", "empty_node"); - - cvc::geometry emptyGeom; - node.setGeometry(emptyGeom); - - // Verify metadata for empty geometry - EXPECT_TRUE(node.hasMetadata("num_vertices")); - EXPECT_TRUE(node.hasMetadata("num_triangles")); - - int numVerts = std::any_cast(node.getMetadata("num_vertices")); - int numTris = std::any_cast(node.getMetadata("num_triangles")); - - EXPECT_EQ(numVerts, 0); - EXPECT_EQ(numTris, 0); -} - -// Test geometry with normals and colors preserves metadata -TEST_F(GraphicsNodeTest, GeometryWithNormalsAndColors) { - GeometryNode node(ctx, "test", "colored_node"); - - cvc::geometry coloredGeom; - coloredGeom.points().push_back({0.0, 0.0, 0.0}); - coloredGeom.points().push_back({1.0, 0.0, 0.0}); - coloredGeom.points().push_back({0.0, 1.0, 0.0}); - coloredGeom.tris().push_back({0, 1, 2}); - - // Add normals - coloredGeom.normals().push_back({0.0, 0.0, 1.0}); - coloredGeom.normals().push_back({0.0, 0.0, 1.0}); - coloredGeom.normals().push_back({0.0, 0.0, 1.0}); - - // Add colors - coloredGeom.colors().push_back({1.0, 0.0, 0.0}); - coloredGeom.colors().push_back({0.0, 1.0, 0.0}); - coloredGeom.colors().push_back({0.0, 0.0, 1.0}); - - node.setGeometry(coloredGeom); - - // Metadata should still be computed correctly - EXPECT_TRUE(node.hasMetadata("num_vertices")); - int numVerts = std::any_cast(node.getMetadata("num_vertices")); - EXPECT_EQ(numVerts, 3); -} -// Label Tests -// ============================================================================ - -TEST_F(GraphicsNodeTest, LabelDefaultState) { - GeometryNode node(ctx, "test", "test_node"); - - // Label should be off by default - EXPECT_FALSE(node.getShowLabel()); - // Default label text should be node name - EXPECT_EQ(node.getLabelText(), "test_node"); - // Default size should be 14 - EXPECT_EQ(node.getLabelSize(), 14); - // Default color should be white - double r, g, b; - node.getLabelColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 1.0); - EXPECT_DOUBLE_EQ(g, 1.0); - EXPECT_DOUBLE_EQ(b, 1.0); -} - -TEST_F(GraphicsNodeTest, SetLabelText) { - GeometryNode node(ctx, "test", "test_node"); - - node.setLabelText("Custom Label"); - EXPECT_EQ(node.getLabelText(), "Custom Label"); - - node.setLabelText("Another Label"); - EXPECT_EQ(node.getLabelText(), "Another Label"); -} - -TEST_F(GraphicsNodeTest, SetLabelSize) { - GeometryNode node(ctx, "test", "test_node"); - - node.setLabelSize(20); - EXPECT_EQ(node.getLabelSize(), 20); - - // Should clamp to minimum 1 - node.setLabelSize(0); - EXPECT_EQ(node.getLabelSize(), 1); - - node.setLabelSize(-5); - EXPECT_EQ(node.getLabelSize(), 1); -} - -TEST_F(GraphicsNodeTest, SetLabelColor) { - GeometryNode node(ctx, "test", "test_node"); - - node.setLabelColor(0.5, 0.75, 1.0); - - double r, g, b; - node.getLabelColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 0.5); - EXPECT_DOUBLE_EQ(g, 0.75); - EXPECT_DOUBLE_EQ(b, 1.0); -} - -TEST_F(GraphicsNodeTest, SetShowLabel) { - GeometryNode node(ctx, "test", "test_node"); - - EXPECT_FALSE(node.getShowLabel()); - - node.setShowLabel(true); - EXPECT_TRUE(node.getShowLabel()); - - node.setShowLabel(false); - EXPECT_FALSE(node.getShowLabel()); -} -// ============================================================================ -// Bounding Box Tests -// ============================================================================ - -TEST_F(GraphicsNodeTest, BBoxDefaultState) { - GeometryNode node(ctx, "test", "test_node"); - - // BBox should be off by default for regular nodes - EXPECT_FALSE(node.getShowBBox()); -} - -TEST_F(GraphicsNodeTest, SetShowBBox) { - GeometryNode node(ctx, "test", "test_node"); - - node.setShowBBox(true); - EXPECT_TRUE(node.getShowBBox()); - - node.setShowBBox(false); - EXPECT_FALSE(node.getShowBBox()); -} -TEST_F(GraphicsNodeTest, BBoxColor) { - GeometryNode node(ctx, "test", "test_node"); - - // Set bbox color - node.setBBoxColor(1.0, 0.0, 0.0); - - // Verify color was set correctly - double r, g, b; - node.getBBoxColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 1.0); - EXPECT_DOUBLE_EQ(g, 0.0); - EXPECT_DOUBLE_EQ(b, 0.0); -} - -TEST_F(GraphicsNodeTest, BBoxBounds) { - GeometryNode node(ctx, "test", "test_node"); - node.setGeometry(testGeom); - - cvc::bounding_box bbox = node.getBoundingBox(); - - // Verify bounding box encompasses all points - EXPECT_LE(bbox[0], 0.0); // min x - EXPECT_LE(bbox[1], 0.0); // min y - EXPECT_LE(bbox[2], 0.0); // min z - EXPECT_GE(bbox[3], 1.0); // max x - EXPECT_GE(bbox[4], 1.0); // max y - EXPECT_GE(bbox[5], 0.0); // max z -} - -// Test SceneGraph computeGraphicsBounds with translation transform -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsWithTranslation) { - SceneGraph sceneGraph(m_statePrefix); - - // Create geometry with unit cube: [0,0,0] to [1,1,1] - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - geom.points().push_back({1.0, 1.0, 0.0}); - geom.points().push_back({0.0, 0.0, 1.0}); - geom.points().push_back({1.0, 0.0, 1.0}); - geom.points().push_back({0.0, 1.0, 1.0}); - geom.points().push_back({1.0, 1.0, 1.0}); - - auto node = sceneGraph.addGraphics("translated_cube", geom); - - // Apply translation: move by (10, 20, 30) - node->setPosition(10.0, 20.0, 30.0); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // Bounds should be [10,20,30] to [11,21,31] - EXPECT_NEAR(bounds[0], 10.0, 1e-6); - EXPECT_NEAR(bounds[1], 20.0, 1e-6); - EXPECT_NEAR(bounds[2], 30.0, 1e-6); - EXPECT_NEAR(bounds[3], 11.0, 1e-6); - EXPECT_NEAR(bounds[4], 21.0, 1e-6); - EXPECT_NEAR(bounds[5], 31.0, 1e-6); -} - -// Test SceneGraph computeGraphicsBounds with scale transform -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsWithScale) { - SceneGraph sceneGraph(m_statePrefix); - - // Create geometry with unit cube: [0,0,0] to [1,1,1] - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - geom.points().push_back({1.0, 1.0, 0.0}); - geom.points().push_back({0.0, 0.0, 1.0}); - geom.points().push_back({1.0, 0.0, 1.0}); - geom.points().push_back({0.0, 1.0, 1.0}); - geom.points().push_back({1.0, 1.0, 1.0}); - - auto node = sceneGraph.addGraphics("scaled_cube", geom); - - // Apply scale: 2x in X, 3x in Y, 4x in Z - node->setScale(2.0, 3.0, 4.0); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // Bounds should be [0,0,0] to [2,3,4] - EXPECT_NEAR(bounds[0], 0.0, 1e-6); - EXPECT_NEAR(bounds[1], 0.0, 1e-6); - EXPECT_NEAR(bounds[2], 0.0, 1e-6); - EXPECT_NEAR(bounds[3], 2.0, 1e-6); - EXPECT_NEAR(bounds[4], 3.0, 1e-6); - EXPECT_NEAR(bounds[5], 4.0, 1e-6); -} - -// Test SceneGraph computeGraphicsBounds with rotation transform -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsWithRotation) { - SceneGraph sceneGraph(m_statePrefix); - - // Create geometry with square in XY plane: [-1,-1,0] to [1,1,0] - cvc::geometry geom; - geom.points().push_back({-1.0, -1.0, 0.0}); - geom.points().push_back({1.0, -1.0, 0.0}); - geom.points().push_back({-1.0, 1.0, 0.0}); - geom.points().push_back({1.0, 1.0, 0.0}); - - auto node = sceneGraph.addGraphics("rotated_square", geom); - - // Rotate 45 degrees around Z axis - node->setRotation(0.0, 0.0, 45.0); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // After 45 degree rotation, the diagonal becomes axis-aligned - // Expected bbox: approximately [-sqrt(2), -sqrt(2), 0] to [sqrt(2), sqrt(2), 0] - double expected = std::sqrt(2.0); - EXPECT_NEAR(bounds[0], -expected, 1e-4); - EXPECT_NEAR(bounds[1], -expected, 1e-4); - EXPECT_NEAR(bounds[2], 0.0, 1e-6); - EXPECT_NEAR(bounds[3], expected, 1e-4); - EXPECT_NEAR(bounds[4], expected, 1e-4); - EXPECT_NEAR(bounds[5], 0.0, 1e-6); -} - -// Test SceneGraph computeGraphicsBounds with combined transforms -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsWithCombinedTransforms) { - SceneGraph sceneGraph(m_statePrefix); - - // Create geometry with unit cube centered at origin: [-0.5,-0.5,-0.5] to [0.5,0.5,0.5] - cvc::geometry geom; - geom.points().push_back({-0.5, -0.5, -0.5}); - geom.points().push_back({0.5, -0.5, -0.5}); - geom.points().push_back({-0.5, 0.5, -0.5}); - geom.points().push_back({0.5, 0.5, -0.5}); - geom.points().push_back({-0.5, -0.5, 0.5}); - geom.points().push_back({0.5, -0.5, 0.5}); - geom.points().push_back({-0.5, 0.5, 0.5}); - geom.points().push_back({0.5, 0.5, 0.5}); - - auto node = sceneGraph.addGraphics("combined_cube", geom); - - // Apply scale then translate - node->setScale(2.0, 2.0, 2.0); // Scale to [-1,-1,-1] to [1,1,1] - node->setPosition(5.0, 10.0, 15.0); // Then translate - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // After scale: [-1,-1,-1] to [1,1,1] - // After translate: [4,9,14] to [6,11,16] - EXPECT_NEAR(bounds[0], 4.0, 1e-6); - EXPECT_NEAR(bounds[1], 9.0, 1e-6); - EXPECT_NEAR(bounds[2], 14.0, 1e-6); - EXPECT_NEAR(bounds[3], 6.0, 1e-6); - EXPECT_NEAR(bounds[4], 11.0, 1e-6); - EXPECT_NEAR(bounds[5], 16.0, 1e-6); -} - -// Test SceneGraph computeGraphicsBounds with multiple transformed objects -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsMultipleTransformed) { - SceneGraph sceneGraph(m_statePrefix); - - // Create first geometry at [0,0,0] to [1,1,1] - cvc::geometry geom1; - geom1.points().push_back({0.0, 0.0, 0.0}); - geom1.points().push_back({1.0, 1.0, 1.0}); - - // Create second geometry at [0,0,0] to [1,1,1] - cvc::geometry geom2; - geom2.points().push_back({0.0, 0.0, 0.0}); - geom2.points().push_back({1.0, 1.0, 1.0}); - - auto node1 = sceneGraph.addGraphics("obj1", geom1); - auto node2 = sceneGraph.addGraphics("obj2", geom2); - - // Translate first to [10,10,10] to [11,11,11] - node1->setPosition(10.0, 10.0, 10.0); - - // Translate second to [-5,-5,-5] to [-4,-4,-4] - node2->setPosition(-5.0, -5.0, -5.0); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // Combined bounds should be [-5,-5,-5] to [11,11,11] - EXPECT_NEAR(bounds[0], -5.0, 1e-6); - EXPECT_NEAR(bounds[1], -5.0, 1e-6); - EXPECT_NEAR(bounds[2], -5.0, 1e-6); - EXPECT_NEAR(bounds[3], 11.0, 1e-6); - EXPECT_NEAR(bounds[4], 11.0, 1e-6); - EXPECT_NEAR(bounds[5], 11.0, 1e-6); -} - -// Test SceneGraph computeGraphicsBounds with hierarchical transforms -TEST_F(GraphicsNodeTest, SceneGraphComputeBoundsHierarchical) { - SceneGraph sceneGraph(m_statePrefix); - - // Create parent geometry at [0,0,0] to [1,1,1] - cvc::geometry geom1; - geom1.points().push_back({0.0, 0.0, 0.0}); - geom1.points().push_back({1.0, 1.0, 1.0}); - - // Create child geometry at [0,0,0] to [1,1,1] - cvc::geometry geom2; - geom2.points().push_back({0.0, 0.0, 0.0}); - geom2.points().push_back({1.0, 1.0, 1.0}); - - auto parent = sceneGraph.addGraphics("parent", geom1); - auto child = std::make_shared(ctx, "test", "child"); - child->setGeometry(geom2); - - // Parent at [10,0,0] - parent->setPosition(10.0, 0.0, 0.0); - - // Child at [5,0,0] relative to parent - parent->addGraphicsChild(child); - child->setPosition(5.0, 0.0, 0.0); - - cvc::bounding_box bounds = sceneGraph.computeGraphicsBounds(); - - // Parent bounds: [10,0,0] to [11,1,1] - // Child world position: [15,0,0] to [16,1,1] - // Combined: [10,0,0] to [16,1,1] - EXPECT_NEAR(bounds[0], 10.0, 1e-6); - EXPECT_NEAR(bounds[1], 0.0, 1e-6); - EXPECT_NEAR(bounds[2], 0.0, 1e-6); - EXPECT_NEAR(bounds[3], 16.0, 1e-6); - EXPECT_NEAR(bounds[4], 1.0, 1e-6); - EXPECT_NEAR(bounds[5], 1.0, 1e-6); -} - -// ============================================================================ -// Bounding Box Transform Tests -// ============================================================================ - -TEST_F(GraphicsNodeTest, BBoxShowsLocalSpace) { - // Verify that bounding box geometry stays in local space - // and transform is applied to the bbox actor - SceneGraph sceneGraph(m_statePrefix); - - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 1.0, 1.0}); - - auto node = sceneGraph.addGraphics("bbox_test", geom); - - // Enable bbox - node->setShowBBox(true); - - // Apply a transform (rotation + translation) - node->setPosition(10.0, 0.0, 0.0); - node->setRotation(0.0, 0.0, 45.0); // 45 degree rotation around Z - - // The node's getBoundingBox should return local space coords - cvc::bounding_box localBBox = node->getBoundingBox(); - EXPECT_NEAR(localBBox[0], 0.0, 1e-6); - EXPECT_NEAR(localBBox[3], 1.0, 1e-6); - - // The bbox visualization will use the transform to render correctly - // This is applied in updateBoundingBoxNode() via setTransform() -} - -// ============================================================================ -// Clip Plane Tests -// ============================================================================ - -TEST_F(GraphicsNodeTest, ClipChildrenDefault) { - // Default clipChildren should be false - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - auto parent = sceneGraph.addGraphics("cliptest.parent", geom); - - EXPECT_FALSE(parent->getClipChildren()); -} - -TEST_F(GraphicsNodeTest, SetClipChildren) { - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - auto parent = sceneGraph.addGraphics("cliptest.setter", geom); - - parent->setClipChildren(true); - EXPECT_TRUE(parent->getClipChildren()); - - parent->setClipChildren(false); - EXPECT_FALSE(parent->getClipChildren()); -} - -TEST_F(GraphicsNodeTest, ClipChildrenStateSync) { - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - auto parent = sceneGraph.addGraphics("cliptest.statesync", geom); - - // Test setter -> state tree - parent->setClipChildren(true); - int stateValue = parent->getState("clip_children").template value(); - EXPECT_EQ(stateValue, 1); - - parent->setClipChildren(false); - stateValue = parent->getState("clip_children").template value(); - EXPECT_EQ(stateValue, 0); - - // Test state tree -> getter - parent->getState("clip_children").value(1); - // Give time for state change to propagate - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - EXPECT_TRUE(parent->getClipChildren()); -} - -TEST_F(GraphicsNodeTest, ClipPlanesGenerated) { - // Create a parent with a known bounding box - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({2.0, 3.0, 4.0}); - - auto parent = sceneGraph.addGraphics("cliptest.planes", geom); - parent->setClipChildren(true); - - // Should have 6 clip planes - vtkPlaneCollection *planes = parent->getClipPlanes(); - ASSERT_NE(planes, nullptr); - EXPECT_EQ(planes->GetNumberOfItems(), 6); -} - -TEST_F(GraphicsNodeTest, ClipPlanesTransform) { - // Create a parent with bounding box and transform - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 1.0, 1.0}); - - auto parent = sceneGraph.addGraphics("cliptest.transform", geom); - parent->setPosition(10.0, 20.0, 30.0); - parent->setClipChildren(true); - - // Planes should be transformed with the parent's transform - vtkPlaneCollection *planes = parent->getClipPlanes(); - ASSERT_NE(planes, nullptr); - EXPECT_EQ(planes->GetNumberOfItems(), 6); - - // Get first plane and check it's been transformed - // The exact values depend on implementation, but planes should exist - planes->InitTraversal(); - vtkPlane *plane = planes->GetNextItem(); - ASSERT_NE(plane, nullptr); -} - -TEST_F(GraphicsNodeTest, ChildrenClipped) { - // Create parent and child geometry nodes - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry parentGeom; - parentGeom.points().push_back({0.0, 0.0, 0.0}); - parentGeom.points().push_back({10.0, 10.0, 10.0}); - - cvc::geometry childGeom; - childGeom.points().push_back({5.0, 5.0, 5.0}); - childGeom.points().push_back({15.0, 15.0, 15.0}); - - auto parent = sceneGraph.addGraphics("cliptest.children", parentGeom); - auto child = std::make_shared(ctx, "cliptest.children.child", "child"); - child->setGeometry(childGeom); - parent->addGraphicsChild(child); - - // Enable clipping on parent - parent->setClipChildren(true); - - // Give time for threading/event queue to process - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Child should have received clip planes - // We can't easily test the mapper directly without VTK rendering context, - // but we can verify the planes were created - vtkPlaneCollection *planes = parent->getClipPlanes(); - ASSERT_NE(planes, nullptr); - EXPECT_EQ(planes->GetNumberOfItems(), 6); -} - -TEST_F(GraphicsNodeTest, ClippingDisabled) { - SceneGraph sceneGraph(m_statePrefix); - cvc::geometry parentGeom; - parentGeom.points().push_back({0.0, 0.0, 0.0}); - parentGeom.points().push_back({10.0, 10.0, 10.0}); - - cvc::geometry childGeom; - childGeom.points().push_back({5.0, 5.0, 5.0}); - - auto parent = sceneGraph.addGraphics("cliptest.disabled", parentGeom); - auto child = std::make_shared(ctx, "cliptest.disabled.child", "child"); - child->setGeometry(childGeom); - parent->addGraphicsChild(child); - - // Enable then disable clipping - parent->setClipChildren(true); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - parent->setClipChildren(false); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Clipping should be disabled - EXPECT_FALSE(parent->getClipChildren()); -} - -// Test createChild with GeometryNode and geometry data -TEST_F(GraphicsNodeTest, CreateChildGeometry) { - SceneGraph sceneGraph(m_statePrefix); - - // Create parent geometry - cvc::geometry parentGeom; - parentGeom.points().push_back({0.0, 0.0, 0.0}); - parentGeom.points().push_back({10.0, 0.0, 0.0}); - parentGeom.points().push_back({0.0, 10.0, 0.0}); - parentGeom.tris().push_back({0, 1, 2}); - - auto parent = sceneGraph.addGraphics("parent", parentGeom); - - // Create child geometry using createChild - cvc::geometry childGeom; - childGeom.points().push_back({1.0, 1.0, 1.0}); - childGeom.points().push_back({2.0, 1.0, 1.0}); - childGeom.points().push_back({1.0, 2.0, 1.0}); - childGeom.tris().push_back({0, 1, 2}); - - auto child = parent->createChild("child", childGeom); - - // Verify child was created correctly - ASSERT_NE(child, nullptr); - EXPECT_TRUE(child->hasGeometry()); - EXPECT_EQ(child->getName(), "child"); - - // Verify parent-child relationship - const auto &children = parent->getGraphicsChildren(); - EXPECT_EQ(children.size(), 1); - EXPECT_EQ(children[0], child); - - // Verify geometry was set correctly - const cvc::geometry *geom = child->getGeometry(); - ASSERT_NE(geom, nullptr); - EXPECT_EQ(geom->points().size(), 3); - EXPECT_EQ(geom->tris().size(), 1); -} - -// Test createChild with VolumeNode and volume data -TEST_F(GraphicsNodeTest, CreateChildVolume) { - SceneGraph sceneGraph(m_statePrefix); - - // Create parent geometry - auto parent = sceneGraph.addGraphics("parent", testGeom); - - // Create child volume using createChild - cvc::volume vol(ctx, cvc::dimension(10, 10, 10), cvc::UChar, - cvc::bounding_box(0.0, 0.0, 0.0, 10.0, 10.0, 10.0)); - auto child = parent->createChild("volume_child", vol); - - // Verify child was created correctly - ASSERT_NE(child, nullptr); - EXPECT_TRUE(child->hasVolume()); - EXPECT_EQ(child->getName(), "volume_child"); - - // Verify parent-child relationship - const auto &children = parent->getGraphicsChildren(); - EXPECT_EQ(children.size(), 1); - EXPECT_EQ(children[0], child); - - // Verify volume was set correctly - const cvc::volume *v = child->getVolume(); - ASSERT_NE(v, nullptr); - EXPECT_EQ(v->XDim(), 10); - EXPECT_EQ(v->YDim(), 10); - EXPECT_EQ(v->ZDim(), 10); -} - -// Test createChild without data (should create NullGraphicNode) -TEST_F(GraphicsNodeTest, CreateChildNoData) { - SceneGraph sceneGraph(m_statePrefix); - - auto parent = sceneGraph.addGraphics("parent", testGeom); - - // Create child without data - auto child = parent->createChild("null_child"); - - // Verify child was created - ASSERT_NE(child, nullptr); - EXPECT_EQ(child->getName(), "null_child"); - - // Verify parent-child relationship - const auto &children = parent->getGraphicsChildren(); - EXPECT_EQ(children.size(), 1); - EXPECT_EQ(children[0], child); - - // Verify it's a NullGraphicNode (default bounds are -0.5 to 0.5) - auto bbox = child->getBoundingBox(); - EXPECT_EQ(bbox.minx, -0.5); - EXPECT_EQ(bbox.maxx, 0.5); - EXPECT_EQ(bbox.miny, -0.5); - EXPECT_EQ(bbox.maxy, 0.5); - EXPECT_EQ(bbox.minz, -0.5); - EXPECT_EQ(bbox.maxz, 0.5); -} - -// Test multiple createChild calls -TEST_F(GraphicsNodeTest, CreateMultipleChildren) { - SceneGraph sceneGraph(m_statePrefix); - - auto parent = sceneGraph.addGraphics("parent", testGeom); - - // Create multiple children of different types - cvc::geometry geom1; - geom1.points().push_back({1.0, 0.0, 0.0}); - auto child1 = parent->createChild("geom1", geom1); - - cvc::volume vol1(ctx, cvc::dimension(5, 5, 5), cvc::UChar, - cvc::bounding_box(0.0, 0.0, 0.0, 5.0, 5.0, 5.0)); - auto child2 = parent->createChild("vol1", vol1); - - cvc::geometry geom2; - geom2.points().push_back({2.0, 0.0, 0.0}); - auto child3 = parent->createChild("geom2", geom2); - - // Verify all children were created - ASSERT_NE(child1, nullptr); - ASSERT_NE(child2, nullptr); - ASSERT_NE(child3, nullptr); - - // Verify parent has all children - const auto &children = parent->getGraphicsChildren(); - EXPECT_EQ(children.size(), 3); - - // Verify correct types - auto geomChild1 = std::dynamic_pointer_cast(child1); - auto volChild = std::dynamic_pointer_cast(child2); - auto geomChild2 = std::dynamic_pointer_cast(child3); - - EXPECT_NE(geomChild1, nullptr); - EXPECT_NE(volChild, nullptr); - EXPECT_NE(geomChild2, nullptr); -} - -// Test createChild state tree path construction -TEST_F(GraphicsNodeTest, CreateChildStatePath) { - SceneGraph sceneGraph(m_statePrefix); - - auto parent = sceneGraph.addGraphics("parent", testGeom); - auto child = parent->createChild("child", testGeom); - - // Verify state path is properly constructed - std::string parentPath = parent->getState().fullName(); - std::string childPath = child->getState().fullName(); - - // Child path should be parent.children.child - std::string expectedPath = parentPath + ".children.child"; - EXPECT_EQ(childPath, expectedPath); -} - -// Test nested createChild calls -TEST_F(GraphicsNodeTest, CreateNestedChildren) { - SceneGraph sceneGraph(m_statePrefix); - - auto root = sceneGraph.addGraphics("root", testGeom); - auto level1 = root->createChild("level1", testGeom); - auto level2 = level1->createChild("level2", testGeom); - auto level3 = level2->createChild("level3", testGeom); - - // Verify hierarchy - ASSERT_NE(level1, nullptr); - ASSERT_NE(level2, nullptr); - ASSERT_NE(level3, nullptr); - - EXPECT_EQ(root->getGraphicsChildren().size(), 1); - EXPECT_EQ(level1->getGraphicsChildren().size(), 1); - EXPECT_EQ(level2->getGraphicsChildren().size(), 1); - EXPECT_EQ(level3->getGraphicsChildren().size(), 0); - - // Verify state paths are properly nested - std::string rootPath = root->getState().fullName(); - std::string level1Path = level1->getState().fullName(); - std::string level2Path = level2->getState().fullName(); - std::string level3Path = level3->getState().fullName(); - - EXPECT_EQ(level1Path, rootPath + ".children.level1"); - EXPECT_EQ(level2Path, level1Path + ".children.level2"); - EXPECT_EQ(level3Path, level2Path + ".children.level3"); -} - -// Test that child volumes are found by SceneGraph::getAllVolumeGraphics() -TEST_F(GraphicsNodeTest, CreateChildVolumeDiscovery) { - SceneGraph sceneGraph(m_statePrefix); - - // Create a parent geometry - auto parent = sceneGraph.addGraphics("parent", testGeom); - - // Initial check - no volumes - EXPECT_EQ(sceneGraph.getAllVolumeGraphics().size(), 0); - EXPECT_EQ(sceneGraph.getVolumeGraphicsCount(), 0); - - // Create child volumes - cvc::volume vol1(ctx, cvc::dimension(5, 5, 5), cvc::UChar, cvc::bounding_box(0, 0, 0, 4, 4, 4)); - auto childVol1 = parent->createChild("vol1", vol1); - - // Should now find the child volume - auto allVolumes = sceneGraph.getAllVolumeGraphics(); - EXPECT_EQ(allVolumes.size(), 1); - EXPECT_EQ(sceneGraph.getVolumeGraphicsCount(), 1); - EXPECT_EQ(allVolumes[0], childVol1); - - // Add another child volume at different level - cvc::volume vol2(ctx, cvc::dimension(3, 3, 3), cvc::UChar, cvc::bounding_box(0, 0, 0, 2, 2, 2)); - auto childVol2 = childVol1->createChild("vol2", vol2); - - // Should find both volumes - allVolumes = sceneGraph.getAllVolumeGraphics(); - EXPECT_EQ(allVolumes.size(), 2); - EXPECT_EQ(sceneGraph.getVolumeGraphicsCount(), 2); - - // Verify both volumes are in the list - bool foundVol1 = false, foundVol2 = false; - for (const auto &vol : allVolumes) { - if (vol == childVol1) - foundVol1 = true; - if (vol == childVol2) - foundVol2 = true; - } - EXPECT_TRUE(foundVol1); - EXPECT_TRUE(foundVol2); -} - -// Test that child geometries are found by SceneGraph::getAllGeometryGraphics() -TEST_F(GraphicsNodeTest, CreateChildGeometryDiscovery) { - SceneGraph sceneGraph(m_statePrefix); - - // Create a parent volume - cvc::volume vol(ctx, cvc::dimension(5, 5, 5), cvc::UChar, cvc::bounding_box(0, 0, 0, 4, 4, 4)); - auto parent = sceneGraph.addGraphics("parent_vol", vol); - - // Initial check - no geometries - EXPECT_EQ(sceneGraph.getAllGeometryGraphics().size(), 0); - EXPECT_EQ(sceneGraph.getGeometryGraphicsCount(), 0); - - // Create child geometries - cvc::geometry geom1; - geom1.points().push_back({1.0, 0.0, 0.0}); - geom1.points().push_back({0.0, 1.0, 0.0}); - geom1.points().push_back({0.0, 0.0, 1.0}); - geom1.tris().push_back({0, 1, 2}); - auto childGeom1 = parent->createChild("geom1", geom1); - - // Should now find the child geometry - auto allGeometries = sceneGraph.getAllGeometryGraphics(); - EXPECT_EQ(allGeometries.size(), 1); - EXPECT_EQ(sceneGraph.getGeometryGraphicsCount(), 1); - EXPECT_EQ(allGeometries[0], childGeom1); - - // Add another child geometry at different level - cvc::geometry geom2; - geom2.points().push_back({2.0, 0.0, 0.0}); - auto childGeom2 = childGeom1->createChild("geom2", geom2); - - // Should find both geometries - allGeometries = sceneGraph.getAllGeometryGraphics(); - EXPECT_EQ(allGeometries.size(), 2); - EXPECT_EQ(sceneGraph.getGeometryGraphicsCount(), 2); - - // Verify both geometries are in the list - bool foundGeom1 = false, foundGeom2 = false; - for (const auto &geom : allGeometries) { - if (geom == childGeom1) - foundGeom1 = true; - if (geom == childGeom2) - foundGeom2 = true; - } - EXPECT_TRUE(foundGeom1); - EXPECT_TRUE(foundGeom2); -} - -// ============================================================================ -// Transform Hierarchy Tests - World Transform Application -// ============================================================================ - -TEST_F(GraphicsNodeTest, ChildVolumeInheritsParentTransform) { - // Create scene graph with parent geometry and child volume - SceneGraph sceneGraph; - auto rootNode = sceneGraph.getGraphicsRoot(); - - // Create parent geometry with scale transform - cvc::geometry parentGeom; - parentGeom.points().push_back({0.0, 0.0, 0.0}); - parentGeom.points().push_back({1.0, 0.0, 0.0}); - parentGeom.points().push_back({0.0, 1.0, 0.0}); - parentGeom.tris().push_back({0, 1, 2}); - - auto parentNode = rootNode->createChild("parent", parentGeom); - - // Apply scale to parent - parentNode->setScale(2.0, 2.0, 2.0); - - // Create child volume (SDF from parent geometry) - cvc::volume childVol(ctx, cvc::dimension(10, 10, 10), cvc::UChar, - cvc::bounding_box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); - - auto childNode = parentNode->createChild("sdf", childVol); - - // Get the world transform of the child - auto childWorldTransform = childNode->getWorldTransform(); - - // The world transform should include parent's scale (2x) - // Extract scale from the transform matrix - double scaleX = - std::sqrt(childWorldTransform->GetElement(0, 0) * childWorldTransform->GetElement(0, 0) + - childWorldTransform->GetElement(1, 0) * childWorldTransform->GetElement(1, 0) + - childWorldTransform->GetElement(2, 0) * childWorldTransform->GetElement(2, 0)); - - EXPECT_NEAR(scaleX, 2.0, 1e-6); -} - -TEST_F(GraphicsNodeTest, ChildGeometryInheritsParentTransform) { - // Create scene graph with parent volume and child geometry - SceneGraph sceneGraph; - auto rootNode = sceneGraph.getGraphicsRoot(); - - // Create parent volume - cvc::volume parentVol(ctx, cvc::dimension(10, 10, 10), cvc::UChar, - cvc::bounding_box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); - - auto parentNode = rootNode->createChild("parent", parentVol); - - // Apply scale to parent - parentNode->setScale(3.0, 3.0, 3.0); - - // Create child geometry (isosurface from parent volume) - cvc::geometry childGeom; - childGeom.points().push_back({0.0, 0.0, 0.0}); - childGeom.points().push_back({1.0, 0.0, 0.0}); - childGeom.points().push_back({0.0, 1.0, 0.0}); - childGeom.tris().push_back({0, 1, 2}); - - auto childNode = parentNode->createChild("isosurface", childGeom); - - // Get the world transform of the child - auto childWorldTransform = childNode->getWorldTransform(); - - // The world transform should include parent's scale (3x) - double scaleX = - std::sqrt(childWorldTransform->GetElement(0, 0) * childWorldTransform->GetElement(0, 0) + - childWorldTransform->GetElement(1, 0) * childWorldTransform->GetElement(1, 0) + - childWorldTransform->GetElement(2, 0) * childWorldTransform->GetElement(2, 0)); - - EXPECT_NEAR(scaleX, 3.0, 1e-6); -} - -TEST_F(GraphicsNodeTest, DeepTransformHierarchy) { - // Test a 3-level hierarchy: root -> parent (scaled) -> child (rotated) -> grandchild (translated) - SceneGraph sceneGraph; - auto rootNode = sceneGraph.getGraphicsRoot(); - - // Create parent with scale - cvc::geometry parentGeom; - parentGeom.points().push_back({0.0, 0.0, 0.0}); - auto parentNode = rootNode->createChild("parent", parentGeom); - parentNode->setScale(2.0, 2.0, 2.0); - - // Create child with rotation (90 degrees around Z) - cvc::volume childVol(ctx, cvc::dimension(5, 5, 5), cvc::UChar, - cvc::bounding_box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); - auto childNode = parentNode->createChild("child", childVol); - childNode->setRotation(0.0, 0.0, 90.0); - - // Create grandchild with translation - cvc::geometry grandchildGeom; - grandchildGeom.points().push_back({1.0, 0.0, 0.0}); - auto grandchildNode = childNode->createChild("grandchild", grandchildGeom); - grandchildNode->setPosition(5.0, 0.0, 0.0); - - // Verify grandchild's world transform includes all transformations - auto worldTransform = grandchildNode->getWorldTransform(); - - // The grandchild should be scaled by parent - double scaleX = std::sqrt(worldTransform->GetElement(0, 0) * worldTransform->GetElement(0, 0) + - worldTransform->GetElement(1, 0) * worldTransform->GetElement(1, 0) + - worldTransform->GetElement(2, 0) * worldTransform->GetElement(2, 0)); - EXPECT_NEAR(scaleX, 2.0, 0.1); // Allow some tolerance due to rotation -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/tests/GridNodeTest.cpp b/src/volrover3/tests/GridNodeTest.cpp deleted file mode 100644 index 3103ca0b..00000000 --- a/src/volrover3/tests/GridNodeTest.cpp +++ /dev/null @@ -1,536 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class GridNodeTest : public ::testing::Test { -protected: - cvc::app ctx; - static void SetUpTestSuite() { - if (!QApplication::instance()) { - int argc = 0; - char **argv = nullptr; - app = new QApplication(argc, argv); - } - // Disable threading for state_object to avoid race conditions during destruction - cvc::state_object::setUseThreading(false); - } - - void SetUp() override { - gridNode = new GridNode(ctx, "test.grid", "grid"); - renderer = vtkRenderer::New(); - appState = &AppState::instance(); - } - - void TearDown() override { - if (gridNode) { - gridNode->removeFromRenderer(renderer); - } - renderer->Delete(); - delete gridNode; - } - - static QApplication *app; - GridNode *gridNode; - vtkRenderer *renderer; - AppState *appState; -}; - -QApplication *GridNodeTest::app = nullptr; - -// =========================== -// Construction and Basic Properties -// =========================== - -TEST_F(GridNodeTest, Construction) { - EXPECT_NE(gridNode, nullptr); - EXPECT_TRUE(gridNode->isVisible()); -} - -TEST_F(GridNodeTest, VisibilityToggle) { - gridNode->setVisible(true); - EXPECT_TRUE(gridNode->isVisible()); - - gridNode->setVisible(false); - EXPECT_FALSE(gridNode->isVisible()); -} - -// =========================== -// Bounding Box and Grid Positioning -// =========================== - -TEST_F(GridNodeTest, SetBounds) { - cvc::bounding_box bounds; - bounds.setMin(-5.0, -3.0, -2.0); - bounds.setMax(5.0, 3.0, 2.0); - - // Should not crash - gridNode->setBounds(bounds); - SUCCEED(); -} - -TEST_F(GridNodeTest, GridAtBoundingBoxMinimum) { - // Grid planes should be positioned at bounding box minimum corner - cvc::bounding_box bounds; - bounds.setMin(10.0, 20.0, 30.0); - bounds.setMax(50.0, 60.0, 70.0); - - gridNode->setBounds(bounds); - - // Grid should create planes at (10, 20, 30) corner - // YZ plane at X=10, XZ plane at Y=20, XY plane at Z=30 - // Can't directly test internal VTK geometry, but verify no crash - SUCCEED(); -} - -TEST_F(GridNodeTest, NegativeBounds) { - cvc::bounding_box bounds; - bounds.setMin(-100.0, -50.0, -25.0); - bounds.setMax(-10.0, -5.0, -2.0); - - gridNode->setBounds(bounds); - SUCCEED(); -} - -TEST_F(GridNodeTest, ZeroBounds) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(0.0, 0.0, 0.0); - - // Edge case: zero-volume bounding box - gridNode->setBounds(bounds); - SUCCEED(); -} - -TEST_F(GridNodeTest, LargeBounds) { - cvc::bounding_box bounds; - bounds.setMin(-1000.0, -1000.0, -1000.0); - bounds.setMax(1000.0, 1000.0, 1000.0); - - gridNode->setBounds(bounds); - SUCCEED(); -} - -// =========================== -// Grid Divisions -// =========================== - -TEST_F(GridNodeTest, SetDivisions) { - gridNode->setGridDivisions(10, 20, 30); - - int x, y, z; - gridNode->getGridDivisions(x, y, z); - EXPECT_EQ(x, 10); - EXPECT_EQ(y, 20); - EXPECT_EQ(z, 30); -} - -TEST_F(GridNodeTest, MinimumDivisions) { - gridNode->setGridDivisions(1, 1, 1); - - int x, y, z; - gridNode->getGridDivisions(x, y, z); - EXPECT_EQ(x, 1); - EXPECT_EQ(y, 1); - EXPECT_EQ(z, 1); -} - -TEST_F(GridNodeTest, LargeDivisions) { - gridNode->setGridDivisions(256, 256, 256); - - int x, y, z; - gridNode->getGridDivisions(x, y, z); - EXPECT_EQ(x, 256); - EXPECT_EQ(y, 256); - EXPECT_EQ(z, 256); -} - -TEST_F(GridNodeTest, DivisionsUpdateGrid) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - - // Change divisions should update grid - gridNode->setGridDivisions(5, 5, 5); - gridNode->setGridDivisions(20, 20, 20); - - int x, y, z; - gridNode->getGridDivisions(x, y, z); - EXPECT_EQ(x, 20); - EXPECT_EQ(y, 20); - EXPECT_EQ(z, 20); -} - -// =========================== -// Plane Visibility -// =========================== - -TEST_F(GridNodeTest, PlaneVisibility) { - gridNode->setYZPlaneVisible(true); - gridNode->setXZPlaneVisible(false); - gridNode->setXYPlaneVisible(true); - - EXPECT_TRUE(gridNode->isYZPlaneVisible()); - EXPECT_FALSE(gridNode->isXZPlaneVisible()); - EXPECT_TRUE(gridNode->isXYPlaneVisible()); -} - -TEST_F(GridNodeTest, AllPlanesHidden) { - gridNode->setYZPlaneVisible(false); - gridNode->setXZPlaneVisible(false); - gridNode->setXYPlaneVisible(false); - - EXPECT_FALSE(gridNode->isYZPlaneVisible()); - EXPECT_FALSE(gridNode->isXZPlaneVisible()); - EXPECT_FALSE(gridNode->isXYPlaneVisible()); -} - -TEST_F(GridNodeTest, AllPlanesVisible) { - gridNode->setYZPlaneVisible(true); - gridNode->setXZPlaneVisible(true); - gridNode->setXYPlaneVisible(true); - - EXPECT_TRUE(gridNode->isYZPlaneVisible()); - EXPECT_TRUE(gridNode->isXZPlaneVisible()); - EXPECT_TRUE(gridNode->isXYPlaneVisible()); -} - -// =========================== -// Tick Intervals -// =========================== - -TEST_F(GridNodeTest, SetTickIntervals) { - gridNode->setTickIntervals(4, 8, 16); - - int x, y, z; - gridNode->getTickIntervals(x, y, z); - EXPECT_EQ(x, 4); - EXPECT_EQ(y, 8); - EXPECT_EQ(z, 16); -} - -TEST_F(GridNodeTest, MinimumTickInterval) { - gridNode->setTickIntervals(1, 1, 1); - - int x, y, z; - gridNode->getTickIntervals(x, y, z); - EXPECT_EQ(x, 1); - EXPECT_EQ(y, 1); - EXPECT_EQ(z, 1); -} - -TEST_F(GridNodeTest, LargeTickInterval) { - gridNode->setTickIntervals(128, 128, 128); - - int x, y, z; - gridNode->getTickIntervals(x, y, z); - EXPECT_EQ(x, 128); - EXPECT_EQ(y, 128); - EXPECT_EQ(z, 128); -} - -TEST_F(GridNodeTest, TickIntervalsWithBounds) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(100.0, 100.0, 100.0); - gridNode->setBounds(bounds); - gridNode->setGridDivisions(100, 100, 100); - - // Set tick intervals - gridNode->setTickIntervals(10, 10, 10); - - int x, y, z; - gridNode->getTickIntervals(x, y, z); - EXPECT_EQ(x, 10); - EXPECT_EQ(y, 10); - EXPECT_EQ(z, 10); -} - -// =========================== -// Tick Label Properties -// =========================== - -TEST_F(GridNodeTest, TickLabelColor) { - gridNode->setTickLabelColor(1.0, 0.5, 0.0); - - double r, g, b; - gridNode->getTickLabelColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 1.0); - EXPECT_DOUBLE_EQ(g, 0.5); - EXPECT_DOUBLE_EQ(b, 0.0); -} - -TEST_F(GridNodeTest, TickLabelFontSize) { - gridNode->setTickLabelFontSize(24); - EXPECT_EQ(gridNode->getTickLabelFontSize(), 24); - - gridNode->setTickLabelFontSize(8); - EXPECT_EQ(gridNode->getTickLabelFontSize(), 8); -} - -// =========================== -// Plane Colors -// =========================== - -TEST_F(GridNodeTest, YZPlaneColor) { - gridNode->setYZPlaneColor(0.8, 0.2, 0.1); - - double r, g, b; - gridNode->getYZPlaneColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 0.8); - EXPECT_DOUBLE_EQ(g, 0.2); - EXPECT_DOUBLE_EQ(b, 0.1); -} - -TEST_F(GridNodeTest, XZPlaneColor) { - gridNode->setXZPlaneColor(0.1, 0.8, 0.2); - - double r, g, b; - gridNode->getXZPlaneColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 0.1); - EXPECT_DOUBLE_EQ(g, 0.8); - EXPECT_DOUBLE_EQ(b, 0.2); -} - -TEST_F(GridNodeTest, XYPlaneColor) { - gridNode->setXYPlaneColor(0.2, 0.1, 0.8); - - double r, g, b; - gridNode->getXYPlaneColor(r, g, b); - EXPECT_DOUBLE_EQ(r, 0.2); - EXPECT_DOUBLE_EQ(g, 0.1); - EXPECT_DOUBLE_EQ(b, 0.8); -} - -// =========================== -// Renderer Management -// =========================== - -TEST_F(GridNodeTest, AddToRenderer) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - - // Should not crash - gridNode->addToRenderer(renderer); - SUCCEED(); -} - -TEST_F(GridNodeTest, RemoveFromRenderer) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - - gridNode->addToRenderer(renderer); - gridNode->removeFromRenderer(renderer); - SUCCEED(); -} - -TEST_F(GridNodeTest, MultipleAddRemove) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - - // Add and remove multiple times - for (int i = 0; i < 5; ++i) { - gridNode->addToRenderer(renderer); - gridNode->removeFromRenderer(renderer); - } - SUCCEED(); -} - -// =========================== -// Tick Visibility and State Integration -// =========================== - -TEST_F(GridNodeTest, TickVisibilityDefault) { - // GridNode initializes tics.visible to false by default - EXPECT_FALSE(gridNode->getState("tics.visible").value()); -} - -TEST_F(GridNodeTest, TickVisibilityToggle) { - // Test state synchronization via GridNode state - gridNode->getState("tics.visible").value(true); - EXPECT_TRUE(gridNode->getState("tics.visible").value()); - - gridNode->getState("tics.visible").value(false); - EXPECT_FALSE(gridNode->getState("tics.visible").value()); -} - -TEST_F(GridNodeTest, TickLabelsWithVisibilityOff) { - // When ticks are not visible, tick labels should not be added to renderer - gridNode->getState("tics.visible").value(false); - - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - gridNode->setTickIntervals(2, 2, 2); - - gridNode->addToRenderer(renderer); - - // Verify no crash and tick actors should not be in renderer - SUCCEED(); -} - -TEST_F(GridNodeTest, TickLabelsWithVisibilityOn) { - // When ticks are visible, tick labels should be added to renderer - gridNode->getState("tics.visible").value(true); - - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - gridNode->setTickIntervals(2, 2, 2); - - gridNode->addToRenderer(renderer); - - // Verify no crash and tick actors should be in renderer - SUCCEED(); -} - -// =========================== -// Grid Update Scenarios -// =========================== - -TEST_F(GridNodeTest, UpdateAfterDataLoad) { - // Simulate data loading scenario - cvc::bounding_box initialBounds; - initialBounds.setMin(0.0, 0.0, 0.0); - initialBounds.setMax(1.0, 1.0, 1.0); - gridNode->setBounds(initialBounds); - gridNode->addToRenderer(renderer); - - // Load new data with different bounds - cvc::bounding_box newBounds; - newBounds.setMin(-10.0, -5.0, -2.0); - newBounds.setMax(10.0, 5.0, 2.0); - gridNode->setBounds(newBounds); - - // Old tick labels should be removed, new ones created - SUCCEED(); -} - -TEST_F(GridNodeTest, TickLabelPositioningAfterBoundsChange) { - gridNode->getState("tics.visible").value(true); - - // Initial setup - cvc::bounding_box bounds1; - bounds1.setMin(0.0, 0.0, 0.0); - bounds1.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds1); - gridNode->setTickIntervals(5, 5, 5); - gridNode->addToRenderer(renderer); - - // Change bounds - cvc::bounding_box bounds2; - bounds2.setMin(-5.0, -5.0, -5.0); - bounds2.setMax(5.0, 5.0, 5.0); - gridNode->setBounds(bounds2); - - // Tick labels should be repositioned correctly - SUCCEED(); -} - -TEST_F(GridNodeTest, MultiplePropertyChanges) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(100.0, 100.0, 100.0); - - gridNode->setBounds(bounds); - gridNode->setGridDivisions(50, 50, 50); - gridNode->setTickIntervals(10, 10, 10); - gridNode->setYZPlaneColor(1.0, 0.0, 0.0); - gridNode->setXZPlaneColor(0.0, 1.0, 0.0); - gridNode->setXYPlaneColor(0.0, 0.0, 1.0); - gridNode->setTickLabelColor(1.0, 1.0, 1.0); - gridNode->setTickLabelFontSize(16); - gridNode->setYZPlaneVisible(true); - gridNode->setXZPlaneVisible(true); - gridNode->setXYPlaneVisible(true); - - gridNode->addToRenderer(renderer); - - // All properties should be applied correctly - int divX, divY, divZ; - gridNode->getGridDivisions(divX, divY, divZ); - EXPECT_EQ(divX, 50); - EXPECT_EQ(divY, 50); - EXPECT_EQ(divZ, 50); - - int tickX, tickY, tickZ; - gridNode->getTickIntervals(tickX, tickY, tickZ); - EXPECT_EQ(tickX, 10); - EXPECT_EQ(tickY, 10); - EXPECT_EQ(tickZ, 10); - - EXPECT_EQ(gridNode->getTickLabelFontSize(), 16); -} - -// =========================== -// Edge Cases -// =========================== - -TEST_F(GridNodeTest, AsymmetricBounds) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(100.0, 10.0, 1.0); - - gridNode->setBounds(bounds); - gridNode->setGridDivisions(100, 10, 1); - gridNode->setTickIntervals(10, 2, 1); - - SUCCEED(); -} - -TEST_F(GridNodeTest, FractionalBounds) { - cvc::bounding_box bounds; - bounds.setMin(0.123, 0.456, 0.789); - bounds.setMax(1.234, 2.345, 3.456); - - gridNode->setBounds(bounds); - SUCCEED(); -} - -TEST_F(GridNodeTest, VisibilityWhileInRenderer) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - gridNode->addToRenderer(renderer); - - // Toggle visibility while in renderer - gridNode->setVisible(false); - gridNode->setVisible(true); - - SUCCEED(); -} - -TEST_F(GridNodeTest, PlaneVisibilityWhileInRenderer) { - cvc::bounding_box bounds; - bounds.setMin(0.0, 0.0, 0.0); - bounds.setMax(10.0, 10.0, 10.0); - gridNode->setBounds(bounds); - gridNode->addToRenderer(renderer); - - // Toggle individual planes while in renderer - gridNode->setYZPlaneVisible(false); - gridNode->setXZPlaneVisible(false); - gridNode->setXYPlaneVisible(false); - - gridNode->setYZPlaneVisible(true); - gridNode->setXZPlaneVisible(true); - gridNode->setXYPlaneVisible(true); - - SUCCEED(); -} diff --git a/src/volrover3/tests/NullGraphicNodeTest.cpp b/src/volrover3/tests/NullGraphicNodeTest.cpp deleted file mode 100644 index b517c63e..00000000 --- a/src/volrover3/tests/NullGraphicNodeTest.cpp +++ /dev/null @@ -1,609 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -class NullGraphicNodeTest : public ::testing::Test { -protected: - cvc::app ctx; - void SetUp() override { - // Disable threading for state_object to avoid destruction race conditions - cvc::state_object::setUseThreading(false); - - m_statePrefix = "test_null_graphic_" + std::to_string(testCounter++); - } - - void TearDown() override { - // disconnectState() in SceneNode destructor prevents callbacks during destruction - } - - std::string m_statePrefix; - static int testCounter; -}; - -int NullGraphicNodeTest::testCounter = 0; - -// Test NullGraphicNode default construction -TEST_F(NullGraphicNodeTest, DefaultConstruction) { - auto nullNode = std::make_shared(ctx, "test.null", "test_null"); - - ASSERT_NE(nullNode, nullptr); - EXPECT_EQ(nullNode->getName(), "test_null"); - - // Check default bounding box (1x1x1 centered at origin) - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], -0.5); // min x - EXPECT_DOUBLE_EQ(bbox[1], -0.5); // min y - EXPECT_DOUBLE_EQ(bbox[2], -0.5); // min z - EXPECT_DOUBLE_EQ(bbox[3], 0.5); // max x - EXPECT_DOUBLE_EQ(bbox[4], 0.5); // max y - EXPECT_DOUBLE_EQ(bbox[5], 0.5); // max z -} - -// Test NullGraphicNode bounds can be modified -TEST_F(NullGraphicNodeTest, SetBoundsArray) { - auto nullNode = std::make_shared(ctx, "test_null"); - - cvc::bounding_box newBounds(-50, -25, -10, 50, 25, 10); - nullNode->setBounds(newBounds); - - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], -50.0); - EXPECT_DOUBLE_EQ(bbox[1], -25.0); - EXPECT_DOUBLE_EQ(bbox[2], -10.0); - EXPECT_DOUBLE_EQ(bbox[3], 50.0); - EXPECT_DOUBLE_EQ(bbox[4], 25.0); - EXPECT_DOUBLE_EQ(bbox[5], 10.0); -} - -// Test NullGraphicNode bounds can be set with individual values -TEST_F(NullGraphicNodeTest, SetBoundsIndividual) { - auto nullNode = std::make_shared(ctx, "test_null"); - - nullNode->setBounds(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); - - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], 1.0); - EXPECT_DOUBLE_EQ(bbox[1], 2.0); - EXPECT_DOUBLE_EQ(bbox[2], 3.0); - EXPECT_DOUBLE_EQ(bbox[3], 4.0); - EXPECT_DOUBLE_EQ(bbox[4], 5.0); - EXPECT_DOUBLE_EQ(bbox[5], 6.0); -} - -// Test SceneGraph creates null graphic as graphics root -TEST_F(NullGraphicNodeTest, SceneGraphInitialNullGraphic) { - SceneGraph sceneGraph(m_statePrefix); - - // Graphics root IS the null graphic node - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - EXPECT_EQ(nullNode->getName(), "root"); - - // Should have bbox visible by default - EXPECT_TRUE(nullNode->getShowBBox()); - - // Should have grid and axis as initial children - auto children = nullNode->getGraphicsChildren(); - EXPECT_EQ(children.size(), 2); -} - -// Test geometry added as child of null graphic root -TEST_F(NullGraphicNodeTest, NullGraphicRemovedOnGeometryAdd) { - SceneGraph sceneGraph(m_statePrefix); - - // Graphics root is the null graphic, starts with grid and axis - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - auto children = nullNode->getGraphicsChildren(); - ASSERT_EQ(children.size(), 2); - - // Add geometry - cvc::geometry geom; - geom.points().resize(3); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 1; - geom.points()[1][1] = 0; - geom.points()[1][2] = 0; - geom.points()[2][0] = 0; - geom.points()[2][1] = 1; - geom.points()[2][2] = 0; - - sceneGraph.addGraphics("test_geom", geom); - - // Null graphic is still the root, now has grid + axis + test_geom = 3 children - children = nullNode->getGraphicsChildren(); - ASSERT_EQ(children.size(), 3); - - // children[0] is grid, children[1] is axis, children[2] should be the geometry we added - auto geomNode = std::dynamic_pointer_cast(children[2]); - ASSERT_NE(geomNode, nullptr); - EXPECT_EQ(geomNode->getName(), "test_geom"); -} - -// Test null graphic root remains when graphics removed -TEST_F(NullGraphicNodeTest, NullGraphicRestoredOnRemove) { - SceneGraph sceneGraph(m_statePrefix); - - // Add geometry - cvc::geometry geom; - geom.points().resize(3); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 1; - geom.points()[1][1] = 0; - geom.points()[1][2] = 0; - geom.points()[2][0] = 0; - geom.points()[2][1] = 1; - geom.points()[2][2] = 0; - sceneGraph.addGraphics("test_geom", geom); - - // Graphics root (null graphic) has grid + axis + test_geom = 3 children - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - auto children = nullNode->getGraphicsChildren(); - ASSERT_EQ(children.size(), 3); - - // Remove the geometry - sceneGraph.removeGraphics("test_geom"); - - // Graphics root (null graphic) is back to grid and axis only - children = nullNode->getGraphicsChildren(); - EXPECT_EQ(children.size(), 2); -} - -// Test null graphic not counted as real graphic -TEST_F(NullGraphicNodeTest, NullGraphicNotInGraphicsMap) { - SceneGraph sceneGraph(m_statePrefix); - - // Graphics root IS the null graphic, starts with grid and axis children - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - auto children = nullNode->getGraphicsChildren(); - ASSERT_EQ(children.size(), 2); - - // Verify null graphic is not accessible via getGraphics (it's the root, not a child) - auto nullFromMap = sceneGraph.getGraphics("root"); - EXPECT_EQ(nullFromMap, nullptr); - - // Add real graphic - cvc::geometry geom; - geom.points().resize(3); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 1; - geom.points()[1][1] = 0; - geom.points()[1][2] = 0; - geom.points()[2][0] = 0; - geom.points()[2][1] = 1; - geom.points()[2][2] = 0; - sceneGraph.addGraphics("test_geom", geom); - - // Now should be able to get the real graphic - auto realGraphic = sceneGraph.getGraphics("test_geom"); - ASSERT_NE(realGraphic, nullptr); - EXPECT_EQ(realGraphic->getName(), "test_geom"); -} - -// Test large custom bounds -TEST_F(NullGraphicNodeTest, LargeCustomBounds) { - auto nullNode = std::make_shared(ctx, "test_null"); - - nullNode->setBounds(-1000.0, -2000.0, -3000.0, 1000.0, 2000.0, 3000.0); - - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], -1000.0); - EXPECT_DOUBLE_EQ(bbox[1], -2000.0); - EXPECT_DOUBLE_EQ(bbox[2], -3000.0); - EXPECT_DOUBLE_EQ(bbox[3], 1000.0); - EXPECT_DOUBLE_EQ(bbox[4], 2000.0); - EXPECT_DOUBLE_EQ(bbox[5], 3000.0); -} - -// Test asymmetric bounds -TEST_F(NullGraphicNodeTest, AsymmetricBounds) { - auto nullNode = std::make_shared(ctx, "test_null"); - - nullNode->setBounds(-500.0, 100.0, -200.0, 300.0, 1000.0, 500.0); - - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], -500.0); - EXPECT_DOUBLE_EQ(bbox[1], 100.0); - EXPECT_DOUBLE_EQ(bbox[2], -200.0); - EXPECT_DOUBLE_EQ(bbox[3], 300.0); - EXPECT_DOUBLE_EQ(bbox[4], 1000.0); - EXPECT_DOUBLE_EQ(bbox[5], 500.0); -} - -// Test includeOwnBounds flag - default should be false -TEST_F(NullGraphicNodeTest, IncludeOwnBoundsDefault) { - auto nullNode = std::make_shared(ctx, "test_null"); - - // Default should be false (typical for root nodes) - EXPECT_FALSE(nullNode->getIncludeOwnBounds()); -} - -// Test setting includeOwnBounds flag -TEST_F(NullGraphicNodeTest, SetIncludeOwnBounds) { - auto nullNode = std::make_shared(ctx, "test_null"); - - nullNode->setIncludeOwnBounds(true); - EXPECT_TRUE(nullNode->getIncludeOwnBounds()); - - nullNode->setIncludeOwnBounds(false); - EXPECT_FALSE(nullNode->getIncludeOwnBounds()); -} - -// Test includeOwnBounds state tree synchronization -TEST_F(NullGraphicNodeTest, IncludeOwnBoundsStateSync) { - auto nullNode = std::make_shared(ctx, "test.null"); - - // Set via method - nullNode->setIncludeOwnBounds(true); - - // Verify state tree was updated - int stateValue = nullNode->getState("include_own_bounds").value(); - EXPECT_EQ(stateValue, 1); - - // Change via state tree - nullNode->getState("include_own_bounds").value(0); - - // Should trigger handleStateChanged and update member - EXPECT_FALSE(nullNode->getIncludeOwnBounds()); -} - -// Test syncBoundsWithChildren flag - default should be true -TEST_F(NullGraphicNodeTest, SyncBoundsWithChildrenDefault) { - auto nullNode = std::make_shared(ctx, "test_null"); - - // Default should be true (auto-sync enabled) - EXPECT_TRUE(nullNode->getSyncBoundsWithChildren()); -} - -// Test setting syncBoundsWithChildren flag -TEST_F(NullGraphicNodeTest, SetSyncBoundsWithChildren) { - auto nullNode = std::make_shared(ctx, "test_null"); - - nullNode->setSyncBoundsWithChildren(false); - EXPECT_FALSE(nullNode->getSyncBoundsWithChildren()); - - nullNode->setSyncBoundsWithChildren(true); - EXPECT_TRUE(nullNode->getSyncBoundsWithChildren()); -} - -// Test syncBoundsWithChildren state tree synchronization -TEST_F(NullGraphicNodeTest, SyncBoundsWithChildrenStateSync) { - auto nullNode = std::make_shared(ctx, "test.null"); - - // Set via method - nullNode->setSyncBoundsWithChildren(false); - - // Verify state tree was updated - int stateValue = nullNode->getState("sync_bounds_with_children").value(); - EXPECT_EQ(stateValue, 0); - - // Change via state tree - nullNode->getState("sync_bounds_with_children").value(1); - - // Should trigger handleStateChanged and update member - EXPECT_TRUE(nullNode->getSyncBoundsWithChildren()); -} - -// Test syncBoundsToChildren with single child -TEST_F(NullGraphicNodeTest, SyncBoundsToChildrenSingleChild) { - SceneGraph sceneGraph(m_statePrefix); - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - - // Set initial bounds - nullNode->setBounds(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0); - - // Add geometry with known bounds - cvc::geometry geom; - geom.points().resize(8); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 10; - geom.points()[1][1] = 0; - geom.points()[1][2] = 0; - geom.points()[2][0] = 10; - geom.points()[2][1] = 10; - geom.points()[2][2] = 0; - geom.points()[3][0] = 0; - geom.points()[3][1] = 10; - geom.points()[3][2] = 0; - geom.points()[4][0] = 0; - geom.points()[4][1] = 0; - geom.points()[4][2] = 10; - geom.points()[5][0] = 10; - geom.points()[5][1] = 0; - geom.points()[5][2] = 10; - geom.points()[6][0] = 10; - geom.points()[6][1] = 10; - geom.points()[6][2] = 10; - geom.points()[7][0] = 0; - geom.points()[7][1] = 10; - geom.points()[7][2] = 10; - - sceneGraph.addGraphics("test_box", geom); - - // Enable sync (which triggers immediate sync) - nullNode->setSyncBoundsWithChildren(true); - nullNode->syncBoundsToChildren(); - - // Bounds should now encompass the geometry (grid and axis are excluded) - auto bbox = nullNode->getBoundingBox(); - EXPECT_NEAR(bbox[0], 0.0, 0.01); - EXPECT_NEAR(bbox[1], 0.0, 0.01); - EXPECT_NEAR(bbox[2], 0.0, 0.01); - EXPECT_NEAR(bbox[3], 10.0, 0.01); - EXPECT_NEAR(bbox[4], 10.0, 0.01); - EXPECT_NEAR(bbox[5], 10.0, 0.01); -} - -// Test syncBoundsToChildren with multiple children -TEST_F(NullGraphicNodeTest, SyncBoundsToChildrenMultipleChildren) { - SceneGraph sceneGraph(m_statePrefix); - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - - // Add first geometry - cvc::geometry geom1; - geom1.points().resize(2); - geom1.points()[0][0] = -5; - geom1.points()[0][1] = -5; - geom1.points()[0][2] = -5; - geom1.points()[1][0] = 0; - geom1.points()[1][1] = 0; - geom1.points()[1][2] = 0; - sceneGraph.addGraphics("geom1", geom1); - - // Add second geometry - cvc::geometry geom2; - geom2.points().resize(2); - geom2.points()[0][0] = 0; - geom2.points()[0][1] = 0; - geom2.points()[0][2] = 0; - geom2.points()[1][0] = 15; - geom2.points()[1][1] = 20; - geom2.points()[1][2] = 25; - sceneGraph.addGraphics("geom2", geom2); - - // Sync to children - nullNode->setSyncBoundsWithChildren(true); - nullNode->syncBoundsToChildren(); - - // Bounds should encompass both geometries - auto bbox = nullNode->getBoundingBox(); - EXPECT_NEAR(bbox[0], -5.0, 0.01); - EXPECT_NEAR(bbox[1], -5.0, 0.01); - EXPECT_NEAR(bbox[2], -5.0, 0.01); - EXPECT_NEAR(bbox[3], 15.0, 0.01); - EXPECT_NEAR(bbox[4], 20.0, 0.01); - EXPECT_NEAR(bbox[5], 25.0, 0.01); -} - -// Test that syncBoundsToChildren does nothing when sync is disabled -TEST_F(NullGraphicNodeTest, SyncBoundsToChildrenDisabled) { - SceneGraph sceneGraph(m_statePrefix); - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - - // Disable sync BEFORE setting custom bounds and adding geometry - nullNode->setSyncBoundsWithChildren(false); - - // Set custom bounds - nullNode->setBounds(-100.0, -100.0, -100.0, 100.0, 100.0, 100.0); - - // Add geometry - cvc::geometry geom; - geom.points().resize(2); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 10; - geom.points()[1][1] = 10; - geom.points()[1][2] = 10; - sceneGraph.addGraphics("test_geom", geom); - - // Call syncBoundsToChildren - should have no effect - nullNode->syncBoundsToChildren(); - - // Bounds should remain unchanged - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], -100.0); - EXPECT_DOUBLE_EQ(bbox[1], -100.0); - EXPECT_DOUBLE_EQ(bbox[2], -100.0); - EXPECT_DOUBLE_EQ(bbox[3], 100.0); - EXPECT_DOUBLE_EQ(bbox[4], 100.0); - EXPECT_DOUBLE_EQ(bbox[5], 100.0); -} - -// Test bounds state tree synchronization -TEST_F(NullGraphicNodeTest, BoundsStateTreeSync) { - auto nullNode = std::make_shared(ctx, "test.null"); - - // Set bounds via method - nullNode->setBounds(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); - - // Check state tree was updated - std::string boundsStr = nullNode->getState("bounds").value(); - EXPECT_EQ(boundsStr, "1,2,3,4,5,6"); - - // Update via state tree - nullNode->getState("bounds").value(std::string("-10,-20,-30,40,50,60")); - - // Should trigger handleStateChanged and update bounds - auto bbox = nullNode->getBoundingBox(); - EXPECT_DOUBLE_EQ(bbox[0], -10.0); - EXPECT_DOUBLE_EQ(bbox[1], -20.0); - EXPECT_DOUBLE_EQ(bbox[2], -30.0); - EXPECT_DOUBLE_EQ(bbox[3], 40.0); - EXPECT_DOUBLE_EQ(bbox[4], 50.0); - EXPECT_DOUBLE_EQ(bbox[5], 60.0); -} - -// Test NullGraphicNode transform propagates to children -TEST_F(NullGraphicNodeTest, TransformPropagationToChildren) { - SceneGraph sceneGraph("test.scenegraph"); - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - - // Set transform on null node (translate by 10 in X) - nullNode->setPosition(10.0, 0.0, 0.0); - - // Add geometry as child (at origin, size 2x2x2) - cvc::geometry geom; - geom.points().resize(2); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 2; - geom.points()[1][1] = 2; - geom.points()[1][2] = 2; - sceneGraph.addGraphics("geom1", geom); - - // Get the geometry child (SceneGraph may have axis/grid children too) - auto child = sceneGraph.getGraphics("geom1"); - ASSERT_NE(child, nullptr); - - auto worldTransform = child->getWorldTransform(); - - // Child's world position should be (10, 0, 0) from parent - EXPECT_DOUBLE_EQ(worldTransform->GetElement(0, 3), 10.0); - EXPECT_DOUBLE_EQ(worldTransform->GetElement(1, 3), 0.0); - EXPECT_DOUBLE_EQ(worldTransform->GetElement(2, 3), 0.0); -} - -// Test NullGraphicNode transform affects combined bounding box calculation -TEST_F(NullGraphicNodeTest, TransformAffectsCombinedBounds) { - SceneGraph sceneGraph("test.scenegraph2"); - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - - // Disable includeOwnBounds so root's default bounds don't affect combined bbox - nullNode->setIncludeOwnBounds(false); - - // Add geometry at origin (size 1x1x1 from 0 to 1) - cvc::geometry geom; - geom.points().resize(2); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 1; - geom.points()[1][1] = 1; - geom.points()[1][2] = 1; - sceneGraph.addGraphics("geom1", geom); - - // Get combined bbox (should be 0-1 in local space) - auto localBBox = nullNode->getCombinedBoundingBox(); - EXPECT_NEAR(localBBox[0], 0.0, 0.01); - EXPECT_NEAR(localBBox[3], 1.0, 0.01); - - // Now translate the null node by (5, 0, 0) - nullNode->setPosition(5.0, 0.0, 0.0); - - // Get combined bbox again - should still be 0-1 in null node's local space - // because getCombinedBoundingBox returns bounds in this node's local space - auto localBBox2 = nullNode->getCombinedBoundingBox(); - EXPECT_NEAR(localBBox2[0], 0.0, 0.01); - EXPECT_NEAR(localBBox2[3], 1.0, 0.01); - - // But the world-space bounds should be 5-6 - // To get world bounds, we need to transform the local bbox by the node's world transform - auto worldTransform = nullNode->getWorldTransform(); - double corner_in[4] = {localBBox2[3], localBBox2[4], localBBox2[5], 1.0}; // max corner - double corner_out[4]; - worldTransform->MultiplyPoint(corner_in, corner_out); - - EXPECT_NEAR(corner_out[0], 6.0, 0.01); // 1.0 + 5.0 translation - EXPECT_NEAR(corner_out[1], 1.0, 0.01); - EXPECT_NEAR(corner_out[2], 1.0, 0.01); -} - -// Test nested transforms with NullGraphicNode -TEST_F(NullGraphicNodeTest, NestedTransforms) { - SceneGraph sceneGraph("test.scenegraph3"); - auto rootNull = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(rootNull, nullptr); - - // Create child null node - auto childNull = std::make_shared(ctx, "test.child_null", "child"); - rootNull->addGraphicsChild(childNull); - - // Set transforms - rootNull->setPosition(10.0, 0.0, 0.0); - childNull->setPosition(5.0, 0.0, 0.0); - - // Add geometry to child null - cvc::geometry geom; - geom.points().resize(2); - geom.points()[0][0] = 0; - geom.points()[0][1] = 0; - geom.points()[0][2] = 0; - geom.points()[1][0] = 1; - geom.points()[1][1] = 1; - geom.points()[1][2] = 1; - - auto geomNode = childNull->addGraphicsChild("geom"); - geomNode->setGeometry(geom); - geomNode->setPosition(2.0, 0.0, 0.0); - - // Geometry node's world transform should accumulate all parent transforms - // Root: +10, Child: +5, Geom: +2 = +17 total - auto worldTransform = geomNode->getWorldTransform(); - EXPECT_NEAR(worldTransform->GetElement(0, 3), 17.0, 0.01); - EXPECT_NEAR(worldTransform->GetElement(1, 3), 0.0, 0.01); - EXPECT_NEAR(worldTransform->GetElement(2, 3), 0.0, 0.01); -} - -// Test syncBoundsToChildren respects child transforms -TEST_F(NullGraphicNodeTest, SyncBoundsRespectsChildTransforms) { - SceneGraph sceneGraph("test.scenegraph4"); - auto nullNode = std::dynamic_pointer_cast(sceneGraph.getGraphicsRoot()); - ASSERT_NE(nullNode, nullptr); - - // Add two geometries at different positions - cvc::geometry geom1; - geom1.points().resize(2); - geom1.points()[0][0] = 0; - geom1.points()[0][1] = 0; - geom1.points()[0][2] = 0; - geom1.points()[1][0] = 1; - geom1.points()[1][1] = 1; - geom1.points()[1][2] = 1; - - auto geomNode1 = sceneGraph.addGraphics("geom1", geom1); - geomNode1->setPosition(0.0, 0.0, 0.0); - - cvc::geometry geom2; - geom2.points().resize(2); - geom2.points()[0][0] = 0; - geom2.points()[0][1] = 0; - geom2.points()[0][2] = 0; - geom2.points()[1][0] = 1; - geom2.points()[1][1] = 1; - geom2.points()[1][2] = 1; - - auto geomNode2 = sceneGraph.addGraphics("geom2", geom2); - geomNode2->setPosition(10.0, 0.0, 0.0); // Offset by 10 in X - - // Sync bounds - nullNode->setSyncBoundsWithChildren(true); - nullNode->syncBoundsToChildren(); - - // Bounds should encompass both geometries with their transforms - // geom1: 0-1, geom2: 10-11, so combined should be 0-11 - auto bbox = nullNode->getBoundingBox(); - EXPECT_NEAR(bbox[0], 0.0, 0.01); - EXPECT_NEAR(bbox[3], 11.0, 0.01); -} diff --git a/src/volrover3/tests/SceneGraphTest.cpp b/src/volrover3/tests/SceneGraphTest.cpp deleted file mode 100644 index f3c5ec93..00000000 --- a/src/volrover3/tests/SceneGraphTest.cpp +++ /dev/null @@ -1,843 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class SceneGraphTest : public ::testing::Test { -protected: - static void SetUpTestSuite() { - // Disable threading for state_object to avoid race conditions during destruction - cvc::state_object::setUseThreading(false); - } - - void SetUp() override { - sceneGraph = new SceneGraph(); - appState = &AppState::instance(); - } - - void TearDown() override { delete sceneGraph; } - - cvc::app ctx; - SceneGraph *sceneGraph; - AppState *appState; -}; - -TEST_F(SceneGraphTest, InitialState) { - EXPECT_NE(sceneGraph, nullptr); - // SceneGraph doesn't expose renderer/renderWindow, just verify it was created - SUCCEED(); -} - -TEST_F(SceneGraphTest, AddGeometryNode) { - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto node = sceneGraph->addGraphics("test_geom", geom); - - ASSERT_NE(node, nullptr); - EXPECT_EQ(sceneGraph->getGraphics("test_geom"), node); -} - -TEST_F(SceneGraphTest, AddVolumeNode) { - cvc::volume vol(ctx, cvc::dimension(4, 4, 4), cvc::UChar); - - auto node = sceneGraph->addGraphics("test_vol", vol); - - ASSERT_NE(node, nullptr); - EXPECT_EQ(sceneGraph->getGraphics("test_vol"), node); -} - -TEST_F(SceneGraphTest, ShowHideGrid) { - sceneGraph->setGridVisible(true); - // Grid should be created and visible - - sceneGraph->setGridVisible(false); - // Grid should be hidden - - SUCCEED(); -} - -TEST_F(SceneGraphTest, ShowHideAxes) { - sceneGraph->setAxisVisible(true); - // Axes should be created and visible - - sceneGraph->setAxisVisible(false); - // Axes should be hidden - - SUCCEED(); -} - -TEST_F(SceneGraphTest, UpdateBoundingBox) { - cvc::bounding_box bounds; - bounds.setMin(-1.0, -1.0, -1.0); - bounds.setMax(1.0, 1.0, 1.0); - sceneGraph->updateGrid(bounds); - - // Grid should be updated to match bounding box - SUCCEED(); -} - -TEST_F(SceneGraphTest, ResetCamera) { - // SceneGraph doesn't expose resetCamera, this would be done via the renderer - SUCCEED(); -} - -TEST_F(SceneGraphTest, TransferFunctionUpdate) { - // Create a volume first - cvc::volume vol(ctx, cvc::dimension(4, 4, 4), cvc::UChar); - auto volNode = sceneGraph->addGraphics("test_vol", vol); - - // Update transfer function - std::vector colorTable = {0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0}; - std::vector opacityTable = {0.0, 0.0, 1.0, 1.0}; - - sceneGraph->updateTransferFunction(colorTable, opacityTable); - - // Transfer function should be applied to volume - SUCCEED(); -} - -TEST_F(SceneGraphTest, MultipleUpdates) { - // Test multiple updates don't cause issues - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - sceneGraph->addGraphics("test_geom", geom); - - cvc::volume vol(ctx, cvc::dimension(2, 2, 2), cvc::UChar); - sceneGraph->addGraphics("test_vol", vol); - - sceneGraph->setGridVisible(true); - sceneGraph->setAxisVisible(true); - - // Camera reset would be done externally - - SUCCEED(); -} - -// =========================== -// State Tree Integration Tests -// =========================== - -TEST_F(SceneGraphTest, VisibilityStateTree) { - // SceneGraph doesn't directly manipulate state tree - // but we can verify it responds to AppState visibility flags - - // Set visibility via scene graph - sceneGraph->setGridVisible(true); - sceneGraph->setAxisVisible(false); - - // These don't save to AppState automatically - // In actual usage, MainWindow coordinates between SceneGraph and AppState - SUCCEED(); -} - -// NOTE: Transfer function storage moved to per-volume state in VolumeNode -// SceneGraph no longer has global transfer function updates -/* -TEST_F(SceneGraphTest, TransferFunctionFromState) { - // Create volume - cvc::volume vol(ctx, cvc::dimension(4, 4, 4), cvc::UChar); - sceneGraph->addGraphics("test_vol", vol); - - // Get transfer function from AppState - auto colorTable = appState->transferFunctionColorTable(); - auto opacityTable = appState->transferFunctionOpacityTable(); - - // Apply to scene graph - sceneGraph->updateTransferFunction(colorTable, opacityTable); - - SUCCEED(); -} -*/ - -TEST_F(SceneGraphTest, WorldBoundsUpdate) { - // Set world bounds in AppState - cvc::bounding_box bounds(-2.0, -2.0, -2.0, 2.0, 2.0, 2.0); - appState->setWorldBounds(bounds); - - // Update scene graph grid - sceneGraph->updateGrid(bounds); - - // Verify state tree has the bounds - auto &stateTree = cvc::state::instance(volrover3::app())("volrover3"); - auto values = stateTree("world_bounds").values(); - ASSERT_EQ(values.size(), size_t(6)); - - SUCCEED(); -} - -// =========================== -// Color Tests -// =========================== - -TEST_F(SceneGraphTest, GridColorUpdate) { - // Set grid color - sceneGraph->setGridColor(0.9, 0.1, 0.5); - - // Verify color was applied (we can't directly inspect VTK properties - // in these tests without a renderer, but we verify it doesn't crash) - SUCCEED(); -} - -// =========================== -// Axis Scaling Tests -// =========================== - -TEST_F(SceneGraphTest, AxisScalingWithBounds) { - // Create bounding boxes of different sizes and verify axis scales - - // Small bounds - cvc::bounding_box smallBounds(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0); - sceneGraph->updateGrid(smallBounds); - // Axis should be scaled to ~20% of max span (2.0), so ~0.4 - - // Large bounds - cvc::bounding_box largeBounds(-50.0, -50.0, -50.0, 50.0, 50.0, 50.0); - sceneGraph->updateGrid(largeBounds); - // Axis should be scaled to ~20% of max span (100.0), so ~20.0 - - // Asymmetric bounds - cvc::bounding_box asymBounds(-5.0, -2.0, -1.0, 5.0, 2.0, 1.0); - sceneGraph->updateGrid(asymBounds); - // Axis should be scaled to ~20% of max span (10.0), so ~2.0 - - SUCCEED(); -} - -TEST_F(SceneGraphTest, AxisScalingEdgeCases) { - // Test with very small bounds - cvc::bounding_box tinyBounds(-0.01, -0.01, -0.01, 0.01, 0.01, 0.01); - sceneGraph->updateGrid(tinyBounds); - - // Test with very large bounds - cvc::bounding_box hugeBounds(-1000.0, -1000.0, -1000.0, 1000.0, 1000.0, 1000.0); - sceneGraph->updateGrid(hugeBounds); - - // Test with zero-volume bounds - cvc::bounding_box zeroBounds(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); - sceneGraph->updateGrid(zeroBounds); - - SUCCEED(); -} - -TEST_F(SceneGraphTest, AxisScalingWithGeometry) { - // Create simple geometry - cvc::geometry geom; - geom.points().push_back({-2.0, -2.0, -2.0}); - geom.points().push_back({2.0, 2.0, 2.0}); - - // Set geometry and get its bounds - sceneGraph->addGraphics("test_geom", geom); - cvc::bounding_box geomBounds = geom.extents(); - - // Update grid/axis with geometry bounds - sceneGraph->updateGrid(geomBounds); - - SUCCEED(); -} - -TEST_F(SceneGraphTest, GridAndAxisUpdateTogether) { - // Verify that updating grid also updates axis scaling - cvc::bounding_box bounds1(-10.0, -10.0, -10.0, 10.0, 10.0, 10.0); - sceneGraph->updateGrid(bounds1); - - cvc::bounding_box bounds2(-5.0, -5.0, -5.0, 5.0, 5.0, 5.0); - sceneGraph->updateGrid(bounds2); - - cvc::bounding_box bounds3(-100.0, -100.0, -100.0, 100.0, 100.0, 100.0); - sceneGraph->updateGrid(bounds3); - - SUCCEED(); -} - -// =========================== -// Integration Tests -// =========================== - -TEST_F(SceneGraphTest, ColorAndBoundsIntegration) { - // Test setting grid color through GridNode and bounds together - auto gridNode = sceneGraph->getGridNode(); - gridNode->getState("color").value("0.5,0.5,0.5"); - - cvc::bounding_box bounds(-25.0, -25.0, -25.0, 25.0, 25.0, 25.0); - sceneGraph->updateGrid(bounds); - - SUCCEED(); -} - -// =========================== -// Event Queue Threading Tests -// =========================== - -TEST_F(SceneGraphTest, EventQueueWithThreading) { - // Enable threading for this test - cvc::state_object::setUseThreading(true); - - // Create a new scene graph that will use threading - SceneGraph *threadedGraph = new SceneGraph(); - - // Create geometry node - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto node = threadedGraph->addGraphics("test_geom", geom); - ASSERT_NE(node, nullptr); - - // Get raw pointer for lambda capture - GeometryNode *geomNode = dynamic_cast(node.get()); - ASSERT_NE(geomNode, nullptr); - - // Track if operations were executed - std::atomic operationsQueued{0}; - - // Modify state from a worker thread - std::thread worker([geomNode, &operationsQueued]() { - // These should be queued, not executed immediately - geomNode->setVisible(false); - operationsQueued++; - - geomNode->setColor(1.0, 0.0, 0.0); - operationsQueued++; - - geomNode->setOpacity(0.5); - operationsQueued++; - }); - - worker.join(); - - // Verify operations were queued - EXPECT_EQ(operationsQueued.load(), 3); - - // Process the event queue (simulating main thread) - threadedGraph->processEvents(); - - // Node should still be valid after processing - ASSERT_NE(geomNode, nullptr); - - // Cleanup - delete threadedGraph; - - // Disable threading again - cvc::state_object::setUseThreading(false); -} - -TEST_F(SceneGraphTest, EventQueueMultipleThreads) { - // Enable threading - cvc::state_object::setUseThreading(true); - - SceneGraph *threadedGraph = new SceneGraph(); - - // Create multiple geometry nodes - std::vector nodes; - for (int i = 0; i < 5; i++) { - cvc::geometry geom; - geom.points().push_back({static_cast(i), 0.0, 0.0}); - geom.points().push_back({static_cast(i + 1), 0.0, 0.0}); - geom.points().push_back({static_cast(i), 1.0, 0.0}); - - auto node = threadedGraph->addGraphics("geom_" + std::to_string(i), geom); - nodes.push_back(dynamic_cast(node.get())); - } - - std::atomic totalOps{0}; - - // Launch multiple worker threads - std::vector workers; - for (int i = 0; i < 5; i++) { - workers.emplace_back([&nodes, i, &totalOps]() { - if (nodes[i]) { - nodes[i]->setVisible(i % 2 == 0); - totalOps++; - - nodes[i]->setColor(i * 0.2, 0.0, 1.0 - i * 0.2); - totalOps++; - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - nodes[i]->setOpacity(0.1 + i * 0.15); - totalOps++; - } - }); - } - - // Wait for all workers - for (auto &w : workers) { - w.join(); - } - - EXPECT_EQ(totalOps.load(), 15); // 5 nodes * 3 operations each - - // Process all queued events - threadedGraph->processEvents(); - - // Verify all nodes are still valid - for (int i = 0; i < 5; i++) { - ASSERT_NE(nodes[i], nullptr); - } - - delete threadedGraph; - cvc::state_object::setUseThreading(false); -} - -TEST_F(SceneGraphTest, EventQueueProcessingOrder) { - // Enable threading - cvc::state_object::setUseThreading(true); - - SceneGraph *threadedGraph = new SceneGraph(); - - // Track execution order - std::vector executionOrder; - std::mutex orderMutex; - - // Post several events from worker thread - std::thread worker([threadedGraph, &executionOrder, &orderMutex]() { - for (int i = 0; i < 10; i++) { - threadedGraph->postEvent([i, &executionOrder, &orderMutex]() { - std::lock_guard lock(orderMutex); - executionOrder.push_back(i); - }); - } - }); - - worker.join(); - - // Process events - they should execute in FIFO order - threadedGraph->processEvents(); - - ASSERT_EQ(executionOrder.size(), size_t(10)); - for (int i = 0; i < 10; i++) { - EXPECT_EQ(executionOrder[i], i) << "Event " << i << " executed out of order"; - } - - delete threadedGraph; - cvc::state_object::setUseThreading(false); -} - -TEST_F(SceneGraphTest, EventQueueWithVolumeNode) { - // Enable threading - cvc::state_object::setUseThreading(true); - - SceneGraph *threadedGraph = new SceneGraph(); - - // Create volume node - cvc::volume vol(ctx, cvc::dimension(4, 4, 4), cvc::UChar); - auto node = threadedGraph->addGraphics("test_vol", vol); - - // Get raw pointer for lambda capture - VolumeNode *volNode = dynamic_cast(node.get()); - ASSERT_NE(volNode, nullptr); - - std::atomic opsExecuted{0}; - - // Modify volume properties from worker thread - std::thread worker([volNode, &opsExecuted]() { - volNode->setVisible(false); - opsExecuted++; - - volNode->setShading(true); - opsExecuted++; - - volNode->setAmbient(0.5); - opsExecuted++; - }); - - worker.join(); - - EXPECT_EQ(opsExecuted.load(), 3); - - // Process queue - threadedGraph->processEvents(); - - // Verify node still valid - ASSERT_NE(volNode, nullptr); - - delete threadedGraph; - cvc::state_object::setUseThreading(false); -} - -TEST_F(SceneGraphTest, EventQueueStressTest) { - // Enable threading - cvc::state_object::setUseThreading(true); - - SceneGraph *threadedGraph = new SceneGraph(); - - // Create a geometry node - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto node = threadedGraph->addGraphics("stress_test", geom); - GeometryNode *geomNode = dynamic_cast(node.get()); - ASSERT_NE(geomNode, nullptr); - - std::atomic totalOps{0}; - - // Launch many threads doing many operations - std::vector workers; - for (int t = 0; t < 10; t++) { - workers.emplace_back([geomNode, &totalOps, t]() { - for (int i = 0; i < 20; i++) { - geomNode->setVisible(i % 2 == 0); - totalOps++; - - geomNode->setColor((t * 0.1), (i * 0.05), 0.5); - totalOps++; - - geomNode->setOpacity(0.1 + (i % 10) * 0.09); - totalOps++; - } - }); - } - - for (auto &w : workers) { - w.join(); - } - - EXPECT_EQ(totalOps.load(), 600); // 10 threads * 20 iterations * 3 ops - - // Process all events - threadedGraph->processEvents(); - - // Just verify it didn't crash and node is still valid - ASSERT_NE(geomNode, nullptr); - - delete threadedGraph; - cvc::state_object::setUseThreading(false); -} - -// =========================== -// Per-Instance Threading Tests -// =========================== - -TEST_F(SceneGraphTest, PerInstanceThreadingEnabled) { - // Keep global threading disabled - ASSERT_FALSE(cvc::state_object::getUseThreading()); - - SceneGraph *graph = new SceneGraph(); - - // Create a geometry node - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto node = graph->addGraphics("test_geom", geom); - GeometryNode *geomNode = dynamic_cast(node.get()); - ASSERT_NE(geomNode, nullptr); - - // After setSceneGraph, instance threading follows global flag (which is false) - EXPECT_FALSE(geomNode->getInstanceThreading()); - - // Manually enable instance threading on this specific node - geomNode->setInstanceThreading(true); - EXPECT_TRUE(geomNode->getInstanceThreading()); - - // Track if operation was queued - std::atomic operationQueued{false}; - - // Modify from worker thread - should be queued due to per-instance threading - std::thread worker([geomNode, &operationQueued]() { - geomNode->setColor(1.0, 0.5, 0.0); - operationQueued = true; - }); - - worker.join(); - EXPECT_TRUE(operationQueued.load()); - - // Process events - graph->processEvents(); - - delete graph; -} - -TEST_F(SceneGraphTest, PerInstanceThreadingDisabled) { - // Keep global threading disabled - ASSERT_FALSE(cvc::state_object::getUseThreading()); - - SceneGraph *graph = new SceneGraph(); - - // Create a geometry node - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto node = graph->addGraphics("test_geom", geom); - GeometryNode *geomNode = dynamic_cast(node.get()); - ASSERT_NE(geomNode, nullptr); - - // Manually disable instance threading - geomNode->setInstanceThreading(false); - EXPECT_FALSE(geomNode->getInstanceThreading()); - - // Modify from worker thread - should execute immediately (no queueing) - std::atomic operationCompleted{false}; - - std::thread worker([geomNode, &operationCompleted]() { - geomNode->setColor(0.2, 0.8, 0.6); - operationCompleted = true; - }); - - worker.join(); - EXPECT_TRUE(operationCompleted.load()); - - delete graph; -} - -TEST_F(SceneGraphTest, MixedPerInstanceThreading) { - // Keep global threading disabled - ASSERT_FALSE(cvc::state_object::getUseThreading()); - - SceneGraph *graph = new SceneGraph(); - - // Create multiple geometry nodes - std::vector nodes; - for (int i = 0; i < 3; i++) { - cvc::geometry geom; - geom.points().push_back({static_cast(i), 0.0, 0.0}); - geom.points().push_back({static_cast(i + 1), 0.0, 0.0}); - geom.points().push_back({static_cast(i), 1.0, 0.0}); - - auto node = graph->addGraphics("geom_" + std::to_string(i), geom); - nodes.push_back(dynamic_cast(node.get())); - } - - // All nodes start with threading disabled (global flag is false) - EXPECT_FALSE(nodes[0]->getInstanceThreading()); - EXPECT_FALSE(nodes[1]->getInstanceThreading()); - EXPECT_FALSE(nodes[2]->getInstanceThreading()); - - // Enable threading on nodes 0 and 2 - nodes[0]->setInstanceThreading(true); - nodes[2]->setInstanceThreading(true); - - // Verify settings - EXPECT_TRUE(nodes[0]->getInstanceThreading()); - EXPECT_FALSE(nodes[1]->getInstanceThreading()); - EXPECT_TRUE(nodes[2]->getInstanceThreading()); - - // Modify all from worker threads - std::atomic opsCompleted{0}; - std::vector workers; - - for (int i = 0; i < 3; i++) { - workers.emplace_back([&nodes, i, &opsCompleted]() { - nodes[i]->setColor(i * 0.3, 0.5, 1.0 - i * 0.3); - opsCompleted++; - }); - } - - for (auto &w : workers) { - w.join(); - } - - EXPECT_EQ(opsCompleted.load(), 3); - - // Process events (will process nodes 0 and 2, node 1 already executed) - graph->processEvents(); - - delete graph; -} - -TEST_F(SceneGraphTest, PerInstanceThreadingBeforeSceneGraph) { - // Keep global threading disabled - ASSERT_FALSE(cvc::state_object::getUseThreading()); - - SceneGraph *graph = new SceneGraph(); - - // Create a geometry node - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto node = graph->addGraphics("test_geom", geom); - GeometryNode *geomNode = dynamic_cast(node.get()); - ASSERT_NE(geomNode, nullptr); - - // After SceneGraph is set, threading follows global flag (false) - EXPECT_FALSE(geomNode->getInstanceThreading()); - - // Enable instance threading - geomNode->setInstanceThreading(true); - EXPECT_TRUE(geomNode->getInstanceThreading()); - - // Clear instance threading (revert to using global flag) - geomNode->clearInstanceThreading(); - - // Should now use global flag (which is false) - EXPECT_FALSE(geomNode->getInstanceThreading()); - - delete graph; -} - -TEST_F(SceneGraphTest, PerInstanceThreadingWithGlobalEnabled) { - // Enable global threading - cvc::state_object::setUseThreading(true); - - SceneGraph *graph = new SceneGraph(); - - // Create multiple nodes - std::vector nodes; - for (int i = 0; i < 2; i++) { - cvc::geometry geom; - geom.points().push_back({static_cast(i), 0.0, 0.0}); - geom.points().push_back({static_cast(i + 1), 0.0, 0.0}); - geom.points().push_back({static_cast(i), 1.0, 0.0}); - - auto node = graph->addGraphics("geom_" + std::to_string(i), geom); - nodes.push_back(dynamic_cast(node.get())); - } - - // Both should have instance threading enabled (from SceneGraph) - EXPECT_TRUE(nodes[0]->getInstanceThreading()); - EXPECT_TRUE(nodes[1]->getInstanceThreading()); - - // Disable instance threading on node 0 - nodes[0]->setInstanceThreading(false); - EXPECT_FALSE(nodes[0]->getInstanceThreading()); - - // Node 1 should still have it enabled - EXPECT_TRUE(nodes[1]->getInstanceThreading()); - - std::atomic opsCompleted{0}; - - // Node 0: Should execute immediately (instance threading disabled) - std::thread worker0([&nodes, &opsCompleted]() { - nodes[0]->setColor(1.0, 0.0, 0.0); - opsCompleted++; - }); - - // Node 1: Should queue (instance threading enabled) - std::thread worker1([&nodes, &opsCompleted]() { - nodes[1]->setColor(0.0, 1.0, 0.0); - opsCompleted++; - }); - - worker0.join(); - worker1.join(); - - EXPECT_EQ(opsCompleted.load(), 2); - - // Process queued events (node 1) - graph->processEvents(); - - delete graph; - cvc::state_object::setUseThreading(false); -} - -TEST_F(SceneGraphTest, PerInstanceThreadingPropagation) { - // Keep global threading disabled - ASSERT_FALSE(cvc::state_object::getUseThreading()); - - SceneGraph *graph = new SceneGraph(); - - // Create a parent node - auto parent = graph->addGraphics("parent"); - - // Create a child geometry node - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - - auto child = graph->addGraphics("child", geom); - GeometryNode *childGeom = dynamic_cast(child.get()); - ASSERT_NE(childGeom, nullptr); - - // Both should have instance threading disabled (global flag is false) - EXPECT_FALSE(parent->getInstanceThreading()); - EXPECT_FALSE(childGeom->getInstanceThreading()); - - // Enable threading on both - parent->setInstanceThreading(true); - childGeom->setInstanceThreading(true); - EXPECT_TRUE(parent->getInstanceThreading()); - EXPECT_TRUE(childGeom->getInstanceThreading()); - - // Manually disable on parent - parent->setInstanceThreading(false); - EXPECT_FALSE(parent->getInstanceThreading()); - - // Child should maintain its own setting - EXPECT_TRUE(childGeom->getInstanceThreading()); - - delete graph; -} - -TEST_F(SceneGraphTest, PerInstanceThreadingStressTest) { - // Keep global threading disabled - ASSERT_FALSE(cvc::state_object::getUseThreading()); - - SceneGraph *graph = new SceneGraph(); - - // Create nodes with mixed threading settings - std::vector threadedNodes; - std::vector nonThreadedNodes; - - for (int i = 0; i < 5; i++) { - cvc::geometry geom; - geom.points().push_back({static_cast(i), 0.0, 0.0}); - geom.points().push_back({static_cast(i + 1), 0.0, 0.0}); - geom.points().push_back({static_cast(i), 1.0, 0.0}); - - auto node = graph->addGraphics("threaded_" + std::to_string(i), geom); - threadedNodes.push_back(dynamic_cast(node.get())); - - auto node2 = graph->addGraphics("nonthreaded_" + std::to_string(i), geom); - GeometryNode *geomNode = dynamic_cast(node2.get()); - geomNode->setInstanceThreading(false); - nonThreadedNodes.push_back(geomNode); - } - - std::atomic totalOps{0}; - std::vector workers; - - // Launch threads for threaded nodes (should queue) - for (int i = 0; i < 5; i++) { - workers.emplace_back([&threadedNodes, i, &totalOps]() { - for (int j = 0; j < 10; j++) { - threadedNodes[i]->setColor(i * 0.2, j * 0.1, 0.5); - totalOps++; - } - }); - } - - // Launch threads for non-threaded nodes (should execute immediately) - for (int i = 0; i < 5; i++) { - workers.emplace_back([&nonThreadedNodes, i, &totalOps]() { - for (int j = 0; j < 10; j++) { - nonThreadedNodes[i]->setOpacity(0.1 + j * 0.09); - totalOps++; - } - }); - } - - for (auto &w : workers) { - w.join(); - } - - EXPECT_EQ(totalOps.load(), 100); // 10 nodes * 10 operations each - - // Process events (should only process the threaded nodes) - graph->processEvents(); - - delete graph; -} diff --git a/src/volrover3/tests/StateDashboardWidgetTest.cpp b/src/volrover3/tests/StateDashboardWidgetTest.cpp deleted file mode 100644 index 684b4690..00000000 --- a/src/volrover3/tests/StateDashboardWidgetTest.cpp +++ /dev/null @@ -1,1324 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cvc; -using namespace cvc::state_exec; - -// ═══════════════════════════════════════════════════════════════════════════ -// State Tree Tab Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardTreeTest : public ::testing::Test { -protected: - void SetUp() override { - root = &state::instance(volrover3::app())("dashboard_test"); - root->operator()("alpha").value("aaa"); - root->operator()("beta").value("bbb"); - root->operator()("gamma").value("parent"); - root->operator()("gamma")("child1").value("c1"); - root->operator()("gamma")("child2").value("c2"); - - widget = new StateDashboardWidget(); - widget->setRootState(root); - } - - void TearDown() override { - delete widget; - root->reset(); - } - - state *root; - StateDashboardWidget *widget; -}; - -TEST_F(StateDashboardTreeTest, WidgetCreation) { - ASSERT_NE(widget, nullptr); - EXPECT_FALSE(widget->isVisible()); -} - -TEST_F(StateDashboardTreeTest, RefreshDoesNotCrash) { EXPECT_NO_THROW(widget->refresh()); } - -TEST_F(StateDashboardTreeTest, SetRootStatePopulatesTree) { - // Re-set to force population - widget->setRootState(root); - QCoreApplication::processEvents(); - - // Verify the state hierarchy is there - auto children = root->children(); - bool hasAlpha = false, hasBeta = false, hasGamma = false; - for (auto &c : children) { - if (c.find("alpha") != std::string::npos) - hasAlpha = true; - if (c.find("beta") != std::string::npos) - hasBeta = true; - if (c.find("gamma") != std::string::npos) - hasGamma = true; - } - EXPECT_TRUE(hasAlpha); - EXPECT_TRUE(hasBeta); - EXPECT_TRUE(hasGamma); -} - -TEST_F(StateDashboardTreeTest, StateValueReadBack) { - EXPECT_EQ(root->operator()("alpha").value(), "aaa"); - EXPECT_EQ(root->operator()("beta").value(), "bbb"); - EXPECT_EQ(root->operator()("gamma")("child1").value(), "c1"); -} - -TEST_F(StateDashboardTreeTest, SetRootNullSafe) { - EXPECT_NO_THROW(widget->setRootState(nullptr)); - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardTreeTest, DynamicStateAdd) { - widget->show(); - QCoreApplication::processEvents(); - - root->operator()("dynamic_node").value("dyn"); - QCoreApplication::processEvents(); - - EXPECT_TRUE(root->operator()("dynamic_node").initialized()); - EXPECT_EQ(root->operator()("dynamic_node").value(), "dyn"); -} - -TEST_F(StateDashboardTreeTest, StateReset) { - auto &child = root->operator()("alpha"); - EXPECT_TRUE(child.initialized()); - child.reset(); - EXPECT_FALSE(child.initialized()); -} - -TEST_F(StateDashboardTreeTest, NestedStateNavigation) { - auto &gamma = root->operator()("gamma"); - EXPECT_TRUE(gamma.initialized()); - EXPECT_EQ(gamma("child1").value(), "c1"); - EXPECT_EQ(gamma("child2").value(), "c2"); -} - -TEST_F(StateDashboardTreeTest, StateMetadata) { - auto &s = root->operator()("alpha"); - - s.comment("test comment"); - EXPECT_EQ(s.comment(), "test comment"); - - EXPECT_FALSE(s.readOnly()); - EXPECT_FALSE(s.hidden()); - - s.readOnly(true); - EXPECT_TRUE(s.readOnly()); - - s.hidden(true); - EXPECT_TRUE(s.hidden()); - - s.readOnly(false); - s.hidden(false); -} - -TEST_F(StateDashboardTreeTest, StateLastModified) { - auto &s = root->operator()("alpha"); - auto mod = s.lastMod(); - EXPECT_FALSE(mod.is_not_a_date_time()); -} - -TEST_F(StateDashboardTreeTest, StateLinkProperties) { - auto &link = root->operator()("link_node"); - EXPECT_FALSE(link.isLink()); - - link.linkTo("dashboard_test.alpha"); - EXPECT_TRUE(link.isLink()); - EXPECT_EQ(link.linkTarget(), "dashboard_test.alpha"); - - link.clearLink(); - EXPECT_FALSE(link.isLink()); -} - -TEST_F(StateDashboardTreeTest, StateExpiryProperties) { - auto &s = root->operator()("expire_test"); - s.value("will expire"); - - EXPECT_FALSE(s.hasExpiry()); - - auto future = boost::posix_time::microsec_clock::universal_time() + boost::posix_time::hours(1); - s.expireAt(future); - - EXPECT_TRUE(s.hasExpiry()); - EXPECT_FALSE(s.isExpired()); - - s.clearExpiry(); - EXPECT_FALSE(s.hasExpiry()); -} - -TEST_F(StateDashboardTreeTest, TypedValueRoundTrips) { - auto &s = root->operator()("typed"); - - s.value(42); - EXPECT_EQ(s.value(), "42"); - - s.value(3.14); - EXPECT_NE(s.value(), ""); - EXPECT_TRUE(s.value().find("3.14") == 0); - - s.value(true); - EXPECT_EQ(s.value(), "1"); - - s.value(std::string("hello")); - EXPECT_EQ(s.value(), "hello"); -} - -TEST_F(StateDashboardTreeTest, RefreshAfterMutation) { - root->operator()("new_child").value("nc"); - EXPECT_NO_THROW(widget->refresh()); - - root->operator()("new_child").reset(); - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardTreeTest, MultipleSetRootCalls) { - EXPECT_NO_THROW(widget->setRootState(root)); - EXPECT_NO_THROW(widget->setRootState(root)); - EXPECT_NO_THROW(widget->setRootState(nullptr)); - EXPECT_NO_THROW(widget->setRootState(root)); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Exec Console Tab Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardExecTest : public ::testing::Test { -protected: - void SetUp() override { - env = builtins::make_default_environment(); - sched = std::make_unique(); - widget = new StateDashboardWidget(); - widget->setScheduler(sched.get()); - } - - void TearDown() override { delete widget; } - - environment_ptr env; - std::unique_ptr sched; - StateDashboardWidget *widget; -}; - -TEST_F(StateDashboardExecTest, SetSchedulerNullSafe) { - EXPECT_NO_THROW(widget->setScheduler(nullptr)); - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardExecTest, ProcessListEmpty) { - auto procs = sched->list_processes(); - EXPECT_TRUE(procs.empty()); - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardExecTest, ProcessSubmitAndList) { - execute_options opts; - opts.name = "test_proc"; - int pid = sched->execute(std::string("(+ 1 2)"), opts); - EXPECT_GT(pid, 0); - - auto procs = sched->list_processes(); - EXPECT_EQ(procs.size(), 1u); - EXPECT_EQ(procs[0].pid, pid); - EXPECT_EQ(procs[0].name, "test_proc"); -} - -TEST_F(StateDashboardExecTest, ProcessPauseResumeKill) { - execute_options opts; - opts.name = "infinite"; - int pid = sched->execute(std::string("(begin (while t nil))"), opts); - - // Run one step to make it running - sched->step(); - - auto info = sched->get_process_info(pid); - ASSERT_TRUE(info.has_value()); - EXPECT_EQ(info->status, process_status::ready); - - // Pause - EXPECT_TRUE(sched->pause(pid)); - info = sched->get_process_info(pid); - EXPECT_EQ(info->status, process_status::paused); - - // Resume - EXPECT_TRUE(sched->resume(pid)); - info = sched->get_process_info(pid); - EXPECT_EQ(info->status, process_status::ready); - - // Kill - EXPECT_TRUE(sched->kill(pid)); - info = sched->get_process_info(pid); - EXPECT_EQ(info->status, process_status::killed); -} - -TEST_F(StateDashboardExecTest, ProcessRunToCompletion) { - execute_options opts; - opts.name = "simple_add"; - int pid = sched->execute(std::string("(+ 10 20)"), opts); - auto results = sched->run(); - - EXPECT_TRUE(results.count(pid)); - auto &result = results[pid]; - EXPECT_EQ(std::get(result.v), 30); -} - -TEST_F(StateDashboardExecTest, MultipleProcesses) { - execute_options opts_a; - opts_a.name = "proc_a"; - execute_options opts_b; - opts_b.name = "proc_b"; - execute_options opts_c; - opts_c.name = "proc_c"; - sched->execute(std::string("(+ 1 1)"), opts_a); - sched->execute(std::string("(+ 2 2)"), opts_b); - sched->execute(std::string("(+ 3 3)"), opts_c); - - auto procs = sched->list_processes(); - EXPECT_EQ(procs.size(), 3u); - - auto results = sched->run(); - EXPECT_EQ(results.size(), 3u); -} - -TEST_F(StateDashboardExecTest, SchedulerStats) { - sched->execute(std::string("(+ 1 1)")); - sched->execute(std::string("(+ 2 2)")); - - auto stats = sched->get_stats(); - EXPECT_EQ(stats.total_processes, 2u); - - sched->run(); - stats = sched->get_stats(); - EXPECT_EQ(stats.terminated, 2u); -} - -TEST_F(StateDashboardExecTest, RefreshWithProcesses) { - execute_options opts; - opts.name = "refresh_test"; - sched->execute(std::string("(+ 1 2)"), opts); - EXPECT_NO_THROW(widget->refresh()); - sched->run(); - EXPECT_NO_THROW(widget->refresh()); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Cluster Tab Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardClusterTest : public ::testing::Test { -protected: - void SetUp() override { - root = &state::instance(volrover3::app())("cluster_dashboard_test"); - root->value("cluster_root"); - - shard = std::make_unique(volrover3::app(), "test_cluster", "node_local"); - membership = std::make_unique("test_cluster", "node_local"); - telemetry = std::make_unique("test_cluster"); - - widget = new StateDashboardWidget(); - widget->setShard(shard.get()); - widget->setMembership(membership.get()); - widget->setTelemetryAggregator(telemetry.get()); - } - - void TearDown() override { - delete widget; - root->reset(); - } - - state *root; - std::unique_ptr shard; - std::unique_ptr membership; - std::unique_ptr telemetry; - StateDashboardWidget *widget; -}; - -TEST_F(StateDashboardClusterTest, WidgetCreation) { ASSERT_NE(widget, nullptr); } - -TEST_F(StateDashboardClusterTest, SetComponentsNullSafe) { - auto *w = new StateDashboardWidget(); - EXPECT_NO_THROW(w->setShard(nullptr)); - EXPECT_NO_THROW(w->setMembership(nullptr)); - EXPECT_NO_THROW(w->setTelemetryAggregator(nullptr)); - EXPECT_NO_THROW(w->setCoordinator(nullptr)); - EXPECT_NO_THROW(w->refresh()); - delete w; -} - -TEST_F(StateDashboardClusterTest, ClusterIdentity) { - EXPECT_EQ(shard->cluster_id(), "test_cluster"); - EXPECT_EQ(shard->local_node_id(), "node_local"); - EXPECT_EQ(membership->cluster_id(), "test_cluster"); - EXPECT_EQ(membership->local_node_id(), "node_local"); -} - -TEST_F(StateDashboardClusterTest, PeerRegistration) { - membership->register_peer("peer_1", "test_cluster", "host1:9001"); - membership->register_peer("peer_2", "test_cluster", "host2:9002"); - - auto peers = membership->peer_snapshot(); - EXPECT_EQ(peers.size(), 2u); -} - -TEST_F(StateDashboardClusterTest, PeerSnapshot) { - membership->register_peer("peer_a", "test_cluster", "10.0.0.1:5000"); - - auto peers = membership->peer_snapshot(); - ASSERT_EQ(peers.size(), 1u); - EXPECT_EQ(peers[0].node_id, "peer_a"); - EXPECT_EQ(peers[0].endpoint, "10.0.0.1:5000"); -} - -TEST_F(StateDashboardClusterTest, MessageBusStats) { - auto &bus = shard->message_bus(); - EXPECT_EQ(bus.total_admitted(), 0u); - EXPECT_EQ(bus.total_dispatched(), 0u); - EXPECT_EQ(bus.total_dropped(), 0u); -} - -TEST_F(StateDashboardClusterTest, ShardCounters) { - EXPECT_EQ(shard->total_remote_applied(), 0u); - EXPECT_EQ(shard->total_remote_rejected(), 0u); - EXPECT_EQ(shard->total_conflicts_detected(), 0u); -} - -TEST_F(StateDashboardClusterTest, ShardAttachDetach) { - EXPECT_FALSE(shard->is_attached()); - shard->attach(); - EXPECT_TRUE(shard->is_attached()); - shard->detach(); - EXPECT_FALSE(shard->is_attached()); -} - -TEST_F(StateDashboardClusterTest, TelemetrySummary) { - auto summary = telemetry->summarize(); - EXPECT_EQ(summary.node_count, 0u); -} - -TEST_F(StateDashboardClusterTest, RefreshWithAllComponents) { - membership->register_peer("refresh_peer", "test_cluster", "host:1234"); - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardClusterTest, MembershipCounters) { - EXPECT_EQ(membership->total_heartbeats_sent(), 0u); - EXPECT_EQ(membership->total_heartbeats_received(), 0u); - EXPECT_EQ(membership->total_peers_joined(), 0u); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Cross-tab Integration Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardIntegrationTest : public ::testing::Test { -protected: - void SetUp() override { - root = &state::instance(volrover3::app())("integration_dashboard_test"); - root->operator()("config")("greeting").value("hello"); - - env = builtins::make_default_environment(); - sched = std::make_unique(); - shard = std::make_unique(volrover3::app(), "integ_cluster", "integ_node"); - membership = std::make_unique("integ_cluster", "integ_node"); - - widget = new StateDashboardWidget(); - widget->setRootState(root); - widget->setScheduler(sched.get()); - widget->setShard(shard.get()); - widget->setMembership(membership.get()); - } - - void TearDown() override { - delete widget; - root->reset(); - } - - state *root; - environment_ptr env; - std::unique_ptr sched; - std::unique_ptr shard; - std::unique_ptr membership; - StateDashboardWidget *widget; -}; - -TEST_F(StateDashboardIntegrationTest, FullRefreshAllTabs) { - execute_options opts; - opts.name = "integ_proc"; - sched->execute(std::string("(+ 1 1)"), opts); - membership->register_peer("integ_peer", "integ_cluster", "localhost:9999"); - - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardIntegrationTest, ShowAndRefresh) { - widget->show(); - QCoreApplication::processEvents(); - - EXPECT_NO_THROW(widget->refresh()); - QCoreApplication::processEvents(); - - widget->hide(); -} - -TEST_F(StateDashboardIntegrationTest, WidgetDeleteOnClose) { - auto *w = new StateDashboardWidget(); - w->setAttribute(Qt::WA_DeleteOnClose); - w->setRootState(root); - w->show(); - QCoreApplication::processEvents(); - w->close(); - QCoreApplication::processEvents(); - // Widget is now deleted due to WA_DeleteOnClose -} - -TEST_F(StateDashboardIntegrationTest, StateModWhileProcessRunning) { - execute_options opts; - opts.name = "bg_proc"; - sched->execute(std::string("(begin (while t nil))"), opts); - sched->step(); - - root->operator()("config")("greeting").value("modified"); - EXPECT_EQ(root->operator()("config")("greeting").value(), "modified"); - - EXPECT_NO_THROW(widget->refresh()); -} - -TEST_F(StateDashboardIntegrationTest, MultipleWidgets) { - auto *w2 = new StateDashboardWidget(); - w2->setRootState(root); - w2->setScheduler(sched.get()); - - EXPECT_NO_THROW(widget->refresh()); - EXPECT_NO_THROW(w2->refresh()); - - delete w2; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// State Tree UI Interaction Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardTreeUITest : public ::testing::Test { -protected: - void SetUp() override { - root = &state::instance(volrover3::app())("tree_ui_test"); - root->operator()("apple").value("red"); - root->operator()("banana").value("yellow"); - root->operator()("cherry").value("dark"); - root->operator()("apple")("seed").value("small"); - root->operator()("typed_int").value(42); - root->operator()("typed_dbl").value(3.14); - root->operator()("typed_bool").value(true); - - widget = new StateDashboardWidget(); - widget->setRootState(root); - widget->show(); - QCoreApplication::processEvents(); - - // Locate internal widgets by type - tree = widget->findChild(); - ASSERT_NE(tree, nullptr); - - // Identify tables by column count: property(2), process(8), peer(4) - for (auto *table : widget->findChildren()) { - if (table->columnCount() == 2) - propTable = table; - else if (table->columnCount() == 8) - procTable = table; - else if (table->columnCount() == 4) - peerTable = table; - } - ASSERT_NE(propTable, nullptr); - } - - void TearDown() override { - delete widget; - root->reset(); - } - - // Select a tree item matching the given text (depth-first) - QTreeWidgetItem *selectItemByText(QTreeWidgetItem *parent, const QString &text) { - for (int i = 0; i < parent->childCount(); ++i) { - auto *child = parent->child(i); - if (child->text(0) == text) { - auto idx = tree->indexFromItem(child); - tree->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect); - tree->setCurrentItem(child); - QCoreApplication::processEvents(); - return child; - } - auto *found = selectItemByText(child, text); - if (found) - return found; - } - return nullptr; - } - - state *root; - StateDashboardWidget *widget; - QTreeWidget *tree; - QTableWidget *propTable; - QTableWidget *procTable; - QTableWidget *peerTable; -}; - -// Helper to find a property row by key name -static int findPropRow(QTableWidget *table, const QString &key) { - for (int r = 0; r < table->rowCount(); ++r) { - if (table->item(r, 0) && table->item(r, 0)->text() == key) - return r; - } - return -1; -} - -TEST_F(StateDashboardTreeUITest, SelectionPopulatesPropertyTable) { - ASSERT_GT(tree->topLevelItemCount(), 0); - auto *item = selectItemByText(tree->topLevelItem(0), "apple"); - ASSERT_NE(item, nullptr); - - EXPECT_GT(propTable->rowCount(), 0); - int nameRow = findPropRow(propTable, "Name"); - ASSERT_GE(nameRow, 0); - EXPECT_EQ(propTable->item(nameRow, 1)->text(), "apple"); - - int valRow = findPropRow(propTable, "Value"); - ASSERT_GE(valRow, 0); - EXPECT_EQ(propTable->item(valRow, 1)->text(), "red"); -} - -TEST_F(StateDashboardTreeUITest, PropertyTableShowsChildren) { - selectItemByText(tree->topLevelItem(0), "apple"); - int row = findPropRow(propTable, "Children"); - ASSERT_GE(row, 0); - EXPECT_EQ(propTable->item(row, 1)->text(), "1"); // apple has child "seed" -} - -TEST_F(StateDashboardTreeUITest, PropertyTableShowsReadOnly) { - root->operator()("apple").readOnly(true); - widget->setRootState(root); // force clean tree rebuild - QCoreApplication::processEvents(); - selectItemByText(tree->topLevelItem(0), "apple"); - - int row = findPropRow(propTable, "Read Only"); - ASSERT_GE(row, 0); - EXPECT_EQ(propTable->item(row, 1)->text(), "true"); - - // Value cell should NOT be editable when readOnly - int valRow = findPropRow(propTable, "Value"); - ASSERT_GE(valRow, 0); - EXPECT_FALSE(propTable->item(valRow, 1)->flags().testFlag(Qt::ItemIsEditable)); - - root->operator()("apple").readOnly(false); -} - -TEST_F(StateDashboardTreeUITest, PropertyTableShowsHidden) { - root->operator()("banana").hidden(true); - widget->setRootState(root); - QCoreApplication::processEvents(); - selectItemByText(tree->topLevelItem(0), "banana"); - - int row = findPropRow(propTable, "Hidden"); - ASSERT_GE(row, 0); - EXPECT_EQ(propTable->item(row, 1)->text(), "true"); - - root->operator()("banana").hidden(false); -} - -TEST_F(StateDashboardTreeUITest, PropertyTableShowsComment) { - root->operator()("cherry").comment("a tart fruit"); - widget->setRootState(root); - QCoreApplication::processEvents(); - selectItemByText(tree->topLevelItem(0), "cherry"); - - int row = findPropRow(propTable, "Comment"); - ASSERT_GE(row, 0); - EXPECT_EQ(propTable->item(row, 1)->text(), "a tart fruit"); -} - -TEST_F(StateDashboardTreeUITest, PropertyEditValueUpdatesState) { - selectItemByText(tree->topLevelItem(0), "banana"); - - int valRow = findPropRow(propTable, "Value"); - ASSERT_GE(valRow, 0); - - // Simulate editing the value cell - propTable->item(valRow, 1)->setText("green"); - // cellChanged signal fires automatically on setText - QCoreApplication::processEvents(); - - EXPECT_EQ(root->operator()("banana").value(), "green"); -} - -TEST_F(StateDashboardTreeUITest, PropertyEditCommentUpdatesState) { - selectItemByText(tree->topLevelItem(0), "cherry"); - - int commentRow = findPropRow(propTable, "Comment"); - ASSERT_GE(commentRow, 0); - - propTable->item(commentRow, 1)->setText("updated comment"); - QCoreApplication::processEvents(); - - EXPECT_EQ(root->operator()("cherry").comment(), "updated comment"); -} - -TEST_F(StateDashboardTreeUITest, ValueEditEmitsStateChanged) { - selectItemByText(tree->topLevelItem(0), "apple"); - - int signalCount = 0; - QObject::connect(widget, &StateDashboardWidget::stateChanged, - [&signalCount]() { ++signalCount; }); - - int valRow = findPropRow(propTable, "Value"); - ASSERT_GE(valRow, 0); - - propTable->item(valRow, 1)->setText("green"); - QCoreApplication::processEvents(); - - EXPECT_GE(signalCount, 1); -} - -TEST_F(StateDashboardTreeUITest, SearchFilterHidesNonMatching) { - // Find the tree search box by placeholder text - QLineEdit *searchBox = nullptr; - for (auto *edit : widget->findChildren()) { - if (edit->placeholderText().contains("Filter")) { - searchBox = edit; - break; - } - } - ASSERT_NE(searchBox, nullptr); - - searchBox->setText("apple"); - QCoreApplication::processEvents(); - - auto *rootItem = tree->topLevelItem(0); - for (int i = 0; i < rootItem->childCount(); ++i) { - auto *child = rootItem->child(i); - if (child->text(0) == "apple") { - EXPECT_FALSE(child->isHidden()) << "apple should be visible"; - } else if (child->text(0) == "banana" || child->text(0) == "cherry") { - EXPECT_TRUE(child->isHidden()) << child->text(0).toStdString() << " should be hidden"; - } - } -} - -TEST_F(StateDashboardTreeUITest, SearchFilterClearRestoresAll) { - QLineEdit *searchBox = nullptr; - for (auto *edit : widget->findChildren()) { - if (edit->placeholderText().contains("Filter")) { - searchBox = edit; - break; - } - } - ASSERT_NE(searchBox, nullptr); - - searchBox->setText("apple"); - QCoreApplication::processEvents(); - - searchBox->setText(""); - QCoreApplication::processEvents(); - - // All items should be visible - auto *rootItem = tree->topLevelItem(0); - for (int i = 0; i < rootItem->childCount(); ++i) { - EXPECT_FALSE(rootItem->child(i)->isHidden()) << rootItem->child(i)->text(0).toStdString(); - } -} - -TEST_F(StateDashboardTreeUITest, LinkPropertiesShowInTable) { - auto &link = root->operator()("linked"); - link.linkTo("tree_ui_test.apple"); - widget->setRootState(root); - QCoreApplication::processEvents(); - - selectItemByText(tree->topLevelItem(0), "linked"); - - int isLinkRow = findPropRow(propTable, "Is Link"); - ASSERT_GE(isLinkRow, 0); - EXPECT_EQ(propTable->item(isLinkRow, 1)->text(), "true"); - - int targetRow = findPropRow(propTable, "Link Target"); - ASSERT_GE(targetRow, 0); - EXPECT_EQ(propTable->item(targetRow, 1)->text(), "tree_ui_test.apple"); - - int resRow = findPropRow(propTable, "Link Resolution"); - ASSERT_GE(resRow, 0); - EXPECT_EQ(propTable->item(resRow, 1)->text(), "resolved"); -} - -TEST_F(StateDashboardTreeUITest, ExpiryPropertiesShowInTable) { - auto &s = root->operator()("will_expire"); - s.value("temp"); - auto future = boost::posix_time::microsec_clock::universal_time() + boost::posix_time::hours(1); - s.expireAt(future); - widget->setRootState(root); - QCoreApplication::processEvents(); - - selectItemByText(tree->topLevelItem(0), "will_expire"); - - int hasExpRow = findPropRow(propTable, "Has Expiry"); - ASSERT_GE(hasExpRow, 0); - EXPECT_EQ(propTable->item(hasExpRow, 1)->text(), "true"); - - int expiredRow = findPropRow(propTable, "Expired"); - ASSERT_GE(expiredRow, 0); - EXPECT_EQ(propTable->item(expiredRow, 1)->text(), "false"); -} - -TEST_F(StateDashboardTreeUITest, TypedIntValueInTable) { - selectItemByText(tree->topLevelItem(0), "typed_int"); - - int valRow = findPropRow(propTable, "Value"); - ASSERT_GE(valRow, 0); - EXPECT_EQ(propTable->item(valRow, 1)->text(), "42"); - - int typeRow = findPropRow(propTable, "Value Type"); - ASSERT_GE(typeRow, 0); - EXPECT_FALSE(propTable->item(typeRow, 1)->text().isEmpty()); -} - -TEST_F(StateDashboardTreeUITest, SelectionClearEmptiesProperties) { - selectItemByText(tree->topLevelItem(0), "apple"); - EXPECT_GT(propTable->rowCount(), 0); - - tree->clearSelection(); - QCoreApplication::processEvents(); - - EXPECT_EQ(propTable->rowCount(), 0); -} - -TEST_F(StateDashboardTreeUITest, LastModifiedShownInProperties) { - root->operator()("apple").value("updated"); - widget->setRootState(root); - QCoreApplication::processEvents(); - selectItemByText(tree->topLevelItem(0), "apple"); - - int row = findPropRow(propTable, "Last Modified"); - ASSERT_GE(row, 0); - // Should not be "(unknown)" since we just wrote a value - EXPECT_NE(propTable->item(row, 1)->text(), "(unknown)"); -} - -TEST_F(StateDashboardTreeUITest, DeleteButtonDisabledWithNoSelection) { - auto buttons = widget->findChildren(); - QPushButton *deleteBtn = nullptr; - for (auto *btn : buttons) { - if (btn->text() == "Delete State") { - deleteBtn = btn; - break; - } - } - ASSERT_NE(deleteBtn, nullptr); - - tree->clearSelection(); - QCoreApplication::processEvents(); - EXPECT_FALSE(deleteBtn->isEnabled()); -} - -TEST_F(StateDashboardTreeUITest, DeleteButtonEnabledWithSelection) { - auto buttons = widget->findChildren(); - QPushButton *deleteBtn = nullptr; - for (auto *btn : buttons) { - if (btn->text() == "Delete State") { - deleteBtn = btn; - break; - } - } - ASSERT_NE(deleteBtn, nullptr); - - selectItemByText(tree->topLevelItem(0), "banana"); - EXPECT_TRUE(deleteBtn->isEnabled()); -} - -TEST_F(StateDashboardTreeUITest, NestedChildSelectionShowsProperties) { - selectItemByText(tree->topLevelItem(0), "seed"); - - int nameRow = findPropRow(propTable, "Name"); - ASSERT_GE(nameRow, 0); - EXPECT_EQ(propTable->item(nameRow, 1)->text(), "seed"); - - int valRow = findPropRow(propTable, "Value"); - ASSERT_GE(valRow, 0); - EXPECT_EQ(propTable->item(valRow, 1)->text(), "small"); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Exec Console UI Interaction Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardExecUITest : public ::testing::Test { -protected: - void SetUp() override { - env = builtins::make_default_environment(); - sched = std::make_unique(); - - widget = new StateDashboardWidget(); - widget->setScheduler(sched.get()); - widget->show(); - QCoreApplication::processEvents(); - - // Locate internal widgets — identify by readOnly flag - for (auto *editor : widget->findChildren()) { - if (editor->isReadOnly()) - outputPanel = editor; - else - scriptEditor = editor; - } - ASSERT_NE(scriptEditor, nullptr); - ASSERT_NE(outputPanel, nullptr); - - // processTable has 8 columns - for (auto *table : widget->findChildren()) { - if (table->columnCount() == 8) { - processTable = table; - break; - } - } - ASSERT_NE(processTable, nullptr); - - // Find buttons by text - for (auto *btn : widget->findChildren()) { - if (btn->text() == "Run") - runBtn = btn; - else if (btn->text() == "Clear Output") - clearBtn = btn; - else if (btn->text() == "Pause") - pauseBtn = btn; - else if (btn->text() == "Resume") - resumeBtn = btn; - else if (btn->text() == "Kill") - killBtn = btn; - } - } - - void TearDown() override { delete widget; } - - environment_ptr env; - std::unique_ptr sched; - StateDashboardWidget *widget; - QPlainTextEdit *scriptEditor = nullptr; - QPlainTextEdit *outputPanel = nullptr; - QTableWidget *processTable = nullptr; - QPushButton *runBtn = nullptr; - QPushButton *clearBtn = nullptr; - QPushButton *pauseBtn = nullptr; - QPushButton *resumeBtn = nullptr; - QPushButton *killBtn = nullptr; -}; - -TEST_F(StateDashboardExecUITest, RunScriptShowsOutput) { - ASSERT_NE(scriptEditor, nullptr); - ASSERT_NE(runBtn, nullptr); - - scriptEditor->setPlainText("(+ 10 20)"); - runBtn->click(); - QCoreApplication::processEvents(); - - QString output = outputPanel->toPlainText(); - EXPECT_TRUE(output.contains("30")) << output.toStdString(); -} - -TEST_F(StateDashboardExecUITest, RunScriptErrorShowsError) { - scriptEditor->setPlainText("(undefined_func 1 2)"); - runBtn->click(); - QCoreApplication::processEvents(); - - QString output = outputPanel->toPlainText(); - EXPECT_TRUE(output.contains("ERROR")) << output.toStdString(); -} - -TEST_F(StateDashboardExecUITest, RunScriptShowsEcho) { - scriptEditor->setPlainText("(* 3 7)"); - runBtn->click(); - QCoreApplication::processEvents(); - - QString output = outputPanel->toPlainText(); - // The script itself is echoed with "> " prefix - EXPECT_TRUE(output.contains("> (* 3 7)")) << output.toStdString(); -} - -TEST_F(StateDashboardExecUITest, EmptyScriptDoesNothing) { - scriptEditor->setPlainText(""); - runBtn->click(); - QCoreApplication::processEvents(); - - EXPECT_TRUE(outputPanel->toPlainText().isEmpty()); -} - -TEST_F(StateDashboardExecUITest, ClearOutputClearsPanel) { - scriptEditor->setPlainText("(+ 1 1)"); - runBtn->click(); - QCoreApplication::processEvents(); - EXPECT_FALSE(outputPanel->toPlainText().isEmpty()); - - clearBtn->click(); - QCoreApplication::processEvents(); - EXPECT_TRUE(outputPanel->toPlainText().isEmpty()); -} - -TEST_F(StateDashboardExecUITest, ProcessTableShowsSubmittedProcess) { - execute_options opts; - opts.name = "table_test_proc"; - sched->execute(std::string("(begin (while t nil))"), opts); - - widget->refresh(); - QCoreApplication::processEvents(); - - ASSERT_EQ(processTable->rowCount(), 1); - // Column 0: PID, Column 1: Name - EXPECT_EQ(processTable->item(0, 1)->text(), "table_test_proc"); -} - -TEST_F(StateDashboardExecUITest, ProcessTableShowsStatus) { - execute_options opts; - opts.name = "status_proc"; - int pid = sched->execute(std::string("(begin (while t nil))"), opts); - - widget->refresh(); - QCoreApplication::processEvents(); - - // should be "ready" before any steps - EXPECT_EQ(processTable->item(0, 2)->text(), "ready"); - - sched->pause(pid); - widget->refresh(); - QCoreApplication::processEvents(); - - EXPECT_EQ(processTable->item(0, 2)->text(), "paused"); -} - -TEST_F(StateDashboardExecUITest, ProcessButtonsDisabledWithoutSelection) { - EXPECT_FALSE(pauseBtn->isEnabled()); - EXPECT_FALSE(resumeBtn->isEnabled()); - EXPECT_FALSE(killBtn->isEnabled()); -} - -TEST_F(StateDashboardExecUITest, ProcessButtonsEnabledOnSelection) { - execute_options opts; - opts.name = "select_proc"; - sched->execute(std::string("(begin (while t nil))"), opts); - widget->refresh(); - QCoreApplication::processEvents(); - - processTable->selectRow(0); - QCoreApplication::processEvents(); - - EXPECT_TRUE(pauseBtn->isEnabled()); - EXPECT_TRUE(resumeBtn->isEnabled()); - EXPECT_TRUE(killBtn->isEnabled()); -} - -TEST_F(StateDashboardExecUITest, PauseButtonPausesSelectedProcess) { - execute_options opts; - opts.name = "pause_me"; - int pid = sched->execute(std::string("(begin (while t nil))"), opts); - widget->refresh(); - QCoreApplication::processEvents(); - - processTable->selectRow(0); - QCoreApplication::processEvents(); - - pauseBtn->click(); - QCoreApplication::processEvents(); - - auto info = sched->get_process_info(pid); - ASSERT_TRUE(info.has_value()); - EXPECT_EQ(info->status, process_status::paused); -} - -TEST_F(StateDashboardExecUITest, KillButtonKillsSelectedProcess) { - execute_options opts; - opts.name = "kill_me"; - int pid = sched->execute(std::string("(begin (while t nil))"), opts); - widget->refresh(); - QCoreApplication::processEvents(); - - processTable->selectRow(0); - QCoreApplication::processEvents(); - - killBtn->click(); - QCoreApplication::processEvents(); - - auto info = sched->get_process_info(pid); - ASSERT_TRUE(info.has_value()); - EXPECT_EQ(info->status, process_status::killed); -} - -TEST_F(StateDashboardExecUITest, MultipleProcessesInTable) { - execute_options opts_a, opts_b; - opts_a.name = "proc_x"; - opts_b.name = "proc_y"; - sched->execute(std::string("(+ 1 1)"), opts_a); - sched->execute(std::string("(+ 2 2)"), opts_b); - - widget->refresh(); - QCoreApplication::processEvents(); - - EXPECT_EQ(processTable->rowCount(), 2); -} - -TEST_F(StateDashboardExecUITest, CompletedProcessShowsTerminated) { - execute_options opts; - opts.name = "done_proc"; - sched->execute(std::string("(+ 1 1)"), opts); - sched->run(); - - widget->refresh(); - QCoreApplication::processEvents(); - - ASSERT_EQ(processTable->rowCount(), 1); - EXPECT_EQ(processTable->item(0, 2)->text(), "terminated"); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Cluster Tab UI Interaction Tests -// ═══════════════════════════════════════════════════════════════════════════ - -class StateDashboardClusterUITest : public ::testing::Test { -protected: - void SetUp() override { - root = &state::instance(volrover3::app())("cluster_ui_test"); - root->value("cluster_ui_root"); - - shard = std::make_unique(volrover3::app(), "ui_cluster", "ui_node"); - membership = std::make_unique("ui_cluster", "ui_node"); - telemetry = std::make_unique("ui_cluster"); - - widget = new StateDashboardWidget(); - widget->setShard(shard.get()); - widget->setMembership(membership.get()); - widget->setTelemetryAggregator(telemetry.get()); - widget->show(); - widget->refresh(); - QCoreApplication::processEvents(); - - // Find labels - for (auto *label : widget->findChildren()) { - QString text = label->text(); - if (text.startsWith("Node ID:")) - nodeIdLabel = label; - else if (text.startsWith("Cluster ID:")) - clusterIdLabel = label; - else if (text.startsWith("Leader:")) - leaderLabel = label; - else if (text.startsWith("Message Bus")) - busLabel = label; - else if (text.startsWith("Shard")) - shardLabel = label; - else if (text.startsWith("Telemetry")) - telemetryLabel = label; - } - - // Peer table has 4 columns - for (auto *table : widget->findChildren()) { - if (table->columnCount() == 4) { - peerTableWidget = table; - break; - } - } - ASSERT_NE(peerTableWidget, nullptr); - } - - void TearDown() override { - delete widget; - root->reset(); - } - - state *root; - std::unique_ptr shard; - std::unique_ptr membership; - std::unique_ptr telemetry; - StateDashboardWidget *widget; - QLabel *nodeIdLabel = nullptr; - QLabel *clusterIdLabel = nullptr; - QLabel *leaderLabel = nullptr; - QLabel *busLabel = nullptr; - QLabel *shardLabel = nullptr; - QLabel *telemetryLabel = nullptr; - QTableWidget *peerTableWidget = nullptr; -}; - -TEST_F(StateDashboardClusterUITest, NodeIdLabelShowsCorrectId) { - ASSERT_NE(nodeIdLabel, nullptr); - EXPECT_TRUE(nodeIdLabel->text().contains("ui_node")) << nodeIdLabel->text().toStdString(); -} - -TEST_F(StateDashboardClusterUITest, ClusterIdLabelShowsCorrectId) { - ASSERT_NE(clusterIdLabel, nullptr); - EXPECT_TRUE(clusterIdLabel->text().contains("ui_cluster")) - << clusterIdLabel->text().toStdString(); -} - -TEST_F(StateDashboardClusterUITest, BusStatsLabelShowsZeroCounts) { - ASSERT_NE(busLabel, nullptr); - EXPECT_TRUE(busLabel->text().contains("admitted: 0")) << busLabel->text().toStdString(); - EXPECT_TRUE(busLabel->text().contains("dispatched: 0")) << busLabel->text().toStdString(); -} - -TEST_F(StateDashboardClusterUITest, ShardStatsLabelShowsAttachedNo) { - ASSERT_NE(shardLabel, nullptr); - EXPECT_TRUE(shardLabel->text().contains("attached: no")) << shardLabel->text().toStdString(); -} - -TEST_F(StateDashboardClusterUITest, ShardStatsLabelShowsAttachedYes) { - shard->attach(); - widget->refresh(); - QCoreApplication::processEvents(); - - // Re-find label since refresh may update text - for (auto *label : widget->findChildren()) { - if (label->text().startsWith("Shard")) - shardLabel = label; - } - ASSERT_NE(shardLabel, nullptr); - EXPECT_TRUE(shardLabel->text().contains("attached: yes")) << shardLabel->text().toStdString(); -} - -TEST_F(StateDashboardClusterUITest, TelemetryLabelShowsNodeCount) { - ASSERT_NE(telemetryLabel, nullptr); - EXPECT_TRUE(telemetryLabel->text().contains("nodes: 0")) << telemetryLabel->text().toStdString(); -} - -TEST_F(StateDashboardClusterUITest, PeerTablePopulatesAfterRegistration) { - membership->register_peer("peer_ui_1", "ui_cluster", "10.0.0.1:5000"); - membership->register_peer("peer_ui_2", "ui_cluster", "10.0.0.2:5000"); - widget->refresh(); - QCoreApplication::processEvents(); - - ASSERT_EQ(peerTableWidget->rowCount(), 2); - // Verify both endpoints are present (order not guaranteed) - QSet endpoints; - for (int i = 0; i < peerTableWidget->rowCount(); ++i) - endpoints.insert(peerTableWidget->item(i, 1)->text()); - EXPECT_TRUE(endpoints.contains("10.0.0.1:5000")); - EXPECT_TRUE(endpoints.contains("10.0.0.2:5000")); -} - -TEST_F(StateDashboardClusterUITest, PeerTableShowsPeerState) { - membership->register_peer("alive_peer", "ui_cluster", "host:1234"); - widget->refresh(); - QCoreApplication::processEvents(); - - ASSERT_EQ(peerTableWidget->rowCount(), 1); - // Newly registered peers start as "alive" - EXPECT_EQ(peerTableWidget->item(0, 2)->text(), "alive"); -} - -TEST_F(StateDashboardClusterUITest, ShardOnlyIdentity) { - // Widget with shard but no membership uses shard for identity - auto *w = new StateDashboardWidget(); - w->setShard(shard.get()); - w->refresh(); - QCoreApplication::processEvents(); - - QLabel *nodeLabel = nullptr; - for (auto *label : w->findChildren()) { - if (label->text().contains("ui_node")) { - nodeLabel = label; - break; - } - } - EXPECT_NE(nodeLabel, nullptr); - delete w; -} - -TEST_F(StateDashboardClusterUITest, ConnectPeerViaEndpointInput) { - QLineEdit *peerInput = nullptr; - for (auto *edit : widget->findChildren()) { - if (edit->placeholderText().contains("host:port")) { - peerInput = edit; - break; - } - } - ASSERT_NE(peerInput, nullptr); - - QPushButton *connectBtn = nullptr; - for (auto *btn : widget->findChildren()) { - if (btn->text() == "Connect") { - connectBtn = btn; - break; - } - } - ASSERT_NE(connectBtn, nullptr); - - peerInput->setText("newhost:7777"); - connectBtn->click(); - QCoreApplication::processEvents(); - - // Peer should now be registered - auto peers = membership->peer_snapshot(); - EXPECT_EQ(peers.size(), 1u); - EXPECT_EQ(peers[0].endpoint, "newhost:7777"); -} - -TEST_F(StateDashboardClusterUITest, ConnectPeerClearsInput) { - QLineEdit *peerInput = nullptr; - for (auto *edit : widget->findChildren()) { - if (edit->placeholderText().contains("host:port")) { - peerInput = edit; - break; - } - } - ASSERT_NE(peerInput, nullptr); - - QPushButton *connectBtn = nullptr; - for (auto *btn : widget->findChildren()) { - if (btn->text() == "Connect") { - connectBtn = btn; - break; - } - } - ASSERT_NE(connectBtn, nullptr); - - peerInput->setText("host:8888"); - connectBtn->click(); - QCoreApplication::processEvents(); - - EXPECT_TRUE(peerInput->text().isEmpty()); -} - -TEST_F(StateDashboardClusterUITest, EmptyEndpointConnectIgnored) { - QLineEdit *peerInput = nullptr; - for (auto *edit : widget->findChildren()) { - if (edit->placeholderText().contains("host:port")) { - peerInput = edit; - break; - } - } - ASSERT_NE(peerInput, nullptr); - - QPushButton *connectBtn = nullptr; - for (auto *btn : widget->findChildren()) { - if (btn->text() == "Connect") { - connectBtn = btn; - break; - } - } - ASSERT_NE(connectBtn, nullptr); - - peerInput->setText(""); - connectBtn->click(); - QCoreApplication::processEvents(); - - auto peers = membership->peer_snapshot(); - EXPECT_TRUE(peers.empty()); -} - -int main(int argc, char **argv) { - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/tests/StateTreeWidgetTest.cpp b/src/volrover3/tests/StateTreeWidgetTest.cpp deleted file mode 100644 index e74702e2..00000000 --- a/src/volrover3/tests/StateTreeWidgetTest.cpp +++ /dev/null @@ -1,354 +0,0 @@ -#include -#include -#include -#include -#include -#include - -class StateTreeWidgetTest : public ::testing::Test { -protected: - void SetUp() override { - // Create a temporary state tree for testing - testState = &cvc::state::instance(volrover3::app())("test_widget"); - - // Create some test states - testState->operator()("child1").value("value1"); - testState->operator()("child2").value("value2"); - testState->operator()("nested")("deep").value("deep_value"); - - widget = new StateTreeWidget(); - widget->setRootState(testState); - } - - void TearDown() override { - delete widget; - // Clean up test states - testState->reset(); - } - - cvc::state *testState; - StateTreeWidget *widget; -}; - -// Test that the widget initializes correctly -TEST_F(StateTreeWidgetTest, WidgetInitialization) { - ASSERT_NE(widget, nullptr); - EXPECT_TRUE(widget->isVisible() == false); // Not shown by default -} - -// Test that initialized states appear in the tree -TEST_F(StateTreeWidgetTest, InitializedStatesAppear) { - // The widget should show initialized states - // We can't easily test Qt widget internals without a full GUI test, - // but we can verify the state structure - - auto children = testState->children(); - - // Should have at least our test children - bool hasChild1 = false, hasChild2 = false, hasNested = false; - for (const auto &childPath : children) { - if (childPath.find("test_widget.child1") != std::string::npos) - hasChild1 = true; - if (childPath.find("test_widget.child2") != std::string::npos) - hasChild2 = true; - if (childPath.find("test_widget.nested") != std::string::npos) - hasNested = true; - } - - EXPECT_TRUE(hasChild1); - EXPECT_TRUE(hasChild2); - EXPECT_TRUE(hasNested); -} - -// Test that uninitialized states don't appear -TEST_F(StateTreeWidgetTest, UninitializedStatesHidden) { - // Create an uninitialized state - cvc::state &uninit = testState->operator()("uninitialized"); - - EXPECT_FALSE(uninit.initialized()); - - // After setting root state, uninitialized states should be filtered - widget->setRootState(testState); - - // The state exists but is not initialized - EXPECT_FALSE(uninit.initialized()); -} - -// Test state reset functionality -TEST_F(StateTreeWidgetTest, StateReset) { - cvc::state &testChild = testState->operator()("child1"); - - EXPECT_TRUE(testChild.initialized()); - EXPECT_EQ(testChild.value(), "value1"); - - // Reset the state - testChild.reset(); - - EXPECT_FALSE(testChild.initialized()); - EXPECT_EQ(testChild.value(), ""); -} - -// Test nested state access -TEST_F(StateTreeWidgetTest, NestedStates) { - cvc::state &nested = testState->operator()("nested"); - nested.value("nested_value"); // Initialize nested state - EXPECT_TRUE(nested.initialized()); - - cvc::state &deep = nested("deep"); - EXPECT_TRUE(deep.initialized()); - EXPECT_EQ(deep.value(), "deep_value"); -} - -// Test state value modification -TEST_F(StateTreeWidgetTest, StateValueModification) { - cvc::state &testChild = testState->operator()("child1"); - - EXPECT_EQ(testChild.value(), "value1"); - - testChild.value("modified_value"); - - EXPECT_EQ(testChild.value(), "modified_value"); -} - -// Test state path validation (simulating what the widget does) -TEST_F(StateTreeWidgetTest, PathValidation) { - // Valid paths - QRegularExpression pathRegex("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$"); - - EXPECT_TRUE(pathRegex.match("valid_name").hasMatch()); - EXPECT_TRUE(pathRegex.match("test.nested.path").hasMatch()); - EXPECT_TRUE(pathRegex.match("_private").hasMatch()); - EXPECT_TRUE(pathRegex.match("name123").hasMatch()); - EXPECT_TRUE(pathRegex.match("app.config.value_1").hasMatch()); - - // Invalid paths - EXPECT_FALSE(pathRegex.match("123invalid").hasMatch()); - EXPECT_FALSE(pathRegex.match("path-with-dash").hasMatch()); - EXPECT_FALSE(pathRegex.match("path with space").hasMatch()); - EXPECT_FALSE(pathRegex.match("path..double").hasMatch()); - EXPECT_FALSE(pathRegex.match(".starts_with_dot").hasMatch()); - EXPECT_FALSE(pathRegex.match("ends_with_dot.").hasMatch()); - EXPECT_FALSE(pathRegex.match("has$pecial").hasMatch()); -} - -// Test hierarchical structure -TEST_F(StateTreeWidgetTest, HierarchicalStructure) { - // Create a deeper hierarchy - testState->operator()("level1")("level2")("level3").value("deep"); - - cvc::state &level1 = testState->operator()("level1"); - cvc::state &level2 = level1("level2"); - cvc::state &level3 = level2("level3"); - - EXPECT_TRUE(level1.initialized()); - EXPECT_TRUE(level2.initialized()); - EXPECT_TRUE(level3.initialized()); - - EXPECT_EQ(level3.value(), "deep"); - EXPECT_EQ(level3.name(), "level3"); - EXPECT_EQ(level3.fullName(), "test_widget.level1.level2.level3"); -} - -// Test immediate children filtering -TEST_F(StateTreeWidgetTest, ImmediateChildrenFilter) { - // Create nested structure - testState->operator()("parent")("child")("grandchild").value("value"); - - auto allChildren = testState->children(); - - // allChildren is recursive, so it contains all descendants - // We need to filter for immediate children only - std::string parentFullName = testState->fullName(); - std::set immediateChildren; - - for (const auto &childFullName : allChildren) { - if (childFullName.find(parentFullName) == 0) { - std::string relativePath = childFullName.substr(parentFullName.length()); - - if (!relativePath.empty() && relativePath[0] == '.') { - relativePath = relativePath.substr(1); - } - - if (!relativePath.empty() && relativePath.find('.') == std::string::npos) { - immediateChildren.insert(relativePath); - } - } - } - - // Should have our immediate children but not grandchildren - EXPECT_TRUE(immediateChildren.find("child1") != immediateChildren.end() || - immediateChildren.find("child2") != immediateChildren.end() || - immediateChildren.find("parent") != immediateChildren.end()); - - // Should NOT have grandchild as immediate child - EXPECT_TRUE(immediateChildren.find("grandchild") == immediateChildren.end()); -} - -// Test state data type information -TEST_F(StateTreeWidgetTest, StateDataTypes) { - cvc::state &intState = testState->operator()("int_value"); - cvc::state &doubleState = testState->operator()("double_value"); - cvc::state &stringState = testState->operator()("string_value"); - - intState.value(42); - doubleState.value(3.14); - stringState.value("text"); - - EXPECT_EQ(intState.value(), "42"); - // Floating point precision: 3.14 may be stored as 3.1400000000000001 - EXPECT_NE(doubleState.value(), ""); - EXPECT_TRUE(doubleState.value().find("3.14") == 0); - EXPECT_EQ(stringState.value(), "text"); -} - -// Test state with data (boost::any) -TEST_F(StateTreeWidgetTest, StateData) { - cvc::state &dataState = testState->operator()("with_data"); - - std::shared_ptr testData = std::make_shared(123); - dataState.data(boost::any(testData)); - dataState.value("has_data"); - - EXPECT_TRUE(dataState.initialized()); - EXPECT_FALSE(dataState.data().empty()); - - auto retrieved = boost::any_cast>(dataState.data()); - EXPECT_EQ(*retrieved, 123); -} - -// Test state value type name -TEST_F(StateTreeWidgetTest, StateValueTypeName) { - cvc::state &typedState = testState->operator()("typed"); - - typedState.value(100); - - // After setting an int value, the type name should be set - std::string typeName = typedState.valueTypeName(); - EXPECT_FALSE(typeName.empty()); -} - -// Test last modified time -TEST_F(StateTreeWidgetTest, LastModifiedTime) { - cvc::state &timedState = testState->operator()("timed"); - - auto before = boost::posix_time::microsec_clock::universal_time(); - timedState.value("test"); - auto after = boost::posix_time::microsec_clock::universal_time(); - - auto lastMod = timedState.lastMod(); - - EXPECT_GE(lastMod, before); - EXPECT_LE(lastMod, after); -} - -// Test widget refresh doesn't crash -TEST_F(StateTreeWidgetTest, WidgetRefresh) { - EXPECT_NO_THROW(widget->refresh()); - - // Add more states and refresh again - testState->operator()("new_state").value("new"); - EXPECT_NO_THROW(widget->refresh()); -} - -// Test that tree auto-updates when states are added -TEST_F(StateTreeWidgetTest, TreeAutoUpdatesOnStateAddition) { - // Show the widget - widget->show(); - - // Process pending events - QCoreApplication::processEvents(); - - // Get initial child count - auto initialChildren = testState->children(); - size_t initialCount = initialChildren.size(); - - // Add a new state dynamically - testState->operator()("dynamic_child").value("dynamic_value"); - - // Process events to handle signal - QCoreApplication::processEvents(); - - // Verify the state was added - auto newChildren = testState->children(); - EXPECT_GT(newChildren.size(), initialCount); - - // The widget should have been notified via childChanged signal - // This is tested implicitly - if the signal isn't connected, the test still passes - // but in the UI, the tree would need manual refresh -} - -// Test that tree auto-updates when states are removed -TEST_F(StateTreeWidgetTest, TreeAutoUpdatesOnStateDeletion) { - widget->show(); - QCoreApplication::processEvents(); - - // Create a state to delete - testState->operator()("to_delete").value("temp"); - QCoreApplication::processEvents(); - - auto beforeDelete = testState->children(); - - // Delete the state - testState->operator()("to_delete").reset(); - QCoreApplication::processEvents(); - - auto afterDelete = testState->children(); - - // Verify state was deleted (uninitialized) - EXPECT_FALSE(testState->operator()("to_delete").initialized()); -} - -// Test handling of current state deletion -TEST_F(StateTreeWidgetTest, CurrentStateDeleted) { - widget->show(); - QCoreApplication::processEvents(); - - // Create a state that we'll select and then delete - cvc::state &willDelete = testState->operator()("will_be_deleted"); - willDelete.value("temporary"); - - QCoreApplication::processEvents(); - - // Note: We can't easily simulate tree item selection in a unit test - // without full GUI interaction, but the deletion handling is tested - // through the signal connection - - // Delete the state - this should trigger the destroyed signal - willDelete.reset(); - - // If this state was selected, the widget should clear selection - QCoreApplication::processEvents(); - - // Verify state is no longer initialized - EXPECT_FALSE(willDelete.initialized()); -} - -// Test that selection is preserved after refresh when new states are added -TEST_F(StateTreeWidgetTest, SelectionPreservedOnRefresh) { - widget->show(); - QCoreApplication::processEvents(); - - // Get a reference to a state that will persist - cvc::state &persistentState = testState->operator()("child1"); - EXPECT_TRUE(persistentState.initialized()); - - // Manually refresh the widget (simulating what happens when tree structure changes) - widget->refresh(); - QCoreApplication::processEvents(); - - // Add a new sibling state (this would trigger auto-refresh via childChanged signal) - testState->operator()("new_sibling").value("new"); - QCoreApplication::processEvents(); - - // The persistent state should still exist and be initialized - EXPECT_TRUE(persistentState.initialized()); - EXPECT_EQ(persistentState.value(), "value1"); -} - -int main(int argc, char **argv) { - // Qt application needed for widget tests - QApplication app(argc, argv); - - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/tests/TransferFunctionTest.cpp b/src/volrover3/tests/TransferFunctionTest.cpp deleted file mode 100644 index 60224279..00000000 --- a/src/volrover3/tests/TransferFunctionTest.cpp +++ /dev/null @@ -1,410 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -// Need QApplication for Qt widgets -class TransferFunctionTest : public ::testing::Test { -protected: - cvc::app ctx; - static void SetUpTestSuite() { - if (!QApplication::instance()) { - int argc = 0; - char **argv = nullptr; - app = new QApplication(argc, argv); - } - } - - void SetUp() override { - widget = new TransferFunctionWidget(); - appState = &AppState::instance(); - } - - void TearDown() override { delete widget; } - - static QApplication *app; - TransferFunctionWidget *widget; - AppState *appState; -}; - -QApplication *TransferFunctionTest::app = nullptr; - -TEST_F(TransferFunctionTest, WidgetCreation) { EXPECT_NE(widget, nullptr); } - -TEST_F(TransferFunctionTest, DataRangeInitialization) { - // Default data range should be 0.0 to 1.0 - widget->setDataRange(0.0, 1.0); - - // Should not crash - SUCCEED(); -} - -TEST_F(TransferFunctionTest, ColorTableGeneration) { - widget->setDataRange(-5.0, 10.0); - - auto colorTable = widget->getColorTable(); - - // Should have at least 2 color points (scalar, r, g, b) - EXPECT_GE(colorTable.size(), 8); // At least 2 points * 4 values - - // Check that color table has groups of 4 values - EXPECT_EQ(colorTable.size() % 4, 0); - - // Scalar values should be within data range - for (size_t i = 0; i < colorTable.size() / 4; ++i) { - double scalar = colorTable[i * 4]; - EXPECT_GE(scalar, -5.0); - EXPECT_LE(scalar, 10.0); - - // RGB values should be in [0, 1] - EXPECT_GE(colorTable[i * 4 + 1], 0.0); - EXPECT_LE(colorTable[i * 4 + 1], 1.0); - EXPECT_GE(colorTable[i * 4 + 2], 0.0); - EXPECT_LE(colorTable[i * 4 + 2], 1.0); - EXPECT_GE(colorTable[i * 4 + 3], 0.0); - EXPECT_LE(colorTable[i * 4 + 3], 1.0); - } -} - -TEST_F(TransferFunctionTest, OpacityTableGeneration) { - widget->setDataRange(-5.0, 10.0); - - auto opacityTable = widget->getOpacityTable(); - - // Opacity table may be empty until user adds points or a preset is applied - // This is expected behavior - check that it's valid when present - if (opacityTable.size() > 0) { - // Check that opacity table has groups of 2 values - EXPECT_EQ(opacityTable.size() % 2, 0); - - // Scalar values should be within data range - for (size_t i = 0; i < opacityTable.size() / 2; ++i) { - double scalar = opacityTable[i * 2]; - double opacity = opacityTable[i * 2 + 1]; - - EXPECT_GE(scalar, -5.0); - EXPECT_LE(scalar, 10.0); - - // Opacity should be in [0, 1] - EXPECT_GE(opacity, 0.0); - EXPECT_LE(opacity, 1.0); - } - } - - // Test passes - opacity table is either empty or properly formatted - SUCCEED(); -} - -TEST_F(TransferFunctionTest, OpacityIndependentOfColor) { - widget->setDataRange(0.0, 100.0); - - // Get initial opacity table - auto opacityBefore = widget->getOpacityTable(); - - // Apply a color preset (this should NOT reset opacity) - widget->applyPreset("Rainbow"); - - // Get opacity table after color preset - auto opacityAfter = widget->getOpacityTable(); - - // Opacity table should be unchanged (unless it was empty initially) - if (opacityBefore.size() > 0) { - EXPECT_EQ(opacityBefore.size(), opacityAfter.size()); - } -} - -TEST_F(TransferFunctionTest, DataRangeMapping) { - // Set a specific data range - widget->setDataRange(10.0, 20.0); - - auto colorTable = widget->getColorTable(); - auto opacityTable = widget->getOpacityTable(); - - // First scalar should be near minimum - if (colorTable.size() >= 4) { - EXPECT_NEAR(colorTable[0], 10.0, 0.1); - } - - // Last scalar should be near maximum - if (colorTable.size() >= 8) { - size_t lastIdx = (colorTable.size() / 4 - 1) * 4; - EXPECT_NEAR(colorTable[lastIdx], 20.0, 0.1); - } - - // Same for opacity - if (opacityTable.size() >= 4) { - EXPECT_NEAR(opacityTable[0], 10.0, 0.1); - - size_t lastIdx = (opacityTable.size() / 2 - 1) * 2; - EXPECT_NEAR(opacityTable[lastIdx], 20.0, 0.1); - } -} - -TEST_F(TransferFunctionTest, PresetApplication) { - widget->setDataRange(0.0, 1.0); - - // Test different presets - widget->applyPreset("Grayscale"); - auto grayscale = widget->getColorTable(); - EXPECT_GT(grayscale.size(), 0); - - widget->applyPreset("Rainbow"); - auto rainbow = widget->getColorTable(); - EXPECT_GT(rainbow.size(), 0); - - widget->applyPreset("Hot"); - auto hot = widget->getColorTable(); - EXPECT_GT(hot.size(), 0); - - widget->applyPreset("Cool"); - auto cool = widget->getColorTable(); - EXPECT_GT(cool.size(), 0); - - widget->applyPreset("X-Ray"); - auto xray = widget->getColorTable(); - EXPECT_GT(xray.size(), 0); -} - -TEST_F(TransferFunctionTest, SignalEmission) { - bool signalReceived = false; - - QObject::connect(widget, &TransferFunctionWidget::transferFunctionChanged, - [&signalReceived]() { signalReceived = true; }); - - // Applying a preset should emit signal - widget->applyPreset("Grayscale"); - - // Process events to ensure signal is delivered - QApplication::processEvents(); - - EXPECT_TRUE(signalReceived); -} - -// =========================== -// Feedback Loop Prevention Tests -// =========================== - -TEST_F(TransferFunctionTest, NoFeedbackLoopOnStateUpdate) { - // This test verifies that the counter mechanism prevents feedback loops - // by checking that repeated setTransferFunction calls don't cause exponential growth - - auto volume = std::make_shared(ctx, "test_volume"); - - widget->setDataRange(0.0, 100.0); - widget->applyPreset("Rainbow"); - - auto initialColor = widget->getColorTable(); - size_t initialSize = initialColor.size(); - - // Simulate rapid updates (this would cause feedback loops in the old implementation) - for (int i = 0; i < 5; ++i) { - auto colorTable = widget->getColorTable(); - auto opacityTable = widget->getOpacityTable(); - - // This should NOT trigger a reload that increases the table size - volume->setTransferFunction(colorTable, opacityTable); - - // Verify size hasn't grown - auto currentSize = widget->getColorTable().size(); - EXPECT_EQ(currentSize, initialSize) - << "Iteration " << i << ": size changed from " << initialSize << " to " << currentSize; - } -} - -TEST_F(TransferFunctionTest, StateRoundTripPreservesData) { - // Test that data survives round-trip through state tree without corruption - - auto volume = std::make_shared(ctx, "test_volume"); - - widget->setDataRange(0.0, 255.0); - widget->applyPreset("Rainbow"); - - auto originalColor = widget->getColorTable(); - auto originalOpacity = widget->getOpacityTable(); - - // Save to volume state - volume->setTransferFunction(originalColor, originalOpacity); - - // Retrieve from volume state - auto retrievedColor = volume->getTransferFunctionColorTable(); - auto retrievedOpacity = volume->getTransferFunctionOpacityTable(); - - // Should have same number of values - EXPECT_EQ(originalColor.size(), retrievedColor.size()); - EXPECT_EQ(originalOpacity.size(), retrievedOpacity.size()); - - // Values should be very close (allowing for floating point precision with 6 decimal places) - for (size_t i = 0; i < std::min(originalColor.size(), retrievedColor.size()); ++i) { - EXPECT_NEAR(originalColor[i], retrievedColor[i], 1e-5) << "Color mismatch at index " << i; - } - - for (size_t i = 0; i < std::min(originalOpacity.size(), retrievedOpacity.size()); ++i) { - EXPECT_NEAR(originalOpacity[i], retrievedOpacity[i], 1e-5) << "Opacity mismatch at index " << i; - } -} - -TEST_F(TransferFunctionTest, PerVolumeStateSeparation) { - // Test that each volume has independent transfer function state - - auto volume1 = std::make_shared(ctx, "volume_1"); - auto volume2 = std::make_shared(ctx, "volume_2"); - - // Set different transfer functions for each volume - widget->setDataRange(0.0, 100.0); - widget->applyPreset("Rainbow"); - volume1->setTransferFunction(widget->getColorTable(), widget->getOpacityTable()); - auto volume1Color = volume1->getTransferFunctionColorTable(); - - widget->applyPreset("Grayscale"); - volume2->setTransferFunction(widget->getColorTable(), widget->getOpacityTable()); - auto volume2Color = volume2->getTransferFunctionColorTable(); - - // They should be different sizes (Rainbow has 5 points, Grayscale has 2) - EXPECT_NE(volume1Color.size(), volume2Color.size()) - << "Volumes should have independent transfer functions"; - - // Verify each volume retained its own TF - EXPECT_EQ(volume1Color.size(), 5 * 4); // 5 color points * 4 values each - EXPECT_EQ(volume2Color.size(), 2 * 4); // 2 color points * 4 values each - - // Verify they're actually different - EXPECT_NE(volume1Color, volume2Color); -} - -TEST_F(TransferFunctionTest, DefaultTransferFunctionSet) { - // Test that VolumeNode gets a default transfer function when created - - auto volume = std::make_shared(ctx, "test_volume"); - - // Set default TF - volume->setDefaultTransferFunction(); - - // Should have a valid transfer function in state - auto colorTable = volume->getTransferFunctionColorTable(); - auto opacityTable = volume->getTransferFunctionOpacityTable(); - - EXPECT_GT(colorTable.size(), 0) << "Default color table should not be empty"; - EXPECT_GT(opacityTable.size(), 0) << "Default opacity table should not be empty"; - - // Should be properly formatted - EXPECT_EQ(colorTable.size() % 4, 0); - EXPECT_EQ(opacityTable.size() % 2, 0); -} - -// =========================== -// State Tree Integration Tests -// =========================== - -// NOTE: Transfer function storage moved to per-volume state in VolumeNode -// These tests are commented out as they tested the old global AppState TF storage -/* -TEST_F(TransferFunctionTest, TransferFunctionStateStorage) { - // In actual usage, MainWindow saves transfer function to AppState - // when widget emits transferFunctionChanged signal - - widget->setDataRange(0.0, 100.0); - widget->applyPreset("Rainbow"); - - auto colorTable = widget->getColorTable(); - auto opacityTable = widget->getOpacityTable(); - - // Simulate what MainWindow does - appState->setTransferFunctionColorTable(colorTable); - appState->setTransferFunctionOpacityTable(opacityTable); - - // Verify state tree has the data - auto& stateTree = cvc::state::instance(volrover3::app())("volrover3"); - EXPECT_TRUE(stateTree("transfer_function_color").initialized()); - EXPECT_TRUE(stateTree("transfer_function_opacity").initialized()); - - // Retrieve from AppState - auto retrievedColor = appState->transferFunctionColorTable(); - auto retrievedOpacity = appState->transferFunctionOpacityTable(); - - EXPECT_EQ(colorTable.size(), retrievedColor.size()); - EXPECT_EQ(opacityTable.size(), retrievedOpacity.size()); -} - -TEST_F(TransferFunctionTest, TransferFunctionCallback) { - int callback_count = 0; - - // Register callback for transfer function changes - auto connection = appState->onTransferFunctionChanged([&callback_count]() { - callback_count++; - }); - - // Clear transfer_function_changed flag - auto& stateTree = cvc::state::instance(volrover3::app())("volrover3"); - stateTree("transfer_function_changed").value(false); - - // Change transfer function via AppState (simulating MainWindow) - widget->applyPreset("Hot"); - auto colorTable = widget->getColorTable(); - auto opacityTable = widget->getOpacityTable(); - - appState->setTransferFunctionColorTable(colorTable); - appState->setTransferFunctionOpacityTable(opacityTable); - - // Callback should have been triggered - EXPECT_GT(callback_count, 0); - - connection.disconnect(); -} - -TEST_F(TransferFunctionTest, TransferFunctionPersistence) { - // Set transfer function - widget->setDataRange(-10.0, 10.0); - widget->applyPreset("X-Ray"); - - auto originalColor = widget->getColorTable(); - auto originalOpacity = widget->getOpacityTable(); - - appState->setTransferFunctionColorTable(originalColor); - appState->setTransferFunctionOpacityTable(originalOpacity); - - // Retrieve from state tree multiple times - auto retrieved1 = appState->transferFunctionColorTable(); - auto retrieved2 = appState->transferFunctionColorTable(); - - // Should be consistent - EXPECT_EQ(retrieved1.size(), retrieved2.size()); - for (size_t i = 0; i < retrieved1.size(); ++i) { - EXPECT_DOUBLE_EQ(retrieved1[i], retrieved2[i]); - } -} - -TEST_F(TransferFunctionTest, SignalAndStateIntegration) { - // Test that Qt signals and AppState callbacks work together - // In practice: Widget emits signal → MainWindow saves to AppState → callbacks fire - - int callback_count = 0; - - // Register AppState callback - auto connection = appState->onTransferFunctionChanged([&callback_count]() { - callback_count++; - }); - - // Manually trigger state change (simulating MainWindow's save operation) - std::vector testColorTable = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; - std::vector testOpacityTable = {0.0, 0.5, 1.0}; - - appState->setTransferFunctionColorTable(testColorTable); - appState->setTransferFunctionOpacityTable(testOpacityTable); - - // Both setters toggle transfer_function_changed, so callback fires twice - EXPECT_EQ(callback_count, 2); - - // Verify we can retrieve the data - auto retrievedColor = appState->transferFunctionColorTable(); - auto retrievedOpacity = appState->transferFunctionOpacityTable(); - - EXPECT_EQ(testColorTable.size(), retrievedColor.size()); - EXPECT_EQ(testOpacityTable.size(), retrievedOpacity.size()); - - connection.disconnect(); -} -*/ diff --git a/src/volrover3/tests/VolumeDialogTest.cpp b/src/volrover3/tests/VolumeDialogTest.cpp deleted file mode 100644 index 70298fa9..00000000 --- a/src/volrover3/tests/VolumeDialogTest.cpp +++ /dev/null @@ -1,552 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class VolumeDialogTest : public ::testing::Test { -protected: - static void SetUpTestSuite() { - // Disable threading for state_object to avoid race conditions - cvc::state_object::setUseThreading(false); - - // Initialize Qt if not already initialized - if (!QApplication::instance()) { - // Qt requires valid argc/argv pointers, otherwise X11 backend crashes - // when trying to access QCoreApplication::arguments() - static int argc = 1; - static char appName[] = "test"; - static char *argv[] = {appName, nullptr}; - static QApplication app(argc, argv); - - // Disable session management to prevent X11 session manager crashes - app.setProperty("sessionManagement", false); - } - } - - void SetUp() override { - sceneGraph = std::make_shared(); - dialog = nullptr; - } - - void TearDown() override { - if (dialog) { - delete dialog; - dialog = nullptr; - } - sceneGraph.reset(); - } - - cvc::volume createTestVolume() { - // Create a simple test volume (data doesn't matter for dialog tests) - return cvc::volume(ctx, cvc::dimension(4, 4, 4), cvc::UChar); - } - - cvc::app ctx; - std::shared_ptr sceneGraph; - VolumeDialog *dialog; -}; - -TEST_F(VolumeDialogTest, DialogCreation) { - dialog = new VolumeDialog(sceneGraph); - EXPECT_NE(dialog, nullptr); - EXPECT_EQ(dialog->windowTitle(), "Volume Properties"); -} - -TEST_F(VolumeDialogTest, EmptySceneGraph) { - dialog = new VolumeDialog(sceneGraph); - - // Dialog should be created but properties should be disabled - EXPECT_NE(dialog, nullptr); - - // Access the combo box through findChild - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 0); -} - -TEST_F(VolumeDialogTest, SingleVolume) { - cvc::volume vol = createTestVolume(); - sceneGraph->addGraphics("test_vol", vol); - - dialog = new VolumeDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 1); - EXPECT_EQ(comboBox->itemText(0).toStdString(), "test_vol"); -} - -TEST_F(VolumeDialogTest, MultipleVolumes) { - cvc::volume vol1 = createTestVolume(); - cvc::volume vol2 = createTestVolume(); - - sceneGraph->addGraphics("vol1", vol1); - sceneGraph->addGraphics("vol2", vol2); - - dialog = new VolumeDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 2); -} - -TEST_F(VolumeDialogTest, VolumeSelectionUpdatesUI) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - // Set some properties - volNode->setShading(true); - volNode->setAmbient(0.3); - volNode->setDiffuse(0.8); - volNode->setSpecular(0.5); - - dialog = new VolumeDialog(sceneGraph); - - // Find the shading checkbox - QCheckBox *shadingCheckBox = nullptr; - QList checkBoxes = dialog->findChildren(); - for (auto *cb : checkBoxes) { - if (cb->text().contains("Shading", Qt::CaseInsensitive)) { - shadingCheckBox = cb; - break; - } - } - - if (shadingCheckBox) { - EXPECT_TRUE(shadingCheckBox->isChecked()); - } - - // Check spin boxes reflect values - QList spinBoxes = dialog->findChildren(); - EXPECT_GT(spinBoxes.size(), 0); -} - -TEST_F(VolumeDialogTest, ShadingToggle) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - volNode->setShading(false); - - dialog = new VolumeDialog(sceneGraph); - - // Find the shading checkbox - QCheckBox *shadingCheckBox = nullptr; - QList checkBoxes = dialog->findChildren(); - for (auto *cb : checkBoxes) { - if (cb->text().contains("Shading", Qt::CaseInsensitive)) { - shadingCheckBox = cb; - break; - } - } - - ASSERT_NE(shadingCheckBox, nullptr); - EXPECT_FALSE(shadingCheckBox->isChecked()); - - // Toggle shading - shadingCheckBox->setChecked(true); - QTest::qWait(10); - - // Verify the node's shading changed - EXPECT_TRUE(volNode->getShading()); -} - -TEST_F(VolumeDialogTest, AmbientPropertyChange) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - // Find ambient spin box (should be one of the first few with range 0-1) - QList spinBoxes = dialog->findChildren(); - ASSERT_GT(spinBoxes.size(), 0); - - // Try to find and set ambient (it's typically the first material property) - for (auto *spinBox : spinBoxes) { - if (spinBox->minimum() == 0.0 && spinBox->maximum() == 1.0) { - double oldValue = volNode->getAmbient(); - double newValue = 0.42; - - spinBox->setValue(newValue); - QTest::qWait(10); - - // Check if this changed the ambient - if (!qFuzzyCompare(volNode->getAmbient(), oldValue)) { - EXPECT_DOUBLE_EQ(volNode->getAmbient(), newValue); - return; - } - } - } -} - -TEST_F(VolumeDialogTest, DiffusePropertyChange) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - double originalDiffuse = volNode->getDiffuse(); - double newDiffuse = 0.67; - - // Find and change diffuse - QList spinBoxes = dialog->findChildren(); - for (auto *spinBox : spinBoxes) { - if (spinBox->minimum() == 0.0 && spinBox->maximum() == 1.0) { - spinBox->setValue(newDiffuse); - QTest::qWait(10); - - if (!qFuzzyCompare(volNode->getDiffuse(), originalDiffuse)) { - EXPECT_DOUBLE_EQ(volNode->getDiffuse(), newDiffuse); - return; - } - } - } -} - -TEST_F(VolumeDialogTest, SpecularPropertyChange) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - double originalSpecular = volNode->getSpecular(); - double newSpecular = 0.89; - - QList spinBoxes = dialog->findChildren(); - for (auto *spinBox : spinBoxes) { - if (spinBox->minimum() == 0.0 && spinBox->maximum() == 1.0) { - spinBox->setValue(newSpecular); - QTest::qWait(10); - - if (!qFuzzyCompare(volNode->getSpecular(), originalSpecular)) { - EXPECT_DOUBLE_EQ(volNode->getSpecular(), newSpecular); - return; - } - } - } -} - -TEST_F(VolumeDialogTest, SpecularPowerChange) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - double newSpecularPower = 64.0; - - // Find specular power spin box (range 0-128) - QList spinBoxes = dialog->findChildren(); - for (auto *spinBox : spinBoxes) { - if (spinBox->maximum() == 128.0) { - spinBox->setValue(newSpecularPower); - QTest::qWait(10); - - EXPECT_DOUBLE_EQ(volNode->getSpecularPower(), newSpecularPower); - return; - } - } -} - -TEST_F(VolumeDialogTest, SampleDistanceChange) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - double newSampleDistance = 0.5; - - // Find sample distance spin box (range 0.001-10.0) - QList spinBoxes = dialog->findChildren(); - for (auto *spinBox : spinBoxes) { - if (qFuzzyCompare(spinBox->minimum(), 0.001) && qFuzzyCompare(spinBox->maximum(), 10.0)) { - spinBox->setValue(newSampleDistance); - QTest::qWait(10); - - EXPECT_DOUBLE_EQ(volNode->getSampleDistance(), newSampleDistance); - return; - } - } -} - -TEST_F(VolumeDialogTest, AutoAdjustSampleDistancesToggle) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - volNode->setAutoAdjustSampleDistances(false); - - dialog = new VolumeDialog(sceneGraph); - - // Find auto-adjust checkbox - QCheckBox *autoAdjustCheckBox = nullptr; - QList checkBoxes = dialog->findChildren(); - for (auto *cb : checkBoxes) { - if (cb->text().contains("Auto", Qt::CaseInsensitive)) { - autoAdjustCheckBox = cb; - break; - } - } - - ASSERT_NE(autoAdjustCheckBox, nullptr); - EXPECT_FALSE(autoAdjustCheckBox->isChecked()); - - // Toggle auto-adjust - autoAdjustCheckBox->setChecked(true); - QTest::qWait(10); - - EXPECT_TRUE(volNode->getAutoAdjustSampleDistances()); -} - -TEST_F(VolumeDialogTest, DynamicVolumeAddition) { - dialog = new VolumeDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 0); - - // Add volume after dialog creation - cvc::volume vol = createTestVolume(); - sceneGraph->addGraphics("new_vol", vol); - - // Wait for state tree signals to propagate - QTest::qWait(50); - - // The combo box should update automatically - EXPECT_EQ(comboBox->count(), 1); -} - -TEST_F(VolumeDialogTest, DynamicVolumeRemoval) { - cvc::volume vol = createTestVolume(); - sceneGraph->addGraphics("test_vol", vol); - - dialog = new VolumeDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 1); - - // Remove volume - sceneGraph->removeGraphics("test_vol"); - - // Wait for state tree signal and Qt signal processing - QTest::qWait(100); - - // The combo box should update automatically - EXPECT_EQ(comboBox->count(), 0); -} - -TEST_F(VolumeDialogTest, NestedVolumes) { - cvc::volume vol1 = createTestVolume(); - cvc::volume vol2 = createTestVolume(); - - auto parent = sceneGraph->addGraphics("parent_vol", vol1); - ASSERT_NE(parent, nullptr); - - // Add child volume - auto child = parent->createChild("child_vol", vol2); - ASSERT_NE(child, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - - // Both parent and child should be listed - EXPECT_EQ(comboBox->count(), 2); -} - -TEST_F(VolumeDialogTest, SelectionPreservation) { - cvc::volume vol1 = createTestVolume(); - cvc::volume vol2 = createTestVolume(); - - sceneGraph->addGraphics("vol1", vol1); - sceneGraph->addGraphics("vol2", vol2); - - dialog = new VolumeDialog(sceneGraph); - - QComboBox *comboBox = dialog->findChild(); - ASSERT_NE(comboBox, nullptr); - EXPECT_EQ(comboBox->count(), 2); - - // Select second volume - comboBox->setCurrentIndex(1); - QTest::qWait(10); - - QString selectedName = comboBox->currentText(); - - // Add a third volume - cvc::volume vol3 = createTestVolume(); - sceneGraph->addGraphics("vol3", vol3); - QTest::qWait(50); - - // Selection should be preserved - EXPECT_EQ(comboBox->currentText(), selectedName); -} - -TEST_F(VolumeDialogTest, ScalarOpacityUnitDistanceChange) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - auto volNode = std::dynamic_pointer_cast(node); - ASSERT_NE(volNode, nullptr); - - dialog = new VolumeDialog(sceneGraph); - - double newValue = 2.5; - - // Find scalar opacity unit distance spin box (range 0.001-100.0) - QList spinBoxes = dialog->findChildren(); - for (auto *spinBox : spinBoxes) { - if (qFuzzyCompare(spinBox->minimum(), 0.001) && qFuzzyCompare(spinBox->maximum(), 100.0)) { - spinBox->setValue(newValue); - QTest::qWait(10); - - EXPECT_DOUBLE_EQ(volNode->getScalarOpacityUnitDistance(), newValue); - return; - } - } -} - -// Test safe deletion of volume from state tree -TEST_F(VolumeDialogTest, SafeVolumeDeletion) { - cvc::volume vol = createTestVolume(); - auto node = sceneGraph->addGraphics("test_vol", vol); - ASSERT_NE(node, nullptr); - - // Verify volume is in scene graph - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 1); - - // Get weak pointer to track object lifetime - std::weak_ptr weakNode = std::dynamic_pointer_cast(node); - node.reset(); // Release our reference - - // Object should still exist (held by scene graph) - EXPECT_FALSE(weakNode.expired()); - - // Remove volume - should not crash - sceneGraph->removeGraphics("test_vol"); - - // Verify removal was clean - C++ object should be destroyed - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 0); - - // The VolumeNode object should now be destroyed (no more references) - EXPECT_TRUE(weakNode.expired()); - - // State tree should still be accessible without crashes (even if nodes remain) - std::string statePrefix = sceneGraph->getStatePrefix(); - EXPECT_NO_THROW({ - auto &state = cvc::state::instance(volrover3::app())(statePrefix + ".graphics.root.children"); - // State tree nodes may persist, but accessing them shouldn't crash - size_t childCount = state.numChildren(); - EXPECT_GE(childCount, 0); // Just verify we can read without crashing - }); -} - -// Test multiple volume additions and removals -TEST_F(VolumeDialogTest, MultipleVolumeAddRemoveCycles) { - cvc::volume vol = createTestVolume(); - - // Perform multiple add/remove cycles - for (int i = 0; i < 5; ++i) { - auto node = sceneGraph->addGraphics("cycle_vol", vol); - ASSERT_NE(node, nullptr); - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 1); - - sceneGraph->removeGraphics("cycle_vol"); - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 0); - } - - // State tree should still be valid - std::string statePrefix = sceneGraph->getStatePrefix(); - EXPECT_NO_THROW({ - auto &state = cvc::state::instance(volrover3::app())(statePrefix + ".graphics.root"); - EXPECT_TRUE(true); // Just verify no crash accessing state - }); -} - -// Test repeated add/remove cycles for memory safety -TEST_F(VolumeDialogTest, RepeatedVolumeAddRemoveSafety) { - cvc::volume vol = createTestVolume(); - - // Perform multiple add/remove cycles - for (int i = 0; i < 10; ++i) { - auto node = sceneGraph->addGraphics("test_vol_" + std::to_string(i % 3), vol); - ASSERT_NE(node, nullptr); - - // Immediately remove it - sceneGraph->removeGraphics("test_vol_" + std::to_string(i % 3)); - } - - // Should complete without crashes or memory issues - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 0); -} - -// Test removal of non-existent volume (error handling) -TEST_F(VolumeDialogTest, RemoveNonExistentVolume) { - // Should not crash when removing non-existent volume - EXPECT_NO_THROW({ sceneGraph->removeGraphics("does_not_exist"); }); - - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 0); -} - -// Test replacing volumes (remove + add with same name) -TEST_F(VolumeDialogTest, VolumeReplacementSafety) { - cvc::volume vol1 = createTestVolume(); - cvc::volume vol2 = createTestVolume(); - - // Add initial volume - auto node1 = sceneGraph->addGraphics("replaceable_vol", vol1); - ASSERT_NE(node1, nullptr); - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 1); - - // Get weak pointer to track first object lifetime - std::weak_ptr weakNode1 = std::dynamic_pointer_cast(node1); - node1.reset(); - - // Replace with new volume (addGraphics should handle removal automatically) - auto node2 = sceneGraph->addGraphics("replaceable_vol", vol2); - ASSERT_NE(node2, nullptr); - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 1); - - // First node should be destroyed after replacement - EXPECT_TRUE(weakNode1.expired()); - - // Verify state tree is still accessible (doesn't crash) - std::string statePrefix = sceneGraph->getStatePrefix(); - EXPECT_NO_THROW({ - auto &state = cvc::state::instance(volrover3::app())(statePrefix + ".graphics.root.children"); - size_t childCount = state.numChildren(); - EXPECT_GE(childCount, 0); // Just verify we can read without crashing - }); - - // Final cleanup - sceneGraph->removeGraphics("replaceable_vol"); - EXPECT_EQ(sceneGraph->getVolumeGraphicsCount(), 0); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/tests/VolumeNodeTest.cpp b/src/volrover3/tests/VolumeNodeTest.cpp deleted file mode 100644 index fe20b0d7..00000000 --- a/src/volrover3/tests/VolumeNodeTest.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class VolumeNodeTest : public ::testing::Test { -protected: - static void SetUpTestSuite() { - // Disable threading for state_object to avoid race conditions during destruction - cvc::state_object::setUseThreading(false); - } - - void SetUp() override { - // Create a simple test volume with known data range - testVolume = cvc::volume(ctx, cvc::dimension(10, 10, 10), cvc::UChar, - cvc::bounding_box(0.0, 0.0, 0.0, 9.0, 9.0, 9.0)); - - // Fill with gradient data - for (unsigned int k = 0; k < 10; k++) { - for (unsigned int j = 0; j < 10; j++) { - for (unsigned int i = 0; i < 10; i++) { - testVolume(i, j, k, static_cast(i + j + k)); - } - } - } - - // Set min/max explicitly - testVolume.min(0.0); - testVolume.max(27.0); - - // Create a volume with larger bounds for spacing tests - largeVolume = cvc::volume(ctx, cvc::dimension(171, 171, 171), cvc::Float, - cvc::bounding_box(0.0, 0.0, 0.0, 945.0, 945.0, 945.0)); - - // Fill with test data - for (unsigned int k = 0; k < 171; k++) { - for (unsigned int j = 0; j < 171; j++) { - for (unsigned int i = 0; i < 171; i++) { - largeVolume(i, j, k, static_cast(i * 0.01 + j * 0.01 + k * 0.01)); - } - } - } - - largeVolume.min(-5.185418); - largeVolume.max(8.067500); - } - - void TearDown() override {} - - cvc::app ctx; - cvc::volume testVolume{ctx}; - cvc::volume largeVolume{ctx}; -}; - -// ============================================================================ -// Volume Span and Spacing Tests (Critical for rendering) -// ============================================================================ - -TEST_F(VolumeNodeTest, VolumeSpanCalculation) { - // Test that volume span is calculated correctly - // XSpan = (XMax - XMin) / (XDim - 1) - - // Small volume: (9-0)/(10-1) = 1.0 - EXPECT_DOUBLE_EQ(testVolume.XSpan(), 1.0); - EXPECT_DOUBLE_EQ(testVolume.YSpan(), 1.0); - EXPECT_DOUBLE_EQ(testVolume.ZSpan(), 1.0); - - // Large volume: (945-0)/(171-1) = 5.558823... - double expectedSpan = 945.0 / 170.0; - EXPECT_NEAR(largeVolume.XSpan(), expectedSpan, 0.0001); - EXPECT_NEAR(largeVolume.YSpan(), expectedSpan, 0.0001); - EXPECT_NEAR(largeVolume.ZSpan(), expectedSpan, 0.0001); -} - -TEST_F(VolumeNodeTest, VolumeSpacingForVTK) { - VolumeNode node(ctx, "test_volume"); - node.setVolume(largeVolume); - - // The critical fix: spacing should be (Max - Min) / Dim, NOT Span() / Dim - // Spacing = (945 - 0) / 171 = 5.526315... - double expectedSpacing = 945.0 / 171.0; - - // We don't have direct access to VTK spacing, but we can verify - // the volume bounds are correct - cvc::bounding_box bbox = node.getBoundingBox(); - - EXPECT_DOUBLE_EQ(bbox[0], 0.0); - EXPECT_DOUBLE_EQ(bbox[1], 0.0); - EXPECT_DOUBLE_EQ(bbox[2], 0.0); - EXPECT_DOUBLE_EQ(bbox[3], 945.0); - EXPECT_DOUBLE_EQ(bbox[4], 945.0); - EXPECT_DOUBLE_EQ(bbox[5], 945.0); - - // Verify dimensions - EXPECT_EQ(largeVolume.XDim(), 171); - EXPECT_EQ(largeVolume.YDim(), 171); - EXPECT_EQ(largeVolume.ZDim(), 171); -} - -TEST_F(VolumeNodeTest, VolumeSpacingNotSpanDivDim) { - // This test verifies the critical bug fix - // WRONG: spacing = XSpan() / XDim() - // RIGHT: spacing = (XMax() - XMin()) / XDim() - - double wrongSpacing = largeVolume.XSpan() / largeVolume.XDim(); - double correctSpacing = (largeVolume.XMax() - largeVolume.XMin()) / largeVolume.XDim(); - - // These should be different! - EXPECT_NE(wrongSpacing, correctSpacing); - - // The wrong calculation gives ~0.0325 - EXPECT_NEAR(wrongSpacing, 0.0325, 0.001); - - // The correct calculation gives ~5.526 - EXPECT_NEAR(correctSpacing, 5.526, 0.001); -} - -// ============================================================================ -// Transfer Function and Data Range Tests -// ============================================================================ - -TEST_F(VolumeNodeTest, VolumeDataRange) { - VolumeNode node(ctx, "test_volume"); - node.setVolume(testVolume); - - // Verify metadata contains correct data range - EXPECT_TRUE(node.hasMetadata("data_min")); - EXPECT_TRUE(node.hasMetadata("data_max")); - - double dataMin = std::any_cast(node.getMetadata("data_min")); - double dataMax = std::any_cast(node.getMetadata("data_max")); - - EXPECT_DOUBLE_EQ(dataMin, 0.0); - EXPECT_DOUBLE_EQ(dataMax, 27.0); -} - -TEST_F(VolumeNodeTest, LargeVolumeDataRange) { - VolumeNode node(ctx, "large_volume"); - node.setVolume(largeVolume); - - // Verify the actual data range from the problematic volume - EXPECT_TRUE(node.hasMetadata("data_min")); - EXPECT_TRUE(node.hasMetadata("data_max")); - - double dataMin = std::any_cast(node.getMetadata("data_min")); - double dataMax = std::any_cast(node.getMetadata("data_max")); - - EXPECT_DOUBLE_EQ(dataMin, -5.185418); - EXPECT_DOUBLE_EQ(dataMax, 8.067500); -} - -// ============================================================================ -// Volume Label Tests -// ============================================================================ - -TEST_F(VolumeNodeTest, VolumeLabelDefaultState) { - VolumeNode node(ctx, "test.volume", "test_volume"); - - // Label should be off by default - EXPECT_FALSE(node.getShowLabel()); - // Default label text should be node name - EXPECT_EQ(node.getLabelText(), "test_volume"); - // Default size should be 14 - EXPECT_EQ(node.getLabelSize(), 14); -} - -// ============================================================================ -// Volume Bounding Box Tests -// ============================================================================ - -TEST_F(VolumeNodeTest, VolumeBBoxBounds) { - VolumeNode node(ctx, "test_volume"); - node.setVolume(testVolume); - - cvc::bounding_box bbox = node.getBoundingBox(); - - // Should match volume bounds - EXPECT_DOUBLE_EQ(bbox[0], 0.0); - EXPECT_DOUBLE_EQ(bbox[1], 0.0); - EXPECT_DOUBLE_EQ(bbox[2], 0.0); - EXPECT_DOUBLE_EQ(bbox[3], 9.0); - EXPECT_DOUBLE_EQ(bbox[4], 9.0); - EXPECT_DOUBLE_EQ(bbox[5], 9.0); -} - -// ============================================================================ -// Volume Metadata Tests -// ============================================================================ - -TEST_F(VolumeNodeTest, VolumeMetadataComplete) { - VolumeNode node(ctx, "test_volume"); - node.setVolume(testVolume); - - // Check all expected metadata fields exist - EXPECT_TRUE(node.hasMetadata("dim_x")); - EXPECT_TRUE(node.hasMetadata("dim_y")); - EXPECT_TRUE(node.hasMetadata("dim_z")); - EXPECT_TRUE(node.hasMetadata("data_min")); - EXPECT_TRUE(node.hasMetadata("data_max")); - EXPECT_TRUE(node.hasMetadata("bbox_min_x")); - EXPECT_TRUE(node.hasMetadata("bbox_max_z")); - EXPECT_TRUE(node.hasMetadata("voxel_type")); -} - -TEST_F(VolumeNodeTest, VolumeMetadataValues) { - VolumeNode node(ctx, "test_volume"); - node.setVolume(testVolume); - - // Verify dimension metadata - EXPECT_EQ(std::any_cast(node.getMetadata("dim_x")), 10); - EXPECT_EQ(std::any_cast(node.getMetadata("dim_y")), 10); - EXPECT_EQ(std::any_cast(node.getMetadata("dim_z")), 10); - - // Verify bounding box metadata - EXPECT_DOUBLE_EQ(std::any_cast(node.getMetadata("bbox_min_x")), 0.0); - EXPECT_DOUBLE_EQ(std::any_cast(node.getMetadata("bbox_max_x")), 9.0); - - // Verify voxel type - std::string voxelType = std::any_cast(node.getMetadata("voxel_type")); - EXPECT_EQ(voxelType, "unsigned_char"); -} - -// ============================================================================ -// Clip Plane Tests for VolumeNode -// ============================================================================ - -TEST_F(VolumeNodeTest, ClipChildrenDefault) { - SceneGraph sceneGraph("volume_clip_test"); - auto parent = sceneGraph.addGraphics("volume_clip_test.parent", testVolume); - - EXPECT_FALSE(parent->getClipChildren()); -} - -TEST_F(VolumeNodeTest, SetClipChildren) { - SceneGraph sceneGraph("volume_clip_test"); - auto parent = sceneGraph.addGraphics("volume_clip_test.setter", testVolume); - - parent->setClipChildren(true); - EXPECT_TRUE(parent->getClipChildren()); - - parent->setClipChildren(false); - EXPECT_FALSE(parent->getClipChildren()); -} - -TEST_F(VolumeNodeTest, ClipPlanesGenerated) { - SceneGraph sceneGraph("volume_clip_test"); - auto parent = sceneGraph.addGraphics("volume_clip_test.planes", testVolume); - - parent->setClipChildren(true); - - // Should have 6 clip planes - vtkPlaneCollection *planes = parent->getClipPlanes(); - ASSERT_NE(planes, nullptr); - EXPECT_EQ(planes->GetNumberOfItems(), 6); -} - -TEST_F(VolumeNodeTest, VolumeClipsGeometryChild) { - // Test that a VolumeNode can clip a GeometryNode child - SceneGraph sceneGraph("volume_clip_test"); - - auto volumeParent = sceneGraph.addGraphics("volume_clip_test.vol_parent", testVolume); - - // Create a geometry child - cvc::geometry childGeom; - childGeom.points().push_back({5.0, 5.0, 5.0}); - childGeom.points().push_back({15.0, 15.0, 15.0}); - - auto geomChild = - std::make_shared(ctx, "volume_clip_test.vol_parent.geom_child", "child"); - geomChild->setGeometry(childGeom); - volumeParent->addGraphicsChild(geomChild); - - // Enable clipping on volume parent - volumeParent->setClipChildren(true); - - // Give time for threading/event queue to process - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Verify planes were created - vtkPlaneCollection *planes = volumeParent->getClipPlanes(); - ASSERT_NE(planes, nullptr); - EXPECT_EQ(planes->GetNumberOfItems(), 6); -} - -TEST_F(VolumeNodeTest, GeometryClipsVolumeChild) { - // Test that a GeometryNode can clip a VolumeNode child - SceneGraph sceneGraph("volume_clip_test"); - - cvc::geometry parentGeom; - parentGeom.points().push_back({0.0, 0.0, 0.0}); - parentGeom.points().push_back({20.0, 20.0, 20.0}); - - auto geomParent = sceneGraph.addGraphics("volume_clip_test.geom_parent", parentGeom); - - // Create a volume child - auto volumeChild = - std::make_shared(ctx, "volume_clip_test.geom_parent.vol_child", "volume"); - volumeChild->setVolume(testVolume); - geomParent->addGraphicsChild(volumeChild); - - // Enable clipping on geometry parent - geomParent->setClipChildren(true); - - // Give time for threading/event queue to process - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Verify planes were created - vtkPlaneCollection *planes = geomParent->getClipPlanes(); - ASSERT_NE(planes, nullptr); - EXPECT_EQ(planes->GetNumberOfItems(), 6); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/volrover3/volrover3_app.cpp b/src/volrover3/volrover3_app.cpp deleted file mode 100644 index 9a380507..00000000 --- a/src/volrover3/volrover3_app.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include - -namespace volrover3 { -cvc::app &app() { - static cvc::app instance; - return instance; -} -} // namespace volrover3 diff --git a/test_threading_behavior.cpp b/test_threading_behavior.cpp deleted file mode 100644 index b3a19726..00000000 --- a/test_threading_behavior.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// Quick validation test for per-instance threading behavior -#include -#include -#include -#include -#include -#include -#include - -int main() { - // Test 1: Nodes should have threading disabled during construction - std::cout << "Test 1: Node construction with threading disabled..." << std::endl; - { - SceneGraph sg("test1"); - auto root = sg.getGraphicsRoot(); - - // Root and children should have threading disabled initially (set in constructor) - // Then enabled when SceneGraph reference is set - std::cout << " Root node instance threading: " - << (root->getInstanceThreading() ? "enabled" : "disabled") << std::endl; - } - std::cout << " ✓ Test 1 passed (no crashes during construction)" << std::endl; - - // Test 2: Adding graphics with threading enabled should queue events - std::cout << "\nTest 2: Graphics creation with threading enabled..." << std::endl; - { - cvc::state_object::setUseThreading(true); - - SceneGraph sg("test2"); - - cvc::geometry geom; - geom.points().push_back({0.0, 0.0, 0.0}); - geom.points().push_back({1.0, 0.0, 0.0}); - geom.points().push_back({0.0, 1.0, 0.0}); - geom.tris().push_back({0, 1, 2}); - - auto node = sg.addGraphics("test_geom", geom); - - std::cout << " Node instance threading: " - << (node->getInstanceThreading() ? "enabled" : "disabled") << std::endl; - std::cout << " Node has SceneGraph: " << (node->getSceneGraph() != nullptr ? "yes" : "no") - << std::endl; - - // Process any queued events - sg.processEvents(); - - std::cout << " Node is visible: " << (node->isVisible() ? "yes" : "no") << std::endl; - - cvc::state_object::setUseThreading(false); - } - std::cout << " ✓ Test 2 passed" << std::endl; - - // Test 3: Multiple SceneGraphs should have independent threading - std::cout << "\nTest 3: Independent threading per SceneGraph..." << std::endl; - { - SceneGraph sg1("test3a"); - SceneGraph sg2("test3b"); - - auto node1 = sg1.getGraphicsRoot(); - auto node2 = sg2.getGraphicsRoot(); - - std::cout << " SG1 root has SG1: " << (node1->getSceneGraph() == &sg1 ? "yes" : "no") - << std::endl; - std::cout << " SG2 root has SG2: " << (node2->getSceneGraph() == &sg2 ? "yes" : "no") - << std::endl; - } - std::cout << " ✓ Test 3 passed" << std::endl; - - std::cout << "\n✓ All threading behavior tests passed!" << std::endl; - return 0; -}