diff --git a/.github/superbuild-caching.md b/.github/superbuild-caching.md new file mode 100644 index 0000000000..5624f529ee --- /dev/null +++ b/.github/superbuild-caching.md @@ -0,0 +1,85 @@ +# Superbuild External Caching + +CI caches the compiled external libraries so that builds after the first only need to compile SCIRun itself — not Boost, Qt, Zlib, Python, and ~15 other dependencies from source. + +## How it works + +SCIRun uses a CMake [Superbuild](../Superbuild/): the outer CMake project drives `ExternalProject_Add` for each dependency, then builds SCIRun itself as the final step. Each external has a series of steps (download → update → configure → build → install), and CMake records completion in stamp files under `Externals/Stamp/`. + +### What gets cached + +The `actions/cache` step saves and restores two directories: + +| Directory | Contents | +|-----------|----------| +| `bin/Externals/Install/` | Headers, compiled libs, and cmake config files for every external | +| `bin/Externals/Stamp/` | CMake stamp files that tell `make` which ExternalProject steps are done | + +The cache key is `superbuild-v3----py`. If any Superbuild cmake file changes, the hash changes and the cache is bypassed entirely — the next build is a full cold build that repopulates the cache. + +### Why stamp-touching is needed + +CMake rewrites the helper scripts in `Externals/tmp/` (e.g. `Zlib_external-cfgcmd.cmake`) on every configure run, giving them the current timestamp. The Makefile rules for ExternalProject steps depend on those scripts, so restored stamp files (which carry an older timestamp) always appear stale, causing `make` to try re-running every step against source directories that don't exist. + +The fix is in `build.sh` (and an equivalent block in the Windows workflow): set the modification time of every file in `Externals/Stamp/` to the year **2100**, immediately after `cmake` configure but before `make`. + +A plain `touch` (current time) is not sufficient. CMake's `ExternalProject_Add_Step` generates `add_custom_command` calls whose `DEPENDS` list includes the `ExternalProject.cmake` module file itself. If the GitHub Actions runner has a newer cmake installation than the runner that created the cached stamps, `ExternalProject.cmake` carries a newer timestamp and make considers every step stale regardless of touching. Setting stamps to 2100 makes them unconditionally newer than any cmake module file on any runner. + +### Why `UPDATE_COMMAND ""` + +By default, `ExternalProject_Add` with `GIT_REPOSITORY` generates a `*-gitupdate.cmake` script that fetches the remote and compares the HEAD hash to `GIT_TAG` on every build. With a restored stamp, this script would still run (for the same timestamp reason above), fail with `fatal: not a git repository` because `Source/` isn't cached, and abort the build. + +Setting `UPDATE_COMMAND ""` in every git-based external tells CMake to generate a no-op update step instead — no `gitupdate.cmake` is written, no git operations are needed. + +### Why `EP_UPDATE_DISCONNECTED TRUE` + +Set globally in `Superbuild.cmake` as an extra safeguard. If for any reason an update stamp is missing (e.g. partial cache), CMake will run the (now no-op) update step once rather than triggering a network fetch. + +### Cache key variants + +Each OS × build variant × python flag gets its own cache entry. The restore-key is deliberately limited to same-variant matches (`--` prefix but no python suffix) so that a `headless` cache is never used to satisfy a `gui` build — they compile different externals. + +--- + +## When an external changes + +### Bumping a `GIT_TAG` + +Edit the relevant file in `Superbuild/` (e.g. `Superbuild/ZlibExternal.cmake`) and change `GIT_TAG`. Because the cache key hashes all `Superbuild/*.cmake` files, the existing cache is automatically invalidated. The next CI run does a full cold build (~1-2 hours) and saves a fresh cache. + +**You do not need to bump the cache version number.** + +### Adding a new external + +1. Create `Superbuild/MyLibExternal.cmake` following the pattern of an existing file. +2. Add `UPDATE_COMMAND ""` alongside `GIT_TAG` (or after `URL` for tarball-based externals that don't need it). +3. Wire it into `Superbuild/Superbuild.cmake`. + +The cache key hash will change automatically. Cold build on next CI run, new cache saved. + +### Removing an external + +Delete the cmake file and remove it from `Superbuild.cmake`. Cache key changes; next run is cold. + +### Changing configure or build flags for an external + +Edit the relevant `Superbuild/*.cmake` file. Cache key changes; next run is cold. + +### Manually busting the cache + +If the cache is known-bad (e.g. a broken partial save got promoted), bump the version prefix in `reusable-build.yml`: + +```yaml +key: superbuild-v3-... → key: superbuild-v4-... +``` + +Update the `restore-keys` lines to match. On the next run every platform does a cold build and the old cache entries age out naturally (GitHub Actions evicts caches after 7 days of disuse). + +### Local development note + +The `touch Externals/Stamp/` step in `build.sh` runs on local builds too. If you change a `GIT_TAG` locally without clearing your build directory, `make` will skip the download/configure/build steps for that external and use the old installed version. The safest workflow when updating an external locally is: + +```sh +rm -rf bin/Externals +./build.sh +``` diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 1d8832d9ec..dc13cdcedf 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -53,6 +53,9 @@ on: jobs: build: runs-on: ${{ inputs.runner }} + permissions: + checks: write + contents: read steps: - name: Checkout uses: actions/checkout@v6 @@ -70,6 +73,9 @@ jobs: - name: Print Qt dir run: echo "QT_ROOT_DIR=${QT_ROOT_DIR:-(unset)}" + - name: Set up sccache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Checkout SCIRunTestData if: ${{ inputs.run-regression-tests || inputs.coverage }} uses: actions/checkout@v6 @@ -89,6 +95,8 @@ jobs: run: | set -eux CMD=(./build.sh) + CMD+=("-DCMAKE_C_COMPILER_LAUNCHER=sccache") + CMD+=("-DCMAKE_CXX_COMPILER_LAUNCHER=sccache") # Variant-specific flags if [ "${{ inputs.variant }}" = "gui" ]; then # Pass Qt path (from installer or explicit), and set SCIRUN_QT_MIN_VERSION appropriately @@ -154,6 +162,8 @@ jobs: # Build arguments for cmake $cmakeArgs = @() + $cmakeArgs += "-DCMAKE_C_COMPILER_LAUNCHER=sccache" + $cmakeArgs += "-DCMAKE_CXX_COMPILER_LAUNCHER=sccache" # Always use Visual Studio generator on Windows to ensure MSVC flags are used $cmakeArgs += '-G' @@ -250,7 +260,8 @@ jobs: if: ${{ inputs.run-unit-tests && runner.os != 'Windows' }} working-directory: bin/SCIRun run: | - ctest --output-on-failure -j4 -E "\.Test\.ExampleNetwork\." \ + ctest --output-on-failure --timeout 300 -j4 -E "\.Test\.ExampleNetwork\." \ + --output-junit "$GITHUB_WORKSPACE/junit-unit.xml" \ 2>&1 | tee /tmp/unit-test-results.txt continue-on-error: true @@ -259,6 +270,7 @@ jobs: working-directory: bin/SCIRun run: | ctest --output-on-failure --timeout 300 -j3 -R "\.Test\.ExampleNetwork\." \ + --output-junit "$GITHUB_WORKSPACE/junit-regression.xml" \ 2>&1 | tee /tmp/regression-test-results.txt continue-on-error: true @@ -276,7 +288,8 @@ jobs: working-directory: build/SCIRun shell: pwsh run: | - ctest --output-on-failure -j4 -C Release -E "\.Test\.ExampleNetwork\." ` + ctest --output-on-failure --timeout 300 -j4 -C Release -E "\.Test\.ExampleNetwork\." ` + --output-junit "$env:GITHUB_WORKSPACE\junit-unit.xml" ` 2>&1 | Tee-Object -FilePath "$env:GITHUB_WORKSPACE\unit-test-results.txt" continue-on-error: true @@ -286,9 +299,28 @@ jobs: shell: pwsh run: | ctest --output-on-failure --timeout 300 -j3 -C Release -R "\.Test\.ExampleNetwork\." ` + --output-junit "$env:GITHUB_WORKSPACE\junit-regression.xml" ` 2>&1 | Tee-Object -FilePath "$env:GITHUB_WORKSPACE\regression-test-results.txt" continue-on-error: true + - name: Publish unit test results + uses: dorny/test-reporter@v1 + if: always() && inputs.run-unit-tests + with: + name: "Unit Tests (${{ runner.os }} / ${{ inputs.variant }})" + path: junit-unit.xml + reporter: java-junit + fail-on-error: false + + - name: Publish regression test results + uses: dorny/test-reporter@v1 + if: always() && inputs.run-regression-tests + with: + name: "Regression Tests (${{ runner.os }} / ${{ inputs.variant }})" + path: junit-regression.xml + reporter: java-junit + fail-on-error: false + - name: Upload test logs (Unix) if: ${{ (inputs.run-unit-tests || inputs.run-regression-tests) && runner.os != 'Windows' }} uses: actions/upload-artifact@v6 @@ -299,6 +331,8 @@ jobs: /tmp/unit-test-results.txt /tmp/regression-test-results.txt bin/SCIRun/Testing/Temporary/LastTest.log + junit-unit.xml + junit-regression.xml - name: Upload test logs (Windows) if: ${{ (inputs.run-unit-tests || inputs.run-regression-tests) && runner.os == 'Windows' }} @@ -310,6 +344,8 @@ jobs: unit-test-results.txt regression-test-results.txt build/SCIRun/Testing/Temporary/LastTest.log + junit-unit.xml + junit-regression.xml - name: Run tests + coverage report (macOS) if: ${{ inputs.coverage && runner.os == 'macOS' }} @@ -343,3 +379,4 @@ jobs: bin/SCIRun/SCIRun-*-Darwin.pkg bin/SCIRun/SCIRun-*-win64.exe build/**/*.msi + diff --git a/Superbuild/Cleaver2External.cmake b/Superbuild/Cleaver2External.cmake index ecc82d58ce..5936a4f3f9 100644 --- a/Superbuild/Cleaver2External.cmake +++ b/Superbuild/Cleaver2External.cmake @@ -32,6 +32,7 @@ SET(cleaver2_GIT_TAG "origin/scirun-5.0.0-beta") ExternalProject_Add(Cleaver2_external GIT_REPOSITORY "https://github.com/CIBC-Internal/Cleaver2Library.git" GIT_TAG ${cleaver2_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/CtkExternal.cmake b/Superbuild/CtkExternal.cmake index 3aff0deb8e..3161131a8d 100644 --- a/Superbuild/CtkExternal.cmake +++ b/Superbuild/CtkExternal.cmake @@ -45,6 +45,7 @@ LIST(APPEND CTK_CACHE_ARGS ExternalProject_Add(Ctk_external GIT_REPOSITORY "https://github.com/CIBC-Internal/CTK.git" GIT_TAG ${ctk_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/DataExternal.cmake b/Superbuild/DataExternal.cmake index 6b56559d65..cc69820a03 100644 --- a/Superbuild/DataExternal.cmake +++ b/Superbuild/DataExternal.cmake @@ -37,6 +37,7 @@ SET(data_DIR "${SEG3D_BINARY_DIR}/Seg3DData") ExternalProject_Add(Data_external GIT_REPOSITORY ${data_GIT_URL} GIT_TAG ${data_GIT_TAG} + UPDATE_COMMAND "" SOURCE_DIR ${data_DIR} BUILD_COMMAND "" CONFIGURE_COMMAND "" diff --git a/Superbuild/FreetypeExternal.cmake b/Superbuild/FreetypeExternal.cmake index a07e655e9e..64a055e349 100644 --- a/Superbuild/FreetypeExternal.cmake +++ b/Superbuild/FreetypeExternal.cmake @@ -32,6 +32,7 @@ SET(freetype_GIT_TAG "origin/scirun-5.0.0") ExternalProject_Add(Freetype_external GIT_REPOSITORY "https://github.com/CIBC-Internal/freetype.git" GIT_TAG ${freetype_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/GLMExternal.cmake b/Superbuild/GLMExternal.cmake index 968bf3796f..631e1f7dc5 100644 --- a/Superbuild/GLMExternal.cmake +++ b/Superbuild/GLMExternal.cmake @@ -31,6 +31,7 @@ SET_PROPERTY(DIRECTORY PROPERTY "EP_BASE" ${ep_base}) ExternalProject_Add(GLM_external GIT_REPOSITORY "https://github.com/g-truc/glm.git" GIT_TAG "0.9.9.8" + UPDATE_COMMAND "" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/Superbuild/GlewExternal.cmake b/Superbuild/GlewExternal.cmake index 712edba76f..918fa3bdef 100644 --- a/Superbuild/GlewExternal.cmake +++ b/Superbuild/GlewExternal.cmake @@ -37,6 +37,7 @@ ENDIF() ExternalProject_Add(Glew_external GIT_REPOSITORY "https://github.com/CIBC-Internal/glew.git" GIT_TAG ${glew_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/GoogleTestExternal.cmake b/Superbuild/GoogleTestExternal.cmake index 16af038d8b..915df75de3 100644 --- a/Superbuild/GoogleTestExternal.cmake +++ b/Superbuild/GoogleTestExternal.cmake @@ -38,6 +38,7 @@ SET(googletest_GIT_TAG "origin/cibc") ExternalProject_Add(GoogleTest_external GIT_REPOSITORY "https://github.com/CIBC-Internal/googletest.git" GIT_TAG ${googletest_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/LibPNGExternal.cmake b/Superbuild/LibPNGExternal.cmake index 809facc65a..197c02d559 100644 --- a/Superbuild/LibPNGExternal.cmake +++ b/Superbuild/LibPNGExternal.cmake @@ -34,6 +34,7 @@ ExternalProject_Add(LibPNG_external DEPENDS ${libpng_DEPENDENCIES} GIT_REPOSITORY "https://github.com/CIBC-Internal/libpng.git" GIT_TAG ${libpng_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/LodePngExternal.cmake b/Superbuild/LodePngExternal.cmake index 1bb401ed2d..377c2e4b61 100644 --- a/Superbuild/LodePngExternal.cmake +++ b/Superbuild/LodePngExternal.cmake @@ -31,6 +31,7 @@ SET_PROPERTY(DIRECTORY PROPERTY "EP_BASE" ${ep_base}) ExternalProject_Add(LodePng_external GIT_REPOSITORY "https://github.com/CIBC-Internal/cibc-lodepng.git" GIT_TAG "origin/master" + UPDATE_COMMAND "" INSTALL_COMMAND "" CMAKE_CACHE_ARGS -DCMAKE_VERBOSE_MAKEFILE:BOOL=${CMAKE_VERBOSE_MAKEFILE} diff --git a/Superbuild/PythonExternal.cmake b/Superbuild/PythonExternal.cmake index 5a644ddc01..1b3010ea1f 100644 --- a/Superbuild/PythonExternal.cmake +++ b/Superbuild/PythonExternal.cmake @@ -91,6 +91,7 @@ IF(UNIX) ExternalProject_Add(Python_external GIT_REPOSITORY ${python_GIT_URL} GIT_TAG ${python_GIT_TAG} + UPDATE_COMMAND "" BUILD_IN_SOURCE ON CONFIGURE_COMMAND ./configure ${python_CONFIGURE_FLAGS} PATCH_COMMAND "" @@ -107,6 +108,7 @@ ELSE() ExternalProject_Add(Python_external GIT_REPOSITORY ${python_GIT_URL} GIT_TAG ${python_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" CONFIGURE_COMMAND PCbuild/build.bat BUILD_IN_SOURCE ON diff --git a/Superbuild/SQLiteExternal.cmake b/Superbuild/SQLiteExternal.cmake index f38e240d5c..cedf2785c5 100644 --- a/Superbuild/SQLiteExternal.cmake +++ b/Superbuild/SQLiteExternal.cmake @@ -32,6 +32,7 @@ SET(sqlite_GIT_TAG "origin/scirun-5.0.0-beta") ExternalProject_Add(SQLite_external GIT_REPOSITORY "https://github.com/CIBC-Internal/sqlite.git" GIT_TAG ${sqlite_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/SpdLogExternal.cmake b/Superbuild/SpdLogExternal.cmake index 5f55b302c8..0f0a2282ba 100644 --- a/Superbuild/SpdLogExternal.cmake +++ b/Superbuild/SpdLogExternal.cmake @@ -31,6 +31,7 @@ SET_PROPERTY(DIRECTORY PROPERTY "EP_BASE" ${ep_base}) ExternalProject_Add(SpdLog_external GIT_REPOSITORY "https://github.com/gabime/spdlog" GIT_TAG "v1.10.0" + UPDATE_COMMAND "" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/Superbuild/Superbuild.cmake b/Superbuild/Superbuild.cmake index 33300e30ee..3fa26e1a2a 100644 --- a/Superbuild/Superbuild.cmake +++ b/Superbuild/Superbuild.cmake @@ -28,6 +28,8 @@ # TODO: build from archive - Git not used SET(compress_type "GIT" CACHE INTERNAL "") SET(ep_base "${CMAKE_BINARY_DIR}/Externals" CACHE INTERNAL "") +SET_PROPERTY(DIRECTORY PROPERTY "EP_BASE" ${ep_base}) +SET_PROPERTY(DIRECTORY PROPERTY "EP_UPDATE_DISCONNECTED" TRUE) ########################################### # Force superbuild Python, prevent system Python binding diff --git a/Superbuild/TeemExternal.cmake b/Superbuild/TeemExternal.cmake index ea3c227fdc..efed0c5892 100644 --- a/Superbuild/TeemExternal.cmake +++ b/Superbuild/TeemExternal.cmake @@ -34,6 +34,7 @@ ExternalProject_Add(Teem_external DEPENDS ${teem_DEPENDENCIES} GIT_REPOSITORY "https://github.com/SCIInstitute/teem.git" GIT_TAG ${teem_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/Superbuild/TestDataConfig.cmake b/Superbuild/TestDataConfig.cmake index e6710141f4..501181eedf 100644 --- a/Superbuild/TestDataConfig.cmake +++ b/Superbuild/TestDataConfig.cmake @@ -38,6 +38,7 @@ SET(test_data_DOWNLOAD_DIR "${CMAKE_BINARY_DIR}/download/SCIRunTestData") ExternalProject_Add(SCIRunTestData_external GIT_REPOSITORY ${test_data_GIT_URL} GIT_TAG ${test_data_GIT_TAG} + UPDATE_COMMAND "" SOURCE_DIR ${test_data_DIR} BUILD_COMMAND "" CONFIGURE_COMMAND "" diff --git a/Superbuild/TnyExternal.cmake b/Superbuild/TnyExternal.cmake index a3b52fe754..684c211d86 100644 --- a/Superbuild/TnyExternal.cmake +++ b/Superbuild/TnyExternal.cmake @@ -31,6 +31,7 @@ SET_PROPERTY(DIRECTORY PROPERTY "EP_BASE" ${ep_base}) ExternalProject_Add(Tny_external GIT_REPOSITORY "https://github.com/CIBC-Internal/Tny.git" GIT_TAG "origin/scirun-5.0.0" + UPDATE_COMMAND "" INSTALL_COMMAND "" CMAKE_CACHE_ARGS -DCMAKE_VERBOSE_MAKEFILE:BOOL=${CMAKE_VERBOSE_MAKEFILE} diff --git a/Superbuild/ToolkitsConfig.cmake b/Superbuild/ToolkitsConfig.cmake index 405a98f550..9479b073cc 100644 --- a/Superbuild/ToolkitsConfig.cmake +++ b/Superbuild/ToolkitsConfig.cmake @@ -45,6 +45,7 @@ MACRO(EXTERNAL_TOOLKIT name) ExternalProject_Add(${toolkit_ExternalProject_name} GIT_REPOSITORY "https://github.com/SCIInstitute/${name}.git" GIT_TAG ${toolkit_GIT_TAG} + UPDATE_COMMAND "" BUILD_IN_SOURCE ON SOURCE_DIR ${toolkit_DIR} BUILD_COMMAND "" diff --git a/Superbuild/ZlibExternal.cmake b/Superbuild/ZlibExternal.cmake index 40da2a5d92..86062dafb4 100644 --- a/Superbuild/ZlibExternal.cmake +++ b/Superbuild/ZlibExternal.cmake @@ -32,6 +32,7 @@ SET(zlib_GIT_TAG "origin/1.3.1") ExternalProject_Add(Zlib_external GIT_REPOSITORY "https://github.com/CIBC-Internal/zlib.git" GIT_TAG ${zlib_GIT_TAG} + UPDATE_COMMAND "" PATCH_COMMAND "" INSTALL_DIR "" INSTALL_COMMAND "" diff --git a/docs/dev_doc/Support-Python-Libraries-Design.md b/docs/dev_doc/Support-Python-Libraries-Design.md new file mode 100644 index 0000000000..ffe4bd64b2 --- /dev/null +++ b/docs/dev_doc/Support-Python-Libraries-Design.md @@ -0,0 +1,66 @@ +# Design doc: Support additional Python libraries (numpy, scipy) + +Related issue: https://github.com/SCIInstitute/SCIRun/issues/2479 + +## Overview + +Goal: ensure SCIRun's Python wrappers and packaging work smoothly with common scientific Python libraries (numpy, scipy) so downstream users can interoperate with ndarrays, use numerical routines, and run tests/examples that depend on these libraries. + +## Motivation + +Many users expect conversions between SCIRun data (Fields, Meshes, DenseMatrix) and numpy arrays without costly copies. CI and packaging must validate compatibility and reproducibility for supported numpy/scipy versions. + +## Requirements + +### Functional +- Zero-copy or documented conversion paths between SCIRun DenseMatrix/Field data and numpy ndarray where possible. +- Helper utilities in Python wrappers: to_numpy(), from_numpy(), memoryview access with clear ownership semantics. +- Example notebooks demonstrating workflows (convert, run numpy ops, convert back, visualize). + +### Build / Packaging +- CI jobs that install targeted numpy/scipy versions and run wrapper tests. +- Wheel/build rules documented for binary compatibility (manylinux, ABI tags) if shipping wheels. +- CMake option or wrapper build config to locate Python and numpy include/abi (find_package or pybind11 helpers). + +### Testing +- Pytest suite that covers conversions, roundtrip tests, and small numerical consistency checks across supported numpy versions. +- Tests run in CI matrix: at least two Python versions (e.g., 3.10, 3.11) and current numpy LTS + one older supported version. + +## API design +- Provide explicit conversion functions on Python objects: + - DenseMatrix.to_numpy(copy=False) -> numpy.ndarray + - DenseMatrix.from_numpy(ndarray, copy=False) -> DenseMatrix + - Field/mesh extractors that return coordinate arrays and attribute arrays +- Clearly document ownership and lifetime: when copy=False, returned ndarray references internal memory until original object is GC'd or buffer is detached. +- Prefer pybind11 buffer protocol for native sharing where feasible. + +## Implementation Plan +1. Prototype conversion layer in a feature branch: + - Implement to_numpy/from_numpy for DenseMatrix using pybind11 buffer interface. + - Add helper functions for common types (float32/64) and shape conventions. +2. Add example Jupyter notebook and small integration test. +3. Update build scripts to detect numpy include/ABI for compiling wrappers; ensure pybind11 is used consistently. +4. Extend to Field/mesh conversions: provide arrays for points, connectivity, and per-vertex/per-cell attributes. +5. Add CI matrix entries (Python versions + numpy versions) and run pytest suite. + +## Acceptance criteria +- Conversions implemented and covered by tests. +- CI builds successfully with numpy present; tests pass in at least one CI job. +- Documentation and example notebook present in docs/python_examples/. + +## Risks and mitigations +- Binary compatibility: avoid shipping wheels initially; provide clear build-from-source instructions and CI verification. +- Ownership complexity: prefer explicit API (copy flag) and document lifetime rules. Use pybind11 buffer protocol to reduce errors. +- Large memory copies: benchmark common workflows and document performance expectations; add optional utilities for chunked conversion. + +## Open questions +- Which Python/numpy versions should be formally supported (LTS policy)? +- Is shipping binary wheels a near-term goal, or should initial effort focus on source builds and CI? + +## Next steps +- Assign an owner to implement the DenseMatrix <-> numpy prototype. +- Create a feature branch and open a PR with the prototype and example notebook. +- Add CI job for one Python+numpy configuration and expand later. + + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff --git a/src/Core/CommandLine/CommandLine.cc b/src/Core/CommandLine/CommandLine.cc index 3e030c824f..afe924bba7 100644 --- a/src/Core/CommandLine/CommandLine.cc +++ b/src/Core/CommandLine/CommandLine.cc @@ -53,6 +53,7 @@ class CommandLineParserInternal ("execute,e", "executes the given network on startup") ("Execute,E", "executes the given network on startup and quits when done") ("datadir,d", po::value(), "scirun data directory") + ("image-dir", po::value(), "output directory for regression-mode images") ("regression,r", po::value(), "regression test a network") //("logfile,l", po::value(), "add output messages to a logfile--TODO") ("most-recent,1", "load the most recently used file") @@ -191,11 +192,13 @@ class ApplicationParametersImpl : public ApplicationParameters std::vector&& inputFiles, const std::optional& pythonScriptFile, const std::optional& dataDirectory, + const std::optional& imageOutputDirectory, const std::optional& networkToImport, DeveloperParametersPtr devParams, const Flags& flags ) : entireCommandLine_(entireCommandLine), inputFiles_(inputFiles), pythonScriptFile_(pythonScriptFile), dataDirectory_(dataDirectory), + imageOutputDirectory_(imageOutputDirectory), networkToImport_(networkToImport), devParams_(devParams), flags_(flags) @@ -216,6 +219,11 @@ class ApplicationParametersImpl : public ApplicationParameters return dataDirectory_; } + std::optional imageOutputDirectory() const override + { + return imageOutputDirectory_; + } + std::optional importNetworkFile() const override { return networkToImport_; @@ -301,6 +309,7 @@ class ApplicationParametersImpl : public ApplicationParameters std::vector inputFiles_; std::optional pythonScriptFile_; std::optional dataDirectory_; + std::optional imageOutputDirectory_; std::optional networkToImport_; DeveloperParametersPtr devParams_; Flags flags_; @@ -347,6 +356,11 @@ ApplicationParametersHandle CommandLineParser::parse(int argc, const char* argv[ { dataDirectory = boost::filesystem::path(parsed["datadir"].as()); } + auto imageOutputDirectory = std::optional(); + if (parsed.count("image-dir") != 0 && !parsed["image-dir"].empty() && !parsed["image-dir"].defaulted()) + { + imageOutputDirectory = boost::filesystem::path(parsed["image-dir"].as()); + } auto importNetworkFile = std::optional(); if (parsed.count("import") != 0 && !parsed["import"].empty() && !parsed["import"].defaulted()) { @@ -358,6 +372,7 @@ ApplicationParametersHandle CommandLineParser::parse(int argc, const char* argv[ std::move(inputFiles), pythonScriptFile, dataDirectory, + imageOutputDirectory, importNetworkFile, makeShared( parseOptionalArg(parsed, "threadMode"), diff --git a/src/Core/CommandLine/CommandLine.h b/src/Core/CommandLine/CommandLine.h index 3806557613..199614adca 100644 --- a/src/Core/CommandLine/CommandLine.h +++ b/src/Core/CommandLine/CommandLine.h @@ -50,6 +50,7 @@ namespace SCIRun { virtual const std::vector& inputFiles() const = 0; virtual std::optional pythonScriptFile() const = 0; virtual std::optional dataDirectory() const = 0; + virtual std::optional imageOutputDirectory() const = 0; virtual std::optional importNetworkFile() const = 0; virtual bool help() const = 0; virtual bool version() const = 0; diff --git a/src/Core/CommandLine/Tests/ScirunCommandLineSpecTests.cc b/src/Core/CommandLine/Tests/ScirunCommandLineSpecTests.cc index 00f232d0a9..caa30df79b 100644 --- a/src/Core/CommandLine/Tests/ScirunCommandLineSpecTests.cc +++ b/src/Core/CommandLine/Tests/ScirunCommandLineSpecTests.cc @@ -43,6 +43,7 @@ TEST(ScirunCommandLineSpecTest, CanReadBasicOptions) " -E [ --Execute ] executes the given network on startup and quits when \n" " done\n" " -d [ --datadir ] arg scirun data directory\n" + " --image-dir arg output directory for regression-mode images\n" " -r [ --regression ] arg regression test a network\n" " -1 [ --most-recent ] load the most recently used file\n" " -i [ --interactive ] interactive mode\n" diff --git a/src/Core/GeometryPrimitives/SearchGridT.h b/src/Core/GeometryPrimitives/SearchGridT.h index 7b76ceaa0f..c4cd48bbe7 100644 --- a/src/Core/GeometryPrimitives/SearchGridT.h +++ b/src/Core/GeometryPrimitives/SearchGridT.h @@ -170,7 +170,7 @@ class SearchGridT for (index_type k = mink; k <= maxk; k++) { index_type q = linearize(i, j, k); - std::remove(bin_[q].begin(),bin_[q].end(),val); + bin_[q].erase(std::remove(bin_[q].begin(),bin_[q].end(),val),bin_[q].end()); } } } @@ -188,7 +188,7 @@ class SearchGridT index_type i, j, k; unsafe_locate(i, j, k, point); index_type q = linearize(i, j, k); - std::remove(bin_[q].begin(),bin_[q].end(),val); + bin_[q].erase(std::remove(bin_[q].begin(),bin_[q].end(),val),bin_[q].end()); } inline bool lookup(iterator &begin, iterator &end, const Core::Geometry::Point &p) diff --git a/src/Interface/Modules/Render/CMakeLists.txt b/src/Interface/Modules/Render/CMakeLists.txt index 90e3874575..9eb210da89 100644 --- a/src/Interface/Modules/Render/CMakeLists.txt +++ b/src/Interface/Modules/Render/CMakeLists.txt @@ -51,6 +51,7 @@ SET(Interface_Modules_Render_FORMS DevControls.ui LightControls.ui ViewAxisChooser.ui + ViewSceneShortcuts.ui ) SET(Interface_Modules_Render_HEADERS_TO_MOC @@ -60,6 +61,7 @@ SET(Interface_Modules_Render_HEADERS_TO_MOC ViewSceneControlsDock.h ViewOspraySceneConfig.h GLWidget.h + OffscreenGLRenderer.h GLContextPlatformCompatibility.h Screenshot.h OsprayViewerDialog.h @@ -97,6 +99,7 @@ SET(Interface_Modules_Render_SOURCES ViewSceneUtility.cc ViewSceneControlsDock.cc GLWidget.cc + OffscreenGLRenderer.cc Screenshot.cc OsprayViewerDialog.cc ViewOspraySceneConfig.cc diff --git a/src/Interface/Modules/Render/ES/CoreBootstrap.cc b/src/Interface/Modules/Render/ES/CoreBootstrap.cc index 83d27c292c..4ffc896cb3 100644 --- a/src/Interface/Modules/Render/ES/CoreBootstrap.cc +++ b/src/Interface/Modules/Render/ES/CoreBootstrap.cc @@ -160,8 +160,14 @@ class CoreBootstrap : public spire::EmptySystem core.addExemptComponent(); // Setup default camera projection. + // Use the actual screen dimensions already stored in StaticScreenDims + // (set by SRInterface::setupCore / eventResize) so the initial aspect + // ratio is correct even before the first resizeGL callback. gen::StaticCamera cam; - float aspect = static_cast(800) / static_cast(600); + const gen::StaticScreenDims* screenDims = core.getStaticComponent(); + const float aspect = (screenDims && screenDims->height > 0) + ? static_cast(screenDims->width) / static_cast(screenDims->height) + : static_cast(800) / static_cast(600); float perspFOVY = 0.59f; float perspZNear = 1.0f; diff --git a/src/Interface/Modules/Render/GLWidget.cc b/src/Interface/Modules/Render/GLWidget.cc index 2e0f300076..a3085e125d 100644 --- a/src/Interface/Modules/Render/GLWidget.cc +++ b/src/Interface/Modules/Render/GLWidget.cc @@ -73,6 +73,17 @@ void GLWidget::initializeGL() void GLWidget::paintGL() { + // Guard against the race where paintGL fires before resizeGL has been called + // with the actual widget dimensions (e.g. on first show). Without this the + // orientation glyph renders with the 640x480 default aspect ratio. + if (width() > 0 && height() > 0 && + (graphics_->getScreenWidthPixels() != static_cast(width()) || + graphics_->getScreenHeightPixels() != static_cast(height()))) + { + graphics_->eventResize(static_cast(width()), + static_cast(height())); + } + //set to 200ms to force promise fullfilment every frame if a good frame as been requested const double lUpdateTime = frameRequested_ ? 0.2 : updateTime; graphics_->doFrame(lUpdateTime); diff --git a/src/Interface/Modules/Render/OffscreenGLRenderer.cc b/src/Interface/Modules/Render/OffscreenGLRenderer.cc new file mode 100644 index 0000000000..e1f1f886cf --- /dev/null +++ b/src/Interface/Modules/Render/OffscreenGLRenderer.cc @@ -0,0 +1,134 @@ +/* + For more information, please see: http://software.sci.utah.edu + + The MIT License + + Copyright (c) 2020 Scientific Computing and Imaging Institute, + University of Utah. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +// glew (via gl-platform) must be included before any header that pulls in +// — notably the Qt OpenGL headers below — or MSVC errors with +// "gl.h included before glew.h". +#include +#include +#include +#include +#include +#include +#include +#include + +namespace SCIRun { +namespace Gui { + +OffscreenGLRenderer::OffscreenGLRenderer(int width, int height) +{ + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setVersion(2, 1); + + surface_ = new QOffscreenSurface; + surface_->setFormat(format); + surface_->create(); + + if (!surface_->isValid()) + { + logWarning("OffscreenGLRenderer: failed to create offscreen surface"); + return; + } + + context_ = new QOpenGLContext; + context_->setFormat(format); + if (!context_->create()) + { + logWarning("OffscreenGLRenderer: failed to create OpenGL context"); + return; + } + + if (!context_->makeCurrent(surface_)) + { + logWarning("OffscreenGLRenderer: failed to make context current"); + return; + } + + spire::glPlatformInit(); + + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setAttachment(QOpenGLFramebufferObject::Depth); + fbo_ = new QOpenGLFramebufferObject(width, height, fboFormat); + fbo_->bind(); + + spire_.reset(new Render::SRInterface()); + spire_->setContext(context_); + spire_->eventResize(static_cast(width), static_cast(height)); + + context_->doneCurrent(); + initialized_ = true; +} + +OffscreenGLRenderer::~OffscreenGLRenderer() +{ + if (context_ && surface_) + context_->makeCurrent(surface_); + + spire_.reset(); + delete fbo_; + fbo_ = nullptr; + + if (context_) + context_->doneCurrent(); + + delete context_; + delete surface_; +} + +bool OffscreenGLRenderer::isValid() const +{ + return initialized_ && context_ && context_->isValid() && fbo_ && fbo_->isValid(); +} + +QImage OffscreenGLRenderer::renderToImage() +{ + if (!isValid()) + return {}; + + context_->makeCurrent(surface_); + fbo_->bind(); + + // TODO(golden-images): real VBO/IBO geometry (RenderBasicSys -> glDrawElements) + // crashes in this offscreen context once shaders are ready and it actually + // draws — likely a GL profile/VAO mismatch versus the QOpenGLWidget context. + // Until that's fixed we render only a couple of frames (geometry shaders are + // usually not ready yet, so the scene is background + axes); this keeps the + // existing regression tests stable but produces blank geometry. See branch + // notes / issue. + spire_->doFrame(0.2); + spire_->doFrame(0.2); + + QImage img = fbo_->toImage(); + context_->doneCurrent(); + return img; +} + +} // namespace Gui +} // namespace SCIRun diff --git a/src/Interface/Modules/Render/OffscreenGLRenderer.h b/src/Interface/Modules/Render/OffscreenGLRenderer.h new file mode 100644 index 0000000000..56465e3f56 --- /dev/null +++ b/src/Interface/Modules/Render/OffscreenGLRenderer.h @@ -0,0 +1,76 @@ +/* + For more information, please see: http://software.sci.utah.edu + + The MIT License + + Copyright (c) 2020 Scientific Computing and Imaging Institute, + University of Utah. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef INTERFACE_MODULES_RENDER_OFFSCREENGLRENDERER_H +#define INTERFACE_MODULES_RENDER_OFFSCREENGLRENDERER_H + +// Only QImage is needed in this header (returned by value). The OpenGL Qt +// classes are used as pointer members, so forward-declare them rather than +// including their headers here: on Windows those headers pull in , and +// if that lands before glew.h (included via gl-platform in the .cc) MSVC fails +// with "gl.h included before glew.h". +#include +#include +#include + +class QOffscreenSurface; +class QOpenGLContext; +class QOpenGLFramebufferObject; + +namespace SCIRun { +namespace Gui { + +// Wraps SRInterface with a QOffscreenSurface + QOpenGLContext so regression +// tests can render without a visible window or display server. +class SCISHARE OffscreenGLRenderer +{ +public: + OffscreenGLRenderer(int width, int height); + ~OffscreenGLRenderer(); + + bool isValid() const; + QOpenGLContext* context() const { return context_; } + Render::RendererPtr renderer() const { return spire_; } + + // Renders one frame and returns the pixel data. Returns a null QImage if + // the context is not valid. + QImage renderToImage(); + +private: + QOffscreenSurface* surface_ {nullptr}; + QOpenGLContext* context_ {nullptr}; + QOpenGLFramebufferObject* fbo_ {nullptr}; + Render::RendererPtr spire_; + bool initialized_ {false}; + + void initialize(); +}; + +} // namespace Gui +} // namespace SCIRun + +#endif // INTERFACE_MODULES_RENDER_OFFSCREENGLRENDERER_H diff --git a/src/Interface/Modules/Render/Screenshot.h b/src/Interface/Modules/Render/Screenshot.h index 7e69446e5a..e0cb40b420 100644 --- a/src/Interface/Modules/Render/Screenshot.h +++ b/src/Interface/Modules/Render/Screenshot.h @@ -46,6 +46,7 @@ namespace SCIRun public: explicit Screenshot(QOpenGLWidget *glwidget, QObject *parent = nullptr); void takeScreenshot(); + void setImage(const QImage& img) { screenshot_ = img; } // used by offscreen renderer path QImage getScreenshot(); void saveScreenshot(const QString& filename); Modules::Render::RGBMatrices toMatrix() const; diff --git a/src/Interface/Modules/Render/ViewScene.cc b/src/Interface/Modules/Render/ViewScene.cc index 5942e9ec13..475d85ec06 100644 --- a/src/Interface/Modules/Render/ViewScene.cc +++ b/src/Interface/Modules/Render/ViewScene.cc @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -45,6 +47,8 @@ #include #include #include +#include +#include #include using namespace SCIRun; @@ -157,7 +161,7 @@ namespace Gui { class ViewSceneDialogImpl { public: - GLWidget* mGLWidget {nullptr}; ///< GL widget containing context. + GLWidget* mGLWidget {nullptr}; Render::RendererWeakPtr mSpire {}; ///< Instance of Spire. QToolBar* toolBar1_ {nullptr}; ///< Tool bar. QToolBar* toolBar2_ {nullptr}; ///< Tool bar. @@ -204,6 +208,9 @@ namespace Gui { {0.8f, 0.8f, 0.5f}, {0.4f, 0.7f, 0.3f}, {0.2f, 0.4f, 0.5f}, {0.5f, 0.3f, 0.5f}}; + struct HomeCamera { float distance; glm::vec3 lookAt; glm::quat rotation; }; + std::optional homeView_; + std::optional savedPos_; QColor bgColor_ {}; ScaleBarData scaleBar_ {}; @@ -230,6 +237,9 @@ namespace Gui { QPushButton* toolBar3Position_ {nullptr}; std::vector viewScenesToUpdate {}; + QDialog* shortcutsDialog_ {nullptr}; + QTableWidget* shortcutsTable_ {nullptr}; + std::optional shortcutsDialogPos_ {}; std::unique_ptr gid_; std::string name_; @@ -398,11 +408,12 @@ ViewSceneDialog::ViewSceneDialog(const std::string& name, ModuleStateHandle stat connect(impl_->mGLWidget, &GLWidget::fatalError, this, &ViewSceneDialog::fatalError); connect(impl_->mGLWidget, &GLWidget::finishedFrame, this, &ViewSceneDialog::frameFinished); - connect(this, &ViewSceneDialog::mousePressSignalForGeometryObjectFeedback, - this, &ViewSceneDialog::sendGeometryFeedbackToState); impl_->mSpire = RendererWeakPtr(impl_->mGLWidget->getSpire()); + connect(this, &ViewSceneDialog::mousePressSignalForGeometryObjectFeedback, + this, &ViewSceneDialog::sendGeometryFeedbackToState); + //Set background Color const auto colorStr = state_->getValue(Parameters::BackgroundColor).toString(); impl_->bgColor_ = checkColorSetting(colorStr, Qt::black); @@ -452,7 +463,7 @@ ViewSceneDialog::ViewSceneDialog(const std::string& name, ModuleStateHandle stat { impl_->toolbarHolder_ = new QMainWindow; - impl_->toolbarHolder_->setCentralWidget(impl_->mGLWidget); + impl_->toolbarHolder_->setCentralWidget(impl_->mGLWidget ? static_cast(impl_->mGLWidget) : new QWidget); impl_->toolBar1_ = new QToolBar; impl_->toolBar1_->setMovable(true); @@ -553,6 +564,7 @@ void ViewSceneDialog::addToolBar() addControlLockButton(); addScreenshotButton(); addAutoRotateButton(); + addShortcutsHelpButton(); //TODO: render toolbar members addColorOptionsButton(); @@ -749,11 +761,222 @@ void ViewSceneDialog::addAutoViewButton() impl_->autoViewButton_ = new QPushButton(this); impl_->autoViewButton_->setToolTip("Auto View"); impl_->autoViewButton_->setIcon(QPixmap(":/general/Resources/ViewScene/autoview.png")); - impl_->autoViewButton_->setShortcut(Qt::Key_0); + // Key_0 is handled by dispatchShortcutKey (shows tooltip); don't duplicate with setShortcut. connect(impl_->autoViewButton_, &QPushButton::clicked, this, &ViewSceneDialog::autoViewClicked); addToolbarButton(impl_->autoViewButton_, Qt::TopToolBarArea); } +const ViewSceneDialog::ShortcutTable& ViewSceneDialog::shortcutTable() +{ + using Id = ShortcutDef::Id; + // Lambdas take ViewSceneDialog* so they can call any slot on the instance. + // Protected access is fine here because this is a member function of ViewSceneDialog. + static const ShortcutTable table = {{ + { Id::AxisViews, Qt::Key_1, Qt::NoModifier, + "Axis Views", "1-6", "Preprogrammed views aligning the view with the X, Y, or Z-axis", + [](ViewSceneDialog* d) { d->setAxisView(1); } }, + { Id::Autoview, Qt::Key_0, Qt::NoModifier, + "Autoview", "0", "Find a view that shows all the data", + [](ViewSceneDialog* d) { d->autoViewClicked(); } }, + { Id::AutoviewNoScale, Qt::Key_0, Qt::AltModifier, + "Autoview (no scale)", "Alt+0", "Reset the eye so the data is centered", + // TODO: add a spire->centerView() that moves lookAt to bbox center + // without changing zoom; for now doAutoView is a reasonable stand-in. + [](ViewSceneDialog* d) { d->autoViewClicked(); } }, + { Id::SnapToAxis, Qt::Key_X, Qt::NoModifier, + "Snap to Axis", "X", "Snap to the nearest axis-aligned view", + [](ViewSceneDialog* d) { d->setClosestAxisView(); } }, + // TODO(#2510): Copy View (Ctrl+1-9) — enumerate all live ViewSceneDialog instances via + // ViewSceneManager, let user pick by index, then call spire->setCameraDistance/ + // setCameraLookAt/setCameraRotation with values from the chosen window's spire. + // Blocked on: ViewSceneManager exposing an ordered list of active ViewScenes. + { Id::CopyView, Qt::Key_1, Qt::ControlModifier, + "Copy View", "Ctrl+1-9","Copy view from Viewer Window 1-9", nullptr }, + { Id::SetHome, Qt::Key_H, Qt::AltModifier, + "Set Home", "Alt+H", "Store the current view", + [](ViewSceneDialog* d) { + auto spire = d->impl_->mSpire.lock(); + if (!spire) return; + d->impl_->homeView_ = ViewSceneDialogImpl::HomeCamera{ + spire->getCameraDistance(), + spire->getCameraLookAt(), + spire->getCameraRotation() + }; + } }, + { Id::GotoHome, Qt::Key_H, Qt::NoModifier, + "Home", "H", "Go back to the stored view", + [](ViewSceneDialog* d) { + if (!d->impl_->homeView_) return; + auto spire = d->impl_->mSpire.lock(); + if (!spire) return; + const auto& hv = *d->impl_->homeView_; + spire->setCameraDistance(hv.distance); + spire->setCameraLookAt(hv.lookAt); + spire->setCameraRotation(hv.rotation); + d->pushCameraState(); + } }, + // TODO(#2503): Toggle World Axes (A) — v4 rendered a large XYZ triad at the true world + // origin. v5 has no equivalent. Needs: a new GeometryObject that draws axis lines + // (or glyphs) at (0,0,0), a Parameters::WorldAxesVisible state entry, and wiring + // through newGeometryValue. The corner orientation icon (O) is a separate feature. + { Id::ToggleAxes, Qt::Key_A, Qt::NoModifier, + "Toggle Axes", "A", "Switch axes on/off", nullptr }, + // TODO(#2504): Bounding Box (B) — Parameters::ShowBBox exists but is commented out throughout + // the renderer. Needs: re-enabling ShowBBox, computing the combined scene AABB in + // SRInterface, building a wire-frame box GeometryObject each frame it's on, and + // a toggleBoundingBox() slot here similar to showOrientationChecked(). + { Id::BoundingBox, Qt::Key_B, Qt::NoModifier, + "Bounding Box", "B", "Switch bounding box mode on/off", nullptr }, + { Id::ToggleClipping, Qt::Key_C, Qt::NoModifier, + "Toggle Clipping", "C", "Switch clipping on/off", + [](ViewSceneDialog* d) { + d->setClippingPlaneVisible(!d->impl_->clippingPlaneManager_->active().visible); + } }, + { Id::ToggleFog, Qt::Key_D, Qt::NoModifier, + "Toggle Fog", "D", "Switch fog on/off", + [](ViewSceneDialog* d) { + d->setFogOn(!d->state_->getValue(Parameters::FogOn).toBool()); + } }, + // TODO(#2505): Flat Shading (F) — no flat-shading mode in the v5 renderer. Needs: a uniform + // flag in the object/phong shaders to use face normals (or a flat-shading shader + // variant), a StaticRenderMode or per-pass uniform, SRInterface::setFlatShading(bool), + // and a Parameters::FlatShading state entry with a toggleFlatShading() slot. + { Id::FlatShading, Qt::Key_F, Qt::NoModifier, + "Flat Shading", "F", "Switch flat shading on/off", nullptr }, + { Id::OpenHelp, Qt::Key_I, Qt::NoModifier, + "Open Help", "I", "Open this help window", + [](ViewSceneDialog* d) { d->showShortcutsDialog(); } }, + { Id::ViewLocking, Qt::Key_L, Qt::NoModifier, + "View Locking", "L", "Switch view locking on/off", + [](ViewSceneDialog* d) { + const bool anyLocked = d->impl_->lockRotation_->isChecked() || + d->impl_->lockPan_->isChecked() || + d->impl_->lockZoom_->isChecked(); + if (anyLocked) d->unlockAllTriggered(); else d->lockAllTriggered(); + } }, + { Id::ToggleLighting, Qt::Key_K, Qt::NoModifier, + "Toggle Lighting", "K", "Switch lighting on/off", + [](ViewSceneDialog* d) { + const bool newState = !d->state_->getValue(Parameters::HeadLightOn).toBool(); + for (int i = 0; i < ViewSceneDialogImpl::NUM_LIGHTS; ++i) + d->toggleLight(i, newState); + } }, + { Id::OrientationIcon, Qt::Key_O, Qt::NoModifier, + "Orientation Icon", "O", "Switch orientation icon on/off", + [](ViewSceneDialog* d) { + d->showOrientationChecked(!d->state_->getValue(Parameters::AxesVisible).toBool()); + } }, + // TODO(#2506): Orthographic (P) — SRCamera/SRInterface only expose perspective projection. + // Needs: SRInterface::setOrthographic(bool) that swaps between glm::perspective and + // a glm::ortho sized to the current view frustum width at the lookAt distance, + // SRCamera::setAsPerspective already exists — add setAsOrthographic() alongside it, + // and a Parameters::OrthographicMode state entry with a toggleOrthographic() slot. + { Id::Orthographic, Qt::Key_P, Qt::NoModifier, + "Orthographic", "P", "Switch orthographic projection on/off", nullptr }, + // TODO(#2507): Stereo (S) — not implemented in v5. Needs: a stereo rendering mode in + // SRInterface (side-by-side or anaglyph), likely a second render pass with a + // laterally offset camera, SRInterface::setStereo(bool), and a + // Parameters::StereoMode state entry. Significant renderer work. + { Id::Stereo, Qt::Key_S, Qt::NoModifier, + "Stereo", "S", "Switch stereo mode on/off", nullptr }, + // TODO(#2508): Backculling (U) — no back-face cull toggle in v5. Needs: SRInterface:: + // setBackfaceCulling(bool) that calls glEnable/glDisable(GL_CULL_FACE) + glCullFace + // (GL_BACK) in the render loop (or a StaticGLState flag), a Parameters::BackfaceCulling + // state entry, and a toggleBackfaceCulling() slot. + { Id::Backculling, Qt::Key_U, Qt::NoModifier, + "Backculling", "U", "Switch backculling on/off", nullptr }, + // TODO(#2509): Wireframe (W) — no wireframe mode in v5. Needs: SRInterface::setWireframe(bool) + // using glPolygonMode(GL_FRONT_AND_BACK, GL_LINE/GL_FILL) (desktop GL only; for ES + // compatibility a geometry-shader or line-drawing pass may be needed instead), + // a Parameters::WireframeMode state entry, and a toggleWireframe() slot. + { Id::Wireframe, Qt::Key_W, Qt::NoModifier, + "Wireframe", "W", "Switch wire frame on/off", nullptr }, + }}; + return table; +} + +void ViewSceneDialog::addShortcutsHelpButton() +{ + auto* helpButton = new QPushButton(this); + helpButton->setToolTip("Keyboard Shortcuts (I)"); + { + // Draw a white "?" on a transparent pixmap to match the dark toolbar icon style + QPixmap px(24, 24); + px.fill(Qt::transparent); + QPainter p(&px); + p.setPen(Qt::white); + QFont f = p.font(); + f.setBold(true); + f.setPixelSize(18); + p.setFont(f); + p.drawText(px.rect(), Qt::AlignCenter, "?"); + helpButton->setIcon(QIcon(px)); + } + connect(helpButton, &QPushButton::clicked, this, &ViewSceneDialog::showShortcutsDialog); + addToolbarButton(helpButton, Qt::TopToolBarArea); +} + +void ViewSceneDialog::showShortcutsDialog() +{ + if (!impl_->shortcutsDialog_) + { + impl_->shortcutsDialog_ = new QDialog(this); + impl_->shortcutsDialog_->installEventFilter(this); + Ui::ViewSceneShortcuts shortcutsUi; + shortcutsUi.setupUi(impl_->shortcutsDialog_); + auto* table = shortcutsUi.tableWidget; + impl_->shortcutsTable_ = table; + table->installEventFilter(this); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + + // Only show implemented shortcuts; unimplemented ones are hidden pending + // their own feature work (see TODO comments in shortcutTable()). + int row = 0; + for (int idx = 0; idx < static_cast(numShortcuts); ++idx) + { + const auto& sc = shortcutTable()[static_cast(idx)]; + if (!sc.isImplemented()) continue; + table->insertRow(row); + auto* nameItem = new QTableWidgetItem(sc.actionName); + auto* shortcutItem = new QTableWidgetItem(sc.shortcutDisplay); + auto* descItem = new QTableWidgetItem(sc.description); + // Store the original table index so the double-click handler can find it. + nameItem->setData(Qt::UserRole, idx); + table->setItem(row, 0, nameItem); + table->setItem(row, 1, shortcutItem); + table->setItem(row, 2, descItem); + ++row; + } + table->resizeColumnsToContents(); + table->resizeRowsToContents(); + // Let the table report its exact content size so the dialog can fit it. + table->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + table->setToolTip("Double-click a row to perform that action"); + connect(table, &QTableWidget::cellDoubleClicked, [this](int row, int /*col*/) + { + const auto* nameItem = impl_->shortcutsTable_->item(row, 0); + if (!nameItem) return; + const int idx = nameItem->data(Qt::UserRole).toInt(); + const auto& sc = shortcutTable()[static_cast(idx)]; + if (sc.action) + sc.action(this); + }); + // Size the dialog to fit the content, plus padding so the last row is + // fully visible without scrolling (Windows chrome also needs extra room). + impl_->shortcutsDialog_->adjustSize(); + const QSize padded = impl_->shortcutsDialog_->size() + QSize(40, 60); + impl_->shortcutsDialog_->setFixedSize(padded); + + // Default position: top-right of the ViewScene window to minimise overlap. + // Use saved position if the user has moved it previously this session. + const QPoint defaultPos = mapToGlobal(QPoint(width(), 0)); + impl_->shortcutsDialog_->move(impl_->shortcutsDialogPos_.value_or(defaultPos)); + } + impl_->shortcutsDialog_->show(); + impl_->shortcutsDialog_->raise(); + impl_->shortcutsDialog_->activateWindow(); +} + void ViewSceneDialog::addScreenshotButton() { auto* screenshotButton = new QPushButton(this); @@ -1386,7 +1609,8 @@ void ViewSceneDialog::closeEvent(QCloseEvent *evt) // future. Kept for future reference. //glLayout->removeWidget(impl_->mGLWidget); - impl_->mGLWidget->close(); + if (impl_->mGLWidget) + impl_->mGLWidget->close(); ModuleDialogGeneric::closeEvent(evt); } @@ -1644,16 +1868,68 @@ void ViewSceneDialog::wheelEvent(QWheelEvent* event) } } +void ViewSceneDialog::flashShortcutTooltip(const QString& msg) +{ + QToolTip::showText(mapToGlobal(QPoint(width() / 2, 32)), msg, this, {}, 1500); +} + +bool ViewSceneDialog::dispatchShortcutKey(QKeyEvent* event) +{ + const auto key = static_cast(event->key()); + const auto mods = event->modifiers() & ~Qt::KeypadModifier; + + // Keys 1-6 (no modifier) → axis-aligned views; handled as a range so the + // table only needs one representative entry for the help dialog. + if (mods == Qt::NoModifier && key >= Qt::Key_1 && key <= Qt::Key_6) + { + static const char* const axisNames[] = {"+X","-X","+Y","-Y","+Z","-Z"}; + const int n = key - Qt::Key_0; + setAxisView(n); + flashShortcutTooltip(QString("%1 View").arg(axisNames[n - 1])); + return true; + } + + for (const auto& sc : shortcutTable()) + { + if (sc.key == key && sc.modifiers == mods && sc.action) + { + sc.action(this); + flashShortcutTooltip(sc.actionName); + return true; + } + } + return false; +} + void ViewSceneDialog::keyPressEvent(QKeyEvent* event) { - switch (event->key()) + // Shift is handled separately — it affects cursor state, not a command shortcut. + if (event->key() == Qt::Key_Shift) { - case Qt::Key_Shift: impl_->shiftdown_ = true; updateCursor(); - break; - default: ; + return; } + dispatchShortcutKey(event); +} + +bool ViewSceneDialog::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == impl_->shortcutsDialog_) + { + if (event->type() == QEvent::Move) + { + impl_->shortcutsDialogPos_ = impl_->shortcutsDialog_->pos(); + return false; + } + } + if (event->type() == QEvent::KeyPress && + (obj == impl_->shortcutsDialog_ || obj == impl_->shortcutsTable_)) + { + if (dispatchShortcutKey(static_cast(event))) + return true; // shortcut fired — consume so dialog doesn't also handle it + } + return ModuleDialogGeneric::eventFilter(obj, event); } void ViewSceneDialog::keyReleaseEvent(QKeyEvent* event) @@ -1693,6 +1969,67 @@ void ViewSceneDialog::updateCursor() //---------------- Camera -------------------------------------------------------------------------- //-------------------------------------------------------------------------------------------------- +void ViewSceneDialog::setAxisView(int n) +{ + // Maps key 1-6 to the six cardinal axis-aligned views with sensible up vectors. + static const std::pair views[6] = { + { V( 1, 0, 0), V( 0, 1, 0) }, // 1: +X (Y up) + { V(-1, 0, 0), V( 0, 1, 0) }, // 2: -X (Y up) + { V( 0, 1, 0), V( 0, 0, 1) }, // 3: +Y (Z up) + { V( 0,-1, 0), V( 0, 0, 1) }, // 4: -Y (Z up) + { V( 0, 0, 1), V( 0, 1, 0) }, // 5: +Z (Y up) + { V( 0, 0,-1), V( 0, 1, 0) }, // 6: -Z (Y up) + }; + if (n < 1 || n > 6) return; + auto spire = impl_->mSpire.lock(); + if (!spire) return; + spire->setView(views[n - 1].first, views[n - 1].second); + pushCameraState(); +} + +void ViewSceneDialog::setClosestAxisView() +{ + auto spire = impl_->mSpire.lock(); + if (!spire) return; + + // The camera rotation quaternion is from glm::lookAt (world→camera). + // Transposing its mat3 gives the camera axes in world space: + // Rt[0] = right, Rt[1] = up, Rt[2] = -forward (all in world coords) + const glm::mat3 Rt = glm::transpose(glm::mat3_cast(spire->getCameraRotation())); + const glm::vec3 viewDir = -Rt[2]; // current look direction (world space) + const glm::vec3 upDir = Rt[1]; // current up direction (world space) + + static const glm::vec3 axes[6] = { + { 1, 0, 0}, {-1, 0, 0}, + { 0, 1, 0}, { 0,-1, 0}, + { 0, 0, 1}, { 0, 0,-1}, + }; + + // Find the cardinal axis closest to the current view direction. + int bestView = 0; + float bestViewDot = -2.f; + for (int i = 0; i < 6; ++i) + { + float d = glm::dot(viewDir, axes[i]); + if (d > bestViewDot) { bestViewDot = d; bestView = i; } + } + const glm::vec3 view = axes[bestView]; + + // Find the cardinal axis closest to the current up direction that is + // perpendicular to the chosen view axis. + int bestUp = 0; + float bestUpDot = -2.f; + for (int i = 0; i < 6; ++i) + { + if (std::abs(glm::dot(view, axes[i])) > 0.5f) continue; // skip parallel/anti-parallel + float d = glm::dot(upDir, axes[i]); + if (d > bestUpDot) { bestUpDot = d; bestUp = i; } + } + + spire->setView(view, axes[bestUp]); + pushCameraState(); +} + void ViewSceneDialog::snapToViewAxis() { auto upName = impl_->viewAxisChooser_->upVectorComboBox_->currentText(); @@ -1770,41 +2107,50 @@ void ViewSceneDialog::toggleLockColor(bool locked) void ViewSceneDialog::lockRotationToggled() { - impl_->mGLWidget->setLockRotation(impl_->lockRotation_->isChecked()); + if (impl_->mGLWidget) + impl_->mGLWidget->setLockRotation(impl_->lockRotation_->isChecked()); toggleLockColor(impl_->lockRotation_->isChecked() || impl_->lockPan_->isChecked() || impl_->lockZoom_->isChecked()); } void ViewSceneDialog::lockPanningToggled() { - impl_->mGLWidget->setLockPanning(impl_->lockPan_->isChecked()); + if (impl_->mGLWidget) + impl_->mGLWidget->setLockPanning(impl_->lockPan_->isChecked()); toggleLockColor(impl_->lockRotation_->isChecked() || impl_->lockPan_->isChecked() || impl_->lockZoom_->isChecked()); } void ViewSceneDialog::lockZoomToggled() { - impl_->mGLWidget->setLockZoom(impl_->lockZoom_->isChecked()); + if (impl_->mGLWidget) + impl_->mGLWidget->setLockZoom(impl_->lockZoom_->isChecked()); toggleLockColor(impl_->lockRotation_->isChecked() || impl_->lockPan_->isChecked() || impl_->lockZoom_->isChecked()); } void ViewSceneDialog::lockAllTriggered() { impl_->lockRotation_->setChecked(true); - impl_->mGLWidget->setLockRotation(true); impl_->lockPan_->setChecked(true); - impl_->mGLWidget->setLockPanning(true); impl_->lockZoom_->setChecked(true); - impl_->mGLWidget->setLockZoom(true); + if (impl_->mGLWidget) + { + impl_->mGLWidget->setLockRotation(true); + impl_->mGLWidget->setLockPanning(true); + impl_->mGLWidget->setLockZoom(true); + } toggleLockColor(true); } void ViewSceneDialog::unlockAllTriggered() { impl_->lockRotation_->setChecked(false); - impl_->mGLWidget->setLockRotation(false); impl_->lockPan_->setChecked(false); - impl_->mGLWidget->setLockPanning(false); impl_->lockZoom_->setChecked(false); - impl_->mGLWidget->setLockZoom(false); + if (impl_->mGLWidget) + { + impl_->mGLWidget->setLockRotation(false); + impl_->mGLWidget->setLockPanning(false); + impl_->mGLWidget->setLockZoom(false); + } toggleLockColor(false); } @@ -2798,10 +3144,40 @@ void ViewSceneDialog::saveScreenshot(QString fileName, bool notify) void ViewSceneDialog::autoSaveScreenshot() { + const auto moduleName = QString::fromStdString(getName()).replace(':', '-'); + auto dir = QString::fromStdString(state_->getValue(Parameters::ScreenshotDirectory).toString()); + + if (Application::Instance().parameters()->isRegressionMode()) + { + // Deterministic, golden-image-ready name: /..png, + // keyed to the loaded network file (no wall-clock timestamp). The output + // directory comes from --image-dir when given, else the screenshot pref, + // else the current working directory. + const auto imageDir = Application::Instance().parameters()->imageOutputDirectory(); + if (imageDir) + dir = QString::fromStdString(imageDir->string()); + if (dir.isEmpty()) + dir = "."; + boost::filesystem::create_directories(dir.toStdString()); + const auto network = QString::fromStdString( + boost::filesystem::path(Core::getCurrentFileName()).stem().string()); + const auto file = QString("%1/%2.%3.png").arg(dir).arg(network).arg(moduleName); + + // Save the frame already captured during the run (each frameFinished calls + // takeScreenshot). Do NOT re-render here: this runs at exit, and a fresh + // offscreen doFrame at teardown crashes inside the GL geometry draw for + // networks with real geometry. Only render if nothing was captured yet. + if (!impl_->screenshotTaker_) + takeScreenshot(); + if (impl_->screenshotTaker_) + impl_->screenshotTaker_->saveScreenshot(file); + return; + } + QThread::sleep(1); - const auto file = QString::fromStdString(state_->getValue(Parameters::ScreenshotDirectory).toString()) + + const auto file = dir + QString("/%1_%2.png") - .arg(QString::fromStdString(getName()).replace(':', '-')) + .arg(moduleName) .arg(QTime::currentTime().toString("hh.mm.ss.zzz")); saveScreenshot(file, false); @@ -2861,7 +3237,6 @@ void ViewSceneDialog::takeScreenshot() { if (!impl_->screenshotTaker_) impl_->screenshotTaker_ = new Screenshot(impl_->mGLWidget, this); - impl_->screenshotTaker_->takeScreenshot(); } diff --git a/src/Interface/Modules/Render/ViewScene.h b/src/Interface/Modules/Render/ViewScene.h index 13635f73b6..0d2e2192af 100644 --- a/src/Interface/Modules/Render/ViewScene.h +++ b/src/Interface/Modules/Render/ViewScene.h @@ -39,7 +39,9 @@ #include #include #include +#include #include +#include #include "Interface/Modules/Render/ui_ViewScene.h" #include #include @@ -62,6 +64,34 @@ namespace SCIRun { Q_OBJECT; public: + // -------- Keyboard shortcut registry ---------------------------------------- + struct ShortcutDef + { + enum class Id : int + { + AxisViews, Autoview, AutoviewNoScale, SnapToAxis, CopyView, + SetHome, GotoHome, ToggleAxes, BoundingBox, ToggleClipping, + ToggleFog, FlatShading, OpenHelp, ViewLocking, ToggleLighting, + OrientationIcon, Orthographic, Stereo, Backculling, Wireframe, + NUM_SHORTCUTS + }; + using Action = std::function; + + Id id; + Qt::Key key; + Qt::KeyboardModifiers modifiers {Qt::NoModifier}; + const char* actionName; + const char* shortcutDisplay; // human-readable, e.g. "Ctrl+H" or "1-8" + const char* description; + Action action {nullptr}; // null = not yet implemented / shown grayed + + bool isImplemented() const { return static_cast(action); } + }; + + static constexpr auto numShortcuts = + static_cast(ShortcutDef::Id::NUM_SHORTCUTS); + using ShortcutTable = std::array; + ViewSceneDialog(const std::string& name, Dataflow::Networks::ModuleStateHandle state, QWidget* parent = nullptr); ~ViewSceneDialog() override; @@ -186,6 +216,9 @@ namespace SCIRun { void setFogStartValue(double value); void setFogEndValue(double value); + //---------------- Help ---------------------------------------------------------------------- + void showShortcutsDialog(); + //---------------- Misc. --------------------------------------------------------------------- void assignBackgroundColor(); void setTransparencySortTypeContinuous(bool index); @@ -201,6 +234,7 @@ namespace SCIRun { protected: //---------------- Initialization ------------------------------------------------------------ void pullSpecial() override; + bool eventFilter(QObject* obj, QEvent* event) override; void newGeometryValue(bool forceAllObjectsToUpdate, bool clippingPlanesUpdated); void updateAllGeometries(); @@ -245,6 +279,12 @@ namespace SCIRun { void addDeveloperControlButton(); void addToolbarButton(QWidget* w, Qt::ToolBarArea area, ViewSceneControlPopupWidget* widgetToPopup = nullptr); void addObjectSelectionButton(); + void addShortcutsHelpButton(); + static const ShortcutTable& shortcutTable(); + bool dispatchShortcutKey(QKeyEvent* event); + void setAxisView(int n); // n=1..6 → +X,-X,+Y,-Y,+Z,-Z + void setClosestAxisView(); // snap to nearest cardinal axis + void flashShortcutTooltip(const QString& msg); void addLightButtons(); QColor checkColorSetting(const std::string& rgb, const QColor& defaultColor); void pullCameraState(); diff --git a/src/Interface/Modules/Render/ViewSceneControlsDock.h b/src/Interface/Modules/Render/ViewSceneControlsDock.h index f12d1cbeeb..96ca828dab 100644 --- a/src/Interface/Modules/Render/ViewSceneControlsDock.h +++ b/src/Interface/Modules/Render/ViewSceneControlsDock.h @@ -43,6 +43,7 @@ #include "Interface/Modules/Render/ui_Screenshot.h" #include "Interface/Modules/Render/ui_ViewAxisChooser.h" #include "Interface/Modules/Render/ui_ViewSceneControls.h" +#include "Interface/Modules/Render/ui_ViewSceneShortcuts.h" #ifndef Q_MOC_RUN #include diff --git a/src/Interface/Modules/Render/ViewSceneShortcuts.ui b/src/Interface/Modules/Render/ViewSceneShortcuts.ui index 9baf4c79c0..b44188385b 100644 --- a/src/Interface/Modules/Render/ViewSceneShortcuts.ui +++ b/src/Interface/Modules/Render/ViewSceneShortcuts.ui @@ -1,121 +1,21 @@ - Dialog - + ViewSceneShortcuts + 0 0 - 570 - 614 + 700 + 720 - Dialog + ViewScene Keyboard Shortcuts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Action @@ -131,231 +31,6 @@ Details - - - - - - - - 1-9 - - - - - Preprogrammed views aligning the view with the X, Y, or Z-axis - - - - - Autoview - - - - - 0 - - - - - Find a view that shows all the data - - - - - Autoview without scaling - - - - - Ctrl 0 - - - - - Reset the eye so the data is centered - - - - - X - - - - - Snap to the closest axis-aligned view - - - - - Ctrl 1-9 - - - - - Copy view from Viewer Window 1-9 - - - - - Set Home - - - - - Ctrl H - - - - - Store the current view - - - - - Home - - - - - H - - - - - Go back to the stored view - - - - - A - - - - - Toggle axes - - - - - B - - - - - Toggle bounding box mode - - - - - C - - - - - Toggle clipping - - - - - D - - - - - Toggle fog - - - - - F - - - - - Toggle flat shading - - - - - I - - - - - Open this help window - - - - - L - - - - - Toggle view locking - - - - - K - - - - - Toggle lighting - - - - - O - - - - - Toggle orientation icon - - - - - P - - - - - Toggle orthographic projection - - - - - S - - - - - Toggle stereo mode - - - - - U - - - - - Toggle backculling - - - - - W - - - - - Toggle wire frame - - @@ -375,7 +50,7 @@ buttonBox accepted() - Dialog + ViewSceneShortcuts accept() @@ -391,7 +66,7 @@ buttonBox rejected() - Dialog + ViewSceneShortcuts reject() diff --git a/src/Main/scirunMain.cc b/src/Main/scirunMain.cc index 2c3caadc03..07a54102c1 100644 --- a/src/Main/scirunMain.cc +++ b/src/Main/scirunMain.cc @@ -3,7 +3,7 @@ The MIT License - Copyright (c) 2020 Scientific Computing and Imaging Institute, + Copyright (c) 2020-2026 Scientific Computing and Imaging Institute, University of Utah. Permission is hereby granted, free of charge, to any person obtaining a diff --git a/src/Testing/CMakeLists.txt b/src/Testing/CMakeLists.txt index 165d10e91b..fc762a826a 100644 --- a/src/Testing/CMakeLists.txt +++ b/src/Testing/CMakeLists.txt @@ -146,6 +146,19 @@ IF(RUN_BASIC_REGRESSION_TESTS) ENDIF() IF(RUN_IMPORT_TESTS) + # Networks that reference modules not yet ported to SCIRun 5. + SET(DISABLED_IMPORT_TESTS + ".Test.ImportNetwork.Other.v4nets.example_nets.FusionPSE.MDSplus" + ".Test.ImportNetwork.Other.v4nets.example_nets.FusionPSE.Streamlines-Scalar-Interp" + ".Test.ImportNetwork.Other.v4nets.example_nets.FusionPSE.Volume-Cutting" + ".Test.ImportNetwork.Other.v4nets.example_nets.FusionPSE.Volume-Viz" + ".Test.ImportNetwork.Other.v4nets.example_nets.SCIRun.MeshingPipeline.DistributedParticles" + ".Test.ImportNetwork.Other.v4nets.example_nets.SCIRun.MeshingPipeline.SizingFieldMedialAxisAndBoundaries" + ".Test.ImportNetwork.Other.v4nets.example_nets.Teem.Optional.volume-colored-example" + ".Test.ImportNetwork.Other.v4nets.example_nets.Teem.Optional.volume-colored-gage" + ".Test.ImportNetwork.Other.v4nets.example_nets.Teem.Samples.tooth1" + ) + FILE(GLOB_RECURSE scirun_legacy_srns "${SCIRUN_TEST_RESOURCE_DIR}/**/*.srn") # The glob is broad; drop files that aren't legacy v4 networks: serialization # test artifacts (TransientOutput/), scratch outputs (*_TMP.srn), and any @@ -165,6 +178,9 @@ IF(RUN_IMPORT_TESTS) ADD_TEST("${import_test_name}" ${EXE_LOC} --no_splash --regression 30 --import ${srn}) # Bound a hung import at the ctest level. SET_TESTS_PROPERTIES("${import_test_name}" PROPERTIES TIMEOUT 120) + IF("${import_test_name}" IN_LIST DISABLED_IMPORT_TESTS) + SET_TESTS_PROPERTIES("${import_test_name}" PROPERTIES DISABLED TRUE) + ENDIF() ENDFOREACH() ENDIF()