diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e58249 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,86 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [106.1.0] - 2024-09-17 + +### Added +- Support for containerization of streaming applications and services via `repo package --container` +- Support extension only builds via `repo build` +- Support the ability to launch created containers via `repo launch --container` +- repo_usd tooling dependency +- Support for USD Viewer Template to send scene loading state to client via messaging + +### Changed +- Aligned default testing for applications and extensions +- Update and align code formatting/style across templates + +### Fixed +- Extra setup extensions appear in standard extension template menu +- "Could not find cgroup memory limit" error during build +- Fixed default manipulator pivot back to "bounding box base" in USD Explorer Template + + +## [106.0.2] - 2024-07-29 + +### Added +- Support for local streaming configurations for UI based Applications +- Support for multiple setup extensions per application +- Ability to pass arguments to Kit via the 'repo launch` tool. +- USD Composer Application Template and Documentation +- USD Viewer Application Template and Documentation +- USD Composer Setup Extension and Documentation +- USD Viewer Setup Extension and Documentation +- Repository Issue Templates Bug/Question/Feature Request +- Omniverse Product-Specific Terms (PRODUCT_TERMS_OMNIVERSE) +- Support for type ordering in templates.toml +- Metrics Assembler to Kit Base Editor Template to support unit correct assets +- Support for automatic launch if only single `.kit` file is present in `source/apps` + +### Changed +- Updated all relevant application templates READMEs to reflect the addition of local streaming configurations +- Updated .gitattributes to ensure LFS is used for all relevant file types +- Updated .gitignore to exclude streaming app event traces +- Updated .vscode/launch.json to better support debugging behavior +- Updated LICENSE to separate NVIDIA License from Omniverse Product-Specific Terms +- Updated top level README.md to reflect additional templates and improve documentation clarity +- Updated Developer Bundle extension availability and corresponding documentation +- Updated public extension registry to reflect current Kit 106 registry location +- Updated templates.toml to support multiple setup extensions and new templates + + +## [106.0.0] - 2024-06-07 + +### Added +- Kit Base Editor Application Template and Documentation +- USD Explorer Application Template and Documentation +- USD Explorer Setup Extension and Documentation +- Kit Service Template and Documentation +- Simple Python Extension Template and Documentation +- Simple C++ Extension Template and Documentation +- Python UI Extension Template and Documentation +- Template configuration file (templates.toml) +- Added local `repo launch` tool for launching applications and fat packages directly +- Added local `repo package` functionality to improve package naming +- Omniverse EULA acceptance to Kit App Template via tooling +- tasks.json for better VSCode support +- SECURITY.md for security policy +- Notice for data collection and use +- Early access Developer Bundle extensions +- Kit App Template related Developer Bundle documentation (developer_bundle_extensions.md) +- Kit App Template related repo tools documentation (kit_app_template_tooling_guide.md) +- Usage and troubleshooting documentation for Kit App Template (usage_and_troubleshooting.md) +- repo_tools.toml to configure local repo tools + +### Changed +- Updated repo_kit_template tooling to support Applications and Extensions +- Updated repo_kit_template tooling to allow for application setup extensions +- Updated top level README.md to reflect updated tooling and templates +- Updated LICENSE.md to reflect updated tooling and templates +- Updated .gitattributes to reflect use of templates rather directly from source +- Added configuration to repo.toml to support new tools and templates + +### Removed +- Top level build .bat/.sh scripts in favor of using `repo build` directly +- Predefined `define_app` declarations from `premake5.lua` in favor of developer defined applications +- Predefined source/apps in favor of templates for developers to build from \ No newline at end of file diff --git a/README.md b/README.md index 48d6c16..d7aecfa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@

