├── .bazelrc ├── .bazelversion ├── .github ├── ci.bazelrc ├── ci_setup.bash ├── renovate.json └── workflows │ └── main.yml ├── .gitignore ├── BUILD.bazel ├── CONTRIBUTING.md ├── LICENSE.TXT ├── MODULE.bazel ├── README.md ├── bazel ├── examples ├── BUILD.bazel ├── LICENSE.txt ├── README.md ├── ball_bin.py ├── ball_bin.yaml ├── requirements.in ├── requirements.txt └── test │ ├── ball_bin_test.py │ └── bpy_use_cycles.py ├── pyproject.toml ├── requirements.in ├── requirements.txt ├── server.py ├── test ├── 4_color_texture.png ├── BUILD.bazel ├── depth.png ├── label.png ├── one_gltf_one_blend.label.png ├── one_rgba_box.gltf ├── one_rgba_one_texture_boxes.color.png ├── one_rgba_one_texture_boxes.gltf ├── one_texture_box.blend ├── requirements.in ├── requirements.txt ├── server_test.py ├── two_rgba_boxes.color.png └── two_rgba_boxes.gltf └── tools ├── BUILD.bazel ├── black_main.py ├── buildifier_repositories.bzl ├── buildifier_version.bzl ├── defs.bzl ├── fix_lint.sh ├── isort_main.py ├── pycodestyle_main.py ├── upgrade.sh └── upgrade_helper.py /.bazelrc: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | common --symlink_prefix=.bazel/ 4 | common --keep_going=yes 5 | build --test_output=errors 6 | build --test_summary=terse 7 | 8 | # Add `bazel test --config=lint` shortcut for linting. 9 | build:lint --test_tag_filters=lint 10 | 11 | # Try to import user-specific configuration local to workspace. 12 | try-import %workspace%/user.bazelrc 13 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 8.2.1 2 | -------------------------------------------------------------------------------- /.github/ci.bazelrc: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | # This file adjusts Bazel's configuration for CI jobs. 4 | 5 | # Dump configuration details to the log. 6 | common --announce_rc=yes 7 | 8 | # Put the Bazel caches somewhere the CI runner will preserve them from one run 9 | # to the next. (All of ~/.cache/bazel_ci/ is preserved.) 10 | fetch --repository_cache /home/runner/.cache/bazel_ci/externals 11 | build --repository_cache /home/runner/.cache/bazel_ci/externals 12 | fetch --disk_cache /home/runner/.cache/bazel_ci/local_disk 13 | build --disk_cache /home/runner/.cache/bazel_ci/local_disk 14 | -------------------------------------------------------------------------------- /.github/ci_setup.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | # Rendering in CI (for both Drake and Blender) requires a GL pipeline. 6 | sudo apt-get update 7 | sudo apt-get --assume-yes install libegl1 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchCategories": [ 9 | "python" 10 | ], 11 | "enabled": false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | name: Continuous Integration 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | bazel_test: 15 | runs-on: ubuntu-latest 16 | env: 17 | # This is where bazelisk caches its downloads of Bazel. 18 | BAZELISK_HOME: /home/runner/.cache/bazel_ci/bazelisk 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install Ubuntu dependencies 22 | run: .github/ci_setup.bash 23 | # Restore the most appropriate caches. 24 | # 25 | # In the below: `github.ref` will be either "refs/pull//merge" 26 | # or "refs/heads/main"; `github.run_number` is a monotonically increasing 27 | # serial number; and `github.run_attempt` is a typically 1. 28 | # 29 | # The `key` declared here will never match (since it encodes the current 30 | # run_number) but illustrates the name we'll use to save the caches when 31 | # we're done. 32 | # 33 | # However, one of the `restore-keys` patterns *should* always match. The 34 | # effects we should see from the `restore-keys` patterns are: 35 | # 36 | # * When building the `main` branch, we'll always pull the most recently 37 | # updated main caches. Both restore keys say "pip-refs/heads/main-" and 38 | # the prior archives are named, e.g., "pip-refs/heads/main-####-#". 39 | # GitHub will use the *most recently saved* archive name that matches 40 | # the restore key prefix (no matter the lexicographic ordering of 41 | # the ####-# part). 42 | # 43 | # * Ditto for the first build of a PR; the first restore key will not 44 | # match anything, so it falls back to the main caches. 45 | # 46 | # * For subsequent builds of a PR, the first restore key, e.g., 47 | # "pip-refs/pull/###/merge-" should match the most recently saved PR key 48 | # "pip-refs/pull/###/merge-####-#". 49 | - uses: actions/cache/restore@v4 50 | with: 51 | path: ~/.cache/pip 52 | # N.B. The "-mumble" suffix below will never match; it's a placeholder 53 | # for the nonce appended to the cache key. Instead, the restore-keys 54 | # will always be used for restoring. 55 | key: pip-${{ github.ref }}-${{ github.run_number }}-${{ github.run_attempt }} 56 | restore-keys: | 57 | pip-${{ github.ref }}- 58 | pip-refs/heads/main- 59 | - uses: actions/cache/restore@v4 60 | with: 61 | path: ~/.cache/bazel_ci 62 | key: bazel_ci-${{ github.ref }}-${{ github.run_number }}-${{ github.run_attempt }} 63 | restore-keys: | 64 | bazel_ci-${{ github.ref }}- 65 | bazel_ci-refs/heads/main- 66 | - name: Report cache sizes 67 | run: | 68 | du -ms ~/.cache/pip || true 69 | du -ms ~/.cache/bazel_ci/* || true 70 | # Actual testing. 71 | - name: Bazel Test 72 | run: | 73 | ln -s .github/ci.bazelrc user.bazelrc 74 | ./bazel test //... 75 | # Save the test outputs to allow for eyeball verification when relevant. 76 | - name: Archive test outputs 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: test_outputs 80 | path: | 81 | .bazel/testlogs/examples/ball_bin_test/test.outputs/outputs.zip 82 | # Save the updated cache snapshots, so we never do the same work twice. 83 | # By default, actions/cache only saves after a successful workflow, but 84 | # our caches are bags of files where we only ever add new files (not change 85 | # an existing files), so it's always safe to snapshot. 86 | - uses: actions/cache/save@v4 87 | if: always() 88 | with: 89 | path: ~/.cache/pip 90 | key: pip-${{ github.ref }}-${{ github.run_number }}-${{ github.run_attempt }} 91 | - uses: actions/cache/save@v4 92 | if: always() 93 | with: 94 | path: ~/.cache/bazel_ci 95 | key: bazel_ci-${{ github.ref }}-${{ github.run_number }}-${{ github.run_attempt }} 96 | install_test: 97 | runs-on: ubuntu-latest 98 | strategy: 99 | matrix: 100 | python_version: ["3.10"] 101 | steps: 102 | - uses: actions/checkout@v4 103 | - uses: actions/setup-python@v5 104 | with: 105 | python-version: ${{ matrix.python_version }} 106 | cache: "pip" 107 | - run: pip install . 108 | - run: drake-blender-server --help 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | /.bazel 4 | /MODULE.bazel.lock 5 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | load("@rules_python//python:defs.bzl", "py_binary") 4 | load("@rules_python//python:pip.bzl", "compile_pip_requirements") 5 | load("//tools:defs.bzl", "bazel_lint_test", "pip", "py_lint_test") 6 | 7 | exports_files([ 8 | "pyproject.toml", 9 | "server.py", 10 | ]) 11 | 12 | py_binary( 13 | name = "server", 14 | srcs = ["server.py"], 15 | visibility = ["//visibility:public"], 16 | deps = [ 17 | pip("bpy"), 18 | pip("flask"), 19 | ], 20 | ) 21 | 22 | bazel_lint_test( 23 | name = "bazel_lint_test", 24 | srcs = [ 25 | "BUILD.bazel", 26 | "MODULE.bazel", 27 | ], 28 | ) 29 | 30 | py_lint_test( 31 | name = "py_lint_test", 32 | srcs = [ 33 | "bazel", 34 | "server.py", 35 | ], 36 | ) 37 | 38 | compile_pip_requirements( 39 | name = "requirements", 40 | requirements_in = "requirements.in", 41 | requirements_txt = "requirements.txt", 42 | tags = ["manual"], 43 | ) 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Contributing 4 | 5 | ## Terms and Conditions 6 | 7 | When you make a contribution to this project, you are agreeing to offer your 8 | contribution under the same [LICENSE.TXT](LICENSE.TXT) as the project. 9 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: BSD-2-Clause 2 | 3 | Copyright (c) 2022-2025 by the drake-blender developers. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | module(name = "drake_blender") 4 | 5 | bazel_dep(name = "platforms", version = "1.0.0") 6 | bazel_dep(name = "rules_python", version = "1.4.1") 7 | 8 | # For the most part, a given version of Blender only supports exactly one 9 | # version of Python, so we'll pin ourselves to exactly that version. 10 | PYTHON_VERSION = "3.10" 11 | 12 | python = use_extension("@rules_python//python/extensions:python.bzl", "python") 13 | python.toolchain(python_version = PYTHON_VERSION) 14 | use_repo(python, "python_versions") 15 | 16 | pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") 17 | pip.parse( 18 | hub_name = "requirements", 19 | python_version = PYTHON_VERSION, 20 | requirements_lock = "//:requirements.txt", 21 | ) 22 | pip.parse( 23 | hub_name = "test_requirements", 24 | python_version = PYTHON_VERSION, 25 | requirements_lock = "//:test/requirements.txt", 26 | ) 27 | pip.parse( 28 | hub_name = "examples_requirements", 29 | python_version = PYTHON_VERSION, 30 | requirements_lock = "//examples:requirements.txt", 31 | ) 32 | use_repo(pip, "requirements") 33 | use_repo(pip, "test_requirements") 34 | use_repo(pip, "examples_requirements") 35 | 36 | http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") 37 | 38 | # This is a sample file from https://www.blender.org/download/demo-files/, 39 | # licensed under CC0. Credit goes to Ramil Roosileht for creating it, 40 | # https://twitter.com/limarest_art. 41 | http_file( 42 | name = "color_attribute_painting", 43 | sha256 = "443b213229a4c863b2015beff623a700886c14928707a2fb24a6dd85fd80a207", 44 | urls = [ 45 | base + "/demo/sculpt_mode/color_attribute_painting.blend" 46 | for base in [ 47 | "https://mirrors.ocf.berkeley.edu/blender", 48 | "https://mirror.clarkson.edu/blender", 49 | ] 50 | ], 51 | ) 52 | 53 | buildifier_repositories = use_extension("//tools:buildifier_repositories.bzl", "buildifier_repositories") 54 | use_repo(buildifier_repositories, "buildifier-darwin-amd64") 55 | use_repo(buildifier_repositories, "buildifier-darwin-arm64") 56 | use_repo(buildifier_repositories, "buildifier-linux-amd64") 57 | use_repo(buildifier_repositories, "buildifier-linux-arm64") 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Overview 4 | 5 | `drake-blender` is an implementation of the Drake 6 | [glTF Render Client-Server API](https://drake.mit.edu/doxygen_cxx/group__render__engine__gltf__client__server__api.html) 7 | atop 8 | [Blender](https://www.blender.org/). 9 | 10 | **This is a relatively new project and may still have bugs. 11 | Please share your issues and improvements on GitHub.** 12 | 13 | ## Compatibility 14 | 15 | This software is only tested on Ubuntu 22.04 "Jammy", but should probably 16 | work with any Python interpreter that supports our `requirements.txt`. 17 | 18 | ## Running the render server 19 | 20 | There are three ways to run the server. 21 | 22 | (1) From pip: 23 | 24 | Ensure you are using a virtual environment: 25 | ```sh 26 | python3 -m venv env 27 | ``` 28 | 29 | Install into the virtual environment, either from a release tag or from the 30 | development branch: 31 | 32 | (a) Using a release tag: 33 | ```sh 34 | env/bin/pip install https://github.com/RobotLocomotion/drake-blender/archive/refs/tags/v0.2.1.zip 35 | env/bin/drake-blender-server --help 36 | ``` 37 | 38 | (b) Using the development branch: 39 | 40 | ```sh 41 | env/bin/pip install https://github.com/RobotLocomotion/drake-blender/archive/refs/heads/main.zip 42 | env/bin/drake-blender-server --help 43 | ``` 44 | 45 | (2) From a git checkout of `drake-blender`: 46 | 47 | ```sh 48 | ./bazel run :server -- --help 49 | ``` 50 | 51 | This way has no extra setup steps. It will automatically download the required 52 | dependencies into the Bazel sandbox, using the same versions as pinned by our 53 | requirements lockfile that is tested in our Continuous Integration build. 54 | 55 | (3) From your own virtual environment: 56 | 57 | The `server.py` file is self-contained -- it does not import any other files 58 | from drake-blender. Instead of using Bazel, you can also run it as a standalone 59 | Python program (`python3 server.py`) so long as the packages listed in our 60 | `requirements.in` are available in your Python runtime environment. You are 61 | responsible for preparing and activating an appropriate virtual environment on 62 | your own. 63 | 64 | ## Examples 65 | 66 | See [examples](examples/README.md). 67 | 68 | ## Testing (for developers) 69 | 70 | From a git checkout of `drake-blender`: 71 | 72 | ```sh 73 | ./bazel test //... 74 | ``` 75 | 76 | ### Linting 77 | 78 | Check for lint: 79 | 80 | ```sh 81 | ./bazel test //... --config=lint 82 | ``` 83 | 84 | Fix all lint: 85 | 86 | ```sh 87 | ./tools/fix_lint.sh 88 | ``` 89 | 90 | # Credits 91 | 92 | The Drake-Blender project was created by the 93 | [Robotics Division](https://www.tri.global/our-work/robotics/) 94 | at Toyota Research Institute. Many other people have since contributed their 95 | talents. Here's an alphabetical list (note to contributors: *do add yourself*): 96 | 97 | * Bassam ul Haq 98 | * Cody Simpson 99 | * Eric Cousineau 100 | * Jeremy Nimmer 101 | * John Shepherd 102 | * Kunimatsu Hashimoto 103 | * Matthew Woehlke 104 | * Sean Curtis 105 | * Stephen McDowell 106 | * Zach Fang 107 | 108 | # Licensing 109 | 110 | Per [LICENSE.TXT](LICENSE.TXT), this module is offered under the 111 | [BSD-2-Clause](https://spdx.org/licenses/BSD-2-Clause.html) license, but note 112 | that it loads `import bpy` from Blender so is also governed by the terms of 113 | Blender license [GPL-2.0-or-later](https://www.blender.org/about/license/). 114 | 115 | Per [examples/LICENSE.TXT](examples/LICENSE.TXT), the `examples` code is 116 | offered under the [MIT-0](https://spdx.org/licenses/MIT-0.html) license. 117 | -------------------------------------------------------------------------------- /bazel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | 4 | """If you don't have Bazel installed on your $PATH, you can use this program 5 | to run a copy of Bazel that's automatically downloaded from the internet, e.g.: 6 | 7 | ./bazel test //... 8 | 9 | If you have Bazel or Bazelisk already on your $PATH, you can run that instead, 10 | e.g.: 11 | 12 | bazel test //... 13 | """ 14 | 15 | import hashlib 16 | import os 17 | from pathlib import Path 18 | import sys 19 | import urllib.request as request 20 | 21 | # The pinned version and checksum of bazelisk. 22 | _VERSION = "1.25.0" 23 | _SHA256 = "93eeb74b88b3e24f56b882e8a2c7497bd948375cab876cc4a21138d4c829b706" 24 | _URL = f"https://raw.githubusercontent.com/bazelbuild/bazelisk/v{_VERSION}/bazelisk.py" # noqa 25 | 26 | 27 | def _fetch_bazelisk(): 28 | # Create a home for our downloaded bazelisk. 29 | dot_bazel = Path(__file__).resolve().with_name(".bazel") 30 | dot_bazel.mkdir(exist_ok=True) 31 | bazelisk = dot_bazel / "bazelisk" 32 | 33 | # Check if we've already fetched it. 34 | try: 35 | hasher = hashlib.sha256() 36 | with open(bazelisk, "rb") as f: 37 | hasher.update(f.read()) 38 | if hasher.hexdigest() == _SHA256: 39 | return bazelisk 40 | except IOError: 41 | pass 42 | 43 | # Fetch it. 44 | print(f"Downloading {_URL} ...") 45 | with request.urlopen(url=_URL, timeout=10) as response: 46 | content = response.read() 47 | hasher = hashlib.sha256() 48 | hasher.update(content) 49 | digest = hasher.hexdigest() 50 | if digest != _SHA256: 51 | raise RuntimeError(f"Got wrong sha256 {digest} but wanted {_SHA256}.") 52 | with open(bazelisk, "wb") as f: 53 | f.write(content) 54 | bazelisk.chmod(0o755) 55 | 56 | return bazelisk 57 | 58 | 59 | def _main(): 60 | bazelisk = _fetch_bazelisk() 61 | os.execv(bazelisk, [str(bazelisk)] + sys.argv[1:]) 62 | 63 | 64 | if __name__ == "__main__": 65 | _main() 66 | -------------------------------------------------------------------------------- /examples/BUILD.bazel: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT-0 2 | 3 | load("@rules_python//python:defs.bzl", "py_binary", "py_test") 4 | load("@rules_python//python:pip.bzl", "compile_pip_requirements") 5 | load("//tools:defs.bzl", "bazel_lint_test", "pip", "py_lint_test") 6 | 7 | exports_files(glob(["**"])) 8 | 9 | py_binary( 10 | name = "ball_bin", 11 | srcs = ["ball_bin.py"], 12 | data = [ 13 | ":ball_bin.yaml", 14 | "//:server", 15 | "@color_attribute_painting//file", 16 | ], 17 | deps = [ 18 | "@rules_python//python/runfiles", 19 | pip("drake", "[examples]"), 20 | pip("opencv_python", "[examples]"), 21 | pip("tqdm", "[examples]"), 22 | ], 23 | ) 24 | 25 | py_test( 26 | name = "ball_bin_test", 27 | srcs = ["test/ball_bin_test.py"], 28 | data = [ 29 | ":ball_bin", 30 | ":test/bpy_use_cycles.py", 31 | ], 32 | deps = [pip("drake", "[examples]")], 33 | ) 34 | 35 | bazel_lint_test( 36 | name = "bazel_lint_test", 37 | srcs = [ 38 | "BUILD.bazel", 39 | ], 40 | ) 41 | 42 | py_lint_test( 43 | name = "py_lint_test", 44 | srcs = [ 45 | "ball_bin.py", 46 | "test/ball_bin_test.py", 47 | ], 48 | ) 49 | 50 | compile_pip_requirements( 51 | name = "requirements", 52 | requirements_in = "requirements.in", 53 | requirements_txt = "requirements.txt", 54 | tags = ["manual"], 55 | ) 56 | -------------------------------------------------------------------------------- /examples/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 5 | the Software, and to permit persons to whom the Software is furnished to do so. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 8 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 9 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 10 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 11 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 12 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Overview 4 | 5 | This directory contains an example of how to use drake-blender in your own code. 6 | 7 | TODO(jwnimmer-tri) Add more examples! 8 | 9 | ## Ball Bin example 10 | 11 | This shows how to use the Drake Simulator to create videos of a dynamic scene, 12 | where moving objects (some balls) and a fixed object (a bin) are simulated by 13 | Drake and the static visual background (a room with custom lighting) is provided 14 | by a Blender file. 15 | 16 | https://github.com/RobotLocomotion/drake-blender/assets/17596505/c0f5f6ae-db51-42cb-9a86-09fa1c9ae18e 17 | 18 | From the root directory of the drake-blender source checkout, run: 19 | 20 | ```sh 21 | $ ./bazel run //examples:ball_bin 22 | ``` 23 | 24 | This will create videos named blender_camera.mp4 and vtk_camera.mp4. 25 | The blender-rendered video will show the balls, bin, and room in the background. 26 | The VTK-rendered video will show only the balls and bin. 27 | Expect the video rendering to take 5 minutes or longer. 28 | 29 | To create a single photo instead of a movie, use `--still`: 30 | 31 | ```sh 32 | $ ./bazel run //examples:ball_bin -- --still 33 | ``` 34 | 35 | Thanks to Ramil Roosileht (https://twitter.com/limarest_art) for creating 36 | the blender scene we use (from https://www.blender.org/download/demo-files/). 37 | -------------------------------------------------------------------------------- /examples/ball_bin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT-0 2 | 3 | """ 4 | Demonstrates combining Drake with the Blender render server to create a 5 | simulation video (or still image). 6 | 7 | In this demo, moving objects (some balls) and a fixed object (a bin) are 8 | simulated by Drake and the static visual background (a room with custom 9 | lighting) is provided by a Blender file. 10 | """ 11 | 12 | import argparse 13 | import dataclasses as dc 14 | import logging 15 | import os 16 | from pathlib import Path 17 | import signal 18 | import socket 19 | import subprocess 20 | import tempfile 21 | import time 22 | import typing 23 | 24 | from pydrake.common import configure_logging 25 | from pydrake.common.yaml import yaml_load_typed 26 | from pydrake.multibody.parsing import ( 27 | ModelDirective, 28 | ModelDirectives, 29 | ProcessModelDirectives, 30 | ) 31 | from pydrake.multibody.plant import AddMultibodyPlant, MultibodyPlantConfig 32 | from pydrake.systems.analysis import ( 33 | ApplySimulatorConfig, 34 | Simulator, 35 | SimulatorConfig, 36 | ) 37 | from pydrake.systems.framework import DiagramBuilder 38 | from pydrake.systems.lcm import LcmBuses 39 | from pydrake.systems.sensors import ( 40 | ApplyCameraConfig, 41 | CameraConfig, 42 | ImageWriter, 43 | PixelType, 44 | ) 45 | from pydrake.visualization import VideoWriter 46 | from python import runfiles 47 | import tqdm 48 | 49 | 50 | @dc.dataclass 51 | class Scenario: 52 | """Defines the YAML format for a scenario to be simulated.""" 53 | 54 | # The maximum simulation time (in seconds). 55 | simulation_duration: float = 1.0 56 | 57 | # Simulator configuration (integrator and publisher parameters). 58 | simulator_config: SimulatorConfig = SimulatorConfig() 59 | 60 | # All of the fully deterministic elements of the simulation. 61 | directives: typing.List[ModelDirective] = dc.field(default_factory=list) 62 | 63 | # Cameras to add to the scene. 64 | cameras: typing.Mapping[str, CameraConfig] = dc.field(default_factory=dict) 65 | 66 | 67 | def _find_resource(bazel_path): 68 | """Looks up the path to "runfiles" data, as organized by Bazel.""" 69 | manifest = runfiles.Create() 70 | location = manifest.Rlocation(bazel_path) 71 | assert location is not None, f"Not a resource: {bazel_path}" 72 | result = Path(location) 73 | assert result.exists(), f"Missing resource: {bazel_path}" 74 | return result 75 | 76 | 77 | class _ProgressBar: 78 | def __init__(self, simulation_duration): 79 | self._tqdm = tqdm.tqdm(total=simulation_duration) 80 | self._current_time = 0.0 81 | 82 | def __call__(self, context): 83 | old_time = self._current_time 84 | self._current_time = context.get_time() 85 | self._tqdm.update(self._current_time - old_time) 86 | 87 | 88 | def _run(args): 89 | """Runs the demo.""" 90 | scenario = yaml_load_typed( 91 | schema=Scenario, filename=args.scenario_file, defaults=Scenario() 92 | ) 93 | 94 | # Create the scene. 95 | builder = DiagramBuilder() 96 | plant, scene_graph = AddMultibodyPlant( 97 | config=MultibodyPlantConfig(), builder=builder 98 | ) 99 | added_models = ProcessModelDirectives( 100 | directives=ModelDirectives(directives=scenario.directives), plant=plant 101 | ) 102 | plant.Finalize() 103 | 104 | # Add the camera(s). 105 | video_writers = [] 106 | for _, camera in scenario.cameras.items(): 107 | if args.still: 108 | camera.show_rgb = False 109 | name = camera.name 110 | ApplyCameraConfig(config=camera, builder=builder) 111 | sensor = builder.GetSubsystemByName(f"rgbd_sensor_{name}") 112 | if args.still: 113 | writer = builder.AddSystem(ImageWriter()) 114 | writer.DeclareImageInputPort( 115 | pixel_type=PixelType.kRgba8U, 116 | port_name="color_image", 117 | file_name_format=f"./{name}", 118 | publish_period=1.0, 119 | start_time=0.0, 120 | ) 121 | builder.Connect( 122 | sensor.GetOutputPort("color_image"), 123 | writer.GetInputPort("color_image"), 124 | ) 125 | else: 126 | writer = VideoWriter(filename=f"{name}.mp4", fps=16, backend="cv2") 127 | builder.AddSystem(writer) 128 | writer.ConnectRgbdSensor(builder=builder, sensor=sensor) 129 | video_writers.append(writer) 130 | 131 | # Create the simulator. 132 | simulator = Simulator(builder.Build()) 133 | ApplySimulatorConfig(scenario.simulator_config, simulator) 134 | 135 | # Simulate. 136 | if args.still: 137 | logging.info("Creating still image(s)") 138 | simulator.AdvanceTo(1e-3) 139 | else: 140 | logging.info("Creating video(s)") 141 | simulator.set_monitor(_ProgressBar(scenario.simulation_duration)) 142 | simulator.AdvanceTo(scenario.simulation_duration) 143 | for writer in video_writers: 144 | writer.Save() 145 | 146 | 147 | def main(): 148 | parser = argparse.ArgumentParser(description=__doc__) 149 | parser.add_argument( 150 | "--still", 151 | action="store_true", 152 | help="Don't create a video; instead, capture a single photograph of " 153 | "the initial conditions.", 154 | ) 155 | parser.add_argument( 156 | "--scenario_file", 157 | type=Path, 158 | default=None, 159 | help="The absolute path to a scenario file to construct a simulation. " 160 | "If not provided, `drake_blender/examples/ball_bin.yaml` will be used " 161 | "by default.", 162 | ) 163 | parser.add_argument( 164 | "--no-server", 165 | dest="server", 166 | action="store_false", 167 | help="Don't automatically launch the blender server.", 168 | ) 169 | parser.add_argument( 170 | "--bpy_settings_file", 171 | metavar="FILE", 172 | help="This flag is forward along to the server, unchanged. " 173 | "Refer to its documentation for details.", 174 | ) 175 | args = parser.parse_args() 176 | 177 | if args.scenario_file is None: 178 | scenario_file = _find_resource("drake_blender/examples/ball_bin.yaml") 179 | setattr(args, "scenario_file", scenario_file) 180 | 181 | # Launch the server (if requested). 182 | if args.server: 183 | logging.info("Starting drake-blender server") 184 | server = _find_resource("drake_blender/server") 185 | blend_file = _find_resource("color_attribute_painting/file/downloaded") 186 | log_file = open(os.environ["TMPDIR"] + "/server-log.txt", "w") 187 | # TODO(jwnimmer-tri) Echo the log file to the console. 188 | command = [ 189 | server, 190 | f"--blend_file={blend_file}", 191 | ] 192 | if args.bpy_settings_file: 193 | command.append(f"--bpy_settings_file={args.bpy_settings_file}") 194 | server_process = subprocess.Popen( 195 | command, stdout=log_file, stderr=subprocess.STDOUT 196 | ) 197 | # Wait until the server is ready. 198 | while True: 199 | with socket.socket() as s: 200 | try: 201 | s.connect(("127.0.0.1", 8000)) 202 | # Success! 203 | break 204 | except ConnectionRefusedError as e: 205 | time.sleep(0.1) 206 | assert server_process.poll() is None 207 | logging.info("The drake-blender server is ready") 208 | else: 209 | server_process = None 210 | 211 | # Run the demo. 212 | try: 213 | _run(args) 214 | finally: 215 | if server_process is not None: 216 | server_process.send_signal(signal.SIGINT) 217 | try: 218 | server_process.wait(1.0) 219 | except subprocess.TimeoutExpired: 220 | server_process.terminate() 221 | log_file.flush() 222 | 223 | 224 | def _wrapped_main(): 225 | # Do our best to clean up after ourselves, by advising Drake code to use 226 | # a directory other than /tmp. 227 | with tempfile.TemporaryDirectory(prefix="ball_bin_") as temp_dir: 228 | os.environ["TMPDIR"] = temp_dir 229 | main() 230 | 231 | 232 | if __name__ == "__main__": 233 | # Create output files in $PWD, not runfiles. 234 | if "BUILD_WORKING_DIRECTORY" in os.environ: 235 | os.chdir(os.environ["BUILD_WORKING_DIRECTORY"]) 236 | 237 | configure_logging() 238 | _wrapped_main() 239 | -------------------------------------------------------------------------------- /examples/ball_bin.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT-0 2 | simulation_duration: 2.0 3 | directives: 4 | - add_model: 5 | name: bin 6 | file: package://drake_models/manipulation_station/bin.sdf 7 | - add_weld: 8 | parent: world 9 | child: bin::bin_base 10 | X_PC: 11 | translation: [0, 0, 0.20] 12 | - add_model: 13 | name: ball_1 14 | file: package://drake_models/manipulation_station/sphere.sdf 15 | default_free_body_pose: { base_link: { translation: [0.08, 0.01, 0.3] } } 16 | - add_model: 17 | name: ball_2 18 | file: package://drake_models/manipulation_station/sphere.sdf 19 | default_free_body_pose: { base_link: { translation: [0.07, 0.02, 0.6] } } 20 | - add_model: 21 | name: ball_3 22 | file: package://drake_models/manipulation_station/sphere.sdf 23 | default_free_body_pose: { base_link: { translation: [0.06, 0.03, 0.9] } } 24 | - add_model: 25 | name: ball_4 26 | file: package://drake_models/manipulation_station/sphere.sdf 27 | default_free_body_pose: { base_link: { translation: [0.05, 0.04, 1.2] } } 28 | - add_model: 29 | name: ball_5 30 | file: package://drake_models/manipulation_station/sphere.sdf 31 | default_free_body_pose: { base_link: { translation: [0.04, 0.05, 1.5] } } 32 | - add_model: 33 | name: ball_6 34 | file: package://drake_models/manipulation_station/sphere.sdf 35 | default_free_body_pose: { base_link: { translation: [0.03, 0.06, 1.8] } } 36 | - add_model: 37 | name: ball_7 38 | file: package://drake_models/manipulation_station/sphere.sdf 39 | default_free_body_pose: { base_link: { translation: [0.02, 0.07, 2.1] } } 40 | - add_model: 41 | name: ball_8 42 | file: package://drake_models/manipulation_station/sphere.sdf 43 | default_free_body_pose: { base_link: { translation: [0.01, 0.08, 2.4] } } 44 | - add_model: 45 | name: ball_9 46 | file: package://drake_models/manipulation_station/sphere.sdf 47 | default_free_body_pose: { base_link: { translation: [0.00, 0.09, 2.7] } } 48 | cameras: 49 | blender_camera: 50 | name: blender_camera 51 | renderer_name: blender 52 | renderer_class: !RenderEngineGltfClientParams 53 | base_url: http://127.0.0.1:8000 54 | width: 1024 55 | height: 1024 56 | fps: 8.0 57 | X_PB: 58 | translation: [-4.0, -3.2, 4.8] 59 | rotation: !Rpy { deg: [-130, 5, -55] } 60 | vtk_camera: 61 | name: vtk_camera 62 | renderer_name: vtk 63 | renderer_class: !RenderEngineVtkParams 64 | backend: EGL 65 | # For `show_rgb: True` you must also set the `backend: GLX` on prior line 66 | # and be running locally with an Xorg display server available. 67 | show_rgb: False 68 | width: 1024 69 | height: 1024 70 | fps: 8.0 71 | X_PB: 72 | translation: [-4.0, -3.2, 4.8] 73 | rotation: !Rpy { deg: [-130, 5, -55] } 74 | -------------------------------------------------------------------------------- /examples/requirements.in: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT-0 2 | 3 | # To compile this file into requirements.txt, run: 4 | # ../tools/upgrade.sh 5 | 6 | drake 7 | 8 | # Drake's VideoWriter writes out the MP4 video using cv2. 9 | opencv-python 10 | 11 | # The example uses this to provide a progress bar. 12 | tqdm 13 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # bazel run //examples:requirements.update 6 | # 7 | contourpy==1.3.1 \ 8 | --hash=sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1 \ 9 | --hash=sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda \ 10 | --hash=sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d \ 11 | --hash=sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509 \ 12 | --hash=sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6 \ 13 | --hash=sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f \ 14 | --hash=sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e \ 15 | --hash=sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751 \ 16 | --hash=sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86 \ 17 | --hash=sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b \ 18 | --hash=sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc \ 19 | --hash=sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546 \ 20 | --hash=sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec \ 21 | --hash=sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f \ 22 | --hash=sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82 \ 23 | --hash=sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c \ 24 | --hash=sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b \ 25 | --hash=sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c \ 26 | --hash=sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c \ 27 | --hash=sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53 \ 28 | --hash=sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80 \ 29 | --hash=sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242 \ 30 | --hash=sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85 \ 31 | --hash=sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124 \ 32 | --hash=sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5 \ 33 | --hash=sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2 \ 34 | --hash=sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3 \ 35 | --hash=sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d \ 36 | --hash=sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc \ 37 | --hash=sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342 \ 38 | --hash=sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1 \ 39 | --hash=sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1 \ 40 | --hash=sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595 \ 41 | --hash=sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30 \ 42 | --hash=sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab \ 43 | --hash=sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3 \ 44 | --hash=sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2 \ 45 | --hash=sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd \ 46 | --hash=sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7 \ 47 | --hash=sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277 \ 48 | --hash=sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453 \ 49 | --hash=sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697 \ 50 | --hash=sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b \ 51 | --hash=sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454 \ 52 | --hash=sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9 \ 53 | --hash=sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1 \ 54 | --hash=sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6 \ 55 | --hash=sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291 \ 56 | --hash=sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750 \ 57 | --hash=sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699 \ 58 | --hash=sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e \ 59 | --hash=sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81 \ 60 | --hash=sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9 \ 61 | --hash=sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375 62 | # via matplotlib 63 | cycler==0.12.1 \ 64 | --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ 65 | --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c 66 | # via matplotlib 67 | drake==1.36.0 \ 68 | --hash=sha256:467195c112f5dd54bd4178a262ff84a5e446a7903cd0d3cbd63726cf6312d6ad \ 69 | --hash=sha256:a161ef1ed6727cb2816817d1d8d1cfdfb013821d7e09a0de269391f5b85f86df \ 70 | --hash=sha256:a516dad8623a82d232de5eb97042cf86fea141558f56c866f1e1e54414e0e277 \ 71 | --hash=sha256:b889b8963e96c4c90c360e857d7dbe5b08088de6183a3079fd0cc308c860ca08 \ 72 | --hash=sha256:dc7795f66f9265e283c995432586213034c16b608fa84e5ff41544f1c29f2fc0 73 | # via -r examples/requirements.in 74 | fonttools==4.55.3 \ 75 | --hash=sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7 \ 76 | --hash=sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b \ 77 | --hash=sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261 \ 78 | --hash=sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0 \ 79 | --hash=sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02 \ 80 | --hash=sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841 \ 81 | --hash=sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45 \ 82 | --hash=sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4 \ 83 | --hash=sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b \ 84 | --hash=sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a \ 85 | --hash=sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048 \ 86 | --hash=sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90 \ 87 | --hash=sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd \ 88 | --hash=sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674 \ 89 | --hash=sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72 \ 90 | --hash=sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c \ 91 | --hash=sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07 \ 92 | --hash=sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b \ 93 | --hash=sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de \ 94 | --hash=sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926 \ 95 | --hash=sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e \ 96 | --hash=sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628 \ 97 | --hash=sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca \ 98 | --hash=sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29 \ 99 | --hash=sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa \ 100 | --hash=sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe \ 101 | --hash=sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427 \ 102 | --hash=sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d \ 103 | --hash=sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765 \ 104 | --hash=sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5 \ 105 | --hash=sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d \ 106 | --hash=sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314 \ 107 | --hash=sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b \ 108 | --hash=sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af \ 109 | --hash=sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831 \ 110 | --hash=sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3 \ 111 | --hash=sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56 \ 112 | --hash=sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e \ 113 | --hash=sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276 \ 114 | --hash=sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0 \ 115 | --hash=sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851 \ 116 | --hash=sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5 \ 117 | --hash=sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54 \ 118 | --hash=sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b \ 119 | --hash=sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f \ 120 | --hash=sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4 \ 121 | --hash=sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977 \ 122 | --hash=sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f \ 123 | --hash=sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35 \ 124 | --hash=sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32 125 | # via matplotlib 126 | kiwisolver==1.4.7 \ 127 | --hash=sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a \ 128 | --hash=sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95 \ 129 | --hash=sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5 \ 130 | --hash=sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0 \ 131 | --hash=sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d \ 132 | --hash=sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18 \ 133 | --hash=sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b \ 134 | --hash=sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258 \ 135 | --hash=sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95 \ 136 | --hash=sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e \ 137 | --hash=sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383 \ 138 | --hash=sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02 \ 139 | --hash=sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b \ 140 | --hash=sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523 \ 141 | --hash=sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee \ 142 | --hash=sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88 \ 143 | --hash=sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd \ 144 | --hash=sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb \ 145 | --hash=sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4 \ 146 | --hash=sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e \ 147 | --hash=sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c \ 148 | --hash=sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935 \ 149 | --hash=sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee \ 150 | --hash=sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e \ 151 | --hash=sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038 \ 152 | --hash=sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d \ 153 | --hash=sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b \ 154 | --hash=sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5 \ 155 | --hash=sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107 \ 156 | --hash=sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f \ 157 | --hash=sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2 \ 158 | --hash=sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17 \ 159 | --hash=sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb \ 160 | --hash=sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674 \ 161 | --hash=sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706 \ 162 | --hash=sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327 \ 163 | --hash=sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3 \ 164 | --hash=sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a \ 165 | --hash=sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2 \ 166 | --hash=sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f \ 167 | --hash=sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948 \ 168 | --hash=sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3 \ 169 | --hash=sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e \ 170 | --hash=sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545 \ 171 | --hash=sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc \ 172 | --hash=sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f \ 173 | --hash=sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650 \ 174 | --hash=sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a \ 175 | --hash=sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8 \ 176 | --hash=sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750 \ 177 | --hash=sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b \ 178 | --hash=sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34 \ 179 | --hash=sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225 \ 180 | --hash=sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51 \ 181 | --hash=sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c \ 182 | --hash=sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3 \ 183 | --hash=sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde \ 184 | --hash=sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599 \ 185 | --hash=sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c \ 186 | --hash=sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76 \ 187 | --hash=sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6 \ 188 | --hash=sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39 \ 189 | --hash=sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9 \ 190 | --hash=sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933 \ 191 | --hash=sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad \ 192 | --hash=sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520 \ 193 | --hash=sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1 \ 194 | --hash=sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503 \ 195 | --hash=sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b \ 196 | --hash=sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36 \ 197 | --hash=sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a \ 198 | --hash=sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643 \ 199 | --hash=sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60 \ 200 | --hash=sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483 \ 201 | --hash=sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf \ 202 | --hash=sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d \ 203 | --hash=sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6 \ 204 | --hash=sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644 \ 205 | --hash=sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2 \ 206 | --hash=sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9 \ 207 | --hash=sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2 \ 208 | --hash=sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640 \ 209 | --hash=sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade \ 210 | --hash=sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a \ 211 | --hash=sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c \ 212 | --hash=sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6 \ 213 | --hash=sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00 \ 214 | --hash=sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27 \ 215 | --hash=sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2 \ 216 | --hash=sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4 \ 217 | --hash=sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379 \ 218 | --hash=sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54 \ 219 | --hash=sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09 \ 220 | --hash=sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a \ 221 | --hash=sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c \ 222 | --hash=sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89 \ 223 | --hash=sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407 \ 224 | --hash=sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904 \ 225 | --hash=sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376 \ 226 | --hash=sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583 \ 227 | --hash=sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278 \ 228 | --hash=sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a \ 229 | --hash=sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d \ 230 | --hash=sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935 \ 231 | --hash=sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb \ 232 | --hash=sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895 \ 233 | --hash=sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b \ 234 | --hash=sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417 \ 235 | --hash=sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608 \ 236 | --hash=sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07 \ 237 | --hash=sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05 \ 238 | --hash=sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a \ 239 | --hash=sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d \ 240 | --hash=sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052 241 | # via matplotlib 242 | matplotlib==3.9.4 \ 243 | --hash=sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df \ 244 | --hash=sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a \ 245 | --hash=sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca \ 246 | --hash=sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00 \ 247 | --hash=sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50 \ 248 | --hash=sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3 \ 249 | --hash=sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3 \ 250 | --hash=sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64 \ 251 | --hash=sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249 \ 252 | --hash=sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c \ 253 | --hash=sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099 \ 254 | --hash=sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0 \ 255 | --hash=sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6 \ 256 | --hash=sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c \ 257 | --hash=sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45 \ 258 | --hash=sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c \ 259 | --hash=sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d \ 260 | --hash=sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483 \ 261 | --hash=sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965 \ 262 | --hash=sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c \ 263 | --hash=sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e \ 264 | --hash=sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799 \ 265 | --hash=sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041 \ 266 | --hash=sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5 \ 267 | --hash=sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb \ 268 | --hash=sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e \ 269 | --hash=sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b \ 270 | --hash=sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423 \ 271 | --hash=sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865 \ 272 | --hash=sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb \ 273 | --hash=sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db \ 274 | --hash=sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50 \ 275 | --hash=sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c \ 276 | --hash=sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7 \ 277 | --hash=sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff \ 278 | --hash=sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764 \ 279 | --hash=sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26 \ 280 | --hash=sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70 \ 281 | --hash=sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac \ 282 | --hash=sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858 \ 283 | --hash=sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f 284 | # via drake 285 | numpy==2.2.0 \ 286 | --hash=sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608 \ 287 | --hash=sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef \ 288 | --hash=sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90 \ 289 | --hash=sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae \ 290 | --hash=sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83 \ 291 | --hash=sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0 \ 292 | --hash=sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73 \ 293 | --hash=sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671 \ 294 | --hash=sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69 \ 295 | --hash=sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa \ 296 | --hash=sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066 \ 297 | --hash=sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da \ 298 | --hash=sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9 \ 299 | --hash=sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e \ 300 | --hash=sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3 \ 301 | --hash=sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a \ 302 | --hash=sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74 \ 303 | --hash=sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3 \ 304 | --hash=sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410 \ 305 | --hash=sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72 \ 306 | --hash=sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d \ 307 | --hash=sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4 \ 308 | --hash=sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038 \ 309 | --hash=sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e \ 310 | --hash=sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13 \ 311 | --hash=sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d \ 312 | --hash=sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95 \ 313 | --hash=sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31 \ 314 | --hash=sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3 \ 315 | --hash=sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03 \ 316 | --hash=sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6 \ 317 | --hash=sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2 \ 318 | --hash=sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b \ 319 | --hash=sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7 \ 320 | --hash=sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab \ 321 | --hash=sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219 \ 322 | --hash=sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571 \ 323 | --hash=sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d \ 324 | --hash=sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1 \ 325 | --hash=sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca \ 326 | --hash=sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661 \ 327 | --hash=sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e \ 328 | --hash=sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e \ 329 | --hash=sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e \ 330 | --hash=sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a \ 331 | --hash=sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3 \ 332 | --hash=sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881 \ 333 | --hash=sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221 \ 334 | --hash=sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742 \ 335 | --hash=sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773 \ 336 | --hash=sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e \ 337 | --hash=sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529 \ 338 | --hash=sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67 \ 339 | --hash=sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c \ 340 | --hash=sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367 341 | # via 342 | # contourpy 343 | # drake 344 | # matplotlib 345 | # opencv-python 346 | opencv-python==4.10.0.84 \ 347 | --hash=sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6 \ 348 | --hash=sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236 \ 349 | --hash=sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe \ 350 | --hash=sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98 \ 351 | --hash=sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526 \ 352 | --hash=sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f \ 353 | --hash=sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251 354 | # via -r examples/requirements.in 355 | packaging==24.2 \ 356 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ 357 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f 358 | # via matplotlib 359 | pillow==11.0.0 \ 360 | --hash=sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7 \ 361 | --hash=sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5 \ 362 | --hash=sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903 \ 363 | --hash=sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2 \ 364 | --hash=sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38 \ 365 | --hash=sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2 \ 366 | --hash=sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9 \ 367 | --hash=sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f \ 368 | --hash=sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc \ 369 | --hash=sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8 \ 370 | --hash=sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d \ 371 | --hash=sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2 \ 372 | --hash=sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316 \ 373 | --hash=sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a \ 374 | --hash=sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25 \ 375 | --hash=sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd \ 376 | --hash=sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba \ 377 | --hash=sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc \ 378 | --hash=sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273 \ 379 | --hash=sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa \ 380 | --hash=sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a \ 381 | --hash=sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b \ 382 | --hash=sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a \ 383 | --hash=sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae \ 384 | --hash=sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291 \ 385 | --hash=sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97 \ 386 | --hash=sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06 \ 387 | --hash=sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904 \ 388 | --hash=sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b \ 389 | --hash=sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b \ 390 | --hash=sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8 \ 391 | --hash=sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527 \ 392 | --hash=sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947 \ 393 | --hash=sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb \ 394 | --hash=sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003 \ 395 | --hash=sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5 \ 396 | --hash=sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f \ 397 | --hash=sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739 \ 398 | --hash=sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944 \ 399 | --hash=sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830 \ 400 | --hash=sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f \ 401 | --hash=sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3 \ 402 | --hash=sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4 \ 403 | --hash=sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84 \ 404 | --hash=sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7 \ 405 | --hash=sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6 \ 406 | --hash=sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6 \ 407 | --hash=sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9 \ 408 | --hash=sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de \ 409 | --hash=sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4 \ 410 | --hash=sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47 \ 411 | --hash=sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd \ 412 | --hash=sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50 \ 413 | --hash=sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c \ 414 | --hash=sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086 \ 415 | --hash=sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba \ 416 | --hash=sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306 \ 417 | --hash=sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699 \ 418 | --hash=sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e \ 419 | --hash=sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488 \ 420 | --hash=sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa \ 421 | --hash=sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2 \ 422 | --hash=sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3 \ 423 | --hash=sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9 \ 424 | --hash=sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923 \ 425 | --hash=sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2 \ 426 | --hash=sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790 \ 427 | --hash=sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734 \ 428 | --hash=sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916 \ 429 | --hash=sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1 \ 430 | --hash=sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f \ 431 | --hash=sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798 \ 432 | --hash=sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb \ 433 | --hash=sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2 \ 434 | --hash=sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9 435 | # via matplotlib 436 | pydot==3.0.3 \ 437 | --hash=sha256:5e009d97b2fff92b7a88f09ec1fd5b163f07f3b10469c927d362471d6faa0d50 \ 438 | --hash=sha256:9b0b3081e0bd362d0c61148da10eb1281ec80089b02a28cf06f9093843986f3d 439 | # via drake 440 | pyparsing==3.2.0 \ 441 | --hash=sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84 \ 442 | --hash=sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c 443 | # via 444 | # matplotlib 445 | # pydot 446 | python-dateutil==2.9.0.post0 \ 447 | --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ 448 | --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 449 | # via matplotlib 450 | pyyaml==6.0.2 \ 451 | --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ 452 | --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ 453 | --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ 454 | --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ 455 | --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ 456 | --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ 457 | --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ 458 | --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ 459 | --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ 460 | --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ 461 | --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ 462 | --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ 463 | --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ 464 | --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ 465 | --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ 466 | --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ 467 | --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ 468 | --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ 469 | --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ 470 | --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ 471 | --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ 472 | --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ 473 | --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ 474 | --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ 475 | --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ 476 | --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ 477 | --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ 478 | --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ 479 | --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ 480 | --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ 481 | --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ 482 | --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ 483 | --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ 484 | --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ 485 | --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ 486 | --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ 487 | --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ 488 | --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ 489 | --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ 490 | --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ 491 | --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ 492 | --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ 493 | --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ 494 | --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ 495 | --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ 496 | --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ 497 | --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ 498 | --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ 499 | --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ 500 | --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ 501 | --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ 502 | --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ 503 | --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 504 | # via drake 505 | six==1.17.0 \ 506 | --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ 507 | --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 508 | # via python-dateutil 509 | tqdm==4.67.1 \ 510 | --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ 511 | --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 512 | # via -r examples/requirements.in 513 | -------------------------------------------------------------------------------- /examples/test/ball_bin_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT-0 2 | 3 | import os 4 | from pathlib import Path 5 | import subprocess 6 | import unittest 7 | 8 | from pydrake.common.yaml import yaml_dump, yaml_load_file 9 | 10 | 11 | class BallBinTest(unittest.TestCase): 12 | def setUp(self): 13 | self.tmpdir = Path(os.environ["TEST_TMPDIR"]) 14 | self.assertTrue(self.tmpdir.exists(), self.tmpdir) 15 | 16 | self.out_dir = Path(os.environ["TEST_UNDECLARED_OUTPUTS_DIR"]) 17 | self.assertTrue(self.out_dir.exists(), self.out_dir) 18 | 19 | # Find the demo. 20 | demo_path = Path("examples/ball_bin").absolute().resolve() 21 | self.assertTrue(demo_path.exists(), demo_path) 22 | 23 | settings = Path("examples/test/bpy_use_cycles.py").absolute().resolve() 24 | self.assertTrue(settings.exists(), settings) 25 | 26 | # The default args to launch the subprocess. 27 | self.default_run_args = [ 28 | demo_path, 29 | f"--bpy_settings_file={settings}", 30 | ] 31 | 32 | def _get_scenario_file(self, *, nerf_scenario: bool): 33 | """Returns the path to a scenario file based on whether nerfing the 34 | original scenario, i.e., exmaple/ball_bin.yaml, is desired. If 35 | `nerf_scenario=True`, some rendering configs will be swapped to shorten 36 | the rendering time for testing while keeping the scene the same. 37 | """ 38 | raw_scenario_file = Path("examples/ball_bin.yaml").absolute().resolve() 39 | self.assertTrue(raw_scenario_file.exists(), raw_scenario_file) 40 | 41 | if not nerf_scenario: 42 | return raw_scenario_file 43 | 44 | scenario = yaml_load_file(raw_scenario_file) 45 | scenario = self._nerf_scenario(scenario) 46 | 47 | nerfed_scenario_file = self.tmpdir / "ball_bin_nerfed.yaml" 48 | yaml_dump(scenario, filename=nerfed_scenario_file) 49 | return nerfed_scenario_file 50 | 51 | def _nerf_scenario(self, scenario): 52 | """Shrinks the simulation time and the image size. The modifications 53 | ensure that the generated videos will have more than one frame. 54 | """ 55 | assert "simulation_duration" in scenario 56 | scenario["simulation_duration"] = 0.3 57 | 58 | assert "cameras" in scenario 59 | for camera in ["blender_camera", "vtk_camera"]: 60 | assert camera in scenario["cameras"] 61 | assert "width" in scenario["cameras"][camera] 62 | assert "height" in scenario["cameras"][camera] 63 | scenario["cameras"][camera]["width"] = 100 64 | scenario["cameras"][camera]["height"] = 100 65 | return scenario 66 | 67 | def test_still_images(self): 68 | """Checks that the example creates 2x still image files.""" 69 | scenario_file = self._get_scenario_file(nerf_scenario=False) 70 | 71 | # Add the arg, `--still`, to switch to image creation. 72 | run_args = self.default_run_args + [ 73 | "--still", 74 | f"--scenario_file={scenario_file}", 75 | ] 76 | 77 | result = subprocess.run(run_args, cwd=self.out_dir) 78 | result.check_returncode() 79 | self.assertTrue((self.out_dir / "vtk_camera.png").exists()) 80 | self.assertTrue((self.out_dir / "blender_camera.png").exists()) 81 | 82 | def test_video(self): 83 | """Checks that the example creates 2x video files.""" 84 | scenario_file = self._get_scenario_file(nerf_scenario=True) 85 | 86 | run_args = self.default_run_args + [f"--scenario_file={scenario_file}"] 87 | 88 | result = subprocess.run(run_args, cwd=self.out_dir) 89 | result.check_returncode() 90 | self.assertTrue((self.out_dir / "vtk_camera.mp4").exists()) 91 | self.assertTrue((self.out_dir / "blender_camera.mp4").exists()) 92 | 93 | 94 | if __name__ == "__main__": 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /examples/test/bpy_use_cycles.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT-0 2 | 3 | bpy.context.scene.render.engine = "CYCLES" 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | [project] 4 | name = "drake-blender" 5 | version = "0.2.1.dev0" 6 | # NOTE TO MAINTAINERS: when editing this list, you must copy the identical 7 | # update into requirements.in as well. 8 | dependencies = [ 9 | "bpy==3.6.0", 10 | "flask" 11 | ] 12 | 13 | [project.scripts] 14 | drake-blender-server = "server:main" 15 | 16 | [tool.black] 17 | line-length = 79 18 | 19 | [tool.isort] 20 | profile = "black" 21 | force_sort_within_sections = true 22 | line_length = 79 23 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | # To compile this file into requirements.txt, run: 4 | # ./tools/upgrade.sh 5 | 6 | # NOTE TO MAINTAINERS: when editing this file, you must copy the identical 7 | # update into pyproject.toml as well. 8 | # 9 | # TODO(jwnimmer-tri) Figure out how to keep only a single copy of our list 10 | # of dependencies. 11 | 12 | # TODO(jwnimmer-tri) When upgrading to blender 4.0, we'll need to figure out 13 | # how to make depth and label images work correctly. 14 | bpy==3.6.0 15 | flask 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # bazel run //:requirements.update 6 | # 7 | blinker==1.9.0 \ 8 | --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ 9 | --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc 10 | # via flask 11 | bpy==3.6.0 \ 12 | --hash=sha256:1215d5b972fd71ae1ddcb45108e25b2b3a0d7b89645365bca01341604dcecb5f \ 13 | --hash=sha256:3b4a9d2add44c4689435ee1e7b18cbb4c2be49149443825d9c6181dfb60b4381 \ 14 | --hash=sha256:4bc8bb77cc88143e78c04645351a7791d0e412c84f78a4771e68052ecac7d41c \ 15 | --hash=sha256:ede2c95ace7848f4f5a075a7d8cc3a9e643c335f4596c3c15087d93c7ae5f56a 16 | # via -r requirements.in 17 | certifi==2024.8.30 \ 18 | --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ 19 | --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 20 | # via requests 21 | charset-normalizer==3.4.0 \ 22 | --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ 23 | --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ 24 | --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ 25 | --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ 26 | --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ 27 | --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ 28 | --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ 29 | --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ 30 | --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ 31 | --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ 32 | --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ 33 | --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ 34 | --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ 35 | --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ 36 | --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ 37 | --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ 38 | --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ 39 | --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ 40 | --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ 41 | --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ 42 | --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ 43 | --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ 44 | --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ 45 | --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ 46 | --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ 47 | --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ 48 | --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ 49 | --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ 50 | --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ 51 | --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ 52 | --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ 53 | --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ 54 | --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ 55 | --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ 56 | --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ 57 | --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ 58 | --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ 59 | --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ 60 | --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ 61 | --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ 62 | --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ 63 | --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ 64 | --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ 65 | --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ 66 | --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ 67 | --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ 68 | --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ 69 | --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ 70 | --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ 71 | --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ 72 | --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ 73 | --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ 74 | --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ 75 | --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ 76 | --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ 77 | --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ 78 | --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ 79 | --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ 80 | --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ 81 | --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ 82 | --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ 83 | --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ 84 | --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ 85 | --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ 86 | --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ 87 | --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ 88 | --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ 89 | --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ 90 | --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ 91 | --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ 92 | --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ 93 | --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ 94 | --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ 95 | --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ 96 | --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ 97 | --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ 98 | --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ 99 | --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ 100 | --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ 101 | --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ 102 | --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ 103 | --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ 104 | --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ 105 | --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ 106 | --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ 107 | --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ 108 | --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ 109 | --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ 110 | --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ 111 | --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ 112 | --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ 113 | --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ 114 | --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ 115 | --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ 116 | --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ 117 | --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ 118 | --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ 119 | --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ 120 | --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ 121 | --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ 122 | --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ 123 | --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ 124 | --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ 125 | --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ 126 | --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 127 | # via requests 128 | click==8.1.7 \ 129 | --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ 130 | --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de 131 | # via flask 132 | cython==3.0.11 \ 133 | --hash=sha256:0b1d1f6f94cc5d42a4591f6d60d616786b9cd15576b112bc92a23131fcf38020 \ 134 | --hash=sha256:0e25f6425ad4a700d7f77cd468da9161e63658837d1bc34861a9861a4ef6346d \ 135 | --hash=sha256:0fc6fdd6fa493be7bdda22355689d5446ac944cd71286f6f44a14b0d67ee3ff5 \ 136 | --hash=sha256:104d6f2f2c827ccc5e9e42c80ef6773a6aa94752fe6bc5b24a4eab4306fb7f07 \ 137 | --hash=sha256:11996c40c32abf843ba652a6d53cb15944c88d91f91fc4e6f0028f5df8a8f8a1 \ 138 | --hash=sha256:13062ce556a1e98d2821f7a0253b50569fdc98c36efd6653a65b21e3f8bbbf5f \ 139 | --hash=sha256:14701edb3107a5d9305a82d9d646c4f28bfecbba74b26cc1ee2f4be08f602057 \ 140 | --hash=sha256:187685e25e037320cae513b8cc4bf9dbc4465c037051aede509cbbf207524de2 \ 141 | --hash=sha256:1dd47865f4c0a224da73acf83d113f93488d17624e2457dce1753acdfb1cc40c \ 142 | --hash=sha256:221de0b48bf387f209003508e602ce839a80463522fc6f583ad3c8d5c890d2c1 \ 143 | --hash=sha256:2252b5aa57621848e310fe7fa6f7dce5f73aa452884a183d201a8bcebfa05a00 \ 144 | --hash=sha256:2a8ea2e7e2d3bc0d8630dafe6c4a5a89485598ff8a61885b74f8ed882597efd5 \ 145 | --hash=sha256:301bde949b4f312a1c70e214b0c3bc51a3f955d466010d2f68eb042df36447b0 \ 146 | --hash=sha256:3379c6521e25aa6cd7703bb7d635eaca75c0f9c7f1b0fdd6dd15a03bfac5f68d \ 147 | --hash=sha256:351955559b37e6c98b48aecb178894c311be9d731b297782f2b78d111f0c9015 \ 148 | --hash=sha256:3699391125ab344d8d25438074d1097d9ba0fb674d0320599316cfe7cf5f002a \ 149 | --hash=sha256:3999fb52d3328a6a5e8c63122b0a8bd110dfcdb98dda585a3def1426b991cba7 \ 150 | --hash=sha256:3f2b062f6df67e8a56c75e500ca330cf62c85ac26dd7fd006f07ef0f83aebfa3 \ 151 | --hash=sha256:3ff8ac1f0ecd4f505db4ab051e58e4531f5d098b6ac03b91c3b902e8d10c67b3 \ 152 | --hash=sha256:421017466e9260aca86823974e26e158e6358622f27c0f4da9c682f3b6d2e624 \ 153 | --hash=sha256:4341d6a64d47112884e0bcf31e6c075268220ee4cd02223047182d4dda94d637 \ 154 | --hash=sha256:44292aae17524abb4b70a25111fe7dec1a0ad718711d47e3786a211d5408fdaa \ 155 | --hash=sha256:46aec30f217bdf096175a1a639203d44ac73a36fe7fa3dd06bd012e8f39eca0f \ 156 | --hash=sha256:473d35681d9f93ce380e6a7c8feb2d65fc6333bd7117fbc62989e404e241dbb0 \ 157 | --hash=sha256:4e9a8d92978b15a0c7ca7f98447c6c578dc8923a0941d9d172d0b077cb69c576 \ 158 | --hash=sha256:52186101d51497519e99b60d955fd5cb3bf747c67f00d742e70ab913f1e42d31 \ 159 | --hash=sha256:52205347e916dd65d2400b977df4c697390c3aae0e96275a438cc4ae85dadc08 \ 160 | --hash=sha256:525d09b3405534763fa73bd78c8e51ac8264036ce4c16d37dfd1555a7da6d3a7 \ 161 | --hash=sha256:53b6072a89049a991d07f42060f65398448365c59c9cb515c5925b9bdc9d71f8 \ 162 | --hash=sha256:598699165cfa7c6d69513ee1bffc9e1fdd63b00b624409174c388538aa217975 \ 163 | --hash=sha256:63f2c892e9f9c1698ecfee78205541623eb31cd3a1b682668be7ac12de94aa8e \ 164 | --hash=sha256:6823aef13669a32caf18bbb036de56065c485d9f558551a9b55061acf9c4c27f \ 165 | --hash=sha256:6fb68cef33684f8cc97987bee6ae919eee7e18ee6a3ad7ed9516b8386ef95ae6 \ 166 | --hash=sha256:7146dd2af8682b4ca61331851e6aebce9fe5158e75300343f80c07ca80b1faff \ 167 | --hash=sha256:75ba1c70b6deeaffbac123856b8d35f253da13552207aa969078611c197377e4 \ 168 | --hash=sha256:780f89c95b8aec1e403005b3bf2f0a2afa060b3eba168c86830f079339adad89 \ 169 | --hash=sha256:790263b74432cb997740d73665f4d8d00b9cd1cecbdd981d93591ddf993d4f12 \ 170 | --hash=sha256:8948802e1f5677a673ea5d22a1e7e273ca5f83e7a452786ca286eebf97cee67c \ 171 | --hash=sha256:8acdc87e9009110adbceb7569765eb0980129055cc954c62f99fe9f094c9505e \ 172 | --hash=sha256:8b14c24f1dc4c4c9d997cca8d1b7fb01187a218aab932328247dcf5694a10102 \ 173 | --hash=sha256:989899a85f0d9a57cebb508bd1f194cb52f0e3f7e22ac259f33d148d6422375c \ 174 | --hash=sha256:9c02361af9bfa10ff1ccf967fc75159e56b1c8093caf565739ed77a559c1f29f \ 175 | --hash=sha256:a0583076c4152b417a3a8a5d81ec02f58c09b67d3f22d5857e64c8734ceada8c \ 176 | --hash=sha256:a1f4cbc70f6b7f0c939522118820e708e0d490edca42d852fa8004ec16780be2 \ 177 | --hash=sha256:a690f2ff460682ea985e8d38ec541be97e0977fa0544aadc21efc116ff8d7579 \ 178 | --hash=sha256:a75d45fbc20651c1b72e4111149fed3b33d270b0a4fb78328c54d965f28d55e1 \ 179 | --hash=sha256:aedceb6090a60854b31bf9571dc55f642a3fa5b91f11b62bcef167c52cac93d8 \ 180 | --hash=sha256:af91497dc098718e634d6ec8f91b182aea6bb3690f333fc9a7777bc70abe8810 \ 181 | --hash=sha256:b4ab2b92a3e6ed552adbe9350fd2ef3aa0cc7853cf91569f9dbed0c0699bbeab \ 182 | --hash=sha256:b8c7e514075696ca0f60c337f9e416e61d7ccbc1aa879a56c39181ed90ec3059 \ 183 | --hash=sha256:bcd29945fafd12484cf37b1d84f12f0e7a33ba3eac5836531c6bd5283a6b3a0c \ 184 | --hash=sha256:bfa550d9ae39e827a6e7198076df763571cb53397084974a6948af558355e028 \ 185 | --hash=sha256:c3d68751668c66c7a140b6023dba5d5d507f72063407bb609d3a5b0f3b8dfbe4 \ 186 | --hash=sha256:c69d5cad51388522b98a99b4be1b77316de85b0c0523fa865e0ea58bbb622e0a \ 187 | --hash=sha256:c8eed5c015685106db15dd103fd040948ddca9197b1dd02222711815ea782a27 \ 188 | --hash=sha256:cee29846471ce60226b18e931d8c1c66a158db94853e3e79bc2da9bd22345008 \ 189 | --hash=sha256:d02f4ebe15aac7cdacce1a628e556c1983f26d140fd2e0ac5e0a090e605a2d38 \ 190 | --hash=sha256:d566a4e09b8979be8ab9f843bac0dd216c81f5e5f45661a9b25cd162ed80508c \ 191 | --hash=sha256:d80a7232938d523c1a12f6b1794ab5efb1ae77ad3fde79de4bb558d8ab261619 \ 192 | --hash=sha256:d89a82937ce4037f092e9848a7bbcc65bc8e9fc9aef2bb74f5c15e7d21a73080 \ 193 | --hash=sha256:da394654c6da15c1d37f0b7ec5afd325c69a15ceafee2afba14b67a5df8a82c8 \ 194 | --hash=sha256:ddd1fe25af330f4e003421636746a546474e4ccd8f239f55d2898d80983d20ed \ 195 | --hash=sha256:e6dd395d1a704e34a9fac00b25f0036dce6654c6b898be6f872ac2bb4f2eda48 \ 196 | --hash=sha256:eeb6860b0f4bfa402de8929833fe5370fa34069c7ebacb2d543cb017f21fb891 \ 197 | --hash=sha256:f3953d2f504176f929862e5579cfc421860c33e9707f585d70d24e1096accdf7 \ 198 | --hash=sha256:f988f7f8164a6079c705c39e2d75dbe9967e3dacafe041420d9af7b9ee424162 199 | # via bpy 200 | flask==3.1.0 \ 201 | --hash=sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac \ 202 | --hash=sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136 203 | # via -r requirements.in 204 | idna==3.10 \ 205 | --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ 206 | --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 207 | # via requests 208 | itsdangerous==2.2.0 \ 209 | --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ 210 | --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 211 | # via flask 212 | jinja2==3.1.4 \ 213 | --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ 214 | --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d 215 | # via flask 216 | markupsafe==3.0.2 \ 217 | --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ 218 | --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ 219 | --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ 220 | --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ 221 | --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ 222 | --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ 223 | --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ 224 | --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ 225 | --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ 226 | --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ 227 | --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ 228 | --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ 229 | --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ 230 | --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ 231 | --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ 232 | --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ 233 | --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ 234 | --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ 235 | --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ 236 | --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ 237 | --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ 238 | --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ 239 | --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ 240 | --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ 241 | --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ 242 | --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ 243 | --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ 244 | --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ 245 | --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ 246 | --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ 247 | --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ 248 | --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ 249 | --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ 250 | --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ 251 | --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ 252 | --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ 253 | --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ 254 | --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ 255 | --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ 256 | --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ 257 | --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ 258 | --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ 259 | --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ 260 | --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ 261 | --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ 262 | --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ 263 | --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ 264 | --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ 265 | --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ 266 | --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ 267 | --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ 268 | --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ 269 | --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ 270 | --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ 271 | --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ 272 | --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ 273 | --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ 274 | --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ 275 | --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ 276 | --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ 277 | --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 278 | # via 279 | # jinja2 280 | # werkzeug 281 | numpy==2.2.0 \ 282 | --hash=sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608 \ 283 | --hash=sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef \ 284 | --hash=sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90 \ 285 | --hash=sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae \ 286 | --hash=sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83 \ 287 | --hash=sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0 \ 288 | --hash=sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73 \ 289 | --hash=sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671 \ 290 | --hash=sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69 \ 291 | --hash=sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa \ 292 | --hash=sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066 \ 293 | --hash=sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da \ 294 | --hash=sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9 \ 295 | --hash=sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e \ 296 | --hash=sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3 \ 297 | --hash=sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a \ 298 | --hash=sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74 \ 299 | --hash=sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3 \ 300 | --hash=sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410 \ 301 | --hash=sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72 \ 302 | --hash=sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d \ 303 | --hash=sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4 \ 304 | --hash=sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038 \ 305 | --hash=sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e \ 306 | --hash=sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13 \ 307 | --hash=sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d \ 308 | --hash=sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95 \ 309 | --hash=sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31 \ 310 | --hash=sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3 \ 311 | --hash=sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03 \ 312 | --hash=sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6 \ 313 | --hash=sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2 \ 314 | --hash=sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b \ 315 | --hash=sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7 \ 316 | --hash=sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab \ 317 | --hash=sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219 \ 318 | --hash=sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571 \ 319 | --hash=sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d \ 320 | --hash=sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1 \ 321 | --hash=sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca \ 322 | --hash=sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661 \ 323 | --hash=sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e \ 324 | --hash=sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e \ 325 | --hash=sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e \ 326 | --hash=sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a \ 327 | --hash=sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3 \ 328 | --hash=sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881 \ 329 | --hash=sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221 \ 330 | --hash=sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742 \ 331 | --hash=sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773 \ 332 | --hash=sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e \ 333 | --hash=sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529 \ 334 | --hash=sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67 \ 335 | --hash=sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c \ 336 | --hash=sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367 337 | # via bpy 338 | requests==2.32.3 \ 339 | --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ 340 | --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 341 | # via bpy 342 | urllib3==2.2.3 \ 343 | --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ 344 | --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 345 | # via requests 346 | werkzeug==3.1.3 \ 347 | --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ 348 | --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 349 | # via flask 350 | zstandard==0.23.0 \ 351 | --hash=sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473 \ 352 | --hash=sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916 \ 353 | --hash=sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15 \ 354 | --hash=sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072 \ 355 | --hash=sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4 \ 356 | --hash=sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e \ 357 | --hash=sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26 \ 358 | --hash=sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8 \ 359 | --hash=sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5 \ 360 | --hash=sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd \ 361 | --hash=sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c \ 362 | --hash=sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db \ 363 | --hash=sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5 \ 364 | --hash=sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc \ 365 | --hash=sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152 \ 366 | --hash=sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269 \ 367 | --hash=sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045 \ 368 | --hash=sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e \ 369 | --hash=sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d \ 370 | --hash=sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a \ 371 | --hash=sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb \ 372 | --hash=sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740 \ 373 | --hash=sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105 \ 374 | --hash=sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274 \ 375 | --hash=sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2 \ 376 | --hash=sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58 \ 377 | --hash=sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b \ 378 | --hash=sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4 \ 379 | --hash=sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db \ 380 | --hash=sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e \ 381 | --hash=sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9 \ 382 | --hash=sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0 \ 383 | --hash=sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813 \ 384 | --hash=sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e \ 385 | --hash=sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512 \ 386 | --hash=sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0 \ 387 | --hash=sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b \ 388 | --hash=sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48 \ 389 | --hash=sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a \ 390 | --hash=sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772 \ 391 | --hash=sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed \ 392 | --hash=sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373 \ 393 | --hash=sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea \ 394 | --hash=sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd \ 395 | --hash=sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f \ 396 | --hash=sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc \ 397 | --hash=sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23 \ 398 | --hash=sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2 \ 399 | --hash=sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db \ 400 | --hash=sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70 \ 401 | --hash=sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259 \ 402 | --hash=sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9 \ 403 | --hash=sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700 \ 404 | --hash=sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003 \ 405 | --hash=sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba \ 406 | --hash=sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a \ 407 | --hash=sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c \ 408 | --hash=sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90 \ 409 | --hash=sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690 \ 410 | --hash=sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f \ 411 | --hash=sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840 \ 412 | --hash=sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d \ 413 | --hash=sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9 \ 414 | --hash=sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35 \ 415 | --hash=sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd \ 416 | --hash=sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a \ 417 | --hash=sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea \ 418 | --hash=sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1 \ 419 | --hash=sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573 \ 420 | --hash=sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09 \ 421 | --hash=sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094 \ 422 | --hash=sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78 \ 423 | --hash=sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9 \ 424 | --hash=sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5 \ 425 | --hash=sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9 \ 426 | --hash=sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391 \ 427 | --hash=sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847 \ 428 | --hash=sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2 \ 429 | --hash=sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c \ 430 | --hash=sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2 \ 431 | --hash=sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057 \ 432 | --hash=sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20 \ 433 | --hash=sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d \ 434 | --hash=sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4 \ 435 | --hash=sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54 \ 436 | --hash=sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171 \ 437 | --hash=sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e \ 438 | --hash=sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160 \ 439 | --hash=sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b \ 440 | --hash=sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58 \ 441 | --hash=sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8 \ 442 | --hash=sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33 \ 443 | --hash=sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a \ 444 | --hash=sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880 \ 445 | --hash=sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca \ 446 | --hash=sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b \ 447 | --hash=sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69 448 | # via bpy 449 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | """ 4 | A glTF render server that services HTTP requests using Blender. 5 | """ 6 | 7 | import argparse 8 | import dataclasses as dc 9 | import datetime 10 | import io 11 | import logging 12 | import math 13 | from pathlib import Path 14 | import tempfile 15 | from types import NoneType 16 | import typing 17 | 18 | import bpy 19 | import flask 20 | 21 | _logger = logging.getLogger("server") 22 | 23 | _UINT16_MAX = 2**16 - 1 24 | 25 | 26 | @dc.dataclass 27 | class RenderParams: 28 | """A dataclass that encapsulates all the necessary parameters to render a 29 | color, depth, or label image. 30 | 31 | https://drake.mit.edu/doxygen_cxx/group__render__engine__gltf__client__server__api.html#render-endpoint-form-data 32 | """ 33 | 34 | scene: Path 35 | """The glTF input file.""" 36 | 37 | scene_sha256: str 38 | """The checksum of `scene`.""" 39 | 40 | image_type: typing.Literal["color", "depth", "label"] 41 | """The type of image being rendered.""" 42 | 43 | width: int 44 | """Width of the desired rendered image in pixels.""" 45 | 46 | height: int 47 | """Height of the desired rendered image in pixels.""" 48 | 49 | near: float 50 | """The near clipping plane of the camera as specified by the 51 | RenderCameraCore's ClippingRange::near() value.""" 52 | 53 | far: float 54 | """The far clipping plane of the camera as specified by the 55 | RenderCameraCore's ClippingRange::far() value.""" 56 | 57 | focal_x: float 58 | """The focal length x, in pixels, as specified by the 59 | systems::sensors::CameraInfo::focal_x() value.""" 60 | 61 | focal_y: float 62 | """The focal length y, in pixels, as specified by the 63 | systems::sensors::CameraInfo::focal_y() value.""" 64 | 65 | fov_x: float 66 | """The field of view in the x-direction (in radians) as specified by the 67 | systems::sensors::CameraInfo::fov_x() value.""" 68 | 69 | fov_y: float 70 | """The field of view in the y-direction (in radians) as specified by the 71 | systems::sensors::CameraInfo::fov_y() value.""" 72 | 73 | center_x: float 74 | """The principal point's x coordinate in pixels as specified by the 75 | systems::sensors::CameraInfo::center_x() value.""" 76 | 77 | center_y: float 78 | """The principal point's y coordinate in pixels as specified by the 79 | systems::sensors::CameraInfo::center_y() value.""" 80 | 81 | min_depth: typing.Optional[float] = None 82 | """The minimum depth range as specified by a depth sensor's 83 | DepthRange::min_depth(). Only provided when image_type="depth".""" 84 | 85 | max_depth: typing.Optional[float] = None 86 | """The maximum depth range as specified by a depth sensor's 87 | DepthRange::max_depth(). Only provided when image_type="depth".""" 88 | 89 | 90 | class Blender: 91 | """Encapsulates our access to blender. 92 | 93 | Note that even though this is a class, bpy is a singleton so likewise you 94 | should only ever create one instance of this class. 95 | """ 96 | 97 | def __init__( 98 | self, *, blend_file: Path = None, bpy_settings_file: Path = None 99 | ): 100 | self._blend_file = blend_file 101 | self._bpy_settings_file = bpy_settings_file 102 | self._client_objects = None 103 | 104 | def reset_scene(self): 105 | """ 106 | Resets the scene in Blender by loading the default startup file, and 107 | then removes the default cube object. 108 | """ 109 | bpy.ops.wm.read_factory_settings() 110 | for item in bpy.data.objects: 111 | item.select_set(True) 112 | bpy.ops.object.delete() 113 | 114 | def add_default_light_source(self): 115 | light = bpy.data.lights.new(name="POINT", type="POINT") 116 | light.energy = 100 117 | light_object = bpy.data.objects.new(name="LIGHT", object_data=light) 118 | light_object.location = (0, 0, 5) 119 | bpy.context.collection.objects.link(light_object) 120 | bpy.context.view_layer.objects.active = light_object 121 | 122 | def render_image(self, *, params: RenderParams, output_path: Path): 123 | """ 124 | Renders the current scene with the given parameters. 125 | """ 126 | # Load the blend file to set up the basic scene if provided; otherwise, 127 | # the scene gets reset with default lighting. 128 | if self._blend_file is not None: 129 | bpy.ops.wm.open_mainfile(filepath=str(self._blend_file)) 130 | else: 131 | self.reset_scene() 132 | self.add_default_light_source() 133 | 134 | # Apply the user's custom settings. 135 | if self._bpy_settings_file: 136 | with open(self._bpy_settings_file) as f: 137 | code = compile(f.read(), self._bpy_settings_file, "exec") 138 | exec(code, {"bpy": bpy}, dict()) 139 | 140 | self._client_objects = bpy.data.collections.new("ClientObjects") 141 | old_count = len(bpy.data.objects) 142 | # Import a glTF file. Note that the Blender glTF importer imposes a 143 | # +90 degree rotation around the X-axis when loading meshes. Thus, we 144 | # counterbalance the rotation right after the glTF-loading. 145 | bpy.ops.import_scene.gltf(filepath=str(params.scene)) 146 | new_count = len(bpy.data.objects) 147 | # Reality check that all of the imported objects are selected by 148 | # default. 149 | assert new_count - old_count == len(bpy.context.selected_objects) 150 | 151 | # TODO(#39) This rotation is very suspicious. Get to the bottom of it. 152 | # We explicitly specify the pivot point for the rotation to allow for 153 | # glTF files with root nodes with arbitrary positioning. We simply want 154 | # to rotate around the world origin. 155 | bpy.ops.transform.rotate( 156 | value=math.pi / 2, 157 | orient_axis="X", 158 | orient_type="GLOBAL", 159 | center_override=(0, 0, 0), 160 | ) 161 | 162 | # All imported objects get put in our "client objects" collection. 163 | for obj in bpy.context.selected_objects: 164 | self._client_objects.objects.link(obj) 165 | 166 | # Set rendering parameters. 167 | scene = bpy.context.scene 168 | scene.render.image_settings.file_format = "PNG" 169 | scene.render.filepath = str(output_path) 170 | scene.render.resolution_x = params.width 171 | scene.render.resolution_y = params.height 172 | if params.focal_x > params.focal_y: 173 | scene.render.pixel_aspect_x = 1.0 174 | scene.render.pixel_aspect_y = params.focal_x / params.focal_y 175 | else: 176 | scene.render.pixel_aspect_x = params.focal_y / params.focal_x 177 | scene.render.pixel_aspect_y = 1.0 178 | 179 | # Set camera parameters. 180 | camera = bpy.data.objects.get("Camera Node") 181 | if camera is None: 182 | _logger.error( 183 | "Camera node not found. Check the input glTF file " 184 | f"'{params.scene}'." 185 | ) 186 | return 187 | 188 | scene.camera = camera 189 | camera.data.show_sensor = True 190 | # Set the clipping planes using {min, max}_depth when rendering depth 191 | # images; otherwise, `near` and `far` are set for color and label 192 | # images. 193 | # TODO(#38) This clipping logic fails to implement kTooClose. 194 | # When there is geometry in the range [near, min], we should return 195 | # zero (i.e., too close). As is, it will return non-zero. Fix the code 196 | # here and add regression tests for both too-close and -far. 197 | camera.data.clip_start = ( 198 | params.min_depth if params.min_depth else params.near 199 | ) 200 | camera.data.clip_end = ( 201 | params.max_depth if params.max_depth else params.far 202 | ) 203 | # See: https://www.rojtberg.net/1601/from-Blender-to-opencv-camera-and-back/. # noqa: E501 204 | camera.data.shift_x = -1.0 * (params.center_x / params.width - 0.5) 205 | camera.data.shift_y = ( 206 | params.center_y - 0.5 * params.height 207 | ) / params.width 208 | 209 | # Setting FOV Y also implicitly sets FOV X. 210 | camera.data.lens_unit = "FOV" 211 | camera.data.angle_y = params.fov_y 212 | 213 | # Set image_type specific functionality. 214 | if params.image_type == "color": 215 | scene.render.image_settings.color_mode = "RGBA" 216 | scene.render.image_settings.color_depth = "8" 217 | elif params.image_type == "depth": 218 | scene.render.image_settings.color_mode = "BW" 219 | scene.render.image_settings.color_depth = "16" 220 | # NOTE: Display device is set to 'None' because the pixel values of 221 | # the image are meant to be interpreted as depth measurements. By 222 | # default, Blender applies a filter that changes the values. 223 | scene.display_settings.display_device = "None" 224 | self.depth_render_settings(params.min_depth, params.max_depth) 225 | else: # image_type == "label". 226 | scene.render.image_settings.color_mode = "RGBA" 227 | scene.render.image_settings.color_depth = "8" 228 | scene.display_settings.display_device = "None" 229 | self.label_render_settings() 230 | 231 | # Render the image. 232 | bpy.ops.render.render(write_still=True) 233 | 234 | def depth_render_settings(self, min_depth, max_depth): 235 | # Turn anti-aliasing off. 236 | bpy.context.scene.render.filter_size = 0 237 | 238 | world_nodes = bpy.data.worlds["World"].node_tree.nodes 239 | # Set the background. 240 | world_nodes["Background"].inputs[0].default_value = ( 241 | _UINT16_MAX, 242 | _UINT16_MAX, 243 | _UINT16_MAX, 244 | 1, 245 | ) 246 | 247 | # Update the render method to use depth image. 248 | self.create_depth_node_layer(min_depth, max_depth) 249 | 250 | def label_render_settings(self): 251 | scene = bpy.context.scene 252 | 253 | # Turn anti-aliasing off. 254 | scene.render.filter_size = 0 255 | 256 | # Set dither to zero because the 8-bit color image tries to create a 257 | # better perceived transition in color where there is a limited 258 | # palette. 259 | scene.render.dither_intensity = 0 260 | 261 | # Meshes from a blend file and the background will be painted to white. 262 | background_color = (1.0, 1.0, 1.0, 1.0) 263 | world_nodes = bpy.data.worlds["World"].node_tree.nodes 264 | world_nodes["Background"].inputs[0].default_value = background_color 265 | 266 | # Every object imported from the glTF file has been placed in a 267 | # special collection; simply test for its presence. 268 | assert self._client_objects is not None 269 | 270 | def is_from_gltf(object): 271 | return object.name in self._client_objects.objects 272 | 273 | # Iterate over all meshes and set their label values. 274 | for bpy_object in bpy.data.objects: 275 | assert bpy_object is not None 276 | # Ensure the object is a mesh. 277 | # TODO(zachfang): Revisit if we ever add more types of objects 278 | # other than `MESH`, e.g., primitives. We need to handle their 279 | # label values too. 280 | if bpy_object.type != "MESH": 281 | continue 282 | 283 | # If a mesh is imported from a glTF, we will set its label value to 284 | # its diffuse color. If a mesh is loaded from a blend file, its 285 | # label value will be set to white (same as the background). 286 | if is_from_gltf(bpy_object): 287 | mesh_color = bpy_object.data.materials[0].diffuse_color 288 | else: 289 | mesh_color = background_color 290 | bpy_object.data.materials[0].use_nodes = True 291 | links = bpy_object.data.materials[0].node_tree.links 292 | nodes = bpy_object.data.materials[0].node_tree.nodes 293 | 294 | # Clear all material nodes before adding necessary nodes. 295 | nodes.clear() 296 | rendered_surface = nodes.new("ShaderNodeOutputMaterial") 297 | # Use 'ShaderNodeBackground' node as it produces a flat color. 298 | unlit_flat_mesh_color = nodes.new("ShaderNodeBackground") 299 | 300 | links.new( 301 | unlit_flat_mesh_color.outputs[0], 302 | rendered_surface.inputs["Surface"], 303 | ) 304 | unlit_flat_mesh_color.inputs["Color"].default_value = mesh_color 305 | 306 | def create_depth_node_layer(self, min_depth=0.01, max_depth=10.0): 307 | """ 308 | Creates a node layer to render depth images. 309 | """ 310 | # Get node and node tree. 311 | bpy.context.scene.use_nodes = True 312 | nodes = bpy.data.scenes["Scene"].node_tree.nodes 313 | links = bpy.data.scenes["Scene"].node_tree.links 314 | 315 | # Clear all nodes before adding necessary nodes. 316 | nodes.clear() 317 | render_layers = nodes.new("CompositorNodeRLayers") 318 | composite = nodes.new("CompositorNodeComposite") 319 | map_value = nodes.new("CompositorNodeMapValue") 320 | 321 | # Convert depth measurements via a MapValueNode. The depth values are 322 | # measured in meters, and thus they are converted to millimeters first. 323 | # Blender scales the pixel values by 65535 (2^16 -1) when producing a 324 | # UINT16 image, so we need to offset that to get the correct UINT16 325 | # depth. 326 | assert ( 327 | max_depth * 1000 / _UINT16_MAX <= 1.0 328 | ), f"Provided max_depth '{max_depth}' overflows an UINT16 depth image" 329 | map_value.use_min = True 330 | map_value.use_max = True 331 | map_value.size = [1000 / _UINT16_MAX] 332 | map_value.min = [min_depth * 1000 / _UINT16_MAX] 333 | map_value.max = [1.0] 334 | 335 | # Make links to a depth image. 336 | bpy.data.scenes["Scene"].view_layers["ViewLayer"].use_pass_z = True 337 | links.new( 338 | render_layers.outputs.get("Depth"), map_value.inputs.get("Value") 339 | ) 340 | links.new( 341 | map_value.outputs.get("Value"), composite.inputs.get("Image") 342 | ) 343 | 344 | 345 | class ServerApp(flask.Flask): 346 | """The long-running Flask server application.""" 347 | 348 | def __init__( 349 | self, 350 | *, 351 | temp_dir, 352 | blend_file: Path = None, 353 | bpy_settings_file: Path = None, 354 | ): 355 | super().__init__("drake_render_gltf_blender") 356 | 357 | self._temp_dir = temp_dir 358 | self._blender = Blender( 359 | blend_file=blend_file, bpy_settings_file=bpy_settings_file 360 | ) 361 | 362 | self.add_url_rule("/", view_func=self._root_endpoint) 363 | 364 | endpoint = "/render" 365 | self.add_url_rule( 366 | rule=endpoint, 367 | endpoint=endpoint, 368 | methods=["POST"], 369 | view_func=self._render_endpoint, 370 | ) 371 | 372 | def _root_endpoint(self): 373 | """Displays a banner page at the server root.""" 374 | return """\ 375 | 376 |