-**This branch branch is based on Omniverse Kit SDK 106.0** +## :warning: EA Release Information +**This branch is based on Omniverse Kit 106.1 EA. It includes EA versions of the Kit SDK, associated development tools, and templates. For the latest stable release, see the `106` branch.** ## Overview @@ -59,21 +60,30 @@ These resources empower developers at all experience levels to fully utilize the Ensure your system is set up with the following to work with Omniverse Applications and Extensions: - **Operating System**: Windows 10/11 or Linux (Ubuntu 20.04/22.04 recommended) + - **GPU**: NVIDIA RTX capable GPU (Turing or newer recommended) + - **Driver**: Latest NVIDIA driver compatible with your GPU + - **Internet Access**: Required for downloading the Omniverse Kit SDK, extensions, and tools. ### Required Software Dependencies -- **Git**: For version control and repository management -- **Git LFS**: For managing large files within the repository +- [**Git**](https://git-scm.com/downloads): For version control and repository management + +- [**Git LFS**](https://git-lfs.com/): For managing large files within the repository + - **(Windows) Microsoft Visual C++ Redistributable**: Many Windows systems will already have this, but if not, it can be obtained from [latest-supported-vc-redist](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version) + - **(Linux) build-essentials**: A package that includes `make` and other essential tools for building applications. For Ubuntu, install with `sudo apt-get install build-essential` ### Recommended Software -- **(Linux) Docker**: For containerized development and deployment. -- **VSCode (or your preferred IDE)**: For code editing and development +- [**(Linux) Docker**](https://docs.docker.com/engine/install/ubuntu/): For containerized development and deployment. **Ensure non-root users have Docker permissions.** + +- [**(Linux) NVIDIA Container Toolkit**](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html): For GPU-accelerated containerized development and deployment. **Installation and Configuring Docker steps are required.** + +- [**VSCode**](https://code.visualstudio.com/download) (or your preferred IDE): For code editing and development ## Repository Structure @@ -129,7 +139,7 @@ Run the following command to initiate the configuration wizard: .\repo.bat template new ``` -> **Note:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. +> **NOTE:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. Follow the prompt instructions: - **? Select with arrow keys what you want to create:** Application @@ -180,8 +190,23 @@ Initiate your newly created application using: ![Kit Base Editor Image](readme-assets/kit_base_editor.png) -***NOTE:* The initial startup may take 5 to 8 minutes as shaders compile for the first time. After initial shader compilation, startup time will reduce dramatically** +> **NOTE:** The initial startup may take 5 to 8 minutes as shaders compile for the first time. After initial shader compilation, startup time will reduce dramatically + +**Launch with Developer Bundle (Alternative):** Instead of running the default launch command, developers might prefer to include the developer bundle for access to developer-specific extensions, such as the Script Editor, Extension Manager, and more. + +To launch with the developer bundle, use the `--dev-bundle` or `-d` flag: + +**Linux:** +```bash +./repo.sh launch -d +``` + +**Windows:** +```powershell +.\repo.bat launch -d +``` +For more information on the extensions available in the developer bundle, see the [Developer Bundle Extensions](readme-assets/additional-docs/developer_bundle_extensions.md) document. ## Templates @@ -246,7 +271,7 @@ To learn more about what data is collected, how we use it and how you can change - [Usage and Troubleshooting](readme-assets/additional-docs/usage_and_troubleshooting.md) -- [BETA - Developer Bundle Extensions](readme-assets/additional-docs/developer_bundle_extensions.md) +- [Developer Bundle Extensions](readme-assets/additional-docs/developer_bundle_extensions.md) - [Omniverse Kit SDK Manual](https://docs.omniverse.nvidia.com/kit/docs/kit-manual/latest/index.html) diff --git a/premake5.lua b/premake5.lua index 0b643f5..7f81f94 100644 --- a/premake5.lua +++ b/premake5.lua @@ -12,4 +12,4 @@ repo_build.prebuild_copy { { "%{root}/tools/deps/user.toml", "%{root}/_build/deps/user.toml" }, } --- Apps: for each app generate batch files and a project based on kit files (e.g. my_name.my_app.kit) +-- Apps: for each app generate batch files and a project based on kit files (e.g. my_name.my_app.kit) \ No newline at end of file diff --git a/readme-assets/additional-docs/kit_app_template_tooling_guide.md b/readme-assets/additional-docs/kit_app_template_tooling_guide.md index 6a24169..f64ff44 100644 --- a/readme-assets/additional-docs/kit_app_template_tooling_guide.md +++ b/readme-assets/additional-docs/kit_app_template_tooling_guide.md @@ -69,8 +69,8 @@ Run the build command before testing or packaging your application to ensure all ``` Other common build options: -- `-c` or `--clean`: Cleans the build directory before building. -- `x` or `--rebuild`: Rebuilds the project from scratch. +- **`-c` or `--clean`:** Cleans the build directory before building. +- **`x` or `--rebuild`:** Rebuilds the project from scratch. ## Launch Tool @@ -91,37 +91,49 @@ Select and run a built .kit file from the `source/apps` directory: .\repo.bat launch ``` -Additional launch options: -- `d` or `--dev-bundle`: Launches with a suite of developer tools enabled in UI-based applications. -- `-p` or `--package`: Launches a packaged application from a specified path. +Additional package options: +- **`-d` or `--dev-bundle`:** Launches with a suite of developer tools enabled in UI-based applications. -**Linux:** -```bash -./repo.sh launch -p /path/to/package.zip -``` -**Windows:** -```powershell -.\repo.bat launch -p C:\path\to\package.zip -``` +- **`-p` or `--package`:** Launches a packaged application from a specified path. -Passing args to launched Kit executable: -You can pass through arguments to your targeted Kit executable by appending `--` to your launch command. Anything after `--` will be passed through to Kit. The following examples will pass the `--clear-cache` flag to Kit. + **Linux:** + ```bash + ./repo.sh launch -p + ``` + **Windows:** + ```powershell + .\repo.bat launch -p + ``` -**Linux:** -```bash -./repo.sh launch -- --clear-cache -``` -**Windows:** -```powershell -.\repo.bat launch -- --clear-cache -``` +- **`--container`:** Launches a containerized application (Linux only). + + **Linux:** + ```bash + ./repo.sh launch --container + ``` + **Windows:** + ```powershell + .\repo.bat launch --container + ``` + +- **Passing args to launched Kit executable:** +You can pass through arguments to your targeted Kit executable by appending `--` to your launch command. Any flags added after `--` will be passed through to Kit directly. The following examples will pass the `--clear-cache` flag to Kit. + + **Linux:** + ```bash + ./repo.sh launch -- --clear-cache + ``` + **Windows:** + ```powershell + .\repo.bat launch -- --clear-cache + ``` ## Test Tool **Command:** `./repo.sh test` or `.\repo.bat test` ### Purpose -The test tool facilitates the execution of automated tests on your applications and extensions to help ensure their functionality and stability. +The test tooling facilitates the execution of automated tests on your applications and extensions to help ensure their functionality and stability. Applications configurations (`.kit` files) are tested to ensure they can startup and shutdown without issue. However, the tests written within the extensions will dictate a majority of application functionality testing. Extension templates provided by the Kit App Template repository include sample tests which can be expanded upon to increase test coverage as needed. ### Usage Always run a build before testing: @@ -154,21 +166,42 @@ Always run a build before packaging to ensure the application is up-to-date: .\repo.bat package ``` -By default, the `package` tool creates a `.zip` file named `kit-app-template` in the `_build/packages` directory. The package name can be specified using the `-n` or `--name` flag: -**Linux:** -```bash -./repo.sh package -n my_package -``` -**Windows:** -```powershell -.\repo.bat package -n my_package -``` +Additional launch options: +- **`-n` or `--name`:** Specifies the package (or container image) name. + + **Linux:** + ```bash + ./repo.sh package -n + ``` + **Windows:** + ```powershell + .\repo.bat package -n + ``` + +- **`--thin`:** Creates a thin package that includes only custom extensions and configurations for required registry extensions. + + **Linux:** + ```bash + ./repo.sh package --thin + ``` + **Windows:** + ```powershell + .\repo.bat package --thin + ``` + +- **`--container`:** Packages the application as a container image (Linux only). When using the `--container` flag, the user will be asked to select a `.kit` file to use within the entry point script for the container. This can also be specified without user interaction by passing it appropriate `.kit` file name via the `--target-app` flag. + + **Linux:** + ```bash + ./repo.sh package --container + ``` + **Windows:** + ```powershell + .\repo.bat package --container + ``` :warning: **Important Note for Packaging:** Because the packaging operation will package everything within the `source/` directory the package version will need to be set independently of a given `kit` file. **The version is set within the `tools/VERSION.md` file.** -#### Fat and Thin Packages -Packages can be either 'fat' (including all dependencies) or 'thin' (including only custom extensions and configurations for required registry extensions). Thin packages are created using the `--thin` flag. A fat package is created by default to facilitate ease of use and testing, whereas **thin package distribution is required for broader dissemination.** - ## Additional Resources - [Kit App Template Companion Tutorial](https://docs.omniverse.nvidia.com/kit/docs/kit-app-template/latest/docs/intro.html) \ No newline at end of file diff --git a/readme-assets/additional-docs/usage_and_troubleshooting.md b/readme-assets/additional-docs/usage_and_troubleshooting.md index 611a27d..a266e2a 100644 --- a/readme-assets/additional-docs/usage_and_troubleshooting.md +++ b/readme-assets/additional-docs/usage_and_troubleshooting.md @@ -48,4 +48,26 @@ The `template new` tooling ensures that any created application is properly conf For a clean build, use the command `./repo.sh build -c` or `.\repo.bat build -c` to clean the build directory before building. ### Windows Long Path -Due to path length limitations on Windows it is recommended to place repository artifacts in a location closer to the root of the drive. This will help avoid issues with the path lengths when building and packaging applications. \ No newline at end of file +Due to path length limitations on Windows it is recommended to place repository artifacts in a location closer to the root of the drive. This will help avoid issues with the path lengths when building and packaging applications. + + +### Space Constraints Due to Docker Artifacts +When performing extensive local testing of container images created via `repo package --container`, Docker artifacts can accumulate over time, consuming significant disk space. + +`docker system df` can be used to determine disk space utilized by Docker objects. To reclaim space, consider the following options: + +1. **Regular Safe Cleanup**: + - **Command**: `docker container prune` + - **Description**: This command removes all stopped containers, which is typically safe and helps manage disk space without affecting images, networks, or volumes. + - **Use**: Recommended for regular maintenance. + +2. **Extensive Cleanup (:warning:Use with Caution:warning:)**: + - **Command**: `docker system prune` + - **Description**: This command removes all unused containers, networks, images, and optionally, volumes. It is akin to running a `rm -rf` for Docker resources. + - **Warning**: Use this command carefully, as it will remove many resources indiscriminately. Ensure you review and understand what will be deleted. + +For image-specific cleanup, use `docker images` to list all images and `docker rmi ` to manually remove those that are no longer needed. + + +### exFAT Drive Compatibility Limitations +The Kit App Template repository and associated tooling are designed to work with drive formats that support junctions/symlinks. If you are using an exFAT-formatted drive, you may encounter errors during the build process. To resolve this issue, consider using a different drive format such as NTFS. \ No newline at end of file diff --git a/repo.sh b/repo.sh index 2ca081f..236929b 100755 --- a/repo.sh +++ b/repo.sh @@ -2,9 +2,12 @@ set -e +# Set OMNI_REPO_ROOT early so `repo` bootstrapping can target the repository +# root when writing out Python dependencies. +export OMNI_REPO_ROOT="$( cd "$(dirname "$0")" ; pwd -P )" + SCRIPT_DIR=$(dirname ${BASH_SOURCE}) cd "$SCRIPT_DIR" -# Set OMNI_REPO_ROOT early so `repo` bootstrapping can target the repository -# root when writing out Python dependencies. -OMNI_REPO_ROOT="$( cd "$(dirname "$0")" ; pwd -P )" exec "tools/packman/python.sh" tools/repoman/repoman.py "$@" +# Use "exec" to ensure that envrionment variables don't accidentally affect other processes. +exec "tools/packman/python.sh" tools/repoman/repoman.py "$@" diff --git a/repo.toml b/repo.toml index 79a87a0..75d17c2 100644 --- a/repo.toml +++ b/repo.toml @@ -13,7 +13,18 @@ import_configs = [ # Repository Name name = "kit-app-template" -# Disable linbuild until we know what we want to do with this. +[repo_build] +# These are necessary to avoid a repo_build failure where the source/apps directory +# is expected to always exist. +fetch."platform:linux-x86_64".before_pull_commands = [ + ["mkdir", "--parents", "${root}/source/apps"], +] + +fetch."platform:windows-x86_64".before_pull_commands = [ + ["powershell", "-Command", "New-Item -ItemType Directory -Path ${root}/source/apps -ErrorAction SilentlyContinue", ";", "Write-Host 'Done'"], +] + +# Disable linbuild until we have a public image available. [repo_build.docker] enabled = false diff --git a/templates/apps/kit_base_editor/README.md b/templates/apps/kit_base_editor/README.md index b077af1..728fb19 100644 --- a/templates/apps/kit_base_editor/README.md +++ b/templates/apps/kit_base_editor/README.md @@ -49,7 +49,7 @@ cd kit-app-template .\repo.bat template new ``` -> **Note:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. +> **NOTE:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. Follow the prompt instructions: - **? Select with arrow keys what you want to create:** Application @@ -182,17 +182,67 @@ Alternatively, you can specify a package name using the `--name` flag: **Linux:** ```bash -./repo.sh package --name [package_name] +./repo.sh package --name ``` **Windows:** ```powershell -.\repo.bat package --name [package_name] +.\repo.bat package --name ``` This will bundle your application into a distributable format, ready for deployment on compatible platforms. :warning: **Important Note for Packaging:** Because the packaging operation will package everything within the `source/` directory the package version will need to be set independently of a given `kit` file. **The version is set within the `tools/VERSION.md` file.** +#### Launching a Package + +Applications packaged using the `package` command can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --package +``` +**Windows:** +```powershell +.\repo.bat launch --package +``` + +> **NOTE:** This behavior is not supported when packaging with the `--thin` flag. + +### Containerization (Linux Only) + +**Requires:** `Docker` and `NVIDIA Container Toolkit` + +The packaging tooling provided by the Kit App Template also supports containerization of applications. This is especially useful for deploying headless services and streaming applications in a containerized environment. + +To package your application as a container image, use the `--container` flag: + +**Linux:** +```bash +./repo.sh package --container +``` + +You will be prompted to select a `.kit` file to serve as the application to launch via the container entrypoint script. This will dictate the behavior of your containerized application. + +For example, if you are containerizing an application for streaming, select the `{your-app-name}_streaming.kit` file to ensure the correct application configuration is launched within the container. + +Similar to desktop packaging, the container option allows for specifying a package name using the `--name` flag to name the container image: + +**Linux:** +```bash +./repo.sh package --container --name [container_image_name] +``` + +#### Launching a Container + +Applications packaged as container images can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + ### Local Streaming The UI-based template applications in this repository produce more than a single `.kit` file. For the Kit Base Editor template application, this includes `{your-app-name}_streaming.kit` which we will use for local streaming. This file inherits from the base application and adds necessary streaming components like `omni.kit.livestream.webrtc`. To try local streaming, you need a web client to connect to the streaming server. @@ -229,6 +279,8 @@ import Window from './WindowNoUI'; :warning: **Important**: Launching the streaming application with `--no-window` passes an argument directly to Kit allowing it to run without the main application window to prevent conflicts with the streaming client. +**Launch and stream a desktop application:** + **Linux:** ```bash ./repo.sh launch -- --no-window @@ -240,6 +292,19 @@ import Window from './WindowNoUI'; Select the `{your-app-name}_streaming.kit` and wait for the application to start +**Launch and stream a containerized application:** + +When streaming a containerized application, ensure that the containerized application was configured during packaging to launch a streaming application (e.g., `{your_app_name}_streaming.kit`). + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + +> **NOTE:** The `--no-window` flag is not required for containerized applications as it is the default launch behavior. + #### 4. Start the Streaming Client ```bash npm run dev diff --git a/templates/apps/kit_service/README.md b/templates/apps/kit_service/README.md index e7058b8..3c738a0 100644 --- a/templates/apps/kit_service/README.md +++ b/templates/apps/kit_service/README.md @@ -52,7 +52,7 @@ cd kit-app-template .\repo.bat template new ``` -> **Note:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. +> **NOTE:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. Follow the prompt instructions: - **? Select with arrow keys what you want to create:** Application @@ -174,17 +174,67 @@ Alternatively, you can specify a package name using the `--name` flag: **Linux:** ```bash -./repo.sh package --name [package_name] +./repo.sh package --name ``` **Windows:** ```powershell -.\repo.bat package --name [package_name] +.\repo.bat package --name ``` This will bundle your application into a distributable format, ready for deployment on compatible platforms. :warning: **Important Note for Packaging:** Because the packaging operation will package everything within the `source/` directory the package version will need to be set independently of a given `kit` file. **The version is set within the `tools/VERSION.md` file.** +#### Launching a Package + +Applications packaged using the `package` command can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --package +``` +**Windows:** +```powershell +.\repo.bat launch --package +``` + +> **NOTE:** This behavior is not supported when packaging with the `--thin` flag. + +### Containerization (Linux Only) + +**Requires:** `Docker` and `NVIDIA Container Toolkit` + +The packaging tooling provided by the Kit App Template also supports containerization of applications. This is especially useful for deploying headless services and streaming applications in a containerized environment. + +To package your application as a container image, use the `--container` flag: + +**Linux:** +```bash +./repo.sh package --container +``` + +You will be prompted to select a `.kit` file to serve as the application to launch via the container entrypoint script. This will dictate the behavior of your containerized application. + +For example, if you are containerizing a headless Kit Service, select the `{your-service-name}.kit` file to ensure the correct application configuration is launched within the container. + +Similar to desktop packaging, the container option allows for specifying a package name using the `--name` flag to name the container image: + +**Linux:** +```bash +./repo.sh package --container --name [container_image_name] +``` + +#### Launching a Container + +Applications packaged as container images can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + ## Additional Learning - [Kit App Template Companion Tutorial](https://docs.omniverse.nvidia.com/kit/docs/kit-app-template/latest/docs/intro.html) diff --git a/templates/apps/streaming_configs/default_stream.kit b/templates/apps/streaming_configs/default_stream.kit index e87abef..683ef48 100644 --- a/templates/apps/streaming_configs/default_stream.kit +++ b/templates/apps/streaming_configs/default_stream.kit @@ -21,27 +21,22 @@ template_name = "omni.streaming_configuration" [settings.app] fastShutdown = true -livestream.allowResize = false # Enable or Disable SDK Scaling -livestream.skipCapture = 1 # livestream skipCapture ON for local streaming -livestream.webrtcEtli = true # Only log error or critical level issues. name = "{{ application_display_name }} Streaming" # Application Display Name ovc_deployment = true renderer.skipWhileMinimized = true renderer.resolution.height = 1080 renderer.resolution.width = 1920 -runLoops.present.rateLimitEnabled = false +vsync = false # Vsync disabled by default, can be set to true for L40 or similar window.scaleToMonitor = true window.showStartup = false window.height = 1080 window.width = 1920 - [settings] rtx.post.aa.op = 3 rtx.verifyDriverVersion.enabled = false rtx-transient.dlssg.enabled = false # Disable DLSS otherwise it can push the framerate above the locked limit - [settings.app.extensions] registryEnabled = true supportedTargets.platform = [] # Skip checking supported platform/config when building @@ -52,3 +47,40 @@ folders.'++' = [ # Search paths for extensions. "${app}/../apps", "${app}/../extscache" ] + +[settings.app.file] +ignoreUnsavedOnExit = true + +[settings.app.livestream] +skipCapture = 1 # livestream skipCapture ON for local streaming +webrtcEtli = true # Only log error or critical level issues. + +[settings.app.rendergraph] +maxFramesInFlight = 2 + +[settings.app.runloops] +main.rateLimitEnabled = true # Enable rate limiting on the main thread +main.rateLimitFrequency = 60 # Lock it to 60fps +main.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +main.syncToPresent = true # Sync with the present thread, smooths UI updates +present.rateLimitEnabled = true # Rate limit the present thread +present.rateLimitFrequency = 60 # Lock it to 60fps +present.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +rendering_0.rateLimitEnabled = true # Enable rate limiting for the rendering thread +rendering_0.rateLimitFrequency = 60 # Lock it to 60fps +rendering_0.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +rendering_0.syncToPresent = true # Sync with the present tread, smooths UI updates +rendering_1.rateLimitEnabled = true # Enable rate limiting for the rendering thread +rendering_1.rateLimitFrequency = 60 # Lock it to 60fps +rendering_1.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +rendering_1.syncToPresent = true # Sync with the present tread, smooths UI updates + +[settings.app.runLoopsGlobal] +syncToPresent = true # Sync everything with the present thread + +[settings.app.viewport] +defaults.tickRate = 60 # Lock to 60fps[settings.app] + +[settings.exts."omni.kit.renderer.core"] +present.enabled = true # Enable the present thread +present.presentAfterRendering = true # Ensure the present thread waits for the rendering to complete[settings] diff --git a/templates/apps/streaming_configs/gdn_stream.kit b/templates/apps/streaming_configs/gdn_stream.kit index 685ad1c..40ca9e3 100644 --- a/templates/apps/streaming_configs/gdn_stream.kit +++ b/templates/apps/streaming_configs/gdn_stream.kit @@ -25,6 +25,7 @@ fastShutdown = true # Skip long full shutdown and exit quickly name = "{{ application_display_name }} GDN Streaming" # Skip checking supported platform/config when building titleVersion = "1.0.0" # this is used in our setup file to display some Version to the user in the title bar useFabricSceneDelegate = true # Turn on the Fabric scene delegate by default +vsync = false # Vsync disabled by default, can be set to true for L40 or similar [settings.app.content] @@ -38,6 +39,32 @@ emptyStageOnStart = false ignoreUnsavedOnExit = true # enable quiting without confirmation +[settings.app.rendergraph] +maxFramesInFlight = 2 + + +[settings.app.runloops] +main.rateLimitEnabled = true # Enable rate limiting on the main thread +main.rateLimitFrequency = 60 # Lock it to 60fps +main.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +main.syncToPresent = true # Sync with the present thread, smooths UI updates +present.rateLimitEnabled = true # Rate limit the present thread +present.rateLimitFrequency = 60 # Lock it to 60fps +present.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +rendering_0.rateLimitEnabled = true # Enable rate limiting for the rendering thread +rendering_0.rateLimitFrequency = 60 # Lock it to 60fps +rendering_0.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +rendering_0.syncToPresent = true # Sync with the present tread, smooths UI updates +rendering_1.rateLimitEnabled = true # Enable rate limiting for the rendering thread +rendering_1.rateLimitFrequency = 60 # Lock it to 60fps +rendering_1.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync +rendering_1.syncToPresent = true # Sync with the present tread, smooths UI updates + + +[settings.app.runLoopsGlobal] +syncToPresent = true # Sync everything with the present thread + + [settings.app.window] title = "{{ application_display_name }} GDN Streaming" fullscreen = true # fullscreen on startup @@ -63,22 +90,19 @@ width = 1920 # width of main window enabled = true -[settings.app.usdrt.population.utils] -# When populating Fabric from USD, merge meshes under scene -# graph instances. Helps with e.g. Tesla -# Needs to be tested more before enabling for Runway -mergeInstances = false - [settings.app.viewport] forceHideFps = true # control if performance data is shown in Viewport createCameraModelRep = false +defaults.tickRate = 60 # Lock to 60fps[settings.app] [settings.exts."omni.appwindow"] listenF11 = false listenF7 = false + [settings.exts."omni.kit.gfn"] + auto_startup_gfn = true # Tests diff --git a/templates/apps/usd_composer/README.md b/templates/apps/usd_composer/README.md index 3dbf95f..cb6ca1b 100644 --- a/templates/apps/usd_composer/README.md +++ b/templates/apps/usd_composer/README.md @@ -60,7 +60,7 @@ cd kit-app-template .\repo.bat template new ``` -> **Note:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. +> **NOTE:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. Follow the prompt instructions: - **? Select with arrow keys what you want to create:** Application @@ -114,7 +114,7 @@ Select **Window > Browsers > Configurator Samples** - to open configuration samp ### Testing Applications and their associated extensions can be tested using the `repo test` tooling provided. Each application template includes an initial test suite that can be run to verify the application's functionality. -> **Note:** Testing will only be run on applications and extensions within the build directory. **A successful build is required before testing.** +> **NOTE:** Testing will only be run on applications and extensions within the build directory. **A successful build is required before testing.** **Linux:** ```bash @@ -199,17 +199,67 @@ Alternatively, you can specify a package name using the `--name` flag: **Linux:** ```bash -./repo.sh package --name [package_name] +./repo.sh package --name ``` **Windows:** ```powershell -.\repo.bat package --name [package_name] +.\repo.bat package --name ``` This will bundle your application into a distributable format, ready for deployment on compatible platforms. :warning: **Important Note for Packaging:** Because the packaging operation will package everything within the `source/` directory the package version will need to be set independently of a given `kit` file. **The version is set within the `tools/VERSION.md` file.** +#### Launching a Package + +Applications packaged using the `package` command can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --package +``` +**Windows:** +```powershell +.\repo.bat launch --package +``` + +> **NOTE:** This behavior is not supported when packaging with the `--thin` flag. + +### Containerization (Linux Only) + +**Requires:** `Docker` and `NVIDIA Container Toolkit` + +The packaging tooling provided by the Kit App Template also supports containerization of applications. This is especially useful for deploying headless services and streaming applications in a containerized environment. + +To package your application as a container image, use the `--container` flag: + +**Linux:** +```bash +./repo.sh package --container +``` + +You will be prompted to select a `.kit` file to serve as the application to launch via the container entrypoint script. This will dictate the behavior of your containerized application. + +For example, if you are containerizing an application for streaming, select the `{your-app-name}_streaming.kit` file to ensure the correct application configuration is launched within the container. + +Similar to desktop packaging, the container option allows for specifying a package name using the `--name` flag to name the container image: + +**Linux:** +```bash +./repo.sh package --container --name [container_image_name] +``` + +#### Launching a Container + +Applications packaged as container images can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + ### Local Streaming The UI-based template applications in this repository produce more than a single `.kit` file. For the USD Composer template application, this includes `{your-app-name}_streaming.kit` which we will use for local streaming. This file inherits from the base application and adds necessary streaming components like `omni.kit.livestream.webrtc`. To try local streaming, you need a web client to connect to the streaming server. @@ -246,6 +296,8 @@ import Window from './WindowNoUI'; :warning: **Important**: Launching the streaming application with `--no-window` passes an argument directly to Kit allowing it to run without the main application window to prevent conflicts with the streaming client. +**Launch and stream a desktop application:** + **Linux:** ```bash ./repo.sh launch -- --no-window @@ -257,6 +309,19 @@ import Window from './WindowNoUI'; Select the `{your-app-name}_streaming.kit` and wait for the application to start +**Launch and stream a containerized application:** + +When streaming a containerized application, ensure that the containerized application was configured during packaging to launch a streaming application (e.g., `{your_app_name}_streaming.kit`). + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + +> **NOTE:** The `--no-window` flag is not required for containerized applications as it is the default launch behavior. + #### 4. Start the Streaming Client ```bash npm run dev diff --git a/templates/apps/usd_composer/omni.usd_composer.kit b/templates/apps/usd_composer/omni.usd_composer.kit index 7e20a71..002d9dd 100644 --- a/templates/apps/usd_composer/omni.usd_composer.kit +++ b/templates/apps/usd_composer/omni.usd_composer.kit @@ -203,6 +203,9 @@ skipWhileMinimized = true dev_build = false persistent = true +[settings.app.usdrt.population.utils] +mergeMaterials = false + [settings.app.window] title = "{{ application_display_name }}" iconPath = "${% raw %}{{% endraw %}{{ setup_extension_name }}{% raw %}}{% endraw %}/data/nvidia-omniverse-create.png" diff --git a/templates/apps/usd_explorer/README.md b/templates/apps/usd_explorer/README.md index 3c79e47..bfa6a52 100644 --- a/templates/apps/usd_explorer/README.md +++ b/templates/apps/usd_explorer/README.md @@ -62,7 +62,7 @@ cd kit-app-template .\repo.bat template new ``` -> **Note:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. +> **NOTE:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. Follow the prompt instructions: - **? Select with arrow keys what you want to create:** Application @@ -201,17 +201,66 @@ Alternatively, you can specify a package name using the `--name` flag: **Linux:** ```bash -./repo.sh package --name [package_name] +./repo.sh package --name ``` **Windows:** ```powershell -.\repo.bat package --name [package_name] +.\repo.bat package --name ``` This will bundle your application into a distributable format, ready for deployment on compatible platforms. :warning: **Important Note for Packaging:** Because the packaging operation will package everything within the `source/` directory the package version will need to be set independently of a given `kit` file. **The version is set within the `tools/VERSION.md` file.** +#### Launching a Package + +Applications packaged using the `package` command can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --package +``` +**Windows:** +```powershell +.\repo.bat launch --package +``` + +> **NOTE:** This behavior is not supported when packaging with the `--thin` flag. + +### Containerization (Linux Only) + +**Requires:** `Docker` and `NVIDIA Container Toolkit` + +The packaging tooling provided by the Kit App Template also supports containerization of applications. This is especially useful for deploying headless services and streaming applications in a containerized environment. + +To package your application as a container image, use the `--container` flag: + +**Linux:** +```bash +./repo.sh package --container +``` + +You will be prompted to select a `.kit` file to serve as the application to launch via the container entrypoint script. This will dictate the behavior of your containerized application. + +For example, if you are containerizing an application for streaming, select the `{your-app-name}_streaming.kit` file to ensure the correct application configuration is launched within the container. + +Similar to desktop packaging, the container option allows for specifying a package name using the `--name` flag to name the container image: + +**Linux:** +```bash +./repo.sh package --container --name [container_image_name] +``` + +#### Launching a Container + +Applications packaged as container images can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. ### Local Streaming @@ -249,6 +298,8 @@ import Window from './WindowNoUI'; :warning: **Important**: Launching the streaming application with `--no-window` passes an argument directly to Kit allowing it to run without the main application window to prevent conflicts with the streaming client. +**Launch and stream a desktop application:** + **Linux:** ```bash ./repo.sh launch -- --no-window @@ -260,6 +311,19 @@ import Window from './WindowNoUI'; Select the `{your-app-name}_streaming.kit` and wait for the application to start +**Launch and stream a containerized application:** + +When streaming a containerized application, ensure that the containerized application was configured during packaging to launch a streaming application (e.g., `{your_app_name}_streaming.kit`). + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + +> **NOTE:** The `--no-window` flag is not required for containerized applications as it is the default launch behavior. + #### 4. Start the Streaming Client ```bash npm run dev diff --git a/templates/apps/usd_explorer/omni.usd_explorer.kit b/templates/apps/usd_explorer/omni.usd_explorer.kit index afc17cc..7040a53 100644 --- a/templates/apps/usd_explorer/omni.usd_explorer.kit +++ b/templates/apps/usd_explorer/omni.usd_explorer.kit @@ -548,7 +548,7 @@ enable = false # Making a ground for whatever reason bewteen scene loads is cau inertiaDecay = 3 tumbleSpeed = 720 -[setings.persistent.exts."omni.kit.manipulator.prim".manipulator] +[settings.persistent.exts."omni.kit.manipulator.prim.core".manipulator] placement = "Bounding Box Base" [settings.persistent.exts."omni.kit.manipulator.tool.snap"] @@ -613,7 +613,7 @@ ambientLightIntensity = 1.0 rendermode = 1 [settings.rtx-transient.hydra] -conservativeMemoryLimits = true # Enable a more conservative approach to stop loading geometry when the amount of free GPU and system memory go below specified thresholds +conservativeMemoryLimits = true # Stop loading geometry when memory goes below specified thresholds maxInstanceCount = 12000000 # Limit the number of instances to 12 million to cap performance and memory impact of larger instance counts [settings.rtx-transient.meshlights] @@ -635,7 +635,6 @@ enableAnonymousAppName = true samplingFactor = 1.0 # No test sampling for these tests dependencies = [ - "omni.kit.test_suite.helpers", "omni.kit.ui_test", #"omni.hydra.pxr", # NOTE: test_extensions.py (with imports from omni.kit.core.tests) need dependencies from some extensions diff --git a/templates/apps/usd_viewer/README.md b/templates/apps/usd_viewer/README.md index e784dc1..75678ec 100644 --- a/templates/apps/usd_viewer/README.md +++ b/templates/apps/usd_viewer/README.md @@ -51,7 +51,7 @@ cd kit-app-template .\repo.bat template new ``` -> **Note:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. +> **NOTE:** If this is your first time running the `template new` tool, you'll be prompted to accept the Omniverse Licensing Terms. Follow the prompt instructions: - **? Select with arrow keys what you want to create:** Application @@ -185,17 +185,66 @@ Alternatively, you can specify a package name using the `--name` flag: **Linux:** ```bash -./repo.sh package --name [package_name] +./repo.sh package --name ``` **Windows:** ```powershell -.\repo.bat package --name [package_name] +.\repo.bat package --name ``` This will bundle your application into a distributable format, ready for deployment on compatible platforms. :warning: **Important Note for Packaging:** Because the packaging operation will package everything within the `source/` directory the package version will need to be set independently of a given `kit` file. **The version is set within the `tools/VERSION.md` file.** +#### Launching a Package + +Applications packaged using the `package` command can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --package +``` +**Windows:** +```powershell +.\repo.bat launch --package +``` + +> **NOTE:** This behavior is not supported when packaging with the `--thin` flag. + +### Containerization (Linux Only) + +**Requires:** `Docker` and `NVIDIA Container Toolkit` + +The packaging tooling provided by the Kit App Template also supports containerization of applications. This is especially useful for deploying headless services and streaming applications in a containerized environment. + +To package your application as a container image, use the `--container` flag: + +**Linux:** +```bash +./repo.sh package --container +``` + +You will be prompted to select a `.kit` file to serve as the application to launch via the container entrypoint script. This will dictate the behavior of your containerized application. + +For example, if you are containerizing an application for streaming, select the `{your-app-name}_streaming.kit` file to ensure the correct application configuration is launched within the container. + +Similar to desktop packaging, the container option allows for specifying a package name using the `--name` flag to name the container image: + +**Linux:** +```bash +./repo.sh package --container --name [container_image_name] +``` + +#### Launching a Container + +Applications packaged as container images can be launched using the `launch` command: + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. ### Local Streaming @@ -215,6 +264,8 @@ Follow the instructions in the README to install the necessary dependencies. :warning: **Important**: Launching the streaming application with `--no-window` passes an argument directly to Kit allowing it to run without the main application window to prevent conflicts with the streaming client. +**Launch and stream a desktop application:** + **Linux:** ```bash ./repo.sh launch -- --no-window @@ -226,6 +277,19 @@ Follow the instructions in the README to install the necessary dependencies. Select the `{your-app-name}_streaming.kit` and wait for the application to start +**Launch and stream a containerized application:** + +When streaming a containerized application, ensure that the containerized application was configured during packaging to launch a streaming application (e.g., `{your_app_name}_streaming.kit`). + +**Linux:** +```bash +./repo.sh launch --container +``` + +If only a single container image exists, it will launch automatically. If multiple container images exist, you will be prompted to select the desired container image to launch. + +> **NOTE:** The `--no-window` flag is not required for containerized applications as it is the default launch behavior. + #### 3. Start the Streaming Client ```bash npm run dev diff --git a/templates/apps/usd_viewer/omni.usd_viewer.kit b/templates/apps/usd_viewer/omni.usd_viewer.kit index e266a9e..6e3d587 100644 --- a/templates/apps/usd_viewer/omni.usd_viewer.kit +++ b/templates/apps/usd_viewer/omni.usd_viewer.kit @@ -23,6 +23,7 @@ template_name = "omni.usd_viewer" "omni.graph.scriptnode" = {} "omni.graph.ui_nodes" = {} "omni.hydra.rtx" = {} +"omni.hydra.usdrt_delegate" = {} "omni.kit.manipulator.camera" = {} "omni.kit.manipulator.selection" = {} "omni.kit.renderer.core" = {} @@ -31,18 +32,14 @@ template_name = "omni.usd_viewer" "omni.kit.viewport.utility" = {} "omni.kit.viewport.window" = {} "omni.no_code_ui.bundle" = {} +"omni.ujitso.client" = {} "usdrt.scenegraph" = {} "{{ setup_extension_name }}" = { order = 1000 } [settings.persistent.app] viewport.defaults.tickRate = 60 # Lock to 60fps -viewport.displayOptions = 32255 # Bitmask that controls which performance counters appear in the HUD. -viewport.grid.enabled = false viewport.noPadding = true # Remove default frame around viewport -viewport.show.lights.visible = false -viewport.Viewport.Viewport0.guide.grid.visible = false -viewport.Viewport.Viewport0.guide.axis.visible = false [settings.persistent.exts] "omni.kit.window.sequencer".useSequencerCamera = true # Sequencer Camera Sync ON @@ -63,31 +60,19 @@ livestream.skipCapture = 1 # livestream skipCapture ON for local streaming name = "{{ application_display_name }}" renderer.resolution.width = 1920 renderer.resolution.height = 1080 -runLoops.main.rateLimitEnabled = true # Enable rate limiting on the main thread -runLoops.main.rateLimitFrequency = 60 # Lock it to 60fps -runLoops.main.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync -runLoops.main.syncToPresent = true # Sync with the present thread, smooths UI updates -runLoops.present.rateLimitEnabled = true # Rate limit the present thread -runLoops.present.rateLimitFrequency = 60 # Lock it to 60fps -runLoops.present.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync -runLoops.rendering_0.rateLimitEnabled = true # Enable rate limiting for the rendering thread -runLoops.rendering_0.rateLimitFrequency = 60 # Lock it to 60fps -runLoops.rendering_0.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync -runLoops.rendering_0.syncToPresent = true # Sync with the present tread, smooths UI updates -runLoops.rendering_1.rateLimitEnabled = true # Enable rate limiting for the rendering thread -runLoops.rendering_1.rateLimitFrequency = 60 # Lock it to 60fps -runLoops.rendering_1.rateLimitUsePrecisionSleep = true # Use precise sleep values to ensure threads sync -runLoops.rendering_1.syncToPresent = true # Sync with the present tread, smooths UI updates -runLoopsGlobal.syncToPresent = true # Sync everything with the present thread -# When populating Fabric from USD, merge meshes under scene -# graph instances. Helps with e.g. Tesla -# Needs to be tested more before enabling for Runway -usdrt.population.utils.mergeInstances = false -useFabricSceneDelegate = true # FSD Enabled by default -viewport.createCameraModelRep = false -viewport.forceHideFps = true # Show/Hide the performance counters in the upper right corner of the viewport. -viewport.defaults.fillViewport = true # default to fill viewport -vsync = true # Enable vsync +useFabricSceneDelegate = true # Turn on the Fabric scene delegate by default + +[settings.app.usdrt.population.utils] +mergeInstances = false +mergeMaterials = false + +[settings.app.viewport.defaults] +fillViewport = true +guide.grid.visible = false +guide.axis.visible = false +hud.visible = false +scene.cameras.visible = false +scene.lights.visible = false [settings.app.exts] folders.'++' = [ # Search paths for extensions. @@ -138,10 +123,26 @@ listenF7 = false enableAnonymousData = true enableAnonymousAppName = true +[settings.UJITSO] +# UJITSO supports loading cached representations of assets. +# These settings controls what is loaded from cache and where from. +enabled = true # Enable or disable the use of UJITSO cache. +textures = true +geometry = true +materials = true +datastore.localCachePath="" # The absolute path to the root directory containing cached assets. +readCacheWithAssetRoot="" # The absolute path to the root directory containing the original non-cached assets. + + # Tests ################################ [[test]] + +dependencies = [ + "{{ setup_extension_name }}.tests" +] + args = [ "--/app/window/width=480", "--/app/window/height=480", diff --git a/templates/extensions/basic_cpp/template/config/extension.toml b/templates/extensions/basic_cpp/template/config/extension.toml index ef1ab60..a166b82 100644 --- a/templates/extensions/basic_cpp/template/config/extension.toml +++ b/templates/extensions/basic_cpp/template/config/extension.toml @@ -44,3 +44,7 @@ dependencies = [ args =[ ] + +cppTests.libraries = [ + # "bin/${lib_prefix}{{ extension_name }}.tests${lib_ext}", +] \ No newline at end of file diff --git a/templates/extensions/basic_cpp/template/plugins/{{extension_name}}/CppExtension.cpp b/templates/extensions/basic_cpp/template/plugins/{{extension_name}}/CppExtension.cpp index f612419..961e033 100644 --- a/templates/extensions/basic_cpp/template/plugins/{{extension_name}}/CppExtension.cpp +++ b/templates/extensions/basic_cpp/template/plugins/{{extension_name}}/CppExtension.cpp @@ -1,5 +1,6 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. + * All rights reserved. * SPDX-License-Identifier: LicenseRef-NvidiaProprietary * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -14,6 +15,7 @@ #include #include +#include #include #include diff --git a/templates/extensions/basic_cpp/template/premake5.lua b/templates/extensions/basic_cpp/template/premake5.lua index a3c86c0..84e5682 100644 --- a/templates/extensions/basic_cpp/template/premake5.lua +++ b/templates/extensions/basic_cpp/template/premake5.lua @@ -16,3 +16,9 @@ project_ext_plugin(ext, "{{ extension_name }}.plugin") add_files("iface", "%{root}/include/omni/ext", "IExt.h") add_files("impl", "plugins/"..plugin_name) includedirs { "plugins/"..plugin_name } + +-- To write C++ tests, we have to create a shared library with tests to be loaded +-- project_ext_tests(ext, ext.id..".tests") + -- dependson { ext.id..".plugin" } + -- add_files("impl", "tests.cpp") + -- includedirs { "%{target_deps}/doctest/include" } diff --git a/templates/extensions/basic_cpp/template/tests.cpp/CppExampleTest.cpp b/templates/extensions/basic_cpp/template/tests.cpp/CppExampleTest.cpp new file mode 100644 index 0000000..f10c97e --- /dev/null +++ b/templates/extensions/basic_cpp/template/tests.cpp/CppExampleTest.cpp @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: LicenseRef-NvidiaProprietary + * + * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual + * property and proprietary rights in and to this material, related + * documentation and any modifications thereto. Any use, reproduction, + * disclosure or distribution of this material and related documentation + * without an express license agreement from NVIDIA CORPORATION or + * its affiliates is strictly prohibited. + */ + +#include +#include + +CARB_BINDINGS("{{ extension_name }}.tests") + +TEST_SUITE("{{ extension_name }}.tests Test Suite") { + TEST_CASE("Sample Test Case") { + CHECK(5 == 5); + } +} \ No newline at end of file diff --git a/templates/extensions/basic_python/template/{{python_module_path}}/extension.py b/templates/extensions/basic_python/template/{{python_module_path}}/extension.py index fc98b11..53cc2ee 100644 --- a/templates/extensions/basic_python/template/{{python_module_path}}/extension.py +++ b/templates/extensions/basic_python/template/{{python_module_path}}/extension.py @@ -1,4 +1,5 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. # SPDX-License-Identifier: LicenseRef-NvidiaProprietary # # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -11,20 +12,28 @@ import omni.ext -# Functions and vars are available to other extensions as usual in python: `{{python_module}}.some_public_function(x)` +# Functions and vars are available to other extensions as usual in python: +# `{{python_module}}.some_public_function(x)` def some_public_function(x: int): + """This is a public function that can be called from other extensions.""" print(f"[{{ extension_name }}] some_public_function was called with {x}") return x**x -# Any class derived from `omni.ext.IExt` in the top level module (defined in `python.modules` of `extension.toml`) will -# be instantiated when the extension gets enabled, and `on_startup(ext_id)` will be called. -# Later when the extension gets disabled on_shutdown() is called. +# Any class derived from `omni.ext.IExt` in the top level module (defined in +# `python.modules` of `extension.toml`) will be instantiated when the extension +# gets enabled, and `on_startup(ext_id)` will be called. Later when the +# extension gets disabled on_shutdown() is called. class MyExtension(omni.ext.IExt): - # ext_id is the current extension id. It can be used with the extension manager to query additional information, - # like where this extension is located on the filesystem. - def on_startup(self, ext_id): + """This is a blank extension template.""" + # ext_id is the current extension id. It can be used with the extension + # manager to query additional information, like where this extension is + # located on the filesystem. + def on_startup(self, _ext_id): + """This is called every time the extension is activated.""" print("[{{ extension_name }}] Extension startup") def on_shutdown(self): + """This is called every time the extension is deactivated. It is used + to clean up the extension state.""" print("[{{ extension_name }}] Extension shutdown") diff --git a/templates/extensions/basic_python/template/{{python_module_path}}/tests/test_benchmarks.py b/templates/extensions/basic_python/template/{{python_module_path}}/tests/test_benchmarks.py index ada67c1..b80546b 100644 --- a/templates/extensions/basic_python/template/{{python_module_path}}/tests/test_benchmarks.py +++ b/templates/extensions/basic_python/template/{{python_module_path}}/tests/test_benchmarks.py @@ -1,4 +1,5 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. # SPDX-License-Identifier: LicenseRef-NvidiaProprietary # # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -37,18 +38,23 @@ async def benchmark_sleepy_with_custom_metrics(self): async def benchmark_sleepy_with_custom_metrics_array(self): """ - Another benchmark method using custom metrics to demonstrate setting arrays for a metric, and to show that - there's no crosstalk of metrics between benchmarks. + Another benchmark method using custom metrics to demonstrate setting + arrays for a metric, and to show that there's no crosstalk of metrics + between benchmarks. """ - # array of samples for custom metric 'my_other_metric' is set to [1.2ms, 0.9ms, 1.1ms] - self.set_metric_sample_array(name="my_other_metric", values=[1.2, 0.9, 1.1], unit="ms") + # array of samples for custom metric 'my_other_metric' is set to + # [1.2ms, 0.9ms, 1.1ms] + self.set_metric_sample_array( + name="my_other_metric", values=[1.2, 0.9, 1.1], unit="ms" + ) await asyncio.sleep(0.01) class TestBenchmarksNoCustomMetric(AsyncTestCase): """ Example Benchmark class without custom metrics. - * If you are not planning to use custom metrics you can derive from `AsyncTestCase`. + * If you are not planning to use custom metrics you can derive from + `AsyncTestCase`. * Benchmark methods have to start with the 'benchmark' prefix. * The runtime of the benchmark methods and their skip state belong to the default metrics which are always reported. diff --git a/templates/extensions/python_ui/template/{{python_module_path}}/extension.py b/templates/extensions/python_ui/template/{{python_module_path}}/extension.py index 571ede4..c5901ce 100644 --- a/templates/extensions/python_ui/template/{{python_module_path}}/extension.py +++ b/templates/extensions/python_ui/template/{{python_module_path}}/extension.py @@ -12,24 +12,31 @@ import omni.ui as ui -# Functions and vars are available to other extensions as usual in python: `{{python_module}}.some_public_function(x)` +# Functions and vars are available to other extensions as usual in python: +# `{{python_module}}.some_public_function(x)` def some_public_function(x: int): + """This is a public function that can be called from other extensions.""" print(f"[{{ extension_name }}] some_public_function was called with {x}") return x ** x -# Any class derived from `omni.ext.IExt` in the top level module (defined in `python.modules` of `extension.toml`) will -# be instantiated when the extension gets enabled, and `on_startup(ext_id)` will be called. -# Later when the extension gets disabled on_shutdown() is called. +# Any class derived from `omni.ext.IExt` in the top level module (defined in +# `python.modules` of `extension.toml`) will be instantiated when the extension +# gets enabled, and `on_startup(ext_id)` will be called. Later when the +# extension gets disabled on_shutdown() is called. class MyExtension(omni.ext.IExt): - # ext_id is the current extension id. It can be used with the extension manager to query additional information, - # like where this extension is located on the filesystem. - def on_startup(self, ext_id): + """This extension manages a simple counter UI.""" + # ext_id is the current extension id. It can be used with the extension + # manager to query additional information, like where this extension is + # located on the filesystem. + def on_startup(self, _ext_id): + """This is called every time the extension is activated.""" print("[{{ extension_name }}] Extension startup") self._count = 0 - - self._window = ui.Window("{{ extension_display_name }}", width=300, height=300) + self._window = ui.Window( + "{{ extension_display_name }}", width=300, height=300 + ) with self._window.frame: with ui.VStack(): label = ui.Label("") @@ -49,4 +56,6 @@ def on_reset(): ui.Button("Reset", clicked_fn=on_reset) def on_shutdown(self): + """This is called every time the extension is deactivated. It is used + to clean up the extension state.""" print("[{{ extension_name }}] Extension shutdown") diff --git a/templates/extensions/python_ui/template/{{python_module_path}}/tests/test_hello_world.py b/templates/extensions/python_ui/template/{{python_module_path}}/tests/test_hello_world.py index 6fade0b..eaadc12 100644 --- a/templates/extensions/python_ui/template/{{python_module_path}}/tests/test_hello_world.py +++ b/templates/extensions/python_ui/template/{{python_module_path}}/tests/test_hello_world.py @@ -9,18 +9,22 @@ # its affiliates is strictly prohibited. # NOTE: -# omni.kit.test - std python's unittest module with additional wrapping to add suport for async/await tests -# For most things refer to unittest docs: https://docs.python.org/3/library/unittest.html +# omni.kit.test - std python's unittest module with additional wrapping to add +# suport for async/await tests +# For most things refer to unittest docs: +# https://docs.python.org/3/library/unittest.html import omni.kit.test # Extension for writing UI tests (to simulate UI interaction) import omni.kit.ui_test as ui_test -# Import extension python module we are testing with absolute import path, as if we are external user (other extension) +# Import extension python module we are testing with absolute import path, +# as if we are external user (other extension) import {{ python_module }} -# Having a test class dervived from omni.kit.test.AsyncTestCase declared on the root of module will make it auto-discoverable by omni.kit.test +# Having a test class dervived from omni.kit.test.AsyncTestCase declared on the +# root of module will make it auto-discoverable by omni.kit.test class Test(omni.kit.test.AsyncTestCase): # Before running each test async def setUp(self): @@ -36,13 +40,16 @@ async def test_hello_public_function(self): self.assertEqual(result, 256) async def test_window_button(self): - # Find a label in our window label = ui_test.find("{{ extension_display_name }}//Frame/**/Label[*]") # Find buttons in our window - add_button = ui_test.find("{{ extension_display_name }}//Frame/**/Button[*].text=='Add'") - reset_button = ui_test.find("{{ extension_display_name }}//Frame/**/Button[*].text=='Reset'") + add_button = ui_test.find( + "{{ extension_display_name }}//Frame/**/Button[*].text=='Add'" + ) + reset_button = ui_test.find( + "{{ extension_display_name }}//Frame/**/Button[*].text=='Reset'" + ) # Click reset button await reset_button.click() diff --git a/templates/extensions/service.setup/template/{{python_module_path}}/extension.py b/templates/extensions/service.setup/template/{{python_module_path}}/extension.py index bbd6189..5b08b7d 100644 --- a/templates/extensions/service.setup/template/{{python_module_path}}/extension.py +++ b/templates/extensions/service.setup/template/{{python_module_path}}/extension.py @@ -13,18 +13,22 @@ from .service import router -# Any class derived from `omni.ext.IExt` in the top level module (defined in `python.modules` of `extension.toml`) will -# be instantiated when the extension gets enabled, and `on_startup(ext_id)` will be called. -# Later when the extension gets disabled on_shutdown() is called. +# Any class derived from `omni.ext.IExt` in the top level module (defined in +# `python.modules` of `extension.toml`) will be instantiated when the extension +# gets enabled, and `on_startup(ext_id)` will be called. Later when the +# extension gets disabled on_shutdown() is called. class MyExtension(omni.ext.IExt): - # ext_id is the current extension id. It can be used with the extension manager to - # query additional information, like where this extension is located on the filesystem. - def on_startup(self, ext_id): - + """This extension manages the service setup""" + # ext_id is the current extension id. It can be used with the extension + # manager to query additional information, like where this extension is + # located on the filesystem. + def on_startup(self, _ext_id): + """This is called every time the extension is activated.""" main.register_router(router) - print("[{{ extension_name }}] MyExtension startup : Local Docs - http://localhost:8011/docs") def on_shutdown(self): + """This is called every time the extension is deactivated. It is used + to clean up the extension state.""" main.deregister_router(router) - print("[{{ extension_name }}] MyExtension shutdown") \ No newline at end of file + print("[{{ extension_name }}] MyExtension shutdown") diff --git a/templates/extensions/service.setup/template/{{python_module_path}}/service.py b/templates/extensions/service.setup/template/{{python_module_path}}/service.py index 0254406..ee470e7 100644 --- a/templates/extensions/service.setup/template/{{python_module_path}}/service.py +++ b/templates/extensions/service.setup/template/{{python_module_path}}/service.py @@ -9,11 +9,11 @@ # its affiliates is strictly prohibited. from pathlib import Path -import omni.usd -import omni.kit.commands from pydantic import BaseModel, Field -from omni.services.core.routers import ServiceAPIRouter +import omni.kit.commands +import omni.usd +from omni.services.core.routers import ServiceAPIRouter router = ServiceAPIRouter(tags=["{{ extension_display_name }}"]) @@ -72,7 +72,9 @@ async def generate_cube(cube_data: CubeDataModel): ) # save stage - asset_file_path = str(Path(cube_data.asset_write_location).joinpath(f"{cube_data.asset_name}.usda")) + asset_file_path = str(Path( + cube_data.asset_write_location).joinpath(f"{cube_data.asset_name}.usda") + ) stage.GetRootLayer().Export(asset_file_path) msg = f"[{{ extension_name }}] Wrote a cube to this path: {asset_file_path}" print(msg) diff --git a/templates/extensions/service.setup/template/{{python_module_path}}/tests/test_benchmarks.py b/templates/extensions/service.setup/template/{{python_module_path}}/tests/test_benchmarks.py index ada67c1..1b2e2ee 100644 --- a/templates/extensions/service.setup/template/{{python_module_path}}/tests/test_benchmarks.py +++ b/templates/extensions/service.setup/template/{{python_module_path}}/tests/test_benchmarks.py @@ -37,18 +37,23 @@ async def benchmark_sleepy_with_custom_metrics(self): async def benchmark_sleepy_with_custom_metrics_array(self): """ - Another benchmark method using custom metrics to demonstrate setting arrays for a metric, and to show that - there's no crosstalk of metrics between benchmarks. + Another benchmark method using custom metrics to demonstrate setting + arrays for a metric, and to show that there's no crosstalk of metrics + between benchmarks. """ - # array of samples for custom metric 'my_other_metric' is set to [1.2ms, 0.9ms, 1.1ms] - self.set_metric_sample_array(name="my_other_metric", values=[1.2, 0.9, 1.1], unit="ms") + # array of samples for custom metric 'my_other_metric' is set to + # [1.2ms, 0.9ms, 1.1ms] + self.set_metric_sample_array( + name="my_other_metric", values=[1.2, 0.9, 1.1], unit="ms" + ) await asyncio.sleep(0.01) class TestBenchmarksNoCustomMetric(AsyncTestCase): """ Example Benchmark class without custom metrics. - * If you are not planning to use custom metrics you can derive from `AsyncTestCase`. + * If you are not planning to use custom metrics you can derive + from `AsyncTestCase`. * Benchmark methods have to start with the 'benchmark' prefix. * The runtime of the benchmark methods and their skip state belong to the default metrics which are always reported. diff --git a/templates/extensions/usd_composer.setup/template/config/extension.toml b/templates/extensions/usd_composer.setup/template/config/extension.toml index b5f3603..11bf6d5 100644 --- a/templates/extensions/usd_composer.setup/template/config/extension.toml +++ b/templates/extensions/usd_composer.setup/template/config/extension.toml @@ -56,7 +56,18 @@ pages = [ [[test]] dependencies = [ + "omni.kit.mainwindow", + "omni.kit.ui_test", ] args = [ + "--/app/fastShutdown=1", + "--/app/file/ignoreUnsavedOnExit=true", + "--/app/window/dpiScaleOverride=1.0", + "--/app/window/height=720", + "--/app/window/scaleToMonitor=false", + "--/app/window/width=1280", + "--/exts/omni.kit.viewport.window/startup/windowName=Viewport", + "--no-window", + "--reset-user" ] diff --git a/templates/extensions/usd_composer.setup/template/{{python_module_path}}/extension.py b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/extension.py index fde9e18..c55fa5d 100644 --- a/templates/extensions/usd_composer.setup/template/{{python_module_path}}/extension.py +++ b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/extension.py @@ -8,11 +8,17 @@ # import asyncio +import inspect import logging import os +import platform +import subprocess import sys +import webbrowser from pathlib import Path + + import carb.imgui as _imgui import carb.input import carb.settings @@ -23,78 +29,98 @@ import omni.kit.menu.utils import omni.kit.stage_templates as stage_templates import omni.kit.ui +import omni.kit.window.property as property_window_ext import omni.ui as ui import omni.usd +from omni.kit.menu.utils import MenuLayout +from omni.kit.property.usd import PrimPathWidget +from omni.kit.quicklayout import QuickLayout from omni.kit.window.title import get_main_window_title -DATA_PATH = Path(carb.tokens.get_tokens_interface().resolve("${% raw %}{{% endraw %}{{ extension_name }}{% raw %}}{% endraw %}")) +DATA_PATH = Path(carb.tokens.get_tokens_interface().resolve( + "${% raw %}{{% endraw %}{{ extension_name }}{% raw %}}{% endraw %}") +) async def _load_layout(layout_file: str, keep_windows_open=False): + """Loads a provided layout file and ensures the viewport is set to FILL.""" try: - from omni.kit.quicklayout import QuickLayout - - # few frames delay to avoid the conflict with the layout of omni.kit.mainwindow - for i in range(3): + # few frames delay to avoid the conflict with the + # layout of omni.kit.mainwindow + for _ in range(3): await omni.kit.app.get_app().next_update_async() QuickLayout.load_file(layout_file, keep_windows_open) - - except Exception as exc: - pass - + except: QuickLayout.load_file(layout_file) class CreateSetupExtension(omni.ext.IExt): """Create Final Configuration""" - - def on_startup(self, ext_id): - """setup the window layout, menu, final configuration of the extensions etc""" + def on_startup(self, _ext_id): + """ + setup the window layout, menu, final configuration + of the extensions etc + """ self._settings = carb.settings.get_settings() self._menu_layout = [] telemetry_logger = logging.getLogger("idl.telemetry.opentelemetry") telemetry_logger.setLevel(logging.ERROR) - # this is a work around as some Extensions don't properly setup their default setting in time + # this is a work around as some Extensions don't properly setup their + # default setting in time self._set_defaults() # adjust couple of viewport settings self._settings.set("/app/viewport/boundingBoxes/enabled", True) # Force enable Axis, Grid, Outline and Lights - forceEnable = self._settings.get("/app/create/forceViewportSettings") - if forceEnable: - display_options = self._settings.get("/persistent/app/viewport/displayOptions") + if self._settings.get("/app/create/forceViewportSettings"): + display_options = self._settings.get( + "/persistent/app/viewport/displayOptions" + ) # Note: flags are from omni/kit/ViewportTypes.h - kShowFlagAxis = 1 << 1 - kShowFlagGrid = 1 << 6 - kShowFlagSelectionOutline = 1 << 7 - kShowFlagLight = 1 << 8 + show_flag_axis = 1 << 1 + show_flag_grid = 1 << 6 + show_flag_selection_outline = 1 << 7 + show_flag_light = 1 << 8 display_options = ( - display_options | (kShowFlagAxis) | (kShowFlagGrid) | (kShowFlagSelectionOutline) | (kShowFlagLight) + display_options | (show_flag_axis) | (show_flag_grid) | + (show_flag_selection_outline) | (show_flag_light) + ) + self._settings.set( + "/persistent/app/viewport/displayOptions", display_options ) - self._settings.set("/persistent/app/viewport/displayOptions", display_options) # Make sure these are in sync from changes above self._settings.set("/app/viewport/show/lights", True) self._settings.set("/app/viewport/grid/enabled", True) self._settings.set("/app/viewport/outline/enabled", True) - # Make sure any action-graph setup locking out user from HUD does not persist across re-launch - self._settings.set("/persistent/app/viewport/Viewport/Viewport0/hud/visible", True) - self._settings.set("/persistent/app/viewport/Viewport 2/Viewport0/hud/visible", True) + # Make sure any action-graph setup locking out user from HUD does + # not persist across re-launch + self._settings.set( + "/persistent/app/viewport/Viewport/Viewport0/hud/visible", + True + ) + self._settings.set( + "/persistent/app/viewport/Viewport 2/Viewport0/hud/visible", + True + ) # These two settings do not co-operate well on ADA cards, so for # now simulate a toggle of the present thread on startup to work around - if self._settings.get("/exts/omni.kit.renderer.core/present/enabled") and self._settings.get( + if self._settings.get("/exts/omni.kit.renderer.core/present/enabled") \ + and self._settings.get( "/exts/omni.kit.widget.viewport/autoAttach/mode" ): - async def _toggle_present(settings, n_waits: int = 1): async def _toggle_setting(app, enabled: bool, n_waits: int): for _ in range(n_waits): await app.next_update_async() - settings.set("/exts/omni.kit.renderer.core/present/enabled", enabled) + settings.set( + "/exts/omni.kit.renderer.core/present/enabled", + enabled + ) app = omni.kit.app.get_app() await _toggle_setting(app, False, n_waits) @@ -103,32 +129,40 @@ async def _toggle_setting(app, enabled: bool, n_waits: int): asyncio.ensure_future(_toggle_present(self._settings)) # Setting and Saving FSD as a global change in preferences - # Requires to listen for changes at the local path to update Composer's persistent path. + # Requires to listen for changes at the local path to update + # Composer's persistent path. fabric_app_setting = self._settings.get("/app/useFabricSceneDelegate") - fabric_persistent_setting = self._settings.get("/persistent/app/useFabricSceneDelegate") - fabric_enabled: bool = fabric_app_setting if fabric_persistent_setting == None else fabric_persistent_setting + fabric_persistent_setting = self._settings.get( + "/persistent/app/useFabricSceneDelegate" + ) + fabric_enabled: bool = fabric_app_setting if \ + fabric_persistent_setting is None else fabric_persistent_setting self._settings.set("/app/useFabricSceneDelegate", fabric_enabled) - self._sub_fabric_delegate_changed = omni.kit.app.SettingChangeSubscription( - "/app/useFabricSceneDelegate", - self._on_fabric_delegate_changed - ) + self._sub_fabric_delegate_changed = \ + omni.kit.app.SettingChangeSubscription( + "/app/useFabricSceneDelegate", + self._on_fabric_delegate_changed + ) # Adjust the Window Title to show the Create Version window_title = get_main_window_title() app_version = self._settings.get("/app/version") if not app_version: - app_version = open(carb.tokens.get_tokens_interface().resolve("${app}/../VERSION")).read() + with open( + carb.tokens.get_tokens_interface().resolve("${app}/../VERSION"), + encoding="utf-8" + ) as f: + app_version = f.read() if app_version: if "+" in app_version: app_version, _ = app_version.split("+") # for RC version we remove some details - PRODUCTION = self._settings.get("/privacy/externalBuild") - if PRODUCTION: + if self._settings.get("/privacy/externalBuild"): if "-" in app_version: app_version, _ = app_version.split("-") window_title.set_app_version(app_version) @@ -137,9 +171,18 @@ async def _toggle_setting(app, enabled: bool, n_waits: int): # setup some imgui Style overide imgui = _imgui.acquire_imgui() - imgui.push_style_color(_imgui.StyleColor.ScrollbarGrab, carb.Float4(0.4, 0.4, 0.4, 1)) - imgui.push_style_color(_imgui.StyleColor.ScrollbarGrabHovered, carb.Float4(0.6, 0.6, 0.6, 1)) - imgui.push_style_color(_imgui.StyleColor.ScrollbarGrabActive, carb.Float4(0.8, 0.8, 0.8, 1)) + imgui.push_style_color( + _imgui.StyleColor.ScrollbarGrab, + carb.Float4(0.4, 0.4, 0.4, 1) + ) + imgui.push_style_color( + _imgui.StyleColor.ScrollbarGrabHovered, + carb.Float4(0.6, 0.6, 0.6, 1) + ) + imgui.push_style_color( + _imgui.StyleColor.ScrollbarGrabActive, + carb.Float4(0.8, 0.8, 0.8, 1) + ) imgui.push_style_var_float(_imgui.StyleVar.DockSplitterSize, 2) @@ -149,58 +192,98 @@ async def _toggle_setting(app, enabled: bool, n_waits: int): test_mode = self._settings.get("/app/testMode") if not test_mode: - self.__setup_window_task = asyncio.ensure_future(_load_layout(layout_file, True)) + asyncio.ensure_future(_load_layout(layout_file, True)) - self.__setup_property_window = asyncio.ensure_future(self.__property_window()) + asyncio.ensure_future(self.__property_window()) self.__menu_update() - if not test_mode and not self._settings.get("/app/content/emptyStageOnStart"): - self.__await_new_scene = asyncio.ensure_future(self.__new_stage()) + if not test_mode and not \ + self._settings.get("/app/content/emptyStageOnStart"): + asyncio.ensure_future(self.__new_stage()) - startup_time = omni.kit.app.get_app_interface().get_time_since_start_s() - self._settings.set("/crashreporter/data/startup_time", f"{startup_time}") + startup_time = \ + omni.kit.app.get_app_interface().get_time_since_start_s() + self._settings.set( + "/crashreporter/data/startup_time", f"{startup_time}" + ) - def show_documentation(*x): - import webbrowser - webbrowser.open("https://docs.omniverse.nvidia.com/composer/latest/index.html") + def show_documentation(*args): + webbrowser.open( + "https://docs.omniverse.nvidia.com/composer/latest/index.html" + ) self._help_menu_items = [ - omni.kit.menu.utils.MenuItemDescription(name="Documentation", - onclick_fn=show_documentation, - appear_after=[omni.kit.menu.utils.MenuItemOrder.FIRST]) + omni.kit.menu.utils.MenuItemDescription( + name="Documentation", + onclick_fn=show_documentation, + appear_after=[omni.kit.menu.utils.MenuItemOrder.FIRST] + ) ] omni.kit.menu.utils.add_menu_items(self._help_menu_items, name="Help") def _set_defaults(self): - """this is trying to setup some defaults for extensions to avoid warning""" + """ + This is trying to setup some defaults for extensions to avoid warnings. + """ self._settings.set_default("/persistent/app/omniverse/bookmarks", {}) - self._settings.set_default("/persistent/app/stage/timeCodeRange", [0, 100]) + self._settings.set_default( + "/persistent/app/stage/timeCodeRange", [0, 100] + ) - self._settings.set_default("/persistent/audio/context/closeAudioPlayerOnStop", False) + self._settings.set_default( + "/persistent/audio/context/closeAudioPlayerOnStop", + False + ) - self._settings.set_default("/persistent/app/primCreation/PrimCreationWithDefaultXformOps", True) - self._settings.set_default("/persistent/app/primCreation/DefaultXformOpType", "Scale, Rotate, Translate") - self._settings.set_default("/persistent/app/primCreation/DefaultRotationOrder", "ZYX") - self._settings.set_default("/persistent/app/primCreation/DefaultXformOpPrecision", "Double") + self._settings.set_default( + "/persistent/app/primCreation/PrimCreationWithDefaultXformOps", + True + ) + self._settings.set_default( + "/persistent/app/primCreation/DefaultXformOpType", + "Scale, Rotate, Translate" + ) + self._settings.set_default( + "/persistent/app/primCreation/DefaultRotationOrder", + "ZYX" + ) + self._settings.set_default( + "/persistent/app/primCreation/DefaultXformOpPrecision", + "Double" + ) # omni.kit.property.tagging - self._settings.set_default("/persistent/exts/omni.kit.property.tagging/showAdvancedTagView", False) - self._settings.set_default("/persistent/exts/omni.kit.property.tagging/showHiddenTags", False) - self._settings.set_default("/persistent/exts/omni.kit.property.tagging/modifyHiddenTags", False) + self._settings.set_default( + "/persistent/exts/omni.kit.property.tagging/showAdvancedTagView", + False + ) + self._settings.set_default( + "/persistent/exts/omni.kit.property.tagging/showHiddenTags", + False + ) + self._settings.set_default( + "/persistent/exts/omni.kit.property.tagging/modifyHiddenTags", + False + ) self._settings.set_default( "/rtx/sceneDb/ambientLightIntensity", 0.0 ) # set default ambientLight intensity to Zero - def _on_fabric_delegate_changed(self, value: str, event_type: carb.settings.ChangeEventType): + def _on_fabric_delegate_changed( + self, _v: str, event_type: carb.settings.ChangeEventType): if event_type == carb.settings.ChangeEventType.CHANGED: - enabled: bool = self._settings.get_as_bool("/app/useFabricSceneDelegate") - self._settings.set("/persistent/app/useFabricSceneDelegate", enabled) + enabled: bool = self._settings.get_as_bool( + "/app/useFabricSceneDelegate" + ) + self._settings.set( + "/persistent/app/useFabricSceneDelegate", enabled + ) async def __new_stage(self): - - # 10 frame delay to allow Layout - for i in range(5): + """Create a new stage """ + # 5 frame delay to allow Layout + for _ in range(5): await omni.kit.app.get_app().next_update_async() if omni.usd.get_context().can_open_stage(): @@ -208,10 +291,6 @@ async def __new_stage(self): def _launch_app(self, app_id, console=True, custom_args=None): """launch another Kit app with the same settings""" - import platform - import subprocess - import sys - app_path = carb.tokens.get_tokens_interface().resolve("${app}") kit_file_path = os.path.join(app_path, app_id) @@ -236,7 +315,8 @@ def _launch_app(self, app_id, console=True, custom_args=None): kwargs = {"close_fds": False} if platform.system().lower() == "windows": if console: - kwargs["creationflags"] = subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP + kwargs["creationflags"] = subprocess.CREATE_NEW_CONSOLE | \ + subprocess.CREATE_NEW_PROCESS_GROUP else: kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP @@ -248,25 +328,28 @@ def _show_ui_docs(self): def _show_launcher(self): """show the omniverse ui documentation as an external Application""" - self._launch_app("omni.create.launcher.kit", console=False, custom_args={"--/app/auto_launch=false"}) + self._launch_app( + "omni.create.launcher.kit", + console=False, + custom_args={"--/app/auto_launch=false"} + ) async def __property_window(self): + """Creates a propety window and sets column sizes.""" await omni.kit.app.get_app().next_update_async() - import omni.kit.window.property as property_window_ext - from omni.kit.property.usd import PrimPathWidget property_window = property_window_ext.get_window() property_window.set_scheme_delegate_layout( "Create Layout", - ["basis_curves_prim", "path_prim", "material_prim", "xformable_prim", "shade_prim", "camera_prim"], + ["basis_curves_prim", "path_prim", "material_prim", + "xformable_prim", "shade_prim", "camera_prim"], ) - # expand width of path_items to "Instancable" doesn't get wrapped + # expand width of path_items so "Instancable" doesn't get wrapped PrimPathWidget.set_path_item_padding(3.5) def __menu_update(self): - from omni.kit.menu.utils import MenuLayout - + """Update the menu""" editor_menu = omni.kit.ui.get_editor_menu() self._menu_layout = [ @@ -282,8 +365,6 @@ def __menu_update(self): MenuLayout.Item("Retargeting"), MenuLayout.Item("Animation Graph"), MenuLayout.Item("Animation Graph Samples"), - # MenuLayout.Item("Keyframer"), - # MenuLayout.Item("Recorder"), ], ), MenuLayout.SubMenu( @@ -314,11 +395,23 @@ def __menu_update(self): "Simulation", [ MenuLayout.Group("Flow", source="Window/Flow"), - MenuLayout.Group("Blast Destruction", source="Window/Blast"), - MenuLayout.Group("Blast Destruction", source="Window/Blast Destruction"), - MenuLayout.Group("Boom Collision Audio", source="Window/Boom"), - MenuLayout.Group("Boom Collision Audio", source="Window/Boom Collision Audio"), - MenuLayout.Group("Physics", source="Window/Physics"), + MenuLayout.Group( + "Blast Destruction", source="Window/Blast" + ), + MenuLayout.Group( + "Blast Destruction", + source="Window/Blast Destruction" + ), + MenuLayout.Group( + "Boom Collision Audio", source="Window/Boom" + ), + MenuLayout.Group( + "Boom Collision Audio", + source="Window/Boom Collision Audio" + ), + MenuLayout.Group( + "Physics", source="Window/Physics" + ), ], ), MenuLayout.SubMenu( @@ -333,9 +426,10 @@ def __menu_update(self): MenuLayout.Item("Asset Validator"), ], ), - MenuLayout.Sort(exclude_items=["Extensions"], sort_submenus=True), + MenuLayout.Sort( + exclude_items=["Extensions"], sort_submenus=True + ), MenuLayout.Item("New Viewport Window", remove=True), - # MenuLayout.Item("Material Preview", remove=True), ], ), MenuLayout.Menu( @@ -343,32 +437,40 @@ def __menu_update(self): [ MenuLayout.Item("Default", source="Reset Layout"), MenuLayout.Seperator(), - MenuLayout.Item("UI Toggle Visibility", source="Window/UI Toggle Visibility"), - MenuLayout.Item("Fullscreen Mode", source="Window/Fullscreen Mode"), + MenuLayout.Item( + "UI Toggle Visibility", + source="Window/UI Toggle Visibility" + ), + MenuLayout.Item( + "Fullscreen Mode", source="Window/Fullscreen Mode" + ), MenuLayout.Seperator(), - MenuLayout.Item("Save Layout", source="Window/Layout/Save Layout..."), - MenuLayout.Item("Load Layout", source="Window/Layout/Load Layout..."), + MenuLayout.Item( + "Save Layout", source="Window/Layout/Save Layout..." + ), + MenuLayout.Item( + "Load Layout", source="Window/Layout/Load Layout..." + ), MenuLayout.Seperator(), - MenuLayout.Item("Quick Save", source="Window/Layout/Quick Save"), - MenuLayout.Item("Quick Load", source="Window/Layout/Quick Load"), + MenuLayout.Item( + "Quick Save", source="Window/Layout/Quick Save" + ), + MenuLayout.Item( + "Quick Load", source="Window/Layout/Quick Load" + ), ], ), ] omni.kit.menu.utils.add_layout(self._menu_layout) - # set omnu.ui Help Menu - # self._ui_doc_menu_path = "Help/Omni UI Docs" - # self._ui_doc_menu_item = editor_menu.add_item(self._ui_doc_menu_path, lambda *_: self._show_ui_docs()) - # editor_menu.set_priority(self._ui_doc_menu_path, -10) - self._layout_menu_items = [] self._current_layout_priority = 20 def add_layout_menu_entry(name, parameter, key): - import inspect - menu_path = f"Layout/{name}" - menu = editor_menu.add_item(menu_path, None, False, self._current_layout_priority) + menu = editor_menu.add_item( + menu_path, None, False, self._current_layout_priority + ) self._current_layout_priority = self._current_layout_priority + 1 if inspect.isfunction(parameter): @@ -379,7 +481,6 @@ def add_layout_menu_entry(name, parameter, key): (carb.input.KEYBOARD_MODIFIER_FLAG_CONTROL, key), ) else: - async def _active_layout(layout): await _load_layout(layout) # load layout file again to make sure layout correct @@ -387,36 +488,42 @@ async def _active_layout(layout): menu_action = omni.kit.menu.utils.add_action_to_menu( menu_path, - lambda *_: asyncio.ensure_future(_active_layout(f"{DATA_PATH}/layouts/{parameter}.json")), + lambda *_: asyncio.ensure_future( + _active_layout(f"{DATA_PATH}/layouts/{parameter}.json") + ), name, (carb.input.KEYBOARD_MODIFIER_FLAG_CONTROL, key), ) self._layout_menu_items.append((menu, menu_action)) - add_layout_menu_entry("Reset Layout", "default", carb.input.KeyboardInput.KEY_1) + add_layout_menu_entry( + "Reset Layout", "default", carb.input.KeyboardInput.KEY_1 + ) # create Quick Load & Quick Save - from omni.kit.quicklayout import QuickLayout - async def quick_save(): QuickLayout.quick_save(None, None) async def quick_load(): QuickLayout.quick_load(None, None) - add_layout_menu_entry("Quick Save", quick_save, carb.input.KeyboardInput.KEY_7) - add_layout_menu_entry("Quick Load", quick_load, carb.input.KeyboardInput.KEY_8) + add_layout_menu_entry( + "Quick Save", quick_save, carb.input.KeyboardInput.KEY_7 + ) + add_layout_menu_entry( + "Quick Load", quick_load, carb.input.KeyboardInput.KEY_8 + ) # open "Asset Stores" window ui.Workspace.show_window("Asset Stores") def on_shutdown(self): + """Clean up the extension""" self._sub_fabric_delegate_changed = None omni.kit.menu.utils.remove_layout(self._menu_layout) self._menu_layout = None self._layout_menu_items = None - # self._ui_doc_menu_item = None self._launcher_menu = None self._reset_menu = None diff --git a/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/__init__.py b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/__init__.py new file mode 100644 index 0000000..e866cdc --- /dev/null +++ b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/__init__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# run startup tests first +from .test_app_startup import * +from .test_app_extensions import * diff --git a/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/test_app_extensions.py b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/test_app_extensions.py new file mode 100644 index 0000000..eb85bd2 --- /dev/null +++ b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/test_app_extensions.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +import omni.kit.app +from omni.kit.test import AsyncTestCase + + +class TestUSDComposerExtensions(AsyncTestCase): + # NOTE: Function pulled to remove dependency from omni.kit.core.tests + def _validate_extensions_load(self): + failures = [] + manager = omni.kit.app.get_app().get_extension_manager() + for ext in manager.get_extensions(): + ext_id = ext["id"] + ext_name = ext["name"] + info = manager.get_extension_dict(ext_id) + + enabled = ext.get("enabled", False) + if not enabled: + continue + + failed = info.get("state/failed", False) + if failed: + failures.append(ext_name) + + if len(failures) == 0: + print("\n[success] All extensions loaded successfully!\n") + else: + print("") + print(f"[error] Found {len(failures)} extensions that could not load:") + for count, ext in enumerate(failures): + print(f" {count+1}: {ext}") + print("") + return len(failures) + + async def test_l1_extensions_load(self): + """Loop all enabled extensions to see if they loaded correctly""" + self.assertEqual(self._validate_extensions_load(), 0) \ No newline at end of file diff --git a/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/test_app_startup.py b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/test_app_startup.py new file mode 100644 index 0000000..ef69ade --- /dev/null +++ b/templates/extensions/usd_composer.setup/template/{{python_module_path}}/tests/test_app_startup.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +import json +import time +from typing import Tuple + +import carb.settings +import omni.kit.app +from omni.kit.test import AsyncTestCase + + +class TestAppStartup(AsyncTestCase): + def app_startup_time(self, test_id: str) -> float: + """Get startup time - send to nvdf""" + test_start_time = time.monotonic() + startup_time = omni.kit.app.get_app().get_time_since_start_s() + test_result = {"startup_time_s": startup_time} + print(f"App Startup time: {startup_time}") + return startup_time + + def app_startup_warning_count(self, test_id: str) -> Tuple[int, int]: + """Get the count of warnings during startup - send to nvdf""" + test_start_time = time.monotonic() + warning_count = 0 + error_count = 0 + log_file_path = carb.settings.get_settings().get("/log/file") + with open(log_file_path, "r") as file: + for line in file: + if "[Warning]" in line: + warning_count += 1 + elif "[Error]" in line: + error_count += 1 + + test_result = {"startup_warning_count": warning_count, "startup_error_count": error_count} + print(f"App Startup Warning count: {warning_count}") + print(f"App Startup Error count: {error_count}") + return warning_count, error_count + + async def test_l1_app_startup_time(self): + """Get startup time - send to nvdf""" + for _ in range(60): + await omni.kit.app.get_app().next_update_async() + + self.app_startup_time(self.id()) + self.assertTrue(True) + + async def test_l1_app_startup_warning_count(self): + """Get the count of warnings during startup - send to nvdf""" + for _ in range(60): + await omni.kit.app.get_app().next_update_async() + + self.app_startup_warning_count(self.id()) + self.assertTrue(True) diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menu_helper.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menu_helper.py index 83c7863..5d150ec 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menu_helper.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menu_helper.py @@ -1,4 +1,5 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. # SPDX-License-Identifier: LicenseRef-NvidiaProprietary # # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -21,14 +22,16 @@ class MenuHelper: - def __init__(self) -> None: + """Helper class to manage the main menu layout based on the + application mode.""" + def __init__(self): self._settings = carb.settings.get_settings() self._current_layout = None self._pending_layout = None self._changing_layout_task: asyncio.Task = None - self._menu_layout_empty = [] self._menu_layout_modify = [] + self._app_ready_sub = None omni.kit.menu.utils.add_hook(self._menu_hook) @@ -37,7 +40,8 @@ def __init__(self) -> None: ) self._menu_hook() - def destroy(self) -> None: + def destroy(self): + """Tear down the menu helper.""" omni.kit.menu.utils.remove_hook(self._menu_hook) if self._changing_layout_task and not self._changing_layout_task.done(): @@ -54,12 +58,15 @@ def destroy(self) -> None: omni.kit.menu.utils.remove_layout(self._current_layout) self._current_layout = None - def _menu_hook(self, *args, **kwargs) -> None: + def _menu_hook(self, *args, **kwargs): + """Get the menu instance and build the desired menu.""" if self._settings.get_as_bool("/app/view/debug/menus"): return LAYOUT_EMPTY_ALLOWED_MENUS = set(["Developer",]) - LAYOUT_MODIFY_ALLOWED_MENUS = {"File", "Edit", "Window", "Tools", "Help", "Developer",} + LAYOUT_MODIFY_ALLOWED_MENUS = { + "File", "Edit", "Window", "Tools", "Help", "Developer", + } # make NEW list object instead of clear original # the original list may be held by self._current_layout and omni.kit.menu.utils @@ -71,41 +78,54 @@ def _menu_hook(self, *args, **kwargs) -> None: return # Build new layouts using allowlists - for key in menu_instance._menu_defs: + menu_defs, _menu_order, _menu_delegates = menu_instance.get_menu_data() + + for key in menu_defs: if key.lower().endswith("widget"): continue if key not in LAYOUT_EMPTY_ALLOWED_MENUS: - self._menu_layout_empty.append(MenuLayout.Menu(key, remove=True)) + self._menu_layout_empty.append( + MenuLayout.Menu(key, remove=True) + ) if key not in LAYOUT_MODIFY_ALLOWED_MENUS: - self._menu_layout_modify.append(MenuLayout.Menu(key, remove=True)) + self._menu_layout_modify.append( + MenuLayout.Menu(key, remove=True) + ) # Remove 'Viewport 2' entry if key == "Window": - for menu_item_1 in menu_instance._menu_defs[key]: + for menu_item_1 in menu_defs[key]: for menu_item_2 in menu_item_1: if menu_item_2.name == "Viewport": - menu_item_2.sub_menu = [mi for mi in menu_item_2.sub_menu if mi.name != "Viewport 2"] + menu_item_2.sub_menu = [mi for mi in + menu_item_2.sub_menu if + mi.name != "Viewport 2"] if self._changing_layout_task is None or self._changing_layout_task.done(): - self._changing_layout_task = asyncio.ensure_future(self._delayed_change_layout()) + self._changing_layout_task = \ + asyncio.ensure_future(self._delayed_change_layout()) - def _on_application_mode_changed(self, *args) -> None: + def _on_application_mode_changed(self, *args): + """Callback for when the application mode changes.""" if self._changing_layout_task is None or self._changing_layout_task.done(): - self._changing_layout_task = asyncio.ensure_future(self._delayed_change_layout()) + self._changing_layout_task = \ + asyncio.ensure_future(self._delayed_change_layout()) async def _delayed_change_layout(self): + """Delay the layout change to avoid omni.ui error.""" mode = self._settings.get_as_string(SETTINGS_APPLICATION_MODE_PATH) if mode in ["present", "review"]: pending_layout = self._menu_layout_empty else: pending_layout = self._menu_layout_modify - # Don't change layout inside of menu callback _on_application_mode_changed - # omni.ui throws error + # Don't change layout inside of menu callback + # _on_application_mode_changed omni.ui throws error if self._current_layout: - # Here only check number of layout menu items and name of every of layout menu item + # Here only check number of layout menu items and name of every of + # layout menu item same_layout = len(self._current_layout) == len(pending_layout) if same_layout: for index, item in enumerate(self._current_layout): diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menubar_helper.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menubar_helper.py index 67cbd08..83bb91d 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menubar_helper.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/menubar_helper.py @@ -1,4 +1,5 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. # SPDX-License-Identifier: LicenseRef-NvidiaProprietary # # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -8,79 +9,108 @@ # without an express license agreement from NVIDIA CORPORATION or # its affiliates is strictly prohibited. -from pathlib import Path - import carb import carb.settings import carb.tokens import omni.ui as ui + +from omni.kit.viewport.menubar.core import ( + DEFAULT_MENUBAR_NAME, SettingModel, SliderMenuDelegate, +) +from omni.kit.viewport.menubar.core import get_instance as get_menubar_instance from omni.ui import color as cl ICON_PATH = carb.tokens.get_tokens_interface().resolve("${% raw %}{{% endraw %}{{ extension_name }}{% raw %}}{% endraw %}/data/icons") VIEW_MENUBAR_STYLE = { "MenuBar.Window": {"background_color": 0xA0000000}, - "MenuBar.Item.Background": { "background_color": 0, }, - "Menu.Item.Background": { "background_color": 0, } + "MenuBar.Item.Background": {"background_color": 0}, + "Menu.Item.Background": {"background_color": 0} } VIEWPORT_CAMERA_STYLE = { - "Menu.Item.Icon::Expand": {"image_url": f"{ICON_PATH}/caret_s2_right_dark.svg", "color": cl.viewport_menubar_light}, - "Menu.Item.Icon::Expand:checked": {"image_url": f"{ICON_PATH}/caret_s2_left_dark.svg"}, + "Menu.Item.Icon::Expand": { + "image_url": f"{ICON_PATH}/caret_s2_right_dark.svg", + "color": cl.viewport_menubar_light + }, + "Menu.Item.Icon::Expand:checked": { + "image_url": + f"{ICON_PATH}/caret_s2_left_dark.svg" + }, } + class MenubarHelper: - def __init__(self) -> None: + """Helper class to manage menubar settings and style.""" + def __init__(self): self._settings = carb.settings.get_settings() # Set menubar background and style try: - from omni.kit.viewport.menubar.core import DEFAULT_MENUBAR_NAME - from omni.kit.viewport.menubar.core import get_instance as get_menubar_instance instance = get_menubar_instance() - if not instance: # pragma: no cover + if not instance: # pragma: no cover return default_menubar = instance.get_menubar(DEFAULT_MENUBAR_NAME) default_menubar.background_visible = True default_menubar.style.update(VIEW_MENUBAR_STYLE) default_menubar.show_separator = True - except ImportError: # pragma: no cover + except ImportError: # pragma: no cover carb.log_warn("Viewport menubar not found!") try: - import omni.kit.viewport.menubar.camera - self._camera_menubar_instance = omni.kit.viewport.menubar.camera.get_instance() - if not self._camera_menubar_instance: # pragma: no cover + import omni.kit.viewport.menubar.camera # type: ignore + self._camera_menubar_instance = \ + omni.kit.viewport.menubar.camera.get_instance() + if not self._camera_menubar_instance: # pragma: no cover return # Change expand button icon - self._camera_menubar_instance._camera_menu._style.update(VIEWPORT_CAMERA_STYLE) + self._camera_menubar_instance._camera_menu._style.update( + VIEWPORT_CAMERA_STYLE + ) # New menu item for camera speed - self._camera_menubar_instance.register_menu_item(self._create_camera_speed, order=100) - self._camera_menubar_instance.deregister_menu_item(self._camera_menubar_instance._camera_menu._build_create_camera) + self._camera_menubar_instance.register_menu_item( + self._create_camera_speed, order=100 + ) + self._camera_menubar_instance.deregister_menu_item( + self._camera_menubar_instance._camera_menu._build_create_camera + ) except ImportError: carb.log_warn("Viewport menubar not found!") self._camera_menubar_instance = None - except AttributeError: # pragma: no cover + except AttributeError: # pragma: no cover self._camera_menubar_instance = None # Hide default render and settings menubar - self._settings.set("/persistent/exts/omni.kit.viewport.menubar.render/visible", False) - self._settings.set("/persistent/exts/omni.kit.viewport.menubar.settings/visible", False) + self._settings.set( + "/persistent/exts/omni.kit.viewport.menubar.render/visible", False + ) + self._settings.set( + "/persistent/exts/omni.kit.viewport.menubar.settings/visible", False + ) - def destroy(self) -> None: + def destroy(self): + """Remove the camera speed menu item.""" if self._camera_menubar_instance: - self._camera_menubar_instance.deregister_menu_item(self._create_camera_speed) + self._camera_menubar_instance.deregister_menu_item( + self._create_camera_speed + ) - def _create_camera_speed(self, _vc, _r: ui.Menu) -> None: - from omni.kit.viewport.menubar.core import SettingModel, SliderMenuDelegate + def _create_camera_speed(self, _vc, _r: ui.Menu): + """Create a menu item for camera speed.""" ui.MenuItem( "Speed", hide_on_click=False, delegate=SliderMenuDelegate( - model=SettingModel("/persistent/app/viewport/camMoveVelocity", draggable=True), - min=self._settings.get_as_float("/persistent/app/viewport/camVelocityMin") or 0.01, - max=self._settings.get_as_float("/persistent/app/viewport/camVelocityMax"), + model=SettingModel( + "/persistent/app/viewport/camMoveVelocity", draggable=True + ), + min=self._settings.get_as_float( + "/persistent/app/viewport/camVelocityMin" + ) or 0.01, + max=self._settings.get_as_float( + "/persistent/app/viewport/camVelocityMax" + ), tooltip="Set the Fly Mode navigation speed", width=0, reserve_status=True, diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/navigation.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/navigation.py index 7f1b8c5..695fc4e 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/navigation.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/navigation.py @@ -11,13 +11,13 @@ import asyncio import carb +import carb.dictionary import carb.settings import carb.tokens -import carb.dictionary -import omni.kit.app import omni.ext -import omni.ui as ui import omni.kit.actions.core +import omni.kit.app +import omni.ui as ui from omni.kit.viewport.navigation.core import ( NAVIGATION_TOOL_OPERATION_ACTIVE, ViewportNavigationTooltip, @@ -29,26 +29,54 @@ CURRENT_TOOL_PATH = "/app/viewport/currentTool" SETTING_NAVIGATION_ROOT = "/exts/omni.kit.tool.navigation/" -NAVIGATION_BAR_VISIBLE_PATH = "/exts/omni.kit.viewport.navigation.core/isVisible" +NAVIGATION_BAR_VISIBLE_PATH = \ + "/exts/omni.kit.viewport.navigation.core/isVisible" APPLICATION_MODE_PATH = "/app/application_mode" -WALK_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.walk/visible" -CAPTURE_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.capture/visible" -MARKUP_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.markup/visible" -MEASURE_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.measure/visible" -SECTION_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.section/visible" -TELEPORT_SEPARATOR_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.teleport/spvisible" -WAYPOINT_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.waypoint/visible" -VIEWPORT_CONTEXT_MENU_PATH = "/exts/omni.kit.window.viewport/showContextMenu" -MENUBAR_APP_MODES_PATH = "/exts/omni.kit.usd_explorer.main.menubar/include_modify_mode" -WELCOME_WINDOW_VISIBLE_PATH = "/exts/omni.kit.usd_explorer.window.welcome/visible" -ACTIVE_OPERATION_PATH = "/exts/omni.kit.viewport.navigation.core/activeOperation" +WALK_VISIBLE_PATH = \ + "/persistent/exts/omni.kit.viewport.navigation.walk/visible" +CAPTURE_VISIBLE_PATH = \ + "/persistent/exts/omni.kit.viewport.navigation.capture/visible" +MARKUP_VISIBLE_PATH = \ + "/persistent/exts/omni.kit.viewport.navigation.markup/visible" +MEASURE_VISIBLE_PATH = \ + "/persistent/exts/omni.kit.viewport.navigation.measure/visible" +SECTION_VISIBLE_PATH = \ +"/persistent/exts/omni.kit.viewport.navigation.section/visible" +TELEPORT_SEPARATOR_VISIBLE_PATH = \ + "/persistent/exts/omni.kit.viewport.navigation.teleport/spvisible" +WAYPOINT_VISIBLE_PATH = \ + "/persistent/exts/omni.kit.viewport.navigation.waypoint/visible" +VIEWPORT_CONTEXT_MENU_PATH = \ + "/exts/omni.kit.window.viewport/showContextMenu" +MENUBAR_APP_MODES_PATH = \ + "/exts/omni.kit.usd_explorer.main.menubar/include_modify_mode" +WELCOME_WINDOW_VISIBLE_PATH = \ + "/exts/omni.kit.usd_explorer.window.welcome/visible" +ACTIVE_OPERATION_PATH = \ + "/exts/omni.kit.viewport.navigation.core/activeOperation" + class Navigation: + """Manages the navigation bar and its visibility.""" NAVIGATION_BAR_NAME = None - # ext_id is current extension id. It can be used with extension manager to query additional information, like where - # this extension is located on filesystem. - def on_startup(self, ext_id: str) -> None: + def __init__(self): + self._ext_name = None + self._settings = None + self._navigation_bar = None + self._tool_bar_button = None + self._dict = None + self._panel_visible = None + self._viewport_welcome_window_visibility_changed_sub = None + self._application_mode_changed_sub = None + self._show_tooltips = None + self._nav_bar_visibility_sub = None + + # ext_id is current extension id. It can be used with extension manager + # to query additional information, like where this extension is located + # on filesystem. + def on_startup(self, ext_id: str): + """Initialize the navigation bar and set up event subscriptions.""" sections = ext_id.split("-") self._ext_name = sections[0] @@ -63,9 +91,11 @@ def on_startup(self, ext_id: str) -> None: self._settings.set(CURRENT_TOOL_PATH, "navigation") self._settings.set(NAVIGATION_TOOL_OPERATION_ACTIVE, "teleport") - self._viewport_welcome_window_visibility_changed_sub = self._settings.subscribe_to_node_change_events( - WELCOME_WINDOW_VISIBLE_PATH, self._on_welcome_window_visibility_change - ) + self._viewport_welcome_window_visibility_changed_sub = \ + self._settings.subscribe_to_node_change_events( + WELCOME_WINDOW_VISIBLE_PATH, + self._on_welcome_window_visibility_change + ) self._settings.set(WALK_VISIBLE_PATH, False) self._settings.set(MARKUP_VISIBLE_PATH, True) @@ -75,76 +105,103 @@ def on_startup(self, ext_id: str) -> None: self._settings.set(MEASURE_VISIBLE_PATH, True) self._settings.set(SECTION_VISIBLE_PATH, True) - self._application_mode_changed_sub = self._settings.subscribe_to_node_change_events( - APPLICATION_MODE_PATH, self._on_application_mode_changed - ) + self._application_mode_changed_sub = \ + self._settings.subscribe_to_node_change_events( + APPLICATION_MODE_PATH, self._on_application_mode_changed + ) self._show_tooltips = False - self._nav_bar_visibility_sub = self._settings.subscribe_to_node_change_events( - NAVIGATION_BAR_VISIBLE_PATH, self._delay_reset_tooltip) + self._nav_bar_visibility_sub = \ + self._settings.subscribe_to_node_change_events( + NAVIGATION_BAR_VISIBLE_PATH, self._delay_reset_tooltip + ) _prev_navbar_vis = None _prev_tool = None _prev_operation = None - def _on_welcome_window_visibility_change(self, item: carb.dictionary.Item, *_) -> None: + + def _on_welcome_window_visibility_change( + self, item: carb.dictionary.Item, *_ + ): + """Callback for when the welcome window visibility changes.""" if not isinstance(self._dict, (carb.dictionary.IDictionary, dict)): return welcome_window_vis = self._dict.get(item) - # preserve the state of the navbar upon closing the Welcome window if the app is in Layout mode + # preserve the state of the navbar upon closing the Welcome window if + # the app is in Layout mode if self._settings.get_as_string(APPLICATION_MODE_PATH).lower() == "layout": # preserve the state of the navbar visibility if welcome_window_vis: - self._prev_navbar_vis = self._settings.get_as_bool(NAVIGATION_BAR_VISIBLE_PATH) - self._settings.set(NAVIGATION_BAR_VISIBLE_PATH, not(welcome_window_vis)) + self._prev_navbar_vis = \ + self._settings.get_as_bool(NAVIGATION_BAR_VISIBLE_PATH) + self._settings.set( + NAVIGATION_BAR_VISIBLE_PATH, not (welcome_window_vis) + ) self._prev_tool = self._settings.get(CURRENT_TOOL_PATH) - self._prev_operation = self._settings.get(ACTIVE_OPERATION_PATH) - else: # restore the state of the navbar visibility + self._prev_operation = \ + self._settings.get(ACTIVE_OPERATION_PATH) + else: # restore the state of the navbar visibility if self._prev_navbar_vis is not None: - self._settings.set(NAVIGATION_BAR_VISIBLE_PATH, self._prev_navbar_vis) + self._settings.set( + NAVIGATION_BAR_VISIBLE_PATH, self._prev_navbar_vis + ) self._prev_navbar_vis = None if self._prev_tool is not None: self._settings.set(CURRENT_TOOL_PATH, self._prev_tool) if self._prev_operation is not None: - self._settings.set(ACTIVE_OPERATION_PATH, self._prev_operation) - return + self._settings.set( + ACTIVE_OPERATION_PATH, self._prev_operation + ) else: if welcome_window_vis: self._settings.set(NAVIGATION_TOOL_OPERATION_ACTIVE, "none") else: - self._settings.set(NAVIGATION_TOOL_OPERATION_ACTIVE, "teleport") + self._settings.set( + NAVIGATION_TOOL_OPERATION_ACTIVE, "teleport" + ) - self._settings.set(NAVIGATION_BAR_VISIBLE_PATH, not(welcome_window_vis)) + self._settings.set( + NAVIGATION_BAR_VISIBLE_PATH, not (welcome_window_vis) + ) - def _on_application_mode_changed(self, item: carb.dictionary.Item, *_) -> None: + def _on_application_mode_changed(self, item: carb.dictionary.Item, *_): + """Callback for when the application mode changes.""" if not isinstance(self._dict, (carb.dictionary.IDictionary, dict)): return current_mode = self._dict.get(item) self._test = asyncio.ensure_future(self._switch_by_mode(current_mode)) - async def _switch_by_mode(self, current_mode: str) -> None: + async def _switch_by_mode(self, current_mode: str): + """Switch application mode based on provided mode.""" await omni.kit.app.get_app().next_update_async() state = True if current_mode == "review" else False self._settings.set(NAVIGATION_BAR_VISIBLE_PATH, state) - self._settings.set(VIEWPORT_CONTEXT_MENU_PATH, not(state)) # toggle RMB viewport context menu + # toggle RMB viewport context menu + self._settings.set(VIEWPORT_CONTEXT_MENU_PATH, not (state)) self._delay_reset_tooltip(None) - def _delay_reset_tooltip(self, *_) -> None: - async def delay_set_tooltip() -> None: + def _delay_reset_tooltip(self, *_): + """Delay setting the tooltip visibility.""" + async def delay_set_tooltip(): for _i in range(4): - await omni.kit.app.get_app().next_update_async() # type: ignore + await omni.kit.app.get_app().next_update_async() ViewportNavigationTooltip.set_visible(self._show_tooltips) asyncio.ensure_future(delay_set_tooltip()) - def _on_showtips_click(self, *_) -> None: + def _on_showtips_click(self, *_): + """Toggle the visibility of the tooltips.""" self._show_tooltips = not self._show_tooltips ViewportNavigationTooltip.set_visible(self._show_tooltips) - def on_shutdown(self) -> None: + def on_shutdown(self): + """Clean up event subscriptions.""" self._navigation_bar = None self._viewport_welcome_window_visibility_changed_sub = None - self._settings.unsubscribe_to_change_events(self._application_mode_changed_sub) # type:ignore + self._settings.unsubscribe_to_change_events( + self._application_mode_changed_sub + ) self._application_mode_changed_sub = None self._dict = None diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/setup.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/setup.py index 71b8ad6..d736121 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/setup.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/setup.py @@ -1,4 +1,5 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. # SPDX-License-Identifier: LicenseRef-NvidiaProprietary # # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -9,9 +10,11 @@ # its affiliates is strictly prohibited. import asyncio -import weakref -from functools import partial +import inspect import os +import weakref +import webbrowser +from contextlib import suppress from pathlib import Path from typing import cast, Optional @@ -26,8 +29,6 @@ from omni.kit.quicklayout import QuickLayout from omni.kit.menu.utils import MenuLayout from omni.kit.window.title import get_main_window_title -from omni.kit.usd.layers import LayerUtils - from omni.kit.viewport.menubar.core import get_instance as get_mb_inst, DEFAULT_MENUBAR_NAME from omni.kit.viewport.menubar.core.viewport_menu_model import ViewportMenuModel from omni.kit.viewport.utility import get_active_viewport, get_active_viewport_window, disable_selection @@ -40,7 +41,6 @@ import carb.input import omni.kit.imgui as _imgui -from pxr import Sdf, Usd from .navigation import Navigation from .menu_helper import MenuHelper @@ -59,53 +59,67 @@ TELEPORT_VISIBLE_PATH = "/persistent/exts/omni.kit.viewport.navigation.teleport/visible" -async def _load_layout_startup(layout_file: str, keep_windows_open: bool=False) -> None: +async def _load_layout_startup( + layout_file: str, keep_windows_open: bool = False + ): + """Loads the startup layout file.""" try: - # few frames delay to avoid the conflict with the layout of omni.kit.mainwindow - for i in range(3): - await omni.kit.app.get_app().next_update_async() # type: ignore + # few frames delay to avoid the conflict with the layout of + # omni.kit.mainwindow + for _ in range(3): + await omni.kit.app.get_app().next_update_async() QuickLayout.load_file(layout_file, keep_windows_open) # WOR: some layout don't happy collectly the first time - await omni.kit.app.get_app().next_update_async() # type: ignore + await omni.kit.app.get_app().next_update_async() QuickLayout.load_file(layout_file, keep_windows_open) - except Exception as exc: # pragma: no cover (Can't be tested because a non-existing layout file prints an log_error in QuickLayout and does not throw an exception) + except Exception as exc: # pragma: no coverthrow an exception) carb.log_warn(f"Failed to load layout {layout_file}: {exc}") -async def _load_layout(layout_file: str, keep_windows_open:bool=False) -> None: +async def _load_layout(layout_file: str, keep_windows_open: bool = False): + """Load a layout file and catch any exceptions that occur.""" try: - # few frames delay to avoid the conflict with the layout of omni.kit.mainwindow + # few frames delay to avoid the conflict with the layout of + # omni.kit.mainwindow for i in range(3): - await omni.kit.app.get_app().next_update_async() # type: ignore + await omni.kit.app.get_app().next_update_async() QuickLayout.load_file(layout_file, keep_windows_open) - except Exception as exc: # pragma: no cover (Can't be tested because a non-existing layout file prints an log_error in QuickLayout and does not throw an exception) + except Exception as exc: # pragma: no coverthrow an exception) carb.log_warn(f"Failed to load layout {layout_file}: {exc}") -async def _clear_startup_scene_edits() -> None: + +async def _clear_startup_scene_edits(): try: - for i in range(50): # This could possibly be a smaller value. I want to ensure this happens after RTX startup - await omni.kit.app.get_app().next_update_async() # type: ignore + # This could possibly be a smaller value. + # I want to ensure this happens after RTX startup + for _ in range(50): + await omni.kit.app.get_app().next_update_async() omni.usd.get_context().set_pending_edit(False) - except Exception as exc: # pragma: no cover + except Exception as exc: # pragma: no cover carb.log_warn(f"Failed to clear stage edits on startup: {exc}") # This extension is mostly loading the Layout updating menu class SetupExtension(omni.ext.IExt): - # ext_id is current extension id. It can be used with extension manager to query additional information, like where - # this extension is located on filesystem. + """Setup extension for USD Explorer.""" + # ext_id is current extension id. It can be used with extension manager to + # query additional information, like where this extension is located + # on filesystem. @property def _app(self): + """Return the app instance.""" return omni.kit.app.get_app() @property def _settings(self): + """Return the settings instance.""" return carb.settings.get_settings() - def on_startup(self, ext_id: str) -> None: + def on_startup(self, ext_id: str): + """Setup the extension on startup.""" self._ext_id = ext_id self._menubar_helper = MenubarHelper() self._menu_helper = MenuHelper() @@ -114,24 +128,41 @@ def on_startup(self, ext_id: str) -> None: imgui = _imgui.acquire_imgui() # match Create overides - imgui.push_style_color(_imgui.StyleColor.ScrollbarGrab, carb.Float4(0.4, 0.4, 0.4, 1)) - imgui.push_style_color(_imgui.StyleColor.ScrollbarGrabHovered, carb.Float4(0.6, 0.6, 0.6, 1)) - imgui.push_style_color(_imgui.StyleColor.ScrollbarGrabActive, carb.Float4(0.8, 0.8, 0.8, 1)) + imgui.push_style_color( + _imgui.StyleColor.ScrollbarGrab, + carb.Float4(0.4, 0.4, 0.4, 1) + ) + imgui.push_style_color( + _imgui.StyleColor.ScrollbarGrabHovered, + carb.Float4(0.6, 0.6, 0.6, 1) + ) + imgui.push_style_color( + _imgui.StyleColor.ScrollbarGrabActive, + carb.Float4(0.8, 0.8, 0.8, 1) + ) - # DockSplitterSize is the variable that drive the size of the Dock Split connection + # DockSplitterSize is the variable that drive the size of the Dock + # Split connection imgui.push_style_var_float(_imgui.StyleVar.DockSplitterSize, 2) # setup the Layout for your app self._layouts_path = carb.tokens.get_tokens_interface().resolve("${% raw %}{{% endraw %}{{ extension_name }}{% raw %}}{% endraw %}/layouts") layout_file = Path(self._layouts_path).joinpath(f"{self._settings.get('/app/layout/name')}.json") - self.__setup_window_task = asyncio.ensure_future(_load_layout_startup(f"{layout_file}", True)) + asyncio.ensure_future(_load_layout_startup(f"{layout_file}", True)) - self.review_layout_path = str(Path(self._layouts_path) / "comment_layout.json") - self.default_layout_path = str(Path(self._layouts_path) / "default.json") - self.layout_user_path = str(Path(self._layouts_path) / "layout_user.json") + self.review_layout_path = str( + Path(self._layouts_path) / "comment_layout.json" + ) + self.default_layout_path = str( + Path(self._layouts_path) / "default.json" + ) + self.layout_user_path = str( + Path(self._layouts_path) / "layout_user.json" + ) - # remove the user defined layout so that we always load the default layout when startup - if os.path.exists(self.layout_user_path): + # remove the user defined layout so that we always load the default + # layout when startup + with suppress(FileNotFoundError): os.remove(self.layout_user_path) # setup the menu and their layout @@ -142,7 +173,6 @@ def on_startup(self, ext_id: str) -> None: if self._settings.get_as_bool('/app/view/debug/menus'): self._layout_menu() - # setup the Application Title window_title = get_main_window_title() if window_title: @@ -153,47 +183,60 @@ def on_startup(self, ext_id: str) -> None: self._navigation = Navigation() self._navigation.on_startup(ext_id) - self._application_mode_changed_sub = self._settings.subscribe_to_node_change_events( - APPLICATION_MODE_PATH, weakref.proxy(self)._on_application_mode_changed - ) + self._application_mode_changed_sub = \ + self._settings.subscribe_to_node_change_events( + APPLICATION_MODE_PATH, + weakref.proxy(self)._on_application_mode_changed + ) self._set_viewport_menubar_visibility(False) - self._test = asyncio.ensure_future(_clear_startup_scene_edits()) + asyncio.ensure_future(_clear_startup_scene_edits()) self._usd_context = omni.usd.get_context() - self._stage_event_sub = self._usd_context.get_stage_event_stream().create_subscription_to_pop( - self._on_stage_open_event, name="TeleportDefaultOn" - ) + self._stage_event_sub = \ + self._usd_context.get_stage_event_stream().create_subscription_to_pop( + self._on_stage_open_event, name="TeleportDefaultOn" + ) if self._settings.get_as_bool(SETTINGS_STARTUP_EXPAND_VIEWPORT): self._set_viewport_fill_on() self._stage_templates = [SunnySkyStage()] - disable_selection(get_active_viewport()) self._ui_state_manager = UIStateManager() self._setup_ui_state_changes() omni.kit.menu.utils.add_layout([ MenuLayout.Menu("Window", [ - MenuLayout.Item("Viewport", source="Window/Viewport/Viewport 1"), + MenuLayout.Item( + "Viewport", source="Window/Viewport/Viewport 1" + ), MenuLayout.Item("Playlist", remove=True), MenuLayout.Item("Layout", remove=True), - MenuLayout.Sort(exclude_items=["Extensions"], sort_submenus=True), + MenuLayout.Sort( + exclude_items=["Extensions"], sort_submenus=True + ), ]) ]) - def show_documentation(*x): - import webbrowser - webbrowser.open("http://docs.omniverse.nvidia.com/explorer") + + def show_documentation(*_args): + """Open the documentation in a web browser.""" + webbrowser.open("https://docs.omniverse.nvidia.com/explorer") self._help_menu_items = [ - omni.kit.menu.utils.MenuItemDescription(name="Documentation", - onclick_fn=show_documentation, - appear_after=[omni.kit.menu.utils.MenuItemOrder.FIRST]) + omni.kit.menu.utils.MenuItemDescription( + name="Documentation", + onclick_fn=show_documentation, + appear_after=[omni.kit.menu.utils.MenuItemOrder.FIRST] + ) ] omni.kit.menu.utils.add_menu_items(self._help_menu_items, name="Help") - def _on_stage_open_event(self, event: carb.events.IEvent) -> None: + def _on_stage_open_event(self, event: carb.events.IEvent): + """Callback to clear tools and switch the app mode after a new stage + is opened.""" if event.type == int(omni.usd.StageEventType.OPENED): - app_mode = self._settings.get_as_string(APPLICATION_MODE_PATH).lower() + app_mode = self._settings.get_as_string( + APPLICATION_MODE_PATH + ).lower() # exit all tools self._settings.set(CURRENT_TOOL_PATH, "none") @@ -206,26 +249,37 @@ def _on_stage_open_event(self, event: carb.events.IEvent) -> None: self._settings.set(VIEWPORT_CONTEXT_MENU_PATH, value) # teleport is activated after loading a stage and app is in Review mode - async def _stage_post_open_teleport_toggle(self) -> None: + async def _stage_post_open_teleport_toggle(self): + """Toggle the teleport tool after a stage is opened.""" await self._app.next_update_async() if hasattr(self, "_usd_context") and self._usd_context is not None and not self._usd_context.is_new_stage(): - self._settings.set("/exts/omni.kit.viewport.navigation.core/activeOperation", "teleport") + self._settings.set( + "/exts/omni.kit.viewport.navigation.core/activeOperation", + "teleport" + ) - def _set_viewport_fill_on(self) -> None: + def _set_viewport_fill_on(self): + """Set the viewport fill on.""" vp_window = get_active_viewport_window() vp_widget = vp_window.viewport_widget if vp_window else None if vp_widget: vp_widget.expand_viewport = True - def _set_viewport_menubar_visibility(self, show: bool) -> None: + def _set_viewport_menubar_visibility(self, show: bool): + """Set the viewport menubar visibility.""" mb_inst = get_mb_inst() if mb_inst and hasattr(mb_inst, "get_menubar"): main_menubar = mb_inst.get_menubar(DEFAULT_MENUBAR_NAME) if main_menubar.visible_model.as_bool != show: main_menubar.visible_model.set_value(show) - ViewportMenuModel()._item_changed(None) # type: ignore - - def _on_application_mode_changed(self, item: carb.dictionary.Item, _typ: carb.settings.ChangeEventType) -> None: + ViewportMenuModel()._item_changed(None) + + def _on_application_mode_changed( + self, + item: carb.dictionary.Item, + _typ: carb.settings.ChangeEventType + ): + """Callback for when the application mode changes.""" if self._settings.get_as_string(APPLICATION_MODE_PATH).lower() == "review": omni.usd.get_context().get_selection().clear_selected_prim_paths() disable_selection(get_active_viewport()) @@ -233,10 +287,12 @@ def _on_application_mode_changed(self, item: carb.dictionary.Item, _typ: carb.se current_mode: str = cast(str, item.get_dict()) asyncio.ensure_future(self.defer_load_layout(current_mode)) - async def defer_load_layout(self, current_mode: str) -> None: + async def defer_load_layout(self, current_mode: str): + """Defer loading the layout based on the current mode.""" keep_windows = True # Focus Mode Toolbar - self._settings.set_bool(SETTINGS_PATH_FOCUSED, True) # current_mode not in ("review", "layout")) + # current_mode not in ("review", "layout")) + self._settings.set_bool(SETTINGS_PATH_FOCUSED, True) # Turn off all tools and modal self._settings.set_string(CURRENT_TOOL_PATH, "none") @@ -245,7 +301,8 @@ async def defer_load_layout(self, current_mode: str) -> None: if current_mode == "review": # save the current layout for restoring later if switch back QuickLayout.save_file(self.layout_user_path) - # we don't want to keep any windows except the ones which are visible in self.review_layout_path + # we don't want to keep any windows except the ones which are + # visible in self.review_layout_path await _load_layout(self.review_layout_path, False) else: # current_mode == "layout": # check if there is any user modified layout, if yes use that one @@ -254,17 +311,21 @@ async def defer_load_layout(self, current_mode: str) -> None: self._set_viewport_menubar_visibility(current_mode == "layout") - def _setup_ui_state_changes(self) -> None: + def _setup_ui_state_changes(self): + """Setup the UI state changes.""" windows_to_hide_on_modal = ["Measure", "Section", "Waypoints"] - self._ui_state_manager.add_hide_on_modal(window_names=windows_to_hide_on_modal, restore=True) + self._ui_state_manager.add_hide_on_modal( + window_names=windows_to_hide_on_modal, restore=True + ) window_titles = ["Markups", "Waypoints"] for window in window_titles: setting_name = f'/exts/omni.usd_explorer.setup/{window}/visible' - self._ui_state_manager.add_window_visibility_setting(window, setting_name) + self._ui_state_manager.add_window_visibility_setting( + window, setting_name + ) # toggle icon visibilites based on window visibility - self._ui_state_manager.add_settings_copy_dependency( source_path="/exts/omni.usd_explorer.setup/Markups/visible", target_path="/exts/omni.kit.markup.core/show_icons", @@ -275,20 +336,21 @@ def _setup_ui_state_changes(self) -> None: target_path="/exts/omni.kit.waypoint.core/show_icons", ) - def _custom_quicklayout_menu(self) -> None: + def _custom_quicklayout_menu(self): # we setup a simple ways to Load custom layout from the exts def add_layout_menu_entry(name, parameter, key): - import inspect - + """Add a layout menu entry.""" editor_menu = omni.kit.ui.get_editor_menu() layouts_path = carb.tokens.get_tokens_interface().resolve("${% raw %}{{% endraw %}{{ extension_name }}{% raw %}}{% endraw %}/layouts") menu_path = f"Layout/{name}" - menu = editor_menu.add_item(menu_path, None, False, self._current_layout_priority) # type: ignore + menu = editor_menu.add_item( + menu_path, None, False, self._current_layout_priority + ) self._current_layout_priority = self._current_layout_priority + 1 - if inspect.isfunction(parameter): # pragma: no cover (Never used, see commented out section below regarding quick save/load) + if inspect.isfunction(parameter): # pragma: no cover menu_action = omni.kit.menu.utils.add_action_to_menu( menu_path, lambda *_: asyncio.ensure_future(parameter()), @@ -298,38 +360,34 @@ def add_layout_menu_entry(name, parameter, key): else: menu_action = omni.kit.menu.utils.add_action_to_menu( menu_path, - lambda *_: asyncio.ensure_future(_load_layout(f"{layouts_path}/{parameter}.json")), + lambda *_: asyncio.ensure_future( + _load_layout(f"{layouts_path}/{parameter}.json") + ), name, (carb.input.KEYBOARD_MODIFIER_FLAG_CONTROL, key), ) self._layout_menu_items.append((menu, menu_action)) - add_layout_menu_entry("Reset Layout", "default", carb.input.KeyboardInput.KEY_1) - add_layout_menu_entry("Viewport Only", "viewport_only", carb.input.KeyboardInput.KEY_2) - add_layout_menu_entry("Markup Editor", "markup_editor", carb.input.KeyboardInput.KEY_3) - # add_layout_menu_entry("Waypoint Viewer", "waypoint_viewer", carb.input.KeyboardInput.KEY_4) - - # # you can enable Quick Save and Quick Load here - # if False: - # # create Quick Load & Quick Save - # from omni.kit.quicklayout import QuickLayout - - # async def quick_save(): - # QuickLayout.quick_save(None, None) - - # async def quick_load(): - # QuickLayout.quick_load(None, None) - - # add_layout_menu_entry("Quick Save", quick_save, carb.input.KeyboardInput.KEY_7) - # add_layout_menu_entry("Quick Load", quick_load, carb.input.KeyboardInput.KEY_8) + add_layout_menu_entry( + "Reset Layout", "default", carb.input.KeyboardInput.KEY_1 + ) + add_layout_menu_entry( + "Viewport Only", "viewport_only", carb.input.KeyboardInput.KEY_2 + ) + add_layout_menu_entry( + "Markup Editor", "markup_editor", carb.input.KeyboardInput.KEY_3 + ) - def _register_my_menu(self) -> None: - context_menu: Optional[omni.kit.context_menu.ContextMenuExtension] = omni.kit.context_menu.get_instance() - if not context_menu: # pragma: no cover + def _register_my_menu(self): + """Gets the context menu and adds the menu items.""" + context_menu: Optional[omni.kit.context_menu.ContextMenuExtension] = \ + omni.kit.context_menu.get_instance() + if not context_menu: # pragma: no cover return - def _layout_file_menu(self) -> None: + def _layout_file_menu(self): + """Setup the file menu layout.""" self._menu_file_layout = [ MenuLayout.Menu( "File", @@ -362,8 +420,8 @@ def _layout_file_menu(self) -> None: ] omni.kit.menu.utils.add_layout(self._menu_file_layout) - - def _layout_menu(self) -> None: + def _layout_menu(self): + """Layout the Window menu.""" self._menu_layout = [ MenuLayout.Menu( "Window", @@ -389,7 +447,9 @@ def _layout_menu(self) -> None: MenuLayout.SubMenu( "Browsers", [ - MenuLayout.Item("Content", source="Window/Content"), + MenuLayout.Item( + "Content", source="Window/Content" + ), MenuLayout.Item("Materials"), MenuLayout.Item("Skies"), ], @@ -409,23 +469,37 @@ def _layout_menu(self) -> None: MenuLayout.Group( "Flow", [ - MenuLayout.Item("Presets", source="Window/Flow/Presets"), - MenuLayout.Item("Monitor", source="Window/Flow/Monitor"), + MenuLayout.Item( + "Presets", + source="Window/Flow/Presets" + ), + MenuLayout.Item( + "Monitor", + source="Window/Flow/Monitor" + ), ], ), MenuLayout.Group( "Blast", [ - MenuLayout.Item("Settings", source="Window/Blast/Settings"), + MenuLayout.Item( + "Settings", + source="Window/Blast/Settings" + ), MenuLayout.SubMenu( "Documentation", [ - MenuLayout.Item("Kit UI", source="Window/Blast/Documentation/Kit UI"), MenuLayout.Item( - "Programming", source="Window/Blast/Documentation/Programming" + "Kit UI", + source="Window/Blast/Documentation/Kit UI" + ), + MenuLayout.Item( + "Programming", + source="Window/Blast/Documentation/Programming" ), MenuLayout.Item( - "USD Schemas", source="Window/Blast/Documentation/USD Schemas" + "USD Schemas", + source="Window/Blast/Documentation/USD Schemas" ), ], ), @@ -437,7 +511,10 @@ def _layout_menu(self) -> None: "Physics", [ MenuLayout.Item("Demo Scenes"), - MenuLayout.Item("Settings", source="Window/Physics/Settings"), + MenuLayout.Item( + "Settings", + source="Window/Physics/Settings" + ), MenuLayout.Item("Debug"), MenuLayout.Item("Test Runner"), MenuLayout.Item("Character Controller"), @@ -477,31 +554,45 @@ def _layout_menu(self) -> None: MenuLayout.Item("Markup Editor"), MenuLayout.Item("Waypoint Viewer"), MenuLayout.Seperator(), - MenuLayout.Item("UI Toggle Visibility", source="Window/UI Toggle Visibility"), - MenuLayout.Item("Fullscreen Mode", source="Window/Fullscreen Mode"), + MenuLayout.Item( + "UI Toggle Visibility", + source="Window/UI Toggle Visibility" + ), + MenuLayout.Item( + "Fullscreen Mode", + source="Window/Fullscreen Mode" + ), MenuLayout.Seperator(), - MenuLayout.Item("Save Layout", source="Window/Layout/Save Layout..."), - MenuLayout.Item("Load Layout", source="Window/Layout/Load Layout..."), - # MenuLayout.Seperator(), - # MenuLayout.Item("Quick Save", source="Window/Layout/Quick Save"), - # MenuLayout.Item("Quick Load", source="Window/Layout/Quick Load"), + MenuLayout.Item( + "Save Layout", + source="Window/Layout/Save Layout..." + ), + MenuLayout.Item( + "Load Layout", + source="Window/Layout/Load Layout..." + ), ], ), - MenuLayout.Menu("Tools", [MenuLayout.SubMenu("Animation", remove=True)]), + MenuLayout.Menu( + "Tools", [MenuLayout.SubMenu("Animation", remove=True)] + ), ] - omni.kit.menu.utils.add_layout(self._menu_layout) # type: ignore + omni.kit.menu.utils.add_layout(self._menu_layout) # if you want to support the Quick Layout Menu self._custom_quicklayout_menu() def on_shutdown(self): + """Shutdown the extension.""" if self._menu_layout: - omni.kit.menu.utils.remove_layout(self._menu_layout) # type: ignore + omni.kit.menu.utils.remove_layout(self._menu_layout) self._menu_layout.clear() self._layout_menu_items.clear() self._navigation.on_shutdown() del self._navigation - self._settings.unsubscribe_to_change_events(self._application_mode_changed_sub) + self._settings.unsubscribe_to_change_events( + self._application_mode_changed_sub + ) del self._application_mode_changed_sub self._stage_event_sub = None diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/stage_template.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/stage_template.py index 17e6cd2..a5b37ea 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/stage_template.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/stage_template.py @@ -16,13 +16,16 @@ class SunnySkyStage: + """Stage template for a sunny sky.""" def __init__(self): register_template("SunnySky", self.new_stage) def __del__(self): + """Unregister the template when the object is deleted.""" unregister_template("SunnySky") - def new_stage(self, rootname, usd_context_name): + def new_stage(self, _rootname, usd_context_name): + """Create a new stage with a sunny sky.""" # Create basic DistantLight usd_context = omni.usd.get_context(usd_context_name) stage = usd_context.get_stage() @@ -64,13 +67,23 @@ def new_stage(self, rootname, usd_context_name): context_name=usd_context_name ) prim = stage.GetPrimAtPath("/Environment/Sky") - prim.CreateAttribute("xformOp:scale", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(1, 1, 1)) - prim.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(0, 0, 0)) + prim.CreateAttribute( + "xformOp:scale", Sdf.ValueTypeNames.Double3, False + ).Set(Gf.Vec3d(1, 1, 1)) + prim.CreateAttribute( + "xformOp:translate", Sdf.ValueTypeNames.Double3, False + ).Set(Gf.Vec3d(0, 0, 0)) if up_axis == "Y": - prim.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(270, 0, 0)) + prim.CreateAttribute( + "xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False + ).Set(Gf.Vec3d(270, 0, 0)) else: - prim.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(0, 0, 90)) - prim.CreateAttribute("xformOpOrder", Sdf.ValueTypeNames.String, False).Set(["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]) + prim.CreateAttribute( + "xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False + ).Set(Gf.Vec3d(0, 0, 90)) + prim.CreateAttribute( + "xformOpOrder", Sdf.ValueTypeNames.String, False + ).Set(["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]) # create DistantLight omni.kit.commands.execute( @@ -82,7 +95,7 @@ def new_stage(self, rootname, usd_context_name): UsdLux.Tokens.inputsAngle: 4.3, UsdLux.Tokens.inputsIntensity: 3000, UsdGeom.Tokens.visibility: "inherited", - } if hasattr(UsdLux.Tokens, 'inputsIntensity') else \ + } if hasattr(UsdLux.Tokens, 'inputsIntensity') else { UsdLux.Tokens.angle: 4.3, UsdLux.Tokens.intensity: 3000, @@ -92,10 +105,33 @@ def new_stage(self, rootname, usd_context_name): context_name=usd_context_name ) prim = stage.GetPrimAtPath("/Environment/DistantLight") - prim.CreateAttribute("xformOp:scale", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(1, 1, 1)) - prim.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(0, 0, 0)) + prim.CreateAttribute( + "xformOp:scale", Sdf.ValueTypeNames.Double3, False + ).Set(Gf.Vec3d(1, 1, 1)) + prim.CreateAttribute( + "xformOp:translate", Sdf.ValueTypeNames.Double3, False + ).Set(Gf.Vec3d(0, 0, 0)) if up_axis == "Y": - prim.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(310.6366313590111, -125.93251524567805, 0.8821359067542289)) + prim.CreateAttribute( + "xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False + ).Set( + Gf.Vec3d( + 310.6366313590111, + -125.93251524567805, + 0.8821359067542289 + ) + ) else: - prim.CreateAttribute("xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False).Set(Gf.Vec3d(41.35092544555664, 0.517652153968811, -35.92928695678711)) - prim.CreateAttribute("xformOpOrder", Sdf.ValueTypeNames.String, False).Set(["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]) + prim.CreateAttribute( + "xformOp:rotateXYZ", Sdf.ValueTypeNames.Double3, False + ).Set( + Gf.Vec3d( + 41.35092544555664, + 0.517652153968811, + -35.92928695678711 + ) + ) + prim.CreateAttribute( + "xformOpOrder", Sdf.ValueTypeNames.String, False).Set( + ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"] + ) diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/tests/test_app_startup.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/tests/test_app_startup.py index fdfc8bc..d216165 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/tests/test_app_startup.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/tests/test_app_startup.py @@ -24,7 +24,6 @@ def app_startup_time(self, test_id: str) -> float: startup_time = omni.kit.app.get_app().get_time_since_start_s() test_result = {"startup_time_s": startup_time} print(f"App Startup time: {startup_time}") - self._post_to_nvdf(test_id, test_result, time.monotonic() - test_start_time) return startup_time def app_startup_warning_count(self, test_id: str) -> Tuple[int, int]: @@ -43,41 +42,8 @@ def app_startup_warning_count(self, test_id: str) -> Tuple[int, int]: test_result = {"startup_warning_count": warning_count, "startup_error_count": error_count} print(f"App Startup Warning count: {warning_count}") print(f"App Startup Error count: {error_count}") - self._post_to_nvdf(test_id, test_result, time.monotonic() - test_start_time) return warning_count, error_count - # TODO: should call proper API from Kit - def _post_to_nvdf(self, test_id: str, test_result: dict, test_duration: float): - """Send results to nvdf""" - try: - from omni.kit.test.nvdf import _can_post_to_nvdf, _get_ci_info, _post_json, get_app_info, to_nvdf_form - - if not _can_post_to_nvdf(): - return - - data = {} - data["ts_created"] = int(time.time() * 1000) - data["app"] = get_app_info() - data["ci"] = _get_ci_info() - data["test"] = { - "passed": True, - "skipped": False, - "unreliable": False, - "duration": test_duration, - "test_id": test_id, - "ext_test_id": "omni.create.tests", - "test_type": "unittest", - } - data["test"].update(test_result) - - project = "omniverse-kit-tests-results-v2" - json_str = json.dumps(to_nvdf_form(data), skipkeys=True) - _post_json(project, json_str) - # print(json_str) # uncomment to debug - - except Exception as e: - carb.log_warn(f"Exception occurred: {e}") - async def test_l1_app_startup_time(self): """Get startup time - send to nvdf""" for _ in range(60): diff --git a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/ui_state_manager.py b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/ui_state_manager.py index 8b6404d..9e29516 100644 --- a/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/ui_state_manager.py +++ b/templates/extensions/usd_explorer.setup/template/{{python_module_path}}/ui_state_manager.py @@ -8,62 +8,81 @@ # without an express license agreement from NVIDIA CORPORATION or # its affiliates is strictly prohibited. -import carb.dictionary -import carb.settings -import omni.ui as ui from functools import partial from typing import Any, Dict, List, Tuple, Union +import carb.dictionary +import carb.settings +import omni.ui as ui MODAL_TOOL_ACTIVE_PATH = "/app/tools/modal_tool_active" class UIStateManager: - def __init__(self) -> None: + """Manages the state of UI elements based on settings and modal tool state.""" + def __init__(self): self._settings = carb.settings.acquire_settings_interface() - self._modal_changed_sub = self._settings.subscribe_to_node_change_events( - MODAL_TOOL_ACTIVE_PATH, - self._on_modal_setting_changed + self._modal_changed_sub = \ + self._settings.subscribe_to_node_change_events( + MODAL_TOOL_ACTIVE_PATH, + self._on_modal_setting_changed ) - self._hide_on_modal: List[Tuple[str,bool]] = [] - self._modal_restore_window_states: Dict[str,bool] = {} - self._settings_dependencies: Dict[Tuple(str,str), Dict[Any, Any]] = {} + self._hide_on_modal: List[Tuple[str, bool]] = [] + self._modal_restore_window_states: Dict[str, bool] = {} + self._settings_dependencies: Dict[Tuple[str, str], Dict[Any, Any]] = {} self._settings_changed_subs = {} self._window_settings = {} - self._window_vis_changed_id = ui.Workspace.set_window_visibility_changed_callback(self._on_window_vis_changed) + self._window_vis_changed_id = \ + ui.Workspace.set_window_visibility_changed_callback( + self._on_window_vis_changed + ) - def destroy(self) -> None: + def destroy(self): + """Unsubscribe from all events and clean up.""" if self._settings: if self._modal_changed_sub: - self._settings.unsubscribe_to_change_events(self._modal_changed_sub) + self._settings.unsubscribe_to_change_events( + self._modal_changed_sub + ) self._settings = None self._hide_on_modal = [] self._modal_restore_window_states = {} self._settings_dependencies = {} self._window_settings = {} if self._window_vis_changed_id: - ui.Workspace.remove_window_visibility_changed_callback(self._window_vis_changed_id) + ui.Workspace.remove_window_visibility_changed_callback( + self._window_vis_changed_id + ) self._window_vis_changed_id = None - def __del__(self) -> None: + def __del__(self): + """Ensure that the object is cleaned up.""" self.destroy() - def add_hide_on_modal(self, window_names: Union[str, List[str]], restore: bool) -> None: + def add_hide_on_modal( + self, window_names: Union[str, List[str]], restore: bool + ): + """Add a window to the list of windows to hide when the modal + tool is active.""" if isinstance(window_names, str): window_names = [window_names] for window_name in window_names: if window_name not in self._hide_on_modal: self._hide_on_modal.append((window_name, restore)) - def remove_hide_on_modal(self, window_names: Union[str, List[str]]) -> None: + def remove_hide_on_modal(self, window_names: Union[str, List[str]]): + """Remove a window from the list of windows to hide when the modal""" if isinstance(window_names, str): window_names = [window_names] self._hide_on_modal = [item for item in self._hide_on_modal if item[0] not in window_names] - def add_window_visibility_setting(self, window_name: str, setting_path: str) -> None: + def add_window_visibility_setting( + self, window_name: str, setting_path: str + ): + """Add a setting that controls the visibility of a window.""" window = ui.Workspace.get_window(window_name) if window is not None: self._settings.set(setting_path, window.visible) @@ -74,7 +93,10 @@ def add_window_visibility_setting(self, window_name: str, setting_path: str) -> self._window_settings[window_name] = [] self._window_settings[window_name].append(setting_path) - def remove_window_visibility_setting(self, window_name: str, setting_path: str) -> None: + def remove_window_visibility_setting( + self, window_name: str, setting_path: str + ): + """Remove a setting that controls the visibility of a window.""" if window_name in self._window_settings.keys(): setting_list = self._window_settings[window_name] if setting_path in setting_list: @@ -82,25 +104,33 @@ def remove_window_visibility_setting(self, window_name: str, setting_path: str) if len(setting_list) == 0: del self._window_settings[window_name] - def remove_all_window_visibility_settings(self, window_name: str) -> None: + def remove_all_window_visibility_settings(self, window_name: str): + """Remove all settings that control the visibility of a window.""" if window_name in self._window_settings.keys(): del self._window_settings[window_name] - def add_settings_dependency(self, source_path: str, target_path: str, value_map: Dict[Any, Any]) -> None: + def add_settings_dependency( + self, source_path: str, target_path: str, value_map: Dict[Any, Any] + ): + """Add a dependency between two settings. When the source setting""" key = (source_path, target_path) if key in self._settings_dependencies.keys(): carb.log_error(f'Settings dependency {source_path} -> {target_path} already exists. Ignoring.') return + self._settings_dependencies[key] = value_map - self._settings_changed_subs[key] = self._settings.subscribe_to_node_change_events( - source_path, - partial(self._on_settings_dependency_changed, source_path) - ) + self._settings_changed_subs[key] = \ + self._settings.subscribe_to_node_change_events( + source_path, + partial(self._on_settings_dependency_changed, source_path) + ) - def add_settings_copy_dependency(self, source_path: str, target_path: str) -> None: + def add_settings_copy_dependency(self, source_path: str, target_path: str): + """Add a dependency between two settings. When the source setting""" self.add_settings_dependency(source_path, target_path, None) - def remove_settings_dependency(self, source_path: str, target_path: str) -> None: + def remove_settings_dependency(self, source_path: str, target_path: str): + """Remove a dependency between two settings.""" key = (source_path, target_path) if key in self._settings_dependencies.keys(): del self._settings_dependencies[key] @@ -108,12 +138,15 @@ def remove_settings_dependency(self, source_path: str, target_path: str) -> None sub = self._settings_changed_subs.pop(key) self._settings.unsubscribe_to_change_events(sub) - def _on_settings_dependency_changed(self, path: str, item, event_type) -> None: + def _on_settings_dependency_changed(self, path: str, _item, _event_type): + """Callback for when a setting changes.""" value = self._settings.get(path) # setting does not exist if value is None: return - target_settings = [source_target[1] for source_target in self._settings_dependencies.keys() if source_target[0] == path] + target_settings = [source_target[1] for source_target in + self._settings_dependencies.keys() if + source_target[0] == path] for target_setting in target_settings: value_map = self._settings_dependencies[(path, target_setting)] # None means copy everything @@ -122,35 +155,45 @@ def _on_settings_dependency_changed(self, path: str, item, event_type) -> None: elif value in value_map.keys(): self._settings.set(target_setting, value_map[value]) - def _on_modal_setting_changed(self, item, event_type) -> None: + def _on_modal_setting_changed(self, _item, _event_type): + """Callback to handle changes to window visibility based on the + app mode setting.""" modal = self._settings.get_as_bool(MODAL_TOOL_ACTIVE_PATH) if modal: self._hide_windows() else: self._restore_windows() - def _hide_windows(self) -> None: + def _hide_windows(self): + """Hide all windows that are set to be hidden when the modal tool + is active.""" for window_info in self._hide_on_modal: window_name, restore_later = window_info[0], window_info[1] window = ui.Workspace.get_window(window_name) if window is not None: if restore_later: - self._modal_restore_window_states[window_name] = window.visible + self._modal_restore_window_states[window_name] = \ + window.visible window.visible = False - def _restore_windows(self) -> None: + def _restore_windows(self): + """Restore all windows that were hidden when the modal tool + was active.""" for window_info in self._hide_on_modal: window_name, restore_later = window_info[0], window_info[1] if restore_later: if window_name in self._modal_restore_window_states.keys(): - old_visibility = self._modal_restore_window_states[window_name] + old_visibility = \ + self._modal_restore_window_states[window_name] if old_visibility is not None: window = ui.Workspace.get_window(window_name) if window is not None: window.visible = old_visibility - self._modal_restore_window_states[window_name] = None + self._modal_restore_window_states[window_name] = \ + None - def _on_window_vis_changed(self, title: str, state: bool) -> None: + def _on_window_vis_changed(self, title: str, state: bool): + """Callback to handle changes to window visibility.""" if title in self._window_settings.keys(): for setting in self._window_settings[title]: self._settings.set_bool(setting, state) diff --git a/templates/extensions/usd_viewer.messaging/template/config/extension.toml b/templates/extensions/usd_viewer.messaging/template/config/extension.toml index 14aa034..5beddc6 100644 --- a/templates/extensions/usd_viewer.messaging/template/config/extension.toml +++ b/templates/extensions/usd_viewer.messaging/template/config/extension.toml @@ -41,8 +41,10 @@ pages = [ [[test]] dependencies = [ + "omni.activity.ui", "omni.kit.mainwindow", "omni.kit.stage_templates", + # "omni.kit.test_suite.helpers", "omni.kit.ui_test", "omni.kit.viewport.utility", "omni.kit.viewport.window", diff --git a/templates/extensions/usd_viewer.messaging/template/data/testing.usd b/templates/extensions/usd_viewer.messaging/template/data/testing.usd new file mode 100644 index 0000000..35e52f4 --- /dev/null +++ b/templates/extensions/usd_viewer.messaging/template/data/testing.usd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3686afba555cd20dade989920624ebaf1ed84de4420015fde5a3b4eeaa2518d0 +size 50011 diff --git a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/extension.py b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/extension.py index 9c1af48..d0b0cbf 100644 --- a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/extension.py +++ b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/extension.py @@ -7,21 +7,28 @@ # disclosure or distribution of this material and related documentation # without an express license agreement from NVIDIA CORPORATION or # its affiliates is strictly prohibited. -import omni.ext + from .stage_loading import LoadingManager from .stage_management import StageManager +import omni.ext -# Any class derived from `omni.ext.IExt` in top level module (defined in `python.modules` of `extension.toml`) will be -# instantiated when extension gets enabled and `on_startup(ext_id)` will be called. Later when extension gets disabled -# on_shutdown() is called. -class Extension(omni.ext.IExt): +# Any class derived from `omni.ext.IExt` in top level module (defined in +# `python.modules` of `extension.toml`) will be instantiated when extension +# gets enabled and `on_startup(ext_id)` will be called. Later when extension +# gets disabled on_shutdown() is called. +class Extension(omni.ext.IExt): + """This extension manages creating the loading and stage + messaging managers""" def on_startup(self): + """This is called every time the extension is activated.""" # Internal messaging state self._loading_manager: LoadingManager = LoadingManager() self._stage_manager: StageManager = StageManager() def on_shutdown(self): + """This is called every time the extension is deactivated. It is used to + clean up the extension state.""" # Resetting the state. if self._loading_manager: self._loading_manager.on_shutdown() diff --git a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_loading.py b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_loading.py index 43f91b1..936fd45 100644 --- a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_loading.py +++ b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_loading.py @@ -8,35 +8,46 @@ # without an express license agreement from NVIDIA CORPORATION or # its affiliates is strictly prohibited. -import time -import omni.ext -import omni.kit.app -import omni.usd import asyncio + import carb +import carb.events +import carb.tokens +import omni.ext import omni.log +import omni.kit.app import omni.kit.livestream.messaging as messaging +import omni.usd class LoadingManager: - + """Manages the loading of USD stages and sends messages to the client""" def __init__(self): - self._subscriptions = [] # Holds subscription pointers + self._subscriptions = [] # Holds subscription pointers # -- state variables - self._requested_stage_url: str = "" # URL of stage load request. Can be used in messaging with client. + # URL of stage load request. Can be used in messaging with client. + self._requested_stage_url: str = "" self._stage_is_opening: bool = False - self._opened_stage_url: str = "" # URL of loaded stage. Should not be used in messaging with client because it may reveal directory paths in environment where application runs. + + # URL of loaded stage. Should not be used in messaging with client + # because it may reveal directory paths in environment where + # application runs. + self._opened_stage_url: str = "" self._stage_has_opened = False self._streaming_manager_is_busy: bool = False - self._persisted_stage: bool = False # States if opened stage is opened from storage as in not a new unsaved stage + + # States if opened stage is opened from storage as in not a + # new unsaved stage + self._persisted_stage: bool = False self._is_evaluating_loading_status: bool = False # -- register outgoing events/messages outgoing = [ "openedStageResult", # notify when USD Stage has loaded. "updateProgressAmount", # Status bar event denoting progress - "updateProgressActivity", # Status bar event denoting current activity + "updateProgressActivity", # Status bar event denoting activity + "loadingStateResponse", # Response to loadingStateQuery ] for o in outgoing: @@ -45,8 +56,11 @@ def __init__(self): # -- register incoming events/messages incoming = { 'openStageRequest': self._on_open_stage, # request to open a stage - "omni.kit.window.status_bar@progress": self._on_progress, # internal event to capture progress status - "omni.kit.window.status_bar@activity": self._on_activity, # internal event to capture progress activity + # internal event to capture progress status + "omni.kit.window.status_bar@progress": self._on_progress, + # internal event to capture progress activity + "omni.kit.window.status_bar@activity": self._on_activity, + "loadingStateQuery": self._on_load_state_query, } message_bus = omni.kit.app.get_app().get_message_bus_event_stream() @@ -64,16 +78,34 @@ def __init__(self): ) # -- subscribe to RTX streaming status events - RTX_STREAMING_STATUS_EVENT: int = carb.events.type_from_string("omni.rtx.StreamingStatus") + RTX_STREAMING_STATUS_EVENT: int = carb.events.type_from_string( + "omni.rtx.StreamingStatus" + ) self._subscriptions.append( - event_stream.create_subscription_to_pop_by_type(RTX_STREAMING_STATUS_EVENT, self._on_rxt_streaming_event) + event_stream.create_subscription_to_pop_by_type( + RTX_STREAMING_STATUS_EVENT, self._on_rxt_streaming_event + ) ) + def _on_load_state_query(self, event: carb.events.IEvent) -> None: + if event.type == carb.events.type_from_string("loadingStateQuery"): + message_bus = omni.kit.app.get_app().get_message_bus_event_stream() + event_type = carb.events.type_from_string("loadingStateResponse") + payload = {"loading_state": "idle", "url": self._opened_stage_url} + if self._stage_is_opening: + payload = { "loading_state": "busy", "url": self._requested_stage_url } + elif self._stage_has_opened: + payload = { "loading_state": "idle", "url": self._requested_stage_url } + + message_bus.dispatch(event_type, payload=payload) + + def _on_open_stage(self, event: carb.events.IEvent) -> None: """ Handler for `openStageRequest` event. - Starts loading a given URL, will send success if the layer is already loaded, and an error on any failure. + Starts loading a given URL, will send success if the layer is already + loaded, and an error on any failure. """ if event.type == carb.events.type_from_string("openStageRequest"): @@ -83,12 +115,17 @@ def _on_open_stage(self, event: carb.events.IEvent) -> None: return self._requested_stage_url = event.payload["url"] - carb.log_info(f"Received message to load '{self._requested_stage_url}'") + carb.log_info( + f"Received message to load '{self._requested_stage_url}'" + ) def process_url(url): - # Using a single leading `.` to signify that the path is relative to the ${app} token's parent directory. + # Using a single leading `.` to signify that the path is + # relative to the ${app} token's parent directory. if url.startswith(("./", ".\\")): - return carb.tokens.acquire_tokens_interface().resolve("${app}/.." + url[1:]) + return carb.tokens.acquire_tokens_interface().resolve( + "${app}/.." + url[1:] + ) return url # Check to see if we've already loaded the current stage. @@ -111,22 +148,31 @@ def process_url(url): async def open_stage(): carb.log_info(f'Opening stage per client request: {url}') usd_context = omni.usd.get_context() - result, error = await usd_context.open_stage_async(url, omni.usd.UsdContextInitialLoadSet.LOAD_ALL) + if url: + result, error = await usd_context.open_stage_async(url, omni.usd.UsdContextInitialLoadSet.LOAD_ALL) + else: + result, error = await usd_context.new_stage_async() + + message_bus = omni.kit.app.get_app().get_message_bus_event_stream() + event_type = carb.events.type_from_string("openedStageResult") + if result is not True: # Send message to client that loading failed. carb.log_warn(f'The file that the client requested failed to load: {url} (error: {error})') - message_bus = omni.kit.app.get_app().get_message_bus_event_stream() - event_type = carb.events.type_from_string("openedStageResult") payload = {"url": url, "result": "error", "error": error} message_bus.dispatch(event_type, payload=payload) self._reset_state() + return - asyncio.ensure_future(open_stage()) + payload = {"url": url, "result": "success", "error": ''} + message_bus.dispatch(event_type, payload=payload) + asyncio.ensure_future(open_stage()) def _on_stage_event(self, event: carb.events.IEvent) -> None: - """Manage extension state via the stage event stream. When a new stage is open we reload the data model - and set the state for the UI. + """Manage extension state via the stage event stream. + When a new stage is open we reload the data model and + set the state for the UI. Args: event (carb.events.IEvent): Event type @@ -157,13 +203,15 @@ def _on_rxt_streaming_event(self, event: carb.events.IEvent) -> None: Notes streaming manager's busy state Args: - event (carb.events.IEvent): Contains payload sender and type - https://docs.omniverse.nvidia.com/kit/docs/kit-manual/105.0/carb.events/carb.events.IEvent.html + event (carb.events.IEvent): Contains payload sender and type - + https://docs.omniverse.nvidia.com/kit/docs/kit-manual/105.0/carb.events/carb.events.IEvent.html """ self._streaming_manager_is_busy = event.payload['isBusy'] async def _evaluate_load_status(self): """ - If streaming manager is not busy and the stage is loaded from storage we notify client. + If streaming manager is not busy and the stage is loaded from storage, + notify the client. """ # Only evaluate for stage loaded from storage. if not self._persisted_stage: @@ -183,8 +231,10 @@ async def _evaluate_load_status(self): # Stage has loaded with all dependencies. Send message to client. message_bus = omni.kit.app.get_app().get_message_bus_event_stream() event_type = carb.events.type_from_string("openedStageResult") - url = self._requested_stage_url if self._requested_stage_url else '[obfuscated]' - carb.log_info(f'Sending message to client that stage has loaded: {url}') + url = self._requested_stage_url if self._requested_stage_url else '[obfuscated]' + carb.log_info( + f'Sending message to client that stage has loaded: {url}' + ) payload = {"url": url, "result": "success", "error": ''} message_bus.dispatch(event_type, payload=payload) @@ -200,12 +250,15 @@ def _on_progress(self, event: carb.events.IEvent): # Only notify for stage loaded from storage. if not self._persisted_stage: return - if event.type == carb.events.type_from_string("omni.kit.window.status_bar@progress"): + if event.type == carb.events.type_from_string( + "omni.kit.window.status_bar@progress" + ): message_bus = omni.kit.app.get_app().get_message_bus_event_stream() # Send progress message carb.log_info('Sending message to client about loading progress.') event_type = carb.events.type_from_string("updateProgressAmount") - # event.payload.get_dict() is used to capture a copy of the incoming event's payload as a python dictionary + # event.payload.get_dict() is used to capture a copy of the + # incoming event's payload as a python dictionary message_bus.dispatch(event_type, payload=event.payload.get_dict()) def _on_activity(self, event: carb.events.IEvent): @@ -216,7 +269,9 @@ def _on_activity(self, event: carb.events.IEvent): # Only notify for stage loaded from storage. if not self._persisted_stage: return - if event.type == carb.events.type_from_string("omni.kit.window.status_bar@activity"): + if event.type == carb.events.type_from_string( + "omni.kit.window.status_bar@activity" + ): carb.log_info('Storing message about loading activity.') message_bus = omni.kit.app.get_app().get_message_bus_event_stream() # Send activity message @@ -235,8 +290,9 @@ def _reset_state(self): """ Reset the internal state - ready for new stage to be loaded """ + stage = omni.usd.get_context().get_stage() self._requested_stage_url = "" - self._opened_stage_url = "" + self._opened_stage_url = stage.GetRootLayer().identifier if stage else "" self._stage_has_opened = False self._streaming_manager_is_busy = False self._persisted_stage = False diff --git a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_management.py b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_management.py index 5edbf7d..afc5248 100644 --- a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_management.py +++ b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/stage_management.py @@ -8,27 +8,24 @@ # without an express license agreement from NVIDIA CORPORATION or # its affiliates is strictly prohibited. -import omni.ext -import omni.kit.app +from pxr import UsdGeom, Usd import carb -import carb.settings import carb.dictionary import carb.events +import carb.settings import carb.tokens -import carb.input -import omni.usd import omni.client.utils +import omni.ext +import omni.usd +import omni.kit.app import omni.kit.livestream.messaging as messaging from omni.kit.viewport.utility import get_active_viewport_camera_string -from pxr import UsdGeom, Usd - class StageManager: - + """This class manages the stage and its related events.""" def __init__(self): - # Internal messaging state self._is_external_update: bool = False self._camera_attrs = {} @@ -36,10 +33,14 @@ def __init__(self): # -- register outgoing events/messages outgoing = [ - "stageSelectionChanged", # notify when user selects something in the viewport. - "getChildrenResponse", # response to request for children of a prim - "makePrimsPickableResponse", # response to request for primitive being pickable. - "resetStageResponse", # response to the request to reset camera attributes + # notify when user selects something in the viewport. + "stageSelectionChanged", + # response to request for children of a prim + "getChildrenResponse", + # response to request for primitive being pickable. + "makePrimsPickableResponse", + # response to the request to reset camera attributes + "resetStageResponse", ] for o in outgoing: @@ -47,17 +48,20 @@ def __init__(self): # -- register incoming events/messages incoming = { - 'getChildrenRequest': self._on_get_children, # request to get children of a prim - 'selectPrimsRequest' : self._on_select_prims, # request to select a prim - 'makePrimsPickable' : self._on_make_pickable, # request to make primitives pickable - 'resetStage' : self._on_reset_camera, # request to make primitives pickable + # request to get children of a prim + 'getChildrenRequest': self._on_get_children, + # request to select a prim + 'selectPrimsRequest': self._on_select_prims, + # request to make primitives pickable + 'makePrimsPickable': self._on_make_pickable, + # request to make primitives pickable + 'resetStage': self._on_reset_camera, } for event_type, handler in incoming.items(): self._subscriptions.append( - omni.kit.app.get_app().get_message_bus_event_stream().create_subscription_to_pop( - handler, name=event_type - ) + omni.kit.app.get_app().get_message_bus_event_stream(). + create_subscription_to_pop(handler, name=event_type) ) # -- subscribe to stage events @@ -66,13 +70,14 @@ def __init__(self): event_stream.create_subscription_to_pop(self._on_stage_event) ) - def get_children(self, prim_path, filters=None): """ Collect any children of the given `prim_path`, potentially filtered by `filters` """ stage = omni.usd.get_context().get_stage() prim = stage.GetPrimAtPath(prim_path) + if not prim: + return [] filter_types = { "USDGeom": UsdGeom.Mesh, @@ -96,12 +101,13 @@ def get_children(self, prim_path, filters=None): # Also skipping rendering primitives. if prim_path == '/' and child_name == 'Render': continue - child_path = child_path if not child_path == '/' else '' + child_path = child_path if child_path != '/' else '' carb.log_info(f'child_path: {child_path}') info = {"name": child_name, "path": f'{child_path}/{child_name}'} - # We return an empty list here to indicate that children are available, but - # the current app does not support pagination, so we use this to lazy load the stage tree. + # We return an empty list here to indicate that children are + # available, but the current app does not support pagination, + # so we use this to lazy load the stage tree. if child.GetChildren(): info["children"] = [] @@ -115,12 +121,17 @@ def _on_get_children(self, event: carb.events.IEvent) -> None: Collects a filtered collection of a given primitives children. """ if event.type == carb.events.type_from_string("getChildrenRequest"): - carb.log_info(f"Received message to return list of a prim\'s children") + carb.log_info( + "Received message to return list of a prim\'s children" + ) message_bus = omni.kit.app.get_app().get_message_bus_event_stream() event_type = carb.events.type_from_string("getChildrenResponse") - children = self.get_children(prim_path=event.payload["prim_path"], filters=event.payload["filters"]) + children = self.get_children( + prim_path=event.payload["prim_path"], + filters=event.payload["filters"] + ) payload = { - "prim_path" : event.payload["prim_path"], + "prim_path": event.payload["prim_path"], "children": children } message_bus.dispatch(event_type, payload=payload) @@ -137,7 +148,8 @@ def _on_select_prims(self, event: carb.events.IEvent) -> None: if "paths" in event.payload: new_selection = list(event.payload["paths"]) carb.log_info(f"Received message to select '{new_selection}'") - # Flagging this as an external event because it was initiated by the client. + # Flagging this as an external event because it + # was initiated by the client. self._is_external_update = True sel = omni.usd.get_context().get_selection() sel.clear_selected_prim_paths() @@ -147,25 +159,33 @@ def _on_stage_event(self, event): """ Hanles all stage related events. - `omni.usd.StageEventType.SELECTION_CHANGED`: Informs the StreamerApp that the selection has changed. - `omni.usd.StageEventType.ASSETS_LOADED`: Informs the StreamerApp that a stage has finished loading its assets. - `omni.usd.StageEventType.OPENED`: On stage opened, we collect some of the camera properties to allow for them to be reset. - + `omni.usd.StageEventType.SELECTION_CHANGED`: + Informs the StreamerApp that the selection has changed. + `omni.usd.StageEventType.ASSETS_LOADED`: + Informs the StreamerApp that a stage has finished loading + its assets. + `omni.usd.StageEventType.OPENED`: + On stage opened, we collect some of the camera properties + to allow for them to be reset. """ if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED): - # If the selection changed came from an external event, we don't need to let the streaming client know - # because it initiated the change and is already aware. + # If the selection changed came from an external event, + # we don't need to let the streaming client know because it + # initiated the change and is already aware. if self._is_external_update: self._is_external_update = False else: - message_bus = omni.kit.app.get_app().get_message_bus_event_stream() - event_type = carb.events.type_from_string("stageSelectionChanged") - payload = {"prims": omni.usd.get_context().get_selection().get_selected_prim_paths()} + message_bus = omni.kit.app.get_app()\ + .get_message_bus_event_stream() + event_type = carb.events.type_from_string( + "stageSelectionChanged" + ) + payload = {"prims": omni.usd.get_context().get_selection(). + get_selected_prim_paths()} message_bus.dispatch(event_type, payload=payload) message_bus.pump() carb.log_info(f"Selection changed: Path to USD prims currently selected = {omni.usd.get_context().get_selection().get_selected_prim_paths()}") - # elif event.type == int(omni.usd.StageEventType.ASSETS_LOADED): elif event.type == int(omni.usd.StageEventType.OPENED): stage = omni.usd.get_context().get_stage() stage_url = stage.GetRootLayer().identifier if stage else '' @@ -174,9 +194,11 @@ def _on_stage_event(self, event): # Set the entire stage to not be pickable. ctx = omni.usd.get_context() ctx.set_pickable("/", False) - # Clear before using, so that we're sure the data is only from the new stage. + # Clear before using, so that we're sure the data is only + # from the new stage. self._camera_attrs.clear() - # Capture the active camera's camera data, used to reset the scene to a known good state. + # Capture the active camera's camera data, used to reset + # the scene to a known good state. if (prim := ctx.get_stage().GetPrimAtPath(get_active_viewport_camera_string())): for attr in prim.GetAttributes(): self._camera_attrs[attr.GetName()] = attr.Get() @@ -193,10 +215,15 @@ def _on_reset_camera(self, event: carb.events.IEvent): stage = ctx.get_stage() try: # Reset the camera. - # The camera lives on the session layer, which has a higher opinion than the root stage. - # So we need to explicitly target the session layer when resetting the camera's attributes. - camera_prim = ctx.get_stage().GetPrimAtPath(get_active_viewport_camera_string()) - edit_context = Usd.EditContext(stage, Usd.EditTarget(stage.GetSessionLayer())) + # The camera lives on the session layer, which has a higher + # opinion than the root stage. So we need to explicitly target + # the session layer when resetting the camera's attributes. + camera_prim = ctx.get_stage().GetPrimAtPath( + get_active_viewport_camera_string() + ) + edit_context = Usd.EditContext( + stage, Usd.EditTarget(stage.GetSessionLayer()) + ) with edit_context: for name, value in self._camera_attrs.items(): attr = camera_prim.GetAttribute(name) @@ -215,15 +242,22 @@ def _on_make_pickable(self, event: carb.events.IEvent): Handler for `makePrimsPickable` event. Enables viewport selection for the provided primitives. - Sends 'makePrimsPickableResponse' back to streamer with current success status. + Sends 'makePrimsPickableResponse' back to streamer with + current success status. """ if event.type == carb.events.type_from_string("makePrimsPickable"): message_bus = omni.kit.app.get_app().get_message_bus_event_stream() - event_type = carb.events.type_from_string("makePrimsPickableResponse") + event_type = carb.events.type_from_string( + "makePrimsPickableResponse" + ) + # Reset the stage to not be pickable. + ctx = omni.usd.get_context() + ctx.set_pickable("/", False) + # Set the provided paths to be pickable. try: paths = event.payload['paths'] or [] for path in paths: - omni.usd.get_context().set_pickable(path, True) + ctx.set_pickable(path, True) except Exception as e: payload = {"result": "error", "error": str(e)} else: @@ -231,8 +265,9 @@ def _on_make_pickable(self, event: carb.events.IEvent): message_bus.dispatch(event_type, payload=payload) message_bus.pump() - def on_shutdown(self): + """This is called every time the extension is deactivated. It is used + to clean up the extension state.""" # Reseting the state. self._subscriptions.clear() self._is_external_update: bool = False diff --git a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/tests/__init__.py b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/tests/__init__.py new file mode 100644 index 0000000..170eeda --- /dev/null +++ b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/tests/__init__.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# from .messaging_tests import * diff --git a/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/tests/messaging_tests.py b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/tests/messaging_tests.py new file mode 100644 index 0000000..fb9d414 --- /dev/null +++ b/templates/extensions/usd_viewer.messaging/template/{{python_module_path}}/tests/messaging_tests.py @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# from pathlib import Path +# from typing import Dict, List + +# import carb.events +# import carb.tokens +# import omni.kit.app +# from omni.kit.test import AsyncTestCase +# # from omni.kit.test_suite.helpers import wait_stage_loading + + +# class MessagingTest(AsyncTestCase): +# async def setUp(self): +# self._app = omni.kit.app.get_app() +# self._message_bus = self._app.get_message_bus_event_stream() + +# # Capture extension root path +# extension = "{{ extension_name }}" +# ext_root = Path(carb.tokens.get_tokens_interface().resolve({% raw %}f"${{{extension}}}"{% endraw %})) +# self._data_path = ext_root / "data" + + # async def test_stage_loading_incoming(self): + # """ + # Simulate incoming events of the stage loading messaging system + # """ + # import omni.kit.livestream.messaging as messaging + + # def on_message_event(event: carb.events.IEvent) -> None: + # if event.type == carb.events.type_from_string("updateProgressAmount"): + # outgoing["updateProgressAmount"] = True + # elif event.type == carb.events.type_from_string("updateProgressActivity"): + # outgoing["updateProgressActivity"] = True + + + # # Register the open stage request type + # messaging.register_event_type_to_send("openStageRequest") + + # outgoing: Dict[str, bool] = { + # "updateProgressAmount": False, # Status bar event denoting progress + # "updateProgressActivity": False # Status bar event denoting current activity + # } + + # subscriptions: List[int] = [] + # for event in outgoing.keys(): + # subscriptions.append( + # self._message_bus.create_subscription_to_pop( + # on_message_event, + # name=event + # ) + # ) + # await self._app.next_update_async() + + # # Send the openStageRequest event + # event_type = carb.events.type_from_string("openStageRequest") + # url = self._data_path / "testing.usd" + # self._message_bus.dispatch(event_type, payload={"url": url.as_posix()}) + + # await wait_stage_loading(wait_frames=300) + # self.assertTrue(all(outgoing.values())) + + + # async def test_stage_management_incoming(self): + # """ + # Simulate incoming events of the stage management messaging system + # """ + # import omni.kit.livestream.messaging as messaging + + # subscriptions: List[int] = [] + + # outgoing: Dict[str, bool] = { + # "stageSelectionChanged": False, # notify when user selects something in the viewport. + # "getChildrenResponse": False, # response to request for children of a prim + # "makePrimsPickableResponse": False, # response to request for primitive being pickable. + # "resetStageResponse": False, # response to the request to reset camera attributes + # } + + # def on_message_event(event: carb.events.IEvent) -> None: + # if event.type == carb.events.type_from_string("stageSelectionChanged"): + # outgoing["stageSelectionChanged"] = True + # elif event.type == carb.events.type_from_string("getChildrenResponse"): + # outgoing["getChildrenResponse"] = True + # elif event.type == carb.events.type_from_string("makePrimsPickableResponse"): + # outgoing["makePrimsPickableResponse"] = True + # elif event.type == carb.events.type_from_string("resetStageResponse"): + # outgoing["resetStageResponse"] = True + + # incoming: List[str] = [ + # 'getChildrenRequest', + # 'selectPrimsRequest', + # 'makePrimsPickable', + # 'resetStage' + # ] + + # # Register outgoing event + # # Subscribe to messaging events + # # Send event to validate + # for event in outgoing.keys(): + # subscriptions.append(self._message_bus.create_subscription_to_pop( + # on_message_event, + # name=event + # )) + + # event_type = carb.events.type_from_string(event) + # self._message_bus.dispatch(event_type, payload={}) + + # await self._app.next_update_async() + + # # Send the openStageRequest event + # event_type = carb.events.type_from_string("openStageRequest") + # url = self._data_path / "testing.usd" + # self._message_bus.dispatch(event_type, payload={"url": url.as_posix()}) + + # # Wait for the stage to load + # await wait_stage_loading(wait_frames=30) + + # # Get children of root + # event_type = carb.events.type_from_string("getChildrenRequest") + # self._message_bus.dispatch(event_type, payload={"prim_path": "/World"}) + # await self._app.next_update_async() + + # # Select Prims Request + # event_type = carb.events.type_from_string("selectPrimsRequest") + # self._message_bus.dispatch(event_type, payload={"paths": ["/World/Cube"]}) + # await self._app.next_update_async() + + # # Make Prims Pickable Request + # event_type = carb.events.type_from_string("makePrimsPickable") + # self._message_bus.dispatch(event_type, payload={"paths": ["/World/Cube", "/World/Sphere"]}) + # await self._app.next_update_async() + + # # Reset Stage Request + # event_type = carb.events.type_from_string("resetStage") + # self._message_bus.dispatch(event_type) + # await self._app.next_update_async() + + # self.assertTrue(all(outgoing.values())) diff --git a/templates/extensions/usd_viewer.setup/template/config/extension.toml b/templates/extensions/usd_viewer.setup/template/config/extension.toml index c879025..818dc0c 100644 --- a/templates/extensions/usd_viewer.setup/template/config/extension.toml +++ b/templates/extensions/usd_viewer.setup/template/config/extension.toml @@ -22,12 +22,17 @@ repository = "https://github.com/NVIDIA-Omniverse/kit-app-template" # URL of th [dependencies] -"omni.activity.ui" = {} # Progress activity messages +"omni.kit.usd.layers" = {} +"omni.activity.ui" = {order=1000} # Progress activity messages "omni.kit.quicklayout" = {} "omni.kit.viewport.utility" = {} "{{ extra_extension_name }}" = {} # Required messaging extension +[settings.app] +useFabricSceneDelegate = true # Turn on the Fabric scene delegate by default + + [settings.exts."{{ extension_name }}"] menu_visible = false @@ -45,9 +50,20 @@ pages = [ [[test]] dependencies = [ + "{{ application_name }}", + "omni.kit.mainwindow", + "omni.kit.ui_test", ] args = [ "--/app/layout/name=default", + "--/app/fastShutdown=1", + "--/app/file/ignoreUnsavedOnExit=true", + "--/app/window/dpiScaleOverride=1.0", + "--/app/window/height=720", + "--/app/window/scaleToMonitor=false", + "--/app/window/width=1280", + "--/exts/omni.kit.viewport.window/startup/windowName=Viewport", "--no-window", + "--reset-user" ] diff --git a/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/setup.py b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/setup.py index e224056..a7943e0 100644 --- a/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/setup.py +++ b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/setup.py @@ -17,7 +17,6 @@ import omni.kit.app import omni.kit.imgui as _imgui import omni.kit.viewport -import omni.ui as ui import omni.usd from omni.kit.mainwindow import get_main_window from omni.kit.quicklayout import QuickLayout @@ -26,9 +25,10 @@ COMMAND_MACRO_SETTING = "/exts/omni.kit.command_macro.core/" COMMAND_MACRO_FILE_SETTING = COMMAND_MACRO_SETTING + "macro_file" + async def _load_layout(layout_file: str): - """this private methods just help loading layout, you can use it in the Layout Menu""" - await omni.kit.app.get_app().next_update_async() # type: ignore + """Loads a provided layout file and ensures the viewport is set to FILL.""" + await omni.kit.app.get_app().next_update_async() QuickLayout.load_file(layout_file) # Set viewport to FILL @@ -38,22 +38,25 @@ async def _load_layout(layout_file: str): class SetupExtension(omni.ext.IExt): - # ext_id is current extension id. It can be used with extension manager to query additional information, like where - # this extension is located on filesystem. + """Extension that sets up the USD Viewer application.""" def on_startup(self, _ext_id: str): - - # get the settings + """This is called every time the extension is activated. It is used to + set up the application and load the stage.""" self._settings = carb.settings.get_settings() # get auto load stage name stage_url = self._settings.get_as_string("/app/auto_load_usd") - # check if setup have benchmark macro file to activate - ignore setup auto_load_usd name, in order to run proper benchmark. - benchmark_macro_file_name = self._settings.get(COMMAND_MACRO_FILE_SETTING) + # check if setup have benchmark macro file to activate - ignore setup + # auto_load_usd name, in order to run proper benchmark. + benchmark_macro_file_name = self._settings.get( + COMMAND_MACRO_FILE_SETTING) if benchmark_macro_file_name: stage_url = None - # if no benchmark is activated (not applicable on production - provided macro file name will always be None) - load provided by setup stage. + # if no benchmark is activated (not applicable on production - + # provided macro file name will always be None) - + # load provided by setup stage. if stage_url: stage_url = carb.tokens.get_tokens_interface().resolve(stage_url) asyncio.ensure_future(self.__open_stage(stage_url)) @@ -62,13 +65,15 @@ def on_startup(self, _ext_id: str): get_main_window().get_main_menu_bar().visible = False async def _delayed_layout(self): - # few frame delay to allow automatic Layout of window that want their own positionsdef _setup_menu(self): - # Menubar + """This function is used to delay the layout loading until the + application has finished its initial setup.""" main_menu_bar = get_main_window().get_main_menu_bar() main_menu_bar.visible = False - # few frame delay to allow automatic Layout of window that want their own positions - for _i in range(4): - await omni.kit.app.get_app().next_update_async() # type: ignore + # few frame delay to allow automatic Layout of window that want their + # own positions + app = omni.kit.app.get_app() + for _ in range(4): + await app.next_update_async() # type: ignore settings = carb.settings.get_settings() # setup the Layout for your app @@ -83,16 +88,29 @@ async def _delayed_layout(self): # using imgui directly to adjust some color and Variable imgui = _imgui.acquire_imgui() - # DockSplitterSize is the variable that drive the size of the Dock Split connection + # DockSplitterSize is the variable that drive the size of the + # Dock Split connection imgui.push_style_var_float(_imgui.StyleVar.DockSplitterSize, 2) - async def __open_stage(self, url): - # 10 frame delay to allow Layout - for i in range(5): - await omni.kit.app.get_app().next_update_async() # type: ignore + async def __open_stage(self, url, frame_delay: int = 5): + """Opens the provided USD stage and loads the render settings.""" + # default 5 frame delay to allow for Layout + if frame_delay: + app = omni.kit.app.get_app() + for _ in range(frame_delay): + await app.next_update_async() usd_context = omni.usd.get_context() - await usd_context.open_stage_async(url, omni.usd.UsdContextInitialLoadSet.LOAD_ALL) # type: ignore + await usd_context.open_stage_async( + url, omni.usd.UsdContextInitialLoadSet.LOAD_ALL) + + # If this was the first Usd data opened, explicitly restore + # render-settings now as the renderer may not have been fully + # setup when the stage was opened. + if not bool(self._settings.get("/app/content/emptyStageOnStart")): + usd_context.load_render_settings_from_stage( + usd_context.get_stage_id()) def on_shutdown(self): - pass + """This is called every time the extension is deactivated.""" + return diff --git a/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/__init__.py b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/__init__.py new file mode 100644 index 0000000..e866cdc --- /dev/null +++ b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/__init__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# run startup tests first +from .test_app_startup import * +from .test_app_extensions import * diff --git a/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/test_app_extensions.py b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/test_app_extensions.py new file mode 100644 index 0000000..3f71bf2 --- /dev/null +++ b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/test_app_extensions.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +import omni.kit.app +from omni.kit.test import AsyncTestCase + + +class TestUSDViewerExtensions(AsyncTestCase): + # NOTE: Function pulled to remove dependency from omni.kit.core.tests + def _validate_extensions_load(self): + failures = [] + manager = omni.kit.app.get_app().get_extension_manager() + for ext in manager.get_extensions(): + ext_id = ext["id"] + ext_name = ext["name"] + info = manager.get_extension_dict(ext_id) + + enabled = ext.get("enabled", False) + if not enabled: + continue + + failed = info.get("state/failed", False) + if failed: + failures.append(ext_name) + + if len(failures) == 0: + print("\n[success] All extensions loaded successfully!\n") + else: + print("") + print(f"[error] Found {len(failures)} extensions that could not load:") + for count, ext in enumerate(failures): + print(f" {count+1}: {ext}") + print("") + return len(failures) + + async def test_l1_extensions_load(self): + """Loop all enabled extensions to see if they loaded correctly""" + self.assertEqual(self._validate_extensions_load(), 0) \ No newline at end of file diff --git a/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/test_app_startup.py b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/test_app_startup.py new file mode 100644 index 0000000..30575b1 --- /dev/null +++ b/templates/extensions/usd_viewer.setup/template/{{python_module_path}}/tests/test_app_startup.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +import json +import time +from typing import Tuple + +import carb.settings +import omni.kit.app +from omni.kit.test import AsyncTestCase + + +class TestAppStartup(AsyncTestCase): + def app_startup_time(self, test_id: str) -> float: + """Get startup time - send to nvdf""" + test_start_time = time.monotonic() + startup_time = omni.kit.app.get_app().get_time_since_start_s() + test_result = {"startup_time_s": startup_time} + print(f"App Startup time: {startup_time}") + return startup_time + + def app_startup_warning_count(self, test_id: str) -> Tuple[int, int]: + """Get the count of warnings during startup - send to nvdf""" + test_start_time = time.monotonic() + warning_count = 0 + error_count = 0 + log_file_path = carb.settings.get_settings().get("/log/file") + with open(log_file_path, "r") as file: + for line in file: + if "[Warning]" in line: + warning_count += 1 + elif "[Error]" in line: + error_count += 1 + + test_result = {"startup_warning_count": warning_count, "startup_error_count": error_count} + print(f"App Startup Warning count: {warning_count}") + print(f"App Startup Error count: {error_count}") + return warning_count, error_count + + async def test_l1_app_startup_time(self): + """Get startup time - send to nvdf""" + for _ in range(60): + await omni.kit.app.get_app().next_update_async() + + self.app_startup_time(self.id()) + self.assertTrue(True) + + async def test_l1_app_startup_warning_count(self): + """Get the count of warnings during startup - send to nvdf""" + for _ in range(60): + await omni.kit.app.get_app().next_update_async() + + self.app_startup_warning_count(self.id()) + self.assertTrue(True) + + async def test_l1_app_startup_fsd_enabled(self): + """Check if Fabric Scene Delegate is enabled at startup""" + + fsd_enabled: bool = carb.settings.get_settings().get("/app/useFabricSceneDelegate") + self.assertTrue(fsd_enabled) \ No newline at end of file diff --git a/templates/templates.toml b/templates/templates.toml index b4338ef..4b9f73d 100644 --- a/templates/templates.toml +++ b/templates/templates.toml @@ -80,6 +80,7 @@ type = "extra" template = "omni_usd_viewer_setup" type = "setup" + [[templates."omni_usd_viewer".applications]] template = "omni_gdn_streaming" type = "gdn" @@ -178,7 +179,7 @@ variables.version = "0.1.0" [templates."omni_usd_viewer_messaging"] -class = "ExtensionTemplate" +class = "SetupExtensionTemplate" name = "Omni USD Viewer Messaging" url = "." subpath = "extensions/usd_viewer.messaging/template" diff --git a/tools/VERSION.md b/tools/VERSION.md index de75f5f..b738a70 100644 --- a/tools/VERSION.md +++ b/tools/VERSION.md @@ -1 +1 @@ -106.0.2 \ No newline at end of file +106.1 \ No newline at end of file diff --git a/tools/containers/Dockerfile.j2 b/tools/containers/Dockerfile.j2 index 2e4d05b..40c491e 100644 --- a/tools/containers/Dockerfile.j2 +++ b/tools/containers/Dockerfile.j2 @@ -1,7 +1,7 @@ # This Dockerfile is used as the base for containerizing applications rendered out via repo_kit_template. # Utilize Kit Kernel 106.0.0 -FROM nvcr.io/nvidia/omniverse/ov-kit-kernel:106.0.1-ga.126909.3a7abd1c +FROM nvcr.io/nvidia/omniverse/ov-kit-kernel:106.1.0-ea.140981.10a4b5c0 # root user to bootstrap the container USER root @@ -29,9 +29,9 @@ WORKDIR /app # Copy the startup.sh scripts COPY --chown=ubuntu:ubuntu entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +RUN chmod +x,-w /entrypoint.sh COPY --chown=ubuntu:ubuntu entrypoint_memcached.sh /entrypoint_memcached.sh -RUN chmod +x /entrypoint_memcached.sh +RUN chmod +x,-w /entrypoint_memcached.sh # Create Kit expected caching directories that we'll map a volume onto. RUN mkdir -p /home/ubuntu/.cache/ov diff --git a/tools/deps/kit-sdk-deps.packman.xml b/tools/deps/kit-sdk-deps.packman.xml index 5ecee6b..5c19b04 100644 --- a/tools/deps/kit-sdk-deps.packman.xml +++ b/tools/deps/kit-sdk-deps.packman.xml @@ -27,4 +27,5 @@ + diff --git a/tools/deps/kit-sdk.packman.xml b/tools/deps/kit-sdk.packman.xml index 552a4c9..4f019d7 100644 --- a/tools/deps/kit-sdk.packman.xml +++ b/tools/deps/kit-sdk.packman.xml @@ -1,7 +1,4 @@ - - - - + diff --git a/tools/deps/repo-deps.packman.xml b/tools/deps/repo-deps.packman.xml index b73bde7..72e77df 100644 --- a/tools/deps/repo-deps.packman.xml +++ b/tools/deps/repo-deps.packman.xml @@ -1,20 +1,23 @@ - + - + - + - + - + - + + + + diff --git a/tools/packman/bootstrap/configure.bat b/tools/packman/bootstrap/configure.bat index afaf2fb..1511537 100755 --- a/tools/packman/bootstrap/configure.bat +++ b/tools/packman/bootstrap/configure.bat @@ -12,7 +12,7 @@ :: See the License for the specific language governing permissions and :: limitations under the License. -set PM_PACKMAN_VERSION=7.23 +set PM_PACKMAN_VERSION=7.23.2 :: Specify where packman command is rooted set PM_INSTALL_PATH=%~dp0.. diff --git a/tools/packman/bootstrap/install_package.py b/tools/packman/bootstrap/install_package.py index 706be4f..5baa9ec 100644 --- a/tools/packman/bootstrap/install_package.py +++ b/tools/packman/bootstrap/install_package.py @@ -142,7 +142,7 @@ def generate_sha256_for_file(file_path: Union[str, os.PathLike]) -> str: def install_common_module(package_path, install_path): - COMMON_SHA256 = "b39889fbcf49cbbc66f913f2a3a73817ec3afcf5ae3e4ba9cf9f6fd3e775aa34" + COMMON_SHA256 = "dc5b18d2b507f04ce560b3d3e8b6b1eeac55b8931653096378cb45337874fd33" package_sha256 = generate_sha256_for_file(package_path) if package_sha256 != COMMON_SHA256: raise RuntimeError( diff --git a/tools/packman/packman b/tools/packman/packman index d26c28f..2577fbe 100755 --- a/tools/packman/packman +++ b/tools/packman/packman @@ -24,7 +24,7 @@ else PM_CURL_SILENT="-s -S" PM_WGET_QUIET="--quiet" fi -export PM_PACKMAN_VERSION=7.23 +export PM_PACKMAN_VERSION=7.23.2 # This is necessary for newer macOS if [ `uname` == 'Darwin' ]; then diff --git a/tools/packman/packmanconf.py b/tools/packman/packmanconf.py index db220d2..2a2f8a5 100644 --- a/tools/packman/packmanconf.py +++ b/tools/packman/packmanconf.py @@ -98,7 +98,7 @@ def get_module_dir(conf_dir, packages_root: str, version: str) -> str: tf = tempfile.NamedTemporaryFile(delete=False) target_name = tf.name tf.close() - url = f"http://bootstrap.packman.nvidia.com/packman-common@{version}.zip" + url = f"https://bootstrap.packman.nvidia.com/packman-common@{version}.zip" print(f"Downloading '{url}' ...") import urllib.request diff --git a/tools/repoman/launch.py b/tools/repoman/launch.py index fc175ca..034ea1b 100644 --- a/tools/repoman/launch.py +++ b/tools/repoman/launch.py @@ -4,6 +4,7 @@ import os import platform import shutil +import subprocess import sys from collections import defaultdict from glob import glob @@ -13,11 +14,10 @@ import omni.repo.man from omni.repo.kit_template.backend import read_toml from omni.repo.kit_template.frontend import CLIInput, Separator -from omni.repo.man.configuration import add_config_arg from omni.repo.man.exceptions import QuietExpectedError from omni.repo.man.fileutils import rmtree from omni.repo.man.guidelines import get_host_platform -from omni.repo.man.utils import find_and_extract_package, run_process, run_process_return_output +from omni.repo.man.utils import find_and_extract_package, process_args_to_cmd, run_process, run_process_return_output # These dependencies come from repo_kit_template from rich.console import Console @@ -53,6 +53,48 @@ def _select(query: str, apps: list) -> str: return cli_input.select(message=query, choices=apps, default=apps[0]) +def _run_process(args: List, exit_on_error=False, timeout=None, **kwargs) -> int: + """Run system process and wait for completion. + + This was copy/pasted out of omni.repo.man.utils to work around the KeyboardInterrupt error message. + + Args: + args (List): List of arguments. + exit_on_error (bool, optional): Exit if return code is non zero. + """ + returncode = 0 + message = "" + try: + logger.info(f"running process: {process_args_to_cmd(args)}") + # Optionally map in sys.stdin for local debugging via pdb.set_trace. + # Otherwise use DEVNULL, prevents weird failures on Windows. + stdin = subprocess.DEVNULL + if os.environ.get("repo_diagnostic"): + stdin = sys.stdin + + p = subprocess.run(args, stdin=stdin, stdout=sys.stdout, stderr=subprocess.STDOUT, timeout=timeout, **kwargs) + returncode = p.returncode + except subprocess.CalledProcessError as e: + returncode = e.returncode + message = e + except subprocess.TimeoutExpired as e: + returncode = -1 + message = e + except (FileNotFoundError, OSError, Exception) as e: + returncode = -1 + message = str(e) + except KeyboardInterrupt: + returncode = -1 + message = "KeyboardInterrupt" + + # Do not logger.error for a KeyboardInterrupt + if returncode != 0 and message != "KeyboardInterrupt": + logger.error(f'error running: {process_args_to_cmd(args)}, code: {returncode}, message: "{message}"') + if exit_on_error: + sys.exit(returncode) + return returncode + + def discover_kit_files(target_directory: Path) -> List: if not target_directory.is_dir(): return [] @@ -81,9 +123,14 @@ def discover_typed_kit_files(target_directory: Path) -> Dict: discovered_apps = defaultdict(list) for app in glob("**/*.kit", root_dir=target_directory, recursive=True): app_path = target_directory / app - app_data = read_toml(app_path) - app_type = app_data.get("template", {}).get("type", "ApplicationTemplate") - discovered_apps[app_type].append(app_path.name) + try: + app_data = read_toml(app_path) + app_type = app_data.get("template", {}).get("type", "ApplicationTemplate") + discovered_apps[app_type].append(app_path.name) + except Exception as e: + # For now broadly catching until repo_kit_template's read_toml can instead raise a omni.repo.man.exception + err_msg = f"Failed to read kit file: {app_path.resolve()}. There might be a duplicate toml key: {e}" + _quiet_error(err_msg) return discovered_apps @@ -186,7 +233,10 @@ def run_selected_image(image_id: str, dev_bundle: bool, extra_args: List[str], v if extra_args: docker_run_cmd += extra_args - run_process(docker_run_cmd, exit_on_error=False) + _ = _run_process( + docker_run_cmd, + exit_on_error=False, + ) def nvidia_driver_check(): @@ -381,8 +431,10 @@ def launch_kit( if extra_args: kit_cmd += extra_args - ret_code = run_process(kit_cmd, exit_on_error=False) - # TODO: do something with this ret_code + _ = _run_process( + kit_cmd, + exit_on_error=False, + ) def expand_package(package_path: str) -> Path: @@ -451,6 +503,8 @@ def add_args(parser: argparse.ArgumentParser): help="Enable the developer debugging extension bundle.", ) + +def add_package_arg(parser: argparse.ArgumentParser): parser.add_argument( "-p", PACKAGE_ARG, @@ -460,6 +514,8 @@ def add_args(parser: argparse.ArgumentParser): help="Path to a kit app package that you want to launch", ) + +def add_name_arg(parser: argparse.ArgumentParser): parser.add_argument( "-n", "--name", @@ -474,25 +530,30 @@ def setup_repo_tool(parser: argparse.ArgumentParser, config: Dict) -> Optional[C parser.description = "Simple tool to launch Kit applications" add_args(parser) + add_package_arg(parser) + add_name_arg(parser) # Currently unused # tool_config = config.get("repo_launch", {}) + # Get list of kit apps on filesystem. + app_names = discover_kit_files(KIT_APP_PATH) + + subparsers = parser.add_subparsers() + for app in app_names: + subparser = subparsers.add_parser(app) + subparser.set_defaults(app_name=app) + # Add --dev-bundle and --container args + add_args(subparser) + def run_repo_tool(options: argparse.Namespace, config_dict: Dict): app_name = None config = "release" dev_bundle = False - extra_args = [] # Providing a kit file is optional, otherwise we present a select app_name = options.app_name - - # If a kit file was selected then a config value was optionally set. - if hasattr(options, "config"): - config = options.config - - if hasattr(options, "dev_bundle"): - dev_bundle = options.dev_bundle + dev_bundle = options.dev_bundle try: # Launching from a distributed package