Drake Render glTF Blender Server

377 | """ 378 | 379 | def _render_endpoint(self): 380 | """Accepts a request to render and returns the generated image.""" 381 | try: 382 | params = self._parse_params(flask.request) 383 | buffer = self._render(params) 384 | return flask.send_file(buffer, mimetype="image/png") 385 | except Exception as e: 386 | code = 500 387 | message = f"Internal server error: {repr(e)}" 388 | return ( 389 | { 390 | "error": True, 391 | "message": message, 392 | "code": code, 393 | }, 394 | code, 395 | ) 396 | 397 | def _parse_params(self, request: flask.Request) -> RenderParams: 398 | """Converts an http request to a RenderParams.""" 399 | result = dict() 400 | 401 | # Compute a lookup table for known form field names. 402 | param_fields = {x.name: x for x in dc.fields(RenderParams)} 403 | del param_fields["scene"] 404 | 405 | # Copy all of the form data into the result. 406 | for name, value in request.form.items(): 407 | if name == "submit": 408 | # Ignore the html boilerplate. 409 | continue 410 | field = param_fields[name] 411 | type_origin = typing.get_origin(field.type) 412 | type_args = typing.get_args(field.type) 413 | if field.type in (int, float, str): 414 | result[name] = field.type(value) 415 | elif type_origin == typing.Literal: 416 | if value not in type_args: 417 | raise ValueError(f"Invalid literal for {name}") 418 | result[name] = value 419 | elif type_origin == typing.Union: 420 | # In our dataclass we declare a typing.Optional but that's just 421 | # sugar for typing.Union[T, typing.NoneType]. Here, we need to 422 | # parse the typing.Union spelling; we can assume the only use 423 | # of Union is for an Optional. 424 | assert len(type_args) == 2 425 | assert type_args[1] == NoneType 426 | result[name] = type_args[0](value) 427 | else: 428 | raise NotImplementedError(name) 429 | 430 | # Save the glTF scene data. Note that we don't check the scene_sha256 431 | # checksum; it seems unlikely that it could ever fail without flask 432 | # detecting the error. In any case, the blender glTF loader should 433 | # reject malformed files; we don't need to fail-fast. 434 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S-%f") 435 | scene = Path(f"{self._temp_dir}/{timestamp}.gltf") 436 | assert len(request.files) == 1 437 | request.files["scene"].save(scene) 438 | result["scene"] = scene 439 | 440 | return RenderParams(**result) 441 | 442 | def _render(self, params: RenderParams): 443 | """Renders the given scene, returning the png data buffer.""" 444 | output_path = params.scene.with_suffix(".png") 445 | try: 446 | self._blender.render_image(params=params, output_path=output_path) 447 | with open(output_path, "rb") as f: 448 | buffer = io.BytesIO(f.read()) 449 | return buffer 450 | finally: 451 | params.scene.unlink(missing_ok=True) 452 | output_path.unlink(missing_ok=True) 453 | 454 | 455 | def main(): 456 | parser = argparse.ArgumentParser(description=__doc__) 457 | parser.add_argument( 458 | "--host", 459 | type=str, 460 | default="127.0.0.1", 461 | help="URL to host on, default: %(default)s.", 462 | ) 463 | parser.add_argument( 464 | "--port", 465 | type=int, 466 | default=8000, 467 | help="Port to host on, default: %(default)s.", 468 | ) 469 | parser.add_argument( 470 | "--debug", 471 | action="store_true", 472 | help="When true, flask reloads server.py when it changes.", 473 | ) 474 | parser.add_argument( 475 | "--blend_file", 476 | type=Path, 477 | metavar="FILE", 478 | help="Path to a *.blend file.", 479 | ) 480 | parser.add_argument( 481 | "--bpy_settings_file", 482 | type=Path, 483 | metavar="FILE", 484 | help="Path to a *.py file that the server will exec() to configure " 485 | "blender. For example, the settings file might contain the line " 486 | '`bpy.context.scene.render.engine = "EEVEE"` (with no backticks). ' 487 | "The settings file will be applied after loading the --blend_file " 488 | "(if any) so that it has priority.", 489 | ) 490 | args = parser.parse_args() 491 | 492 | prefix = "drake_blender_" 493 | with tempfile.TemporaryDirectory(prefix=prefix) as temp_dir: 494 | app = ServerApp( 495 | temp_dir=temp_dir, 496 | blend_file=args.blend_file, 497 | bpy_settings_file=args.bpy_settings_file, 498 | ) 499 | app.run( 500 | host=args.host, port=args.port, debug=args.debug, threaded=False 501 | ) 502 | 503 | 504 | if __name__ == "__main__": 505 | main() 506 | -------------------------------------------------------------------------------- /test/4_color_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/4_color_texture.png -------------------------------------------------------------------------------- /test/BUILD.bazel: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | load("@rules_python//python:defs.bzl", "py_test") 4 | load("@rules_python//python:pip.bzl", "compile_pip_requirements") 5 | load("//tools:defs.bzl", "bazel_lint_test", "pip", "py_lint_test") 6 | 7 | py_test( 8 | name = "server_test", 9 | srcs = ["server_test.py"], 10 | data = [ 11 | "//:server", 12 | # This texture file is a dependency for `one_texture_box.blend`. 13 | "4_color_texture.png", 14 | "depth.png", 15 | "label.png", 16 | # TODO(zachfang): Consider generating this image in the test code if 17 | # it's easier to maintain for our future use cases. 18 | "one_gltf_one_blend.label.png", 19 | "one_rgba_box.gltf", 20 | "one_rgba_one_texture_boxes.color.png", 21 | "one_rgba_one_texture_boxes.gltf", 22 | "one_texture_box.blend", 23 | "two_rgba_boxes.color.png", 24 | "two_rgba_boxes.gltf", 25 | ], 26 | deps = [ 27 | pip("numpy", "[test]"), 28 | pip("pillow", "[test]"), 29 | pip("requests", "[test]"), 30 | ], 31 | ) 32 | 33 | bazel_lint_test( 34 | name = "bazel_lint_test", 35 | srcs = [ 36 | "BUILD.bazel", 37 | ], 38 | ) 39 | 40 | py_lint_test( 41 | name = "py_lint_test", 42 | srcs = [ 43 | "server_test.py", 44 | ], 45 | ) 46 | 47 | compile_pip_requirements( 48 | name = "requirements", 49 | requirements_in = "requirements.in", 50 | requirements_txt = "requirements.txt", 51 | tags = ["manual"], 52 | ) 53 | -------------------------------------------------------------------------------- /test/depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/depth.png -------------------------------------------------------------------------------- /test/label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/label.png -------------------------------------------------------------------------------- /test/one_gltf_one_blend.label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/one_gltf_one_blend.label.png -------------------------------------------------------------------------------- /test/one_rgba_box.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors" : 3 | [ 4 | { 5 | "bufferView" : 0, 6 | "byteOffset" : 0, 7 | "componentType" : 5126, 8 | "count" : 24, 9 | "max" : [ 0.050000000000000003, 0.037499999999999999, 0.025000000000000001 ], 10 | "min" : [ -0.050000000000000003, -0.037499999999999999, -0.025000000000000001 ], 11 | "type" : "VEC3" 12 | }, 13 | { 14 | "bufferView" : 1, 15 | "byteOffset" : 0, 16 | "componentType" : 5126, 17 | "count" : 24, 18 | "normalized" : false, 19 | "type" : "VEC2" 20 | }, 21 | { 22 | "bufferView" : 2, 23 | "byteOffset" : 0, 24 | "componentType" : 5125, 25 | "count" : 36, 26 | "type" : "SCALAR" 27 | } 28 | ], 29 | "asset" : 30 | { 31 | "generator" : "VTK", 32 | "version" : "2.0" 33 | }, 34 | "bufferViews" : 35 | [ 36 | { 37 | "buffer" : 0, 38 | "byteLength" : 288, 39 | "byteOffset" : 0 40 | }, 41 | { 42 | "buffer" : 1, 43 | "byteLength" : 192, 44 | "byteOffset" : 0 45 | }, 46 | { 47 | "buffer" : 2, 48 | "byteLength" : 144, 49 | "byteOffset" : 0 50 | } 51 | ], 52 | "buffers" : 53 | [ 54 | { 55 | "byteLength" : 288, 56 | "uri" : "data:application/octet-stream;base64,zcxMPZqZGb3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGT3NzMw8zcxMPZqZGb3NzMw8zcxMvZqZGT3NzMy8zcxMvZqZGb3NzMy8zcxMvZqZGb3NzMw8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMy8zcxMvZqZGT3NzMy8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGb3NzMy8zcxMPZqZGb3NzMy8zcxMPZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMPZqZGb3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGT3NzMw8zcxMvZqZGT3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGb3NzMy8zcxMvZqZGb3NzMy8" 57 | }, 58 | { 59 | "byteLength" : 192, 60 | "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/" 61 | }, 62 | { 63 | "byteLength" : 144, 64 | "uri" : "data:application/octet-stream;base64,AAAAAAEAAAADAAAAAgAAAAMAAAABAAAABAAAAAUAAAAHAAAABgAAAAcAAAAFAAAACAAAAAkAAAALAAAACgAAAAsAAAAJAAAADAAAAA0AAAAPAAAADgAAAA8AAAANAAAAEAAAABEAAAATAAAAEgAAABMAAAARAAAAFAAAABUAAAAXAAAAFgAAABcAAAAVAAAA" 65 | } 66 | ], 67 | "cameras" : 68 | [ 69 | { 70 | "perspective" : 71 | { 72 | "aspectRatio" : 1.3333333333333333, 73 | "yfov" : 0.78539816339744828, 74 | "zfar" : 10.0, 75 | "znear" : 0.01 76 | }, 77 | "type" : "perspective" 78 | } 79 | ], 80 | "materials" : 81 | [ 82 | { 83 | "pbrMetallicRoughness" : 84 | { 85 | "baseColorFactor" : [ 1.0, 0.25, 0.25, 1.0 ], 86 | "metallicFactor" : 0.0, 87 | "roughnessFactor" : 1.0 88 | } 89 | } 90 | ], 91 | "meshes" : 92 | [ 93 | { 94 | "name" : "mesh0", 95 | "primitives" : 96 | [ 97 | { 98 | "attributes" : 99 | { 100 | "POSITION" : 0, 101 | "TEXCOORD_0" : 1 102 | }, 103 | "indices" : 2, 104 | "material" : 0, 105 | "mode" : 4 106 | } 107 | ] 108 | } 109 | ], 110 | "nodes" : 111 | [ 112 | { 113 | "matrix" : 114 | [ 115 | 1.0, 116 | 0.0, 117 | 0.0, 118 | 0.0, 119 | 0.0, 120 | 1.0, 121 | 0.0, 122 | 0.0, 123 | 0.0, 124 | 0.0, 125 | 1.0, 126 | 0.0, 127 | 0.0, 128 | -0.10000000000000001, 129 | 0.0, 130 | 1.0 131 | ], 132 | "mesh" : 0, 133 | "name" : "mesh0" 134 | }, 135 | { 136 | "camera" : 0, 137 | "matrix" : 138 | [ 139 | 0.00079632671073324817, 140 | 0.99999968293183461, 141 | 0.0, 142 | 0.0, 143 | -0.50484594452922937, 144 | 0.00040202243790249793, 145 | 0.86320936664887371, 146 | 0.0, 147 | 0.86320909295266346, 148 | -0.00068739667561762798, 149 | 0.50484610459985757, 150 | 0.0, 151 | 0.29999999999999993, 152 | 4.5536491244391186e-18, 153 | 0.20000000000000004, 154 | 1.0 155 | ], 156 | "name" : "Camera Node" 157 | }, 158 | { 159 | "children" : [ 0, 1 ], 160 | "name" : "Root Node" 161 | } 162 | ], 163 | "scene" : 0, 164 | "scenes" : 165 | [ 166 | { 167 | "name" : "Layer 0", 168 | "nodes" : [ 2 ] 169 | } 170 | ] 171 | } -------------------------------------------------------------------------------- /test/one_rgba_one_texture_boxes.color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/one_rgba_one_texture_boxes.color.png -------------------------------------------------------------------------------- /test/one_rgba_one_texture_boxes.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors" : 3 | [ 4 | { 5 | "bufferView" : 0, 6 | "byteOffset" : 0, 7 | "componentType" : 5126, 8 | "count" : 24, 9 | "max" : [ 0.050000000000000003, 0.037499999999999999, 0.025000000000000001 ], 10 | "min" : [ -0.050000000000000003, -0.037499999999999999, -0.025000000000000001 ], 11 | "type" : "VEC3" 12 | }, 13 | { 14 | "bufferView" : 1, 15 | "byteOffset" : 0, 16 | "componentType" : 5126, 17 | "count" : 24, 18 | "normalized" : false, 19 | "type" : "VEC2" 20 | }, 21 | { 22 | "bufferView" : 2, 23 | "byteOffset" : 0, 24 | "componentType" : 5125, 25 | "count" : 36, 26 | "type" : "SCALAR" 27 | }, 28 | { 29 | "bufferView" : 4, 30 | "byteOffset" : 0, 31 | "componentType" : 5126, 32 | "count" : 24, 33 | "max" : [ 0.050000000000000003, 0.037499999999999999, 0.025000000000000001 ], 34 | "min" : [ -0.050000000000000003, -0.037499999999999999, -0.025000000000000001 ], 35 | "type" : "VEC3" 36 | }, 37 | { 38 | "bufferView" : 5, 39 | "byteOffset" : 0, 40 | "componentType" : 5126, 41 | "count" : 24, 42 | "normalized" : false, 43 | "type" : "VEC2" 44 | }, 45 | { 46 | "bufferView" : 6, 47 | "byteOffset" : 0, 48 | "componentType" : 5125, 49 | "count" : 36, 50 | "type" : "SCALAR" 51 | } 52 | ], 53 | "asset" : 54 | { 55 | "generator" : "VTK", 56 | "version" : "2.0" 57 | }, 58 | "bufferViews" : 59 | [ 60 | { 61 | "buffer" : 0, 62 | "byteLength" : 288, 63 | "byteOffset" : 0 64 | }, 65 | { 66 | "buffer" : 1, 67 | "byteLength" : 192, 68 | "byteOffset" : 0 69 | }, 70 | { 71 | "buffer" : 2, 72 | "byteLength" : 144, 73 | "byteOffset" : 0 74 | }, 75 | { 76 | "buffer" : 3, 77 | "byteLength" : 1123, 78 | "byteOffset" : 0 79 | }, 80 | { 81 | "buffer" : 4, 82 | "byteLength" : 288, 83 | "byteOffset" : 0 84 | }, 85 | { 86 | "buffer" : 5, 87 | "byteLength" : 192, 88 | "byteOffset" : 0 89 | }, 90 | { 91 | "buffer" : 6, 92 | "byteLength" : 144, 93 | "byteOffset" : 0 94 | } 95 | ], 96 | "buffers" : 97 | [ 98 | { 99 | "byteLength" : 288, 100 | "uri" : "data:application/octet-stream;base64,zcxMPZqZGb3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGT3NzMw8zcxMPZqZGb3NzMw8zcxMvZqZGT3NzMy8zcxMvZqZGb3NzMy8zcxMvZqZGb3NzMw8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMy8zcxMvZqZGT3NzMy8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGb3NzMy8zcxMPZqZGb3NzMy8zcxMPZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMPZqZGb3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGT3NzMw8zcxMvZqZGT3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGb3NzMy8zcxMvZqZGb3NzMy8" 101 | }, 102 | { 103 | "byteLength" : 192, 104 | "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/" 105 | }, 106 | { 107 | "byteLength" : 144, 108 | "uri" : "data:application/octet-stream;base64,AAAAAAEAAAADAAAAAgAAAAMAAAABAAAABAAAAAUAAAAHAAAABgAAAAcAAAAFAAAACAAAAAkAAAALAAAACgAAAAsAAAAJAAAADAAAAA0AAAAPAAAADgAAAA8AAAANAAAAEAAAABEAAAATAAAAEgAAABMAAAARAAAAFAAAABUAAAAXAAAAFgAAABcAAAAVAAAA" 109 | }, 110 | { 111 | "byteLength" : 1123, 112 | "uri" : "data:application/octet-stream;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAEKklEQVRIS7WWWW8bRwzH/+TM3pIlHwncALmAJGiQokBQNC/59P0ABfrQAj3ROHLsWHGklVbaYy72QYmtOrFk9Pg/LBbLGf645JC79PKl4HopFYqiHgxGRfFjHP8ATPM8f/z48fPnzx89ejQYDJRSG7YD4M1m77ltE+eKEHZECoDiOO73+0VRxHFMRJu3YysAgAiJKBElwgCISGsdx7HW+r8BMAuzJzJEhghExMw3cb3SFgCzxLFVqlFqRrQUEWuttTaEILKpeBfaCghx3ETRe+Z3QAPAObdcLuu6ds7dhLEJQCRJYrNsEkVj5gngARhjZrNZVVXL5TKEsGH7StcCiBBFLs+rJDnT+pioBARACGE8Ho9Go7Ism6bZytDXGrTv9ZZF8SZJ/lDqFHCr5yJS1/XR0dFgMEjTFECWZUqp9bLLR3nvdRw7a5XIpZlIosj1+1W/P8qyn5R6BcyBy0i992/fvo2iCMCTJ0/29/fzPNdaM7OIhBC8913XdV1XlqXe3z9dLne7LgmBAVEqpGmXZbM8f50kv2r9OzBbZX9d1trj42NrbdM09+/fHw6HvV6PmQF0XVfXdVmW0+l0PB7r4fC7orjXdbdCiEVY61rrWRy/0fo18ynQrFL/qYwxJycndV2fnZ0dHh7u7Oykaeq9b5qmLMvz8/PJZFJVlY6i76PolzTdCyEDFFHNXBNNgebTwK/Iez+dTquqOj09TdM0SZIQgrW2ruumaVbtooEO6JgnzKsyyNp1u0IIxpjJZHKlyBf3F6dIbu70s7qu6bbPon+p/x3wIUUEMBADGmDAARZw/yxxF7UQANAMaGBXZDeE4eqoAg3znOicuSIy6z22QStHfYRcoAWOqAW1pPdFHnj/0JjbxvSc094HolbrmdYnSTLS+kSpOeA3v4qC7Im/5+1d5wc+RIEt84L1VOlvjXm6XN5aLLK2ZedIBIBXyqTpnTS9k+d/JslvUXTO3F7HUAh3g/nKLr+sm73GJs5TUIGVVeki0d9Mpwfzue46Wjtn7L22Nm2awpjdougXxc9RdKRU+znvcijti7Z8Np/vLp1ygT74IaFFr9YHZRkZ8+lGElHW9qoqck6HgF7PJMmI2a0vYsgtaV9006/n5bDy/LfOFxKnvI6sxfVi75O6PiB6qPV7rd8xLy5twBDmmZ0/q2aDq97XVm377HEISdvudN0X1u6uj/UI7p5rnjbz/YVX106tGzUaeZ9Zm4eQXERDkKHYB66+3ZjIbYjxZgCARaIQ1AWAEfaCPbAmtYE2Dd0bAQAI4In8xchUkFR85p32a1n7jG4EECKnVKtUtwZABFEitK2Em80reaXqOJ5f6QMLdszCm//xtgOCUm2ez6NorNScPgbsQYZUrSKjaWOOtgCCUibLZln2Kk1HSl12mQPPWU91WiXaRRsIfwFLR1Yn2H2OFgAAAABJRU5ErkJggg==" 113 | }, 114 | { 115 | "byteLength" : 288, 116 | "uri" : "data:application/octet-stream;base64,zcxMPZqZGb3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGT3NzMw8zcxMPZqZGb3NzMw8zcxMvZqZGT3NzMy8zcxMvZqZGb3NzMy8zcxMvZqZGb3NzMw8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMy8zcxMvZqZGT3NzMy8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGb3NzMy8zcxMPZqZGb3NzMy8zcxMPZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMPZqZGb3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGT3NzMw8zcxMvZqZGT3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGb3NzMy8zcxMvZqZGb3NzMy8" 117 | }, 118 | { 119 | "byteLength" : 192, 120 | "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/" 121 | }, 122 | { 123 | "byteLength" : 144, 124 | "uri" : "data:application/octet-stream;base64,AAAAAAEAAAADAAAAAgAAAAMAAAABAAAABAAAAAUAAAAHAAAABgAAAAcAAAAFAAAACAAAAAkAAAALAAAACgAAAAsAAAAJAAAADAAAAA0AAAAPAAAADgAAAA8AAAANAAAAEAAAABEAAAATAAAAEgAAABMAAAARAAAAFAAAABUAAAAXAAAAFgAAABcAAAAVAAAA" 125 | } 126 | ], 127 | "cameras" : 128 | [ 129 | { 130 | "perspective" : 131 | { 132 | "aspectRatio" : 1.3333333333333333, 133 | "yfov" : 0.78539816339744828, 134 | "zfar" : 10.0, 135 | "znear" : 0.01 136 | }, 137 | "type" : "perspective" 138 | } 139 | ], 140 | "images" : 141 | [ 142 | { 143 | "bufferView" : 3, 144 | "mimeType" : "image/png" 145 | } 146 | ], 147 | "materials" : 148 | [ 149 | { 150 | "pbrMetallicRoughness" : 151 | { 152 | "baseColorFactor" : [ 1.0, 1.0, 1.0, 1.0 ], 153 | "baseColorTexture" : 154 | { 155 | "index" : 0, 156 | "texCoord" : 0 157 | }, 158 | "metallicFactor" : 0.0, 159 | "roughnessFactor" : 1.0 160 | } 161 | }, 162 | { 163 | "pbrMetallicRoughness" : 164 | { 165 | "baseColorFactor" : [ 1.0, 0.25, 0.25, 1.0 ], 166 | "metallicFactor" : 0.0, 167 | "roughnessFactor" : 1.0 168 | } 169 | } 170 | ], 171 | "meshes" : 172 | [ 173 | { 174 | "name" : "mesh0", 175 | "primitives" : 176 | [ 177 | { 178 | "attributes" : 179 | { 180 | "POSITION" : 0, 181 | "TEXCOORD_0" : 1 182 | }, 183 | "indices" : 2, 184 | "material" : 0, 185 | "mode" : 4 186 | } 187 | ] 188 | }, 189 | { 190 | "name" : "mesh1", 191 | "primitives" : 192 | [ 193 | { 194 | "attributes" : 195 | { 196 | "POSITION" : 3, 197 | "TEXCOORD_0" : 4 198 | }, 199 | "indices" : 5, 200 | "material" : 1, 201 | "mode" : 4 202 | } 203 | ] 204 | } 205 | ], 206 | "nodes" : 207 | [ 208 | { 209 | "matrix" : 210 | [ 211 | 1.0, 212 | 0.0, 213 | 0.0, 214 | 0.0, 215 | 0.0, 216 | 1.0, 217 | 0.0, 218 | 0.0, 219 | 0.0, 220 | 0.0, 221 | 1.0, 222 | 0.0, 223 | 0.0, 224 | 0.10000000000000001, 225 | 0.0, 226 | 1.0 227 | ], 228 | "mesh" : 0, 229 | "name" : "mesh0" 230 | }, 231 | { 232 | "matrix" : 233 | [ 234 | 1.0, 235 | 0.0, 236 | 0.0, 237 | 0.0, 238 | 0.0, 239 | 1.0, 240 | 0.0, 241 | 0.0, 242 | 0.0, 243 | 0.0, 244 | 1.0, 245 | 0.0, 246 | 0.0, 247 | -0.10000000000000001, 248 | 0.0, 249 | 1.0 250 | ], 251 | "mesh" : 1, 252 | "name" : "mesh1" 253 | }, 254 | { 255 | "camera" : 0, 256 | "matrix" : 257 | [ 258 | 0.00079632671073324817, 259 | 0.99999968293183461, 260 | 0.0, 261 | 0.0, 262 | -0.50484594452922937, 263 | 0.00040202243790249793, 264 | 0.86320936664887371, 265 | 0.0, 266 | 0.86320909295266346, 267 | -0.00068739667561762798, 268 | 0.50484610459985757, 269 | 0.0, 270 | 0.29999999999999993, 271 | 4.5536491244391186e-18, 272 | 0.20000000000000004, 273 | 1.0 274 | ], 275 | "name" : "Camera Node" 276 | }, 277 | { 278 | "children" : [ 0, 1, 2 ], 279 | "name" : "Renderer Node" 280 | } 281 | ], 282 | "samplers" : 283 | [ 284 | { 285 | "magFilter" : 9729, 286 | "minFilter" : 9729, 287 | "wrapS" : 33071, 288 | "wrapT" : 33071 289 | } 290 | ], 291 | "scene" : 0, 292 | "scenes" : 293 | [ 294 | { 295 | "name" : "Layer 0", 296 | "nodes" : [ 3 ] 297 | } 298 | ], 299 | "textures" : 300 | [ 301 | { 302 | "sampler" : 0, 303 | "source" : 0 304 | } 305 | ] 306 | } -------------------------------------------------------------------------------- /test/one_texture_box.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/one_texture_box.blend -------------------------------------------------------------------------------- /test/requirements.in: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | # To compile this file into requirements.txt, run: 4 | # ../tools/upgrade.sh 5 | 6 | black 7 | isort 8 | numpy 9 | pillow 10 | pycodestyle 11 | requests 12 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # bazel run //test:requirements.update 6 | # 7 | black==24.10.0 \ 8 | --hash=sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f \ 9 | --hash=sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd \ 10 | --hash=sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea \ 11 | --hash=sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981 \ 12 | --hash=sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b \ 13 | --hash=sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7 \ 14 | --hash=sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8 \ 15 | --hash=sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175 \ 16 | --hash=sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d \ 17 | --hash=sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392 \ 18 | --hash=sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad \ 19 | --hash=sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f \ 20 | --hash=sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f \ 21 | --hash=sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b \ 22 | --hash=sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875 \ 23 | --hash=sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3 \ 24 | --hash=sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800 \ 25 | --hash=sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65 \ 26 | --hash=sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2 \ 27 | --hash=sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812 \ 28 | --hash=sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50 \ 29 | --hash=sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e 30 | # via -r test/requirements.in 31 | certifi==2024.8.30 \ 32 | --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ 33 | --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 34 | # via requests 35 | charset-normalizer==3.4.0 \ 36 | --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ 37 | --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ 38 | --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ 39 | --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ 40 | --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ 41 | --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ 42 | --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ 43 | --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ 44 | --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ 45 | --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ 46 | --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ 47 | --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ 48 | --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ 49 | --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ 50 | --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ 51 | --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ 52 | --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ 53 | --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ 54 | --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ 55 | --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ 56 | --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ 57 | --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ 58 | --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ 59 | --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ 60 | --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ 61 | --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ 62 | --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ 63 | --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ 64 | --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ 65 | --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ 66 | --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ 67 | --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ 68 | --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ 69 | --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ 70 | --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ 71 | --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ 72 | --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ 73 | --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ 74 | --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ 75 | --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ 76 | --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ 77 | --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ 78 | --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ 79 | --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ 80 | --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ 81 | --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ 82 | --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ 83 | --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ 84 | --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ 85 | --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ 86 | --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ 87 | --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ 88 | --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ 89 | --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ 90 | --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ 91 | --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ 92 | --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ 93 | --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ 94 | --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ 95 | --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ 96 | --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ 97 | --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ 98 | --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ 99 | --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ 100 | --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ 101 | --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ 102 | --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ 103 | --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ 104 | --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ 105 | --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ 106 | --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ 107 | --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ 108 | --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ 109 | --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ 110 | --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ 111 | --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ 112 | --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ 113 | --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ 114 | --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ 115 | --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ 116 | --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ 117 | --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ 118 | --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ 119 | --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ 120 | --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ 121 | --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ 122 | --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ 123 | --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ 124 | --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ 125 | --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ 126 | --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ 127 | --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ 128 | --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ 129 | --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ 130 | --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ 131 | --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ 132 | --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ 133 | --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ 134 | --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ 135 | --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ 136 | --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ 137 | --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ 138 | --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ 139 | --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ 140 | --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 141 | # via requests 142 | click==8.1.7 \ 143 | --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ 144 | --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de 145 | # via black 146 | idna==3.10 \ 147 | --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ 148 | --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 149 | # via requests 150 | isort==5.13.2 \ 151 | --hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \ 152 | --hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6 153 | # via -r test/requirements.in 154 | mypy-extensions==1.0.0 \ 155 | --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ 156 | --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 157 | # via black 158 | numpy==2.2.0 \ 159 | --hash=sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608 \ 160 | --hash=sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef \ 161 | --hash=sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90 \ 162 | --hash=sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae \ 163 | --hash=sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83 \ 164 | --hash=sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0 \ 165 | --hash=sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73 \ 166 | --hash=sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671 \ 167 | --hash=sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69 \ 168 | --hash=sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa \ 169 | --hash=sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066 \ 170 | --hash=sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da \ 171 | --hash=sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9 \ 172 | --hash=sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e \ 173 | --hash=sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3 \ 174 | --hash=sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a \ 175 | --hash=sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74 \ 176 | --hash=sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3 \ 177 | --hash=sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410 \ 178 | --hash=sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72 \ 179 | --hash=sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d \ 180 | --hash=sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4 \ 181 | --hash=sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038 \ 182 | --hash=sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e \ 183 | --hash=sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13 \ 184 | --hash=sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d \ 185 | --hash=sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95 \ 186 | --hash=sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31 \ 187 | --hash=sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3 \ 188 | --hash=sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03 \ 189 | --hash=sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6 \ 190 | --hash=sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2 \ 191 | --hash=sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b \ 192 | --hash=sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7 \ 193 | --hash=sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab \ 194 | --hash=sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219 \ 195 | --hash=sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571 \ 196 | --hash=sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d \ 197 | --hash=sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1 \ 198 | --hash=sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca \ 199 | --hash=sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661 \ 200 | --hash=sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e \ 201 | --hash=sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e \ 202 | --hash=sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e \ 203 | --hash=sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a \ 204 | --hash=sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3 \ 205 | --hash=sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881 \ 206 | --hash=sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221 \ 207 | --hash=sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742 \ 208 | --hash=sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773 \ 209 | --hash=sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e \ 210 | --hash=sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529 \ 211 | --hash=sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67 \ 212 | --hash=sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c \ 213 | --hash=sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367 214 | # via -r test/requirements.in 215 | packaging==24.2 \ 216 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ 217 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f 218 | # via black 219 | pathspec==0.12.1 \ 220 | --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ 221 | --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 222 | # via black 223 | pillow==11.0.0 \ 224 | --hash=sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7 \ 225 | --hash=sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5 \ 226 | --hash=sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903 \ 227 | --hash=sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2 \ 228 | --hash=sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38 \ 229 | --hash=sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2 \ 230 | --hash=sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9 \ 231 | --hash=sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f \ 232 | --hash=sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc \ 233 | --hash=sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8 \ 234 | --hash=sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d \ 235 | --hash=sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2 \ 236 | --hash=sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316 \ 237 | --hash=sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a \ 238 | --hash=sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25 \ 239 | --hash=sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd \ 240 | --hash=sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba \ 241 | --hash=sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc \ 242 | --hash=sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273 \ 243 | --hash=sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa \ 244 | --hash=sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a \ 245 | --hash=sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b \ 246 | --hash=sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a \ 247 | --hash=sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae \ 248 | --hash=sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291 \ 249 | --hash=sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97 \ 250 | --hash=sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06 \ 251 | --hash=sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904 \ 252 | --hash=sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b \ 253 | --hash=sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b \ 254 | --hash=sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8 \ 255 | --hash=sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527 \ 256 | --hash=sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947 \ 257 | --hash=sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb \ 258 | --hash=sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003 \ 259 | --hash=sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5 \ 260 | --hash=sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f \ 261 | --hash=sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739 \ 262 | --hash=sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944 \ 263 | --hash=sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830 \ 264 | --hash=sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f \ 265 | --hash=sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3 \ 266 | --hash=sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4 \ 267 | --hash=sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84 \ 268 | --hash=sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7 \ 269 | --hash=sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6 \ 270 | --hash=sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6 \ 271 | --hash=sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9 \ 272 | --hash=sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de \ 273 | --hash=sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4 \ 274 | --hash=sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47 \ 275 | --hash=sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd \ 276 | --hash=sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50 \ 277 | --hash=sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c \ 278 | --hash=sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086 \ 279 | --hash=sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba \ 280 | --hash=sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306 \ 281 | --hash=sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699 \ 282 | --hash=sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e \ 283 | --hash=sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488 \ 284 | --hash=sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa \ 285 | --hash=sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2 \ 286 | --hash=sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3 \ 287 | --hash=sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9 \ 288 | --hash=sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923 \ 289 | --hash=sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2 \ 290 | --hash=sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790 \ 291 | --hash=sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734 \ 292 | --hash=sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916 \ 293 | --hash=sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1 \ 294 | --hash=sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f \ 295 | --hash=sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798 \ 296 | --hash=sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb \ 297 | --hash=sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2 \ 298 | --hash=sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9 299 | # via -r test/requirements.in 300 | platformdirs==4.3.6 \ 301 | --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ 302 | --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb 303 | # via black 304 | pycodestyle==2.12.1 \ 305 | --hash=sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3 \ 306 | --hash=sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521 307 | # via -r test/requirements.in 308 | requests==2.32.3 \ 309 | --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ 310 | --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 311 | # via -r test/requirements.in 312 | tomli==2.2.1 \ 313 | --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ 314 | --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ 315 | --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ 316 | --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ 317 | --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ 318 | --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ 319 | --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ 320 | --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ 321 | --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ 322 | --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ 323 | --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ 324 | --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ 325 | --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ 326 | --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ 327 | --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ 328 | --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ 329 | --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ 330 | --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ 331 | --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ 332 | --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ 333 | --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ 334 | --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ 335 | --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ 336 | --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ 337 | --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ 338 | --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ 339 | --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ 340 | --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ 341 | --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ 342 | --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ 343 | --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ 344 | --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 345 | # via black 346 | typing-extensions==4.12.2 \ 347 | --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ 348 | --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 349 | # via black 350 | urllib3==2.2.3 \ 351 | --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ 352 | --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 353 | # via requests 354 | -------------------------------------------------------------------------------- /test/server_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | from collections import namedtuple 4 | import datetime 5 | import json 6 | import os 7 | from pathlib import Path 8 | import re 9 | import shutil 10 | import signal 11 | import subprocess 12 | import sys 13 | import time 14 | import unittest 15 | 16 | from PIL import Image 17 | import numpy as np 18 | import requests 19 | 20 | COLOR_PIXEL_THRESHOLD = 0 21 | DEPTH_PIXEL_THRESHOLD = 1 # Depth measurement tolerance in millimeters. 22 | LABEL_PIXEL_THRESHOLD = 0 23 | INVALID_PIXEL_FRACTION = 0.02 24 | 25 | # The most basic glTF file containing two diffuse color boxes for testing. 26 | DEFAULT_GLTF_FILE = "test/two_rgba_boxes.gltf" 27 | # The basic blend file for testing. It contains only a texture box and a 28 | # default point light that is added in `server.py`. 29 | DEFAULT_BLEND_FILE = "test/one_texture_box.blend" 30 | 31 | 32 | class ServerFixture(unittest.TestCase): 33 | """Encapsulates the testing infrastructure, e.g., starting and stopping the 34 | server subprocess, sending rendering requests, and conducting the per-pixel 35 | image differencing. 36 | """ 37 | 38 | def setUp(self, extra_server_args=None): 39 | # Start the server on the other process. Bind to port 0 and let the OS 40 | # assign an available port later on. 41 | server_path = Path("server").absolute().resolve() 42 | server_args = [ 43 | server_path, 44 | "--host=127.0.0.1", 45 | "--port=0", 46 | ] 47 | # Append extra server args, e.g., the path to a blend file. 48 | if extra_server_args: 49 | server_args.extend(extra_server_args) 50 | 51 | self.server_proc = subprocess.Popen( 52 | server_args, 53 | stdout=subprocess.PIPE, 54 | stderr=subprocess.STDOUT, 55 | ) 56 | 57 | # Wait to hear which port it's using. 58 | self.server_port = None 59 | start_time = time.time() 60 | while time.time() < start_time + 30.0: 61 | line = self.server_proc.stdout.readline().decode("utf-8") 62 | print(f"[server] {line}", file=sys.stderr, end="") 63 | match = re.search(r"Running on http://127.0.0.1:([0-9]+)", line) 64 | if match: 65 | (self.server_port,) = match.groups() 66 | break 67 | else: 68 | self.fail("Could not connect after 30 seconds") 69 | 70 | def tearDown(self): 71 | self.server_proc.terminate() 72 | self.assertEqual(self.server_proc.wait(10.0), -signal.SIGTERM) 73 | 74 | def _render_and_check( 75 | self, 76 | gltf_path, 77 | image_type, 78 | reference_image_path, 79 | threshold, 80 | invalid_fraction=INVALID_PIXEL_FRACTION, 81 | ): 82 | """The implementation of the per-pixel image differencing on a specific 83 | image_type. It first renders an image by calling the server, compares 84 | the result given a reference image and thresholds, and returns the path 85 | of the rendered image. 86 | """ 87 | with open(gltf_path, "rb") as scene: 88 | form_data = self._create_request_form(image_type=image_type) 89 | response = requests.post( 90 | url=f"http://127.0.0.1:{self.server_port}/render", 91 | data=form_data, 92 | files={"scene": scene}, 93 | stream=True, 94 | ) 95 | self.assertEqual(response.status_code, 200) 96 | 97 | # Save the output image for offline inspection. It will be archived 98 | # into `.bazel/testlogs/server_test/test.outputs/outputs.zip`. 99 | save_dir = Path(os.environ["TEST_UNDECLARED_OUTPUTS_DIR"]) 100 | timestamp = datetime.datetime.now().strftime("%H-%M-%S-%f") 101 | rendered_image_path = save_dir / f"{timestamp}.png" 102 | with open(rendered_image_path, "wb") as image: 103 | shutil.copyfileobj(response.raw, image) 104 | 105 | self._assert_images_equal( 106 | rendered_image_path, 107 | reference_image_path, 108 | threshold, 109 | invalid_fraction, 110 | ) 111 | return rendered_image_path 112 | 113 | @staticmethod 114 | def _create_request_form(*, image_type): 115 | # These properties are used when rendering the ground truth images. The 116 | # Blender server should use the same setting for testing. 117 | form_data = { 118 | "scene_sha256": "NOT_USED_IN_THE_TEST", 119 | "image_type": image_type, 120 | "width": "640", 121 | "height": "480", 122 | "near": "0.01", 123 | "far": "10.0", 124 | "focal_x": "579.411", 125 | "focal_y": "579.411", 126 | "fov_x": "0.785398", 127 | "fov_y": "0.785398", 128 | "center_x": "319.5", 129 | "center_y": "239.5", 130 | } 131 | if image_type == "depth": 132 | form_data["min_depth"] = 0.01 133 | form_data["max_depth"] = 10.0 134 | return form_data 135 | 136 | def _assert_images_equal( 137 | self, 138 | rendered_image_path, 139 | reference_image_path, 140 | threshold, 141 | invalid_fraction, 142 | ): 143 | # Compare the output image to the ground truth image (from git). 144 | test = np.array(Image.open(rendered_image_path)) 145 | compare_to = np.array(Image.open(reference_image_path)) 146 | image_diff = ( 147 | np.absolute(compare_to.astype(float) - test.astype(float)) 148 | > threshold 149 | ) 150 | 151 | image_diff_fraction = np.count_nonzero(image_diff) / image_diff.size 152 | self.assertLessEqual(image_diff_fraction, invalid_fraction) 153 | 154 | 155 | class RpcOnlyServerTest(ServerFixture): 156 | """Tests the server with only RPC data as input. No additional command line 157 | arguments nor *.blend files are involved. 158 | """ 159 | 160 | def test_color_render(self): 161 | self._render_and_check( 162 | gltf_path=DEFAULT_GLTF_FILE, 163 | image_type="color", 164 | reference_image_path="test/two_rgba_boxes.color.png", 165 | threshold=COLOR_PIXEL_THRESHOLD, 166 | ) 167 | 168 | def test_depth_render(self): 169 | self._render_and_check( 170 | gltf_path=DEFAULT_GLTF_FILE, 171 | image_type="depth", 172 | reference_image_path="test/depth.png", 173 | threshold=DEPTH_PIXEL_THRESHOLD, 174 | ) 175 | 176 | def test_label_render(self): 177 | self._render_and_check( 178 | gltf_path=DEFAULT_GLTF_FILE, 179 | image_type="label", 180 | reference_image_path="test/label.png", 181 | threshold=LABEL_PIXEL_THRESHOLD, 182 | ) 183 | 184 | # Test color and depth image rendering given a rgba and a textured mesh. 185 | # (We do not check a label image because by definition an RPC for a label 186 | # image will never contain any textured objects.) 187 | def test_texture_color_render(self): 188 | self._render_and_check( 189 | gltf_path="test/one_rgba_one_texture_boxes.gltf", 190 | image_type="color", 191 | reference_image_path="test/one_rgba_one_texture_boxes.color.png", 192 | threshold=COLOR_PIXEL_THRESHOLD, 193 | ) 194 | 195 | def test_texture_depth_render(self): 196 | self._render_and_check( 197 | gltf_path="test/one_rgba_one_texture_boxes.gltf", 198 | image_type="depth", 199 | reference_image_path="test/depth.png", 200 | threshold=DEPTH_PIXEL_THRESHOLD, 201 | ) 202 | 203 | def test_consistency(self): 204 | """Tests the consistency of the render results from consecutive 205 | requests. Each image type is first rendered and compared with the 206 | ground truth images. A second image is then rendered and expected to be 207 | pixel-identical as the first one. As with all things Drake, we expect 208 | reproducible simulations, so if there is any randomness in the render 209 | pipeline it's the responsibility of the server to configure it so that 210 | the exact same RPC call produces the exact same image output no matter 211 | how it's called or how many times it's called. 212 | """ 213 | TestCase = namedtuple( 214 | "TestCase", ["image_type", "reference_image", "threshold"] 215 | ) 216 | test_cases = [ 217 | TestCase( 218 | "color", "test/two_rgba_boxes.color.png", COLOR_PIXEL_THRESHOLD 219 | ), 220 | TestCase("depth", "test/depth.png", DEPTH_PIXEL_THRESHOLD), 221 | TestCase("label", "test/label.png", LABEL_PIXEL_THRESHOLD), 222 | ] 223 | 224 | returned_image_paths = [] 225 | for test_case in test_cases: 226 | first_image = self._render_and_check( 227 | gltf_path=DEFAULT_GLTF_FILE, 228 | image_type=test_case.image_type, 229 | reference_image_path=test_case.reference_image, 230 | threshold=test_case.threshold, 231 | ) 232 | returned_image_paths.append(first_image) 233 | 234 | for index, test_case in enumerate(test_cases): 235 | self._render_and_check( 236 | gltf_path=DEFAULT_GLTF_FILE, 237 | image_type=test_case.image_type, 238 | reference_image_path=returned_image_paths[index], 239 | threshold=0.0, 240 | invalid_fraction=0.0, 241 | ) 242 | 243 | 244 | class BlendFileServerTest(ServerFixture): 245 | """Tests the server with both RPC data and a blend file as input.""" 246 | 247 | def setUp(self): 248 | super().setUp(extra_server_args=[f"--blend_file={DEFAULT_BLEND_FILE}"]) 249 | 250 | def test_rpc_blend_color_render(self): 251 | self._render_and_check( 252 | gltf_path="test/one_rgba_box.gltf", 253 | image_type="color", 254 | reference_image_path="test/one_rgba_one_texture_boxes.color.png", 255 | threshold=COLOR_PIXEL_THRESHOLD, 256 | ) 257 | 258 | def test_rpc_blend_depth_render(self): 259 | self._render_and_check( 260 | gltf_path="test/one_rgba_box.gltf", 261 | image_type="depth", 262 | reference_image_path="test/depth.png", 263 | threshold=DEPTH_PIXEL_THRESHOLD, 264 | ) 265 | 266 | def test_rpc_blend_label_render(self): 267 | # See label_render_settings() for more details. The meshes loaded via a 268 | # blend file will be treated as the background in a label image. 269 | self._render_and_check( 270 | gltf_path="test/one_rgba_box.gltf", 271 | image_type="label", 272 | reference_image_path="test/one_gltf_one_blend.label.png", 273 | threshold=LABEL_PIXEL_THRESHOLD, 274 | ) 275 | 276 | 277 | class ExtraSettingsServerTest(ServerFixture): 278 | """Tests the server against custom settings files.""" 279 | 280 | def setUp(self): 281 | # Create a placeholder settings file. 282 | tmpdir = Path(os.environ["TEST_TMPDIR"]) 283 | self._settings_path = tmpdir / "bpy_settings.py" 284 | with open(self._settings_path, "w", encoding="utf-8") as f: 285 | pass 286 | 287 | # Tell the server to use it. 288 | args = [f"--bpy_settings_file={self._settings_path}"] 289 | super().setUp(extra_server_args=args) 290 | 291 | def _call_rpc(self, status_code=200): 292 | """Makes a basic RPC call and returns the http response.""" 293 | with open("test/one_rgba_box.gltf", "rb") as scene: 294 | response = requests.post( 295 | url=f"http://127.0.0.1:{self.server_port}/render", 296 | data=self._create_request_form(image_type="color"), 297 | files={"scene": scene}, 298 | stream=True, 299 | ) 300 | return response 301 | 302 | def test_settings_no_crash(self): 303 | """Checks that a valid settings file produces no errors.""" 304 | with open(self._settings_path, "w", encoding="utf-8") as f: 305 | f.write("bpy.context.scene.render.threads = 1") 306 | response = self._call_rpc() 307 | self.assertEqual(response.status_code, 200) 308 | 309 | def test_settings_not_noop(self): 310 | """Checks that an invalid settings file produces errors.""" 311 | with open(self._settings_path, "w", encoding="utf-8") as f: 312 | f.write("raise RuntimeError('Kilroy was here')") 313 | response = self._call_rpc() 314 | self.assertEqual(response.status_code, 500) 315 | error = json.loads(response.text) 316 | self.assertIn("Kilroy was here", error["message"]) 317 | 318 | 319 | if __name__ == "__main__": 320 | unittest.main() 321 | -------------------------------------------------------------------------------- /test/two_rgba_boxes.color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLocomotion/drake-blender/9f33419aab004d3c78948ab9041aceae31f7e832/test/two_rgba_boxes.color.png -------------------------------------------------------------------------------- /test/two_rgba_boxes.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors" : 3 | [ 4 | { 5 | "bufferView" : 0, 6 | "byteOffset" : 0, 7 | "componentType" : 5126, 8 | "count" : 24, 9 | "max" : [ 0.050000000000000003, 0.037499999999999999, 0.025000000000000001 ], 10 | "min" : [ -0.050000000000000003, -0.037499999999999999, -0.025000000000000001 ], 11 | "type" : "VEC3" 12 | }, 13 | { 14 | "bufferView" : 1, 15 | "byteOffset" : 0, 16 | "componentType" : 5126, 17 | "count" : 24, 18 | "normalized" : false, 19 | "type" : "VEC2" 20 | }, 21 | { 22 | "bufferView" : 2, 23 | "byteOffset" : 0, 24 | "componentType" : 5125, 25 | "count" : 36, 26 | "type" : "SCALAR" 27 | }, 28 | { 29 | "bufferView" : 3, 30 | "byteOffset" : 0, 31 | "componentType" : 5126, 32 | "count" : 24, 33 | "max" : [ 0.050000000000000003, 0.037499999999999999, 0.025000000000000001 ], 34 | "min" : [ -0.050000000000000003, -0.037499999999999999, -0.025000000000000001 ], 35 | "type" : "VEC3" 36 | }, 37 | { 38 | "bufferView" : 4, 39 | "byteOffset" : 0, 40 | "componentType" : 5126, 41 | "count" : 24, 42 | "normalized" : false, 43 | "type" : "VEC2" 44 | }, 45 | { 46 | "bufferView" : 5, 47 | "byteOffset" : 0, 48 | "componentType" : 5125, 49 | "count" : 36, 50 | "type" : "SCALAR" 51 | } 52 | ], 53 | "asset" : 54 | { 55 | "generator" : "VTK", 56 | "version" : "2.0" 57 | }, 58 | "bufferViews" : 59 | [ 60 | { 61 | "buffer" : 0, 62 | "byteLength" : 288, 63 | "byteOffset" : 0 64 | }, 65 | { 66 | "buffer" : 1, 67 | "byteLength" : 192, 68 | "byteOffset" : 0 69 | }, 70 | { 71 | "buffer" : 2, 72 | "byteLength" : 144, 73 | "byteOffset" : 0 74 | }, 75 | { 76 | "buffer" : 3, 77 | "byteLength" : 288, 78 | "byteOffset" : 0 79 | }, 80 | { 81 | "buffer" : 4, 82 | "byteLength" : 192, 83 | "byteOffset" : 0 84 | }, 85 | { 86 | "buffer" : 5, 87 | "byteLength" : 144, 88 | "byteOffset" : 0 89 | } 90 | ], 91 | "buffers" : 92 | [ 93 | { 94 | "byteLength" : 288, 95 | "uri" : "data:application/octet-stream;base64,zcxMPZqZGb3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGT3NzMw8zcxMPZqZGb3NzMw8zcxMvZqZGT3NzMy8zcxMvZqZGb3NzMy8zcxMvZqZGb3NzMw8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMy8zcxMvZqZGT3NzMy8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGb3NzMy8zcxMPZqZGb3NzMy8zcxMPZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMPZqZGb3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGT3NzMw8zcxMvZqZGT3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGb3NzMy8zcxMvZqZGb3NzMy8" 96 | }, 97 | { 98 | "byteLength" : 192, 99 | "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/" 100 | }, 101 | { 102 | "byteLength" : 144, 103 | "uri" : "data:application/octet-stream;base64,AAAAAAEAAAADAAAAAgAAAAMAAAABAAAABAAAAAUAAAAHAAAABgAAAAcAAAAFAAAACAAAAAkAAAALAAAACgAAAAsAAAAJAAAADAAAAA0AAAAPAAAADgAAAA8AAAANAAAAEAAAABEAAAATAAAAEgAAABMAAAARAAAAFAAAABUAAAAXAAAAFgAAABcAAAAVAAAA" 104 | }, 105 | { 106 | "byteLength" : 288, 107 | "uri" : "data:application/octet-stream;base64,zcxMPZqZGb3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGT3NzMw8zcxMPZqZGb3NzMw8zcxMvZqZGT3NzMy8zcxMvZqZGb3NzMy8zcxMvZqZGb3NzMw8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMy8zcxMvZqZGT3NzMy8zcxMvZqZGT3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGb3NzMy8zcxMPZqZGb3NzMy8zcxMPZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMvZqZGb3NzMw8zcxMPZqZGb3NzMw8zcxMPZqZGT3NzMw8zcxMvZqZGT3NzMw8zcxMvZqZGT3NzMy8zcxMPZqZGT3NzMy8zcxMPZqZGb3NzMy8zcxMvZqZGb3NzMy8" 108 | }, 109 | { 110 | "byteLength" : 192, 111 | "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAIA/AACAPwAAAAAAAIA/" 112 | }, 113 | { 114 | "byteLength" : 144, 115 | "uri" : "data:application/octet-stream;base64,AAAAAAEAAAADAAAAAgAAAAMAAAABAAAABAAAAAUAAAAHAAAABgAAAAcAAAAFAAAACAAAAAkAAAALAAAACgAAAAsAAAAJAAAADAAAAA0AAAAPAAAADgAAAA8AAAANAAAAEAAAABEAAAATAAAAEgAAABMAAAARAAAAFAAAABUAAAAXAAAAFgAAABcAAAAVAAAA" 116 | } 117 | ], 118 | "cameras" : 119 | [ 120 | { 121 | "perspective" : 122 | { 123 | "aspectRatio" : 1.3333333333333333, 124 | "yfov" : 0.78539816339744828, 125 | "zfar" : 10.0, 126 | "znear" : 0.01 127 | }, 128 | "type" : "perspective" 129 | } 130 | ], 131 | "materials" : 132 | [ 133 | { 134 | "pbrMetallicRoughness" : 135 | { 136 | "baseColorFactor" : [ 0.25, 1.0, 0.25, 1.0 ], 137 | "metallicFactor" : 0.0, 138 | "roughnessFactor" : 1.0 139 | } 140 | }, 141 | { 142 | "pbrMetallicRoughness" : 143 | { 144 | "baseColorFactor" : [ 1.0, 0.25, 0.25, 1.0 ], 145 | "metallicFactor" : 0.0, 146 | "roughnessFactor" : 1.0 147 | } 148 | } 149 | ], 150 | "meshes" : 151 | [ 152 | { 153 | "name" : "mesh0", 154 | "primitives" : 155 | [ 156 | { 157 | "attributes" : 158 | { 159 | "POSITION" : 0, 160 | "TEXCOORD_0" : 1 161 | }, 162 | "indices" : 2, 163 | "material" : 0, 164 | "mode" : 4 165 | } 166 | ] 167 | }, 168 | { 169 | "name" : "mesh1", 170 | "primitives" : 171 | [ 172 | { 173 | "attributes" : 174 | { 175 | "POSITION" : 3, 176 | "TEXCOORD_0" : 4 177 | }, 178 | "indices" : 5, 179 | "material" : 1, 180 | "mode" : 4 181 | } 182 | ] 183 | } 184 | ], 185 | "nodes" : 186 | [ 187 | { 188 | "matrix" : 189 | [ 190 | 1.0, 191 | 0.0, 192 | 0.0, 193 | 0.0, 194 | 0.0, 195 | 1.0, 196 | 0.0, 197 | 0.0, 198 | 0.0, 199 | 0.0, 200 | 1.0, 201 | 0.0, 202 | 0.0, 203 | 0.10000000000000001, 204 | 0.0, 205 | 1.0 206 | ], 207 | "mesh" : 0, 208 | "name" : "mesh0" 209 | }, 210 | { 211 | "matrix" : 212 | [ 213 | 1.0, 214 | 0.0, 215 | 0.0, 216 | 0.0, 217 | 0.0, 218 | 1.0, 219 | 0.0, 220 | 0.0, 221 | 0.0, 222 | 0.0, 223 | 1.0, 224 | 0.0, 225 | 0.0, 226 | -0.10000000000000001, 227 | 0.0, 228 | 1.0 229 | ], 230 | "mesh" : 1, 231 | "name" : "mesh1" 232 | }, 233 | { 234 | "camera" : 0, 235 | "matrix" : 236 | [ 237 | 0.00079632671073324817, 238 | 0.99999968293183461, 239 | 0.0, 240 | 0.0, 241 | -0.50484594452922937, 242 | 0.00040202243790249793, 243 | 0.86320936664887371, 244 | 0.0, 245 | 0.86320909295266346, 246 | -0.00068739667561762798, 247 | 0.50484610459985757, 248 | 0.0, 249 | 0.29999999999999993, 250 | 4.5536491244391186e-18, 251 | 0.20000000000000004, 252 | 1.0 253 | ], 254 | "name" : "Camera Node" 255 | }, 256 | { 257 | "children" : [ 1, 2 ], 258 | "name" : "Renderer Node" 259 | } 260 | ], 261 | "scene" : 0, 262 | "scenes" : 263 | [ 264 | { 265 | "name" : "Layer 0", 266 | "nodes" : [ 0, 3 ] 267 | } 268 | ] 269 | } -------------------------------------------------------------------------------- /tools/BUILD.bazel: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | load("@rules_python//python:defs.bzl", "py_binary") 4 | load("//tools:defs.bzl", "bazel_lint_test", "pip", "py_lint_test") 5 | 6 | exports_files([ 7 | "black_main.py", 8 | "isort_main.py", 9 | "pycodestyle_main.py", 10 | ]) 11 | 12 | config_setting( 13 | name = "darwin-amd64", 14 | constraint_values = [ 15 | "@platforms//os:osx", 16 | "@platforms//cpu:x86_64", 17 | ], 18 | ) 19 | 20 | config_setting( 21 | name = "darwin-arm64", 22 | constraint_values = [ 23 | "@platforms//os:osx", 24 | "@platforms//cpu:arm64", 25 | ], 26 | ) 27 | 28 | config_setting( 29 | name = "linux-amd64", 30 | constraint_values = [ 31 | "@platforms//os:linux", 32 | "@platforms//cpu:x86_64", 33 | ], 34 | ) 35 | 36 | config_setting( 37 | name = "linux-arm64", 38 | constraint_values = [ 39 | "@platforms//os:linux", 40 | "@platforms//cpu:arm64", 41 | ], 42 | ) 43 | 44 | alias( 45 | name = "buildifier", 46 | actual = select({ 47 | ":darwin-amd64": "@buildifier-darwin-amd64//file:downloaded", 48 | ":darwin-arm64": "@buildifier-darwin-arm64//file:downloaded", 49 | ":linux-amd64": "@buildifier-linux-amd64//file:downloaded", 50 | ":linux-arm64": "@buildifier-linux-arm64//file:downloaded", 51 | "//conditions:default": "@platforms//:incompatible", 52 | }), 53 | visibility = ["//:__subpackages__"], 54 | ) 55 | 56 | py_binary( 57 | name = "black", 58 | testonly = True, 59 | srcs = ["black_main.py"], 60 | main = "black_main.py", 61 | tags = ["manual"], 62 | deps = [pip("black", "[test]")], 63 | ) 64 | 65 | py_binary( 66 | name = "pycodestyle", 67 | testonly = True, 68 | srcs = ["pycodestyle_main.py"], 69 | main = "pycodestyle_main.py", 70 | tags = ["manual"], 71 | deps = [pip("pycodestyle", "[test]")], 72 | ) 73 | 74 | py_binary( 75 | name = "isort", 76 | testonly = True, 77 | srcs = ["isort_main.py"], 78 | main = "isort_main.py", 79 | tags = ["manual"], 80 | deps = [pip("isort", "[test]")], 81 | ) 82 | 83 | bazel_lint_test( 84 | name = "bazel_lint_test", 85 | srcs = [ 86 | "BUILD.bazel", 87 | "buildifier_repositories.bzl", 88 | "buildifier_version.bzl", 89 | "defs.bzl", 90 | ], 91 | ) 92 | 93 | py_lint_test( 94 | name = "py_lint_test", 95 | srcs = [ 96 | "black_main.py", 97 | "isort_main.py", 98 | "pycodestyle_main.py", 99 | "upgrade_helper.py", 100 | ], 101 | ) 102 | -------------------------------------------------------------------------------- /tools/black_main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | import sys 4 | 5 | from black import patched_main as _black_main 6 | 7 | 8 | def _main(): 9 | try: 10 | _black_main() 11 | except SystemExit as e: 12 | if e.code is not None and e.code != 0: 13 | print( 14 | "INFO: Run ./tools/fix_lint.sh to automatically fix this " 15 | "problem.", 16 | file=sys.stderr, 17 | ) 18 | raise 19 | 20 | 21 | assert __name__ == "__main__" 22 | _main() 23 | -------------------------------------------------------------------------------- /tools/buildifier_repositories.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") 2 | load("//tools:buildifier_version.bzl", "BUILDIFIER_VERSION") 3 | 4 | def _impl(_ctx): 5 | releases = "https://github.com/bazelbuild/buildtools/releases" 6 | version = BUILDIFIER_VERSION["version"] 7 | for name, sha256 in BUILDIFIER_VERSION["binaries"].items(): 8 | http_file( 9 | name = name, 10 | executable = True, 11 | sha256 = sha256, 12 | url = "{releases}/download/v{version}/{name}".format( 13 | releases = releases, 14 | version = version, 15 | name = name, 16 | ), 17 | ) 18 | 19 | buildifier_repositories = module_extension( 20 | implementation = _impl, 21 | ) 22 | -------------------------------------------------------------------------------- /tools/buildifier_version.bzl: -------------------------------------------------------------------------------- 1 | BUILDIFIER_VERSION = { 2 | "version": "7.3.1", 3 | "binaries": { 4 | "buildifier-darwin-amd64": "375f823103d01620aaec20a0c29c6cbca99f4fd0725ae30b93655c6704f44d71", 5 | "buildifier-darwin-arm64": "5a6afc6ac7a09f5455ba0b89bd99d5ae23b4174dc5dc9d6c0ed5ce8caac3f813", 6 | "buildifier-linux-amd64": "5474cc5128a74e806783d54081f581662c4be8ae65022f557e9281ed5dc88009", 7 | "buildifier-linux-arm64": "0bf86c4bfffaf4f08eed77bde5b2082e4ae5039a11e2e8b03984c173c34a561c", 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /tools/defs.bzl: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | load("@examples_requirements//:requirements.bzl", examples_requirement = "requirement") 4 | load("@requirements//:requirements.bzl", "requirement") 5 | load("@rules_python//python:defs.bzl", "py_test") 6 | load("@test_requirements//:requirements.bzl", test_requirement = "requirement") 7 | 8 | def pip(name, extra = None): 9 | """Translates a pip package name to the bazel deps name. 10 | 11 | The `extra` can be used to select which requirements.txt to use. When 12 | None, uses the root `requirements.txt`. Otherwise can be `"[test]"` for 13 | `test/requiremens.txt` or `"[examples]"` for `examples/requirements.txt`. 14 | """ 15 | if not extra: 16 | return requirement(name) 17 | if extra == "[examples]": 18 | return examples_requirement(name) 19 | if extra == "[test]": 20 | return test_requirement(name) 21 | fail("Bad extra: " + repr(extra)) 22 | 23 | def py_lint_test( 24 | name, 25 | srcs, 26 | *, 27 | use_black = True, 28 | use_codestyle = True, 29 | use_isort = True): 30 | """Adds Python linter checks for the given `srcs`.""" 31 | native.filegroup( 32 | name = "_{}_data".format(name), 33 | srcs = srcs, 34 | ) 35 | (not use_codestyle) or py_test( 36 | name = "pycodestyle_{}".format(name), 37 | size = "small", 38 | srcs = ["//tools:pycodestyle_main.py"], 39 | main = "//tools:pycodestyle_main.py", 40 | data = [":_{}_data".format(name)], 41 | args = ["$(rootpaths :_{}_data)".format(name)], 42 | tags = ["lint", "pycodestyle"], 43 | deps = [pip("pycodestyle", "[test]")], 44 | ) 45 | (not use_black) or py_test( 46 | name = "black_{}".format(name), 47 | size = "small", 48 | srcs = ["//tools:black_main.py"], 49 | main = "//tools:black_main.py", 50 | data = [ 51 | ":_{}_data".format(name), 52 | "//:pyproject.toml", 53 | ], 54 | args = [ 55 | "--quiet", 56 | "--check", 57 | "--diff", 58 | "$(rootpaths :_{}_data)".format(name), 59 | ], 60 | tags = ["lint", "black"], 61 | deps = [pip("black", "[test]")], 62 | ) 63 | 64 | (not use_isort) or py_test( 65 | name = "isort_{}".format(name), 66 | size = "small", 67 | srcs = ["//tools:isort_main.py"], 68 | main = "//tools:isort_main.py", 69 | data = [ 70 | ":_{}_data".format(name), 71 | "//:pyproject.toml", 72 | ], 73 | args = [ 74 | "--check-only", 75 | "$(rootpaths :_{}_data)".format(name), 76 | ], 77 | tags = ["lint", "isort"], 78 | deps = [pip("isort", "[test]")], 79 | ) 80 | 81 | def bazel_lint_test(name, srcs): 82 | """Adds Bazel linter checks for the given `srcs`.""" 83 | locations = [ 84 | "$(location {})".format(src) 85 | for src in srcs 86 | ] 87 | native.sh_test( 88 | name = name, 89 | size = "small", 90 | srcs = ["//tools:buildifier"], 91 | data = srcs, 92 | args = ["-mode=check"] + locations, 93 | tags = ["lint"], 94 | ) 95 | -------------------------------------------------------------------------------- /tools/fix_lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | 4 | # This script is intended for use by drake-blender developers. 5 | # Users of the project do not need to run it. 6 | 7 | # This script runs the autoformatter(s) on all source files that are subject to 8 | # automated linter style checks. 9 | 10 | set -eu -o pipefail 11 | 12 | me=$(python3 -c 'import os; print(os.path.realpath("'"$0"'"))') 13 | cd $(dirname "$me")/.. 14 | 15 | ./bazel run //tools:buildifier \ 16 | *.bazel \ 17 | */*.bazel \ 18 | */*.bzl 19 | ./bazel build //tools:black //tools:isort 20 | ./.bazel/bin/tools/black \ 21 | bazel \ 22 | *.py \ 23 | */*.py \ 24 | */*/*.py 25 | ./.bazel/bin/tools/isort \ 26 | bazel \ 27 | *.py \ 28 | */*.py \ 29 | */*/*.py 30 | -------------------------------------------------------------------------------- /tools/isort_main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | import sys 4 | 5 | from isort.main import main as _isort_main 6 | 7 | 8 | def _main(): 9 | try: 10 | _isort_main() 11 | except SystemExit as e: 12 | if e.code is not None and e.code != 0: 13 | print( 14 | "INFO: Run ./tools/fix_lint.sh to automatically fix this " 15 | "problem.", 16 | file=sys.stderr, 17 | ) 18 | raise 19 | 20 | 21 | assert __name__ == "__main__" 22 | _main() 23 | -------------------------------------------------------------------------------- /tools/pycodestyle_main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | 3 | import sys 4 | 5 | import pycodestyle 6 | 7 | 8 | def _main(): 9 | report = pycodestyle.StyleGuide(parse_argv=True).check_files() 10 | if report.total_errors: 11 | print( 12 | "INFO: It's possible that running ./tools/fix_lint.sh could " 13 | "automatically fix this problem.", 14 | file=sys.stderr, 15 | ) 16 | sys.exit(1) 17 | 18 | 19 | assert __name__ == "__main__" 20 | _main() 21 | -------------------------------------------------------------------------------- /tools/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | 4 | # This script is intended for use by drake-blender developers. 5 | # Users of the project do not need to run it. 6 | 7 | # This script upgrades the pinned version of all dependencies. 8 | # TODO(jwnimmer-tri) It doesn't upgrade MODULE.bazel yet. 9 | 10 | set -eu -o pipefail 11 | 12 | me=$(python3 -c 'import os; print(os.path.realpath("'"$0"'"))') 13 | cd $(dirname "$me")/.. 14 | 15 | python3 -B ./tools/upgrade_helper.py 16 | ./bazel run //tools:buildifier tools/buildifier_version.bzl 17 | 18 | ./bazel run //:requirements.update -- --upgrade 19 | ./bazel run //examples:requirements.update -- --upgrade 20 | ./bazel run //test:requirements.update -- --upgrade 21 | -------------------------------------------------------------------------------- /tools/upgrade_helper.py: -------------------------------------------------------------------------------- 1 | # Do not run this by hand. Instead, run the full tools/upgrade.sh. 2 | 3 | import ast 4 | import hashlib 5 | import json 6 | from pprint import pformat 7 | from urllib.request import urlopen 8 | 9 | 10 | def _get_current_bazelisk_version() -> str: 11 | """Parses ``./bazel`` for the old version of bazelisk.""" 12 | with open("bazel", encoding="utf-8") as f: 13 | lines = f.read().splitlines() 14 | prefix = "_VERSION = " 15 | for line in lines: 16 | if line.startswith(prefix): 17 | return line.removeprefix(prefix).strip('"') 18 | raise RuntimeError(f"Could not find a line starting with {prefix!r}") 19 | 20 | 21 | def _write_bazelisk_version(new, sha256): 22 | """Updates ``./bazel`` with the new version of bazelisk.""" 23 | with open("bazel", encoding="utf-8") as f: 24 | lines = f.read().splitlines() 25 | for prefix, value in (("_VERSION = ", new), ("_SHA256 = ", sha256)): 26 | for i in range(len(lines)): 27 | if lines[i].startswith(prefix): 28 | lines[i] = f'{prefix}"{value}"' 29 | break 30 | else: 31 | raise RuntimeError(f"Could not find line starting with {prefix!r}") 32 | new_content = "\n".join(lines) + "\n" 33 | with open("bazel", "w", encoding="utf-8") as f: 34 | f.write(new_content) 35 | 36 | 37 | def _get_current_buildifier_version(): 38 | """Parses ``buildifier_version.bzl`` for the old version.""" 39 | with open("tools/buildifier_version.bzl", encoding="utf-8") as f: 40 | content = f.read() 41 | prefix = "BUILDIFIER_VERSION = " 42 | assert content.startswith(prefix) 43 | return ast.literal_eval(content.removeprefix(prefix)) 44 | 45 | 46 | def _write_buildifier_version(new): 47 | """Overwrites ``buildifier_version.bzl`` with the new version. 48 | We assume that tools/update.sh will run buildifier formatting afterwards. 49 | """ 50 | prefix = "BUILDIFIER_VERSION = " 51 | content = prefix + pformat(new, width=1, sort_dicts=False) 52 | with open("tools/buildifier_version.bzl", "w", encoding="utf-8") as f: 53 | f.write(content) 54 | 55 | 56 | def _find_latest_github_release(repo) -> str: 57 | """Finds the highest-numbered release (excluding prereleases).""" 58 | response = urlopen(f"https://api.github.com/repos/{repo}/releases") 59 | body = response.read() 60 | encoding = response.info().get_content_charset("iso-8859-1") 61 | releases = json.loads(body.decode(encoding)) 62 | tags = sorted( 63 | [ 64 | release["tag_name"].lstrip("v") 65 | for release in releases 66 | if not release["prerelease"] 67 | ], 68 | key=lambda tag: tuple(int(n) for n in tag.split(".")), 69 | reverse=True, 70 | ) 71 | return tags[0] 72 | 73 | 74 | def _get_url_checksum(url) -> str: 75 | """Returns the sha256sum string of the given url.""" 76 | print(f"Downloading {url} ...") 77 | hasher = hashlib.sha256() 78 | with urlopen(url) as response: 79 | while True: 80 | data = response.read(4096) 81 | if not data: 82 | break 83 | hasher.update(data) 84 | return hasher.hexdigest() 85 | 86 | 87 | def _upgrade_bazelisk(): 88 | """Upgrades bazelisk to its latest version (if necessary).""" 89 | old = _get_current_bazelisk_version() 90 | new = _find_latest_github_release("bazelbuild/bazelisk") 91 | if new == old: 92 | print(f"bazelisk is already at the latest version {new}") 93 | return 94 | print(f"bazelisk will be upgraded to version {new}") 95 | raw_download = "https://raw.githubusercontent.com/bazelbuild/bazelisk" 96 | sha256 = _get_url_checksum(f"{raw_download}/v{new}/bazelisk.py") 97 | _write_bazelisk_version(new, sha256) 98 | 99 | 100 | def _upgrade_buildifier(): 101 | """Upgrades buildifier to its latest version (if necessary).""" 102 | buildifier_version = _get_current_buildifier_version() 103 | old = buildifier_version["version"] 104 | new = _find_latest_github_release("bazelbuild/buildtools") 105 | if new == old: 106 | print(f"buildifier is already at the latest version {new}") 107 | return 108 | print(f"buildifier will be upgraded to version {new}") 109 | buildifier_version["version"] = new 110 | names = list(buildifier_version["binaries"].keys()) 111 | releases = "https://github.com/bazelbuild/buildtools/releases" 112 | for name in names: 113 | buildifier_version["binaries"][name] = _get_url_checksum( 114 | f"{releases}/download/v{new}/{name}" 115 | ) 116 | _write_buildifier_version(buildifier_version) 117 | 118 | 119 | def _main(): 120 | _upgrade_bazelisk() 121 | _upgrade_buildifier() 122 | 123 | 124 | assert __name__ == "__main__" 125 | _main() 126 | --------------------------------------------------------------------------